From bbd3d12efefd4f378bb6be576faf6680e8aa1378 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 27 Aug 2024 12:52:54 -0700 Subject: [PATCH 01/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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/89] 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 8a64083786a72b1e0409cc6bc35c3f189ebcabf4 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 14:52:40 -0700 Subject: [PATCH 14/89] progress --- .../meeting/components/TemplatePromptList.tsx | 16 ++-------------- .../MoveReflectTemplatePromptMutation.ts | 2 +- .../mutations/addReflectTemplatePrompt.ts | 18 ++++-------------- .../public/typeDefs/ReflectPrompt.graphql | 2 +- .../graphql/public/types/ReflectPrompt.ts | 13 +++++++++++++ 5 files changed, 21 insertions(+), 30 deletions(-) create mode 100644 packages/server/graphql/public/types/ReflectPrompt.ts diff --git a/packages/client/modules/meeting/components/TemplatePromptList.tsx b/packages/client/modules/meeting/components/TemplatePromptList.tsx index 8c7c8317e05..dd9e9fa14e3 100644 --- a/packages/client/modules/meeting/components/TemplatePromptList.tsx +++ b/packages/client/modules/meeting/components/TemplatePromptList.tsx @@ -6,8 +6,8 @@ import {useFragment} from 'react-relay' import {TemplatePromptList_prompts$key} from '../../../__generated__/TemplatePromptList_prompts.graphql' import useAtmosphere from '../../../hooks/useAtmosphere' import MoveReflectTemplatePromptMutation from '../../../mutations/MoveReflectTemplatePromptMutation' +import {getSortOrder} from '../../../shared/sortOrder' import {TEMPLATE_PROMPT} from '../../../utils/constants' -import dndNoise from '../../../utils/dndNoise' import TemplatePromptItem from './TemplatePromptItem' interface Props { @@ -53,19 +53,7 @@ const TemplatePromptList = (props: Props) => { ) { return } - - let sortOrder - if (destination.index === 0) { - sortOrder = destinationPrompt.sortOrder - 1 + dndNoise() - } else if (destination.index === prompts.length - 1) { - sortOrder = destinationPrompt.sortOrder + 1 + dndNoise() - } else { - const offset = source.index > destination.index ? -1 : 1 - sortOrder = - ((prompts[destination.index + offset]?.sortOrder ?? 0) + destinationPrompt.sortOrder) / 2 + - dndNoise() - } - + const sortOrder = getSortOrder(prompts, source.index, destination.index) const {id: promptId} = sourcePrompt const variables = {promptId, sortOrder} MoveReflectTemplatePromptMutation(atmosphere, variables, {templateId}) diff --git a/packages/client/mutations/MoveReflectTemplatePromptMutation.ts b/packages/client/mutations/MoveReflectTemplatePromptMutation.ts index 531d10357dc..3b87703c536 100644 --- a/packages/client/mutations/MoveReflectTemplatePromptMutation.ts +++ b/packages/client/mutations/MoveReflectTemplatePromptMutation.ts @@ -18,7 +18,7 @@ graphql` ` const mutation = graphql` - mutation MoveReflectTemplatePromptMutation($promptId: ID!, $sortOrder: Float!) { + mutation MoveReflectTemplatePromptMutation($promptId: ID!, $sortOrder: String!) { moveReflectTemplatePrompt(promptId: $promptId, sortOrder: $sortOrder) { error { message diff --git a/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts index 43de3440ea8..95557a587c0 100644 --- a/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts +++ b/packages/server/graphql/public/mutations/addReflectTemplatePrompt.ts @@ -1,9 +1,7 @@ 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' @@ -16,7 +14,6 @@ const addReflectTemplatePrompt: MutationResolvers['addReflectTemplatePrompt'] = {templateId}, {authToken, dataLoader, socketId: mutatorId} ) => { - const r = await getRethink() const pg = getKysely() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -41,10 +38,8 @@ const addReflectTemplatePrompt: MutationResolvers['addReflectTemplatePrompt'] = } // 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 lastPrompt = activePrompts.at(-1) + const sortOrder = positionAfter(lastPrompt?.sortOrder ?? '') const pickedColors = activePrompts.map((prompt) => prompt.groupColor) const availableNewColor = palettePickerOptions.find((color) => !pickedColors.includes(color.hex)) const reflectPrompt = { @@ -58,13 +53,8 @@ const addReflectTemplatePrompt: MutationResolvers['addReflectTemplatePrompt'] = removedAt: null } - await Promise.all([ - r.table('ReflectPrompt').insert(reflectPrompt).run(), - pg - .insertInto('ReflectPrompt') - .values({...reflectPrompt, sortOrder: pgSortOrder}) - .execute() - ]) + await pg.insertInto('ReflectPrompt').values(reflectPrompt).execute() + dataLoader.clearAll('reflectPrompts') const promptId = reflectPrompt.id const data = {promptId} diff --git a/packages/server/graphql/public/typeDefs/ReflectPrompt.graphql b/packages/server/graphql/public/typeDefs/ReflectPrompt.graphql index a6f9c9fd268..c9ab3bd801c 100644 --- a/packages/server/graphql/public/typeDefs/ReflectPrompt.graphql +++ b/packages/server/graphql/public/typeDefs/ReflectPrompt.graphql @@ -22,7 +22,7 @@ type ReflectPrompt { """ the order of the items in the template """ - sortOrder: Float! + sortOrder: String! """ FK for template diff --git a/packages/server/graphql/public/types/ReflectPrompt.ts b/packages/server/graphql/public/types/ReflectPrompt.ts new file mode 100644 index 00000000000..f03675ef27f --- /dev/null +++ b/packages/server/graphql/public/types/ReflectPrompt.ts @@ -0,0 +1,13 @@ +import {ReflectPromptResolvers} from '../resolverTypes' + +const ReflectPrompt: ReflectPromptResolvers = { + team: ({teamId}, _args, {dataLoader}) => { + return dataLoader.get('teams').loadNonNull(teamId) + }, + + template: ({templateId}, _args, {dataLoader}) => { + return dataLoader.get('meetingTemplates').loadNonNull(templateId) + } +} + +export default ReflectPrompt From fcc66a9998f57f0b4c2fa44952de769da6e33792 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 9 Sep 2024 14:59:19 -0700 Subject: [PATCH 15/89] 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 16/89] 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 e628e0b8c1c3f70058690da253e0a7725d9231bb Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 10 Sep 2024 12:03:58 -0700 Subject: [PATCH 17/89] remove writes to R Signed-off-by: Matt Krick --- codegen.json | 2 +- packages/server/database/rethinkDriver.ts | 5 -- .../database/types/RetrospectivePrompt.ts | 48 ------------- .../dataloader/foreignKeyLoaderMakers.ts | 4 +- .../dataloader/primaryKeyLoaderMakers.ts | 2 +- .../rethinkForeignKeyLoaderMakers.ts | 13 ---- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../mutations/removeReflectTemplate.ts | 33 +++------ .../graphql/mutations/updateTemplateScope.ts | 30 ++++---- .../mutations/generateMeetingSummary.ts | 2 +- .../public/mutations/addReflectTemplate.ts | 7 -- .../public/mutations/helpers/getTopics.ts | 2 +- .../mutations/moveReflectTemplatePrompt.ts | 28 +------- .../reflectTemplatePromptUpdateDescription.ts | 23 ++---- .../reflectTemplatePromptUpdateGroupColor.ts | 15 +--- .../mutations/removeReflectTemplatePrompt.ts | 20 ++---- .../mutations/renameReflectTemplatePrompt.ts | 23 ++---- .../graphql/public/typeDefs/Mutation.graphql | 2 +- .../graphql/public/typeDefs/Team.graphql | 1 - .../types/AddReflectTemplatePromptPayload.ts | 4 +- .../types/MoveReflectTemplatePromptPayload.ts | 4 +- .../graphql/public/types/ReflectPhase.ts | 2 +- ...tTemplatePromptUpdateDescriptionPayload.ts | 4 +- ...ctTemplatePromptUpdateGroupColorPayload.ts | 4 +- .../RemoveReflectTemplatePromptPayload.ts | 4 +- .../RenameReflectTemplatePromptPayload.ts | 4 +- .../graphql/public/types/RetroReflection.ts | 2 +- .../public/types/RetroReflectionGroup.ts | 2 +- .../server/graphql/types/ReflectPrompt.ts | 70 ------------------- packages/server/graphql/types/Team.ts | 9 --- 30 files changed, 68 insertions(+), 302 deletions(-) delete mode 100644 packages/server/database/types/RetrospectivePrompt.ts delete mode 100644 packages/server/graphql/types/ReflectPrompt.ts diff --git a/codegen.json b/codegen.json index d7f135aec7c..f3758983568 100644 --- a/codegen.json +++ b/codegen.json @@ -147,7 +147,7 @@ "RRule": "rrule#RRule", "Reactable": "../../database/types/Reactable#Reactable", "Reactji": "../types/Reactji#ReactjiSource", - "ReflectPrompt": "../../database/types/RetrospectivePrompt#default", + "ReflectPrompt": "../../postgres/types/index#ReflectPrompt", "ReflectTemplate": "../../database/types/ReflectTemplate#default", "RemoveApprovedOrganizationDomainsSuccess": "./types/RemoveApprovedOrganizationDomainsSuccess#RemoveApprovedOrganizationDomainsSuccessSource", "RemoveIntegrationSearchQuerySuccess": "./types/RemoveIntegrationSearchQuerySuccess#RemoveIntegrationSearchQuerySuccessSource", diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index 055b5800142..d2ba61b2728 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -16,14 +16,9 @@ import NotificationTeamArchived from './types/NotificationTeamArchived' import NotificationTeamInvitation from './types/NotificationTeamInvitation' import PasswordResetRequest from './types/PasswordResetRequest' import PushInvitation from './types/PushInvitation' -import RetrospectivePrompt from './types/RetrospectivePrompt' import Task from './types/Task' export type RethinkSchema = { - ReflectPrompt: { - type: RetrospectivePrompt - index: 'teamId' | 'templateId' - } MassInvitation: { type: MassInvitation index: 'teamMemberId' diff --git a/packages/server/database/types/RetrospectivePrompt.ts b/packages/server/database/types/RetrospectivePrompt.ts deleted file mode 100644 index 8e06704a71e..00000000000 --- a/packages/server/database/types/RetrospectivePrompt.ts +++ /dev/null @@ -1,48 +0,0 @@ -import generateUID from '../../generateUID' - -interface Input { - teamId: string - templateId: string - sortOrder: number - question: string - description: string - groupColor: string - removedAt: Date | null - parentPromptId?: string | null -} - -export default class RetrospectivePrompt { - id: string - createdAt = new Date() - description: string - groupColor: string - sortOrder: number - teamId: string - templateId: string - question: string - removedAt: Date | null - updatedAt = new Date() - parentPromptId?: string - - constructor(input: Input) { - const { - teamId, - templateId, - sortOrder, - question, - description, - groupColor, - removedAt, - parentPromptId - } = input - this.id = generateUID() - this.sortOrder = sortOrder - this.teamId = teamId - this.templateId = templateId - this.question = question - this.description = description || '' - this.groupColor = groupColor - this.removedAt = removedAt - this.parentPromptId = parentPromptId - } -} diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 2c2381b23a9..77b010bc58e 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -217,8 +217,8 @@ export const commentsByDiscussionId = foreignKeyLoaderMaker( } ) -export const _pgreflectPromptsByTemplateId = foreignKeyLoaderMaker( - '_pgreflectPrompts', +export const reflectPromptsByTemplateId = foreignKeyLoaderMaker( + 'reflectPrompts', 'templateId', async (templateIds) => { return selectReflectPrompts() diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index e80d3785659..24a45d0b660 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -112,6 +112,6 @@ export const comments = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectComments().where('id', 'in', ids).execute() }) -export const _pgreflectPrompts = primaryKeyLoaderMaker((ids: readonly string[]) => { +export const reflectPrompts = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectReflectPrompts().where('id', 'in', ids).execute() }) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index b0bf83cf10a..15317dc9d3c 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -29,19 +29,6 @@ export const completedMeetingsByTeamId = new RethinkForeignKeyLoaderMaker( } ) -export const reflectPromptsByTemplateId = new RethinkForeignKeyLoaderMaker( - 'reflectPrompts', - 'templateId', - async (templateIds) => { - const r = await getRethink() - return r - .table('ReflectPrompt') - .getAll(r.args(templateIds), {index: 'templateId'}) - .orderBy('sortOrder') - .run() - } -) - export const massInvitationsByTeamMemberId = new RethinkForeignKeyLoaderMaker( 'massInvitations', 'teamMemberId', diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index 742b1b9ea39..2a7baaebc59 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 reflectPrompts = new RethinkPrimaryKeyLoaderMaker('ReflectPrompt') export const massInvitations = new RethinkPrimaryKeyLoaderMaker('MassInvitation') export const meetingMembers = new RethinkPrimaryKeyLoaderMaker('MeetingMember') export const newMeetings = new RethinkPrimaryKeyLoaderMaker('NewMeeting') diff --git a/packages/server/graphql/mutations/removeReflectTemplate.ts b/packages/server/graphql/mutations/removeReflectTemplate.ts index 14224dac4da..0518be77d31 100644 --- a/packages/server/graphql/mutations/removeReflectTemplate.ts +++ b/packages/server/graphql/mutations/removeReflectTemplate.ts @@ -1,6 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' +import {sql} from 'kysely' 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' @@ -21,9 +21,7 @@ const removeReflectTemplate = { {templateId}: {templateId: 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 template = await dataLoader.get('meetingTemplates').load(templateId) @@ -46,27 +44,14 @@ const removeReflectTemplate = { // RESOLUTION const {id: settingsId} = settings - await Promise.all([ - r - .table('ReflectPrompt') - .getAll(teamId, {index: 'teamId'}) - .filter({ - templateId - }) - .update({ - removedAt: now, - updatedAt: now - }) - .run(), - pg - .with('RemoveTemplate', (qb) => - qb.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) - ) - .updateTable('ReflectPrompt') - .set({removedAt: now}) - .where('templateId', '=', templateId) - .execute() - ]) + await pg + .with('RemoveTemplate', (qb) => + qb.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) + ) + .updateTable('ReflectPrompt') + .set({removedAt: sql`CURRENT_TIMESTAMP`}) + .where('templateId', '=', templateId) + .execute() dataLoader.clearAll('reflectPrompts') if (settings.selectedTemplateId === templateId) { const nextTemplate = templates.find((template) => template.id !== templateId) diff --git a/packages/server/graphql/mutations/updateTemplateScope.ts b/packages/server/graphql/mutations/updateTemplateScope.ts index 1b7c7aeeca1..41a0c48b590 100644 --- a/packages/server/graphql/mutations/updateTemplateScope.ts +++ b/packages/server/graphql/mutations/updateTemplateScope.ts @@ -100,23 +100,19 @@ const updateTemplateScope = { removedAt: null } }) - await Promise.all([ - pg - .with('MeetingTemplateInsert', (qc) => - qc.insertInto('MeetingTemplate').values(clonedTemplate) - ) - .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() - ]) + await pg + .with('MeetingTemplateInsert', (qc) => + qc.insertInto('MeetingTemplate').values(clonedTemplate) + ) + .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() dataLoader.clearAll(['reflectPrompts', 'meetingTemplates']) } diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 7be80c0d07b..8dbb9b5a071 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -119,7 +119,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn rawReflections.map(async (reflection) => { const {promptId, creatorId, plaintextContent} = reflection const [prompt, creator] = await Promise.all([ - dataLoader.get('reflectPrompts').load(promptId), + dataLoader.get('reflectPrompts').loadNonNull(promptId), creatorId ? dataLoader.get('users').loadNonNull(creatorId) : null ]) const {question} = prompt diff --git a/packages/server/graphql/public/mutations/addReflectTemplate.ts b/packages/server/graphql/public/mutations/addReflectTemplate.ts index 993abe9dc0f..c6ea27edc4b 100644 --- a/packages/server/graphql/public/mutations/addReflectTemplate.ts +++ b/packages/server/graphql/public/mutations/addReflectTemplate.ts @@ -1,6 +1,5 @@ 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 generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' @@ -19,7 +18,6 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( {authToken, dataLoader, socketId: mutatorId} ) => { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) @@ -92,7 +90,6 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( } }) await Promise.all([ - r.table('ReflectPrompt').insert(newTemplatePrompts).run(), pg .with('MeetingTemplateInsert', (qc) => qc.insertInto('MeetingTemplate').values(newTemplate)) .insertInto('ReflectPrompt') @@ -121,10 +118,6 @@ const addPokerTemplate: MutationResolvers['addPokerTemplate'] = async ( const newTemplate = templates[0]! const {id: templateId} = newTemplate await Promise.all([ - r - .table('ReflectPrompt') - .insert(newTemplatePrompts.map((p, idx) => ({...p, sortOrder: idx}))) - .run(), pg .with('MeetingTemplateInsert', (qc) => qc.insertInto('MeetingTemplate').values(newTemplate)) .insertInto('ReflectPrompt') diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index 94d23e228d9..4cb74174c5b 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -151,7 +151,7 @@ export const getTopics = async ( rawReflections.map(async (reflection) => { const {promptId, creatorId, plaintextContent} = reflection const [prompt, creator] = await Promise.all([ - dataLoader.get('reflectPrompts').load(promptId), + dataLoader.get('reflectPrompts').loadNonNull(promptId), creatorId ? dataLoader.get('users').loadNonNull(creatorId) : null ]) const {question} = prompt diff --git a/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts index f3a43d60055..93ff6a7024b 100644 --- a/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts +++ b/packages/server/graphql/public/mutations/moveReflectTemplatePrompt.ts @@ -1,6 +1,4 @@ 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' @@ -12,9 +10,7 @@ const moveReflectTemplatePrompt: MutationResolvers['moveReflectTemplatePrompt'] {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) @@ -30,30 +26,8 @@ const moveReflectTemplatePrompt: MutationResolvers['moveReflectTemplatePrompt'] // 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() - ]) + await pg.updateTable('ReflectPrompt').set({sortOrder}).where('id', '=', promptId).execute() 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/public/mutations/reflectTemplatePromptUpdateDescription.ts b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts index 5fc36d0fb48..1960399d7bc 100644 --- a/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts +++ b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateDescription.ts @@ -1,5 +1,4 @@ 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' @@ -8,9 +7,7 @@ import {MutationResolvers} from '../resolverTypes' const reflectTemplatePromptUpdateDescription: MutationResolvers['reflectTemplatePromptUpdateDescription'] = async (_source, {promptId, description}, {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) @@ -29,21 +26,11 @@ const reflectTemplatePromptUpdateDescription: MutationResolvers['reflectTemplate const normalizedDescription = description.trim().slice(0, 256) || '' // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - description: normalizedDescription, - updatedAt: now - }) - .run(), - pg - .updateTable('ReflectPrompt') - .set({description: normalizedDescription}) - .where('id', '=', promptId) - .execute() - ]) + await pg + .updateTable('ReflectPrompt') + .set({description: normalizedDescription}) + .where('id', '=', promptId) + .execute() dataLoader.clearAll('reflectPrompts') const data = {promptId} publish( diff --git a/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts index f6de5ffc8f2..55fb82ba522 100644 --- a/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts +++ b/packages/server/graphql/public/mutations/reflectTemplatePromptUpdateGroupColor.ts @@ -1,5 +1,4 @@ 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' @@ -8,9 +7,7 @@ import {MutationResolvers} from '../resolverTypes' const reflectTemplatePromptUpdateGroupColor: MutationResolvers['reflectTemplatePromptUpdateGroupColor'] = async (_source, {promptId, groupColor}, {authToken, dataLoader, socketId: mutatorId}) => { - const r = await getRethink() const pg = getKysely() - const now = new Date() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) @@ -29,17 +26,7 @@ const reflectTemplatePromptUpdateGroupColor: MutationResolvers['reflectTemplateP const {teamId} = prompt // RESOLUTION - await Promise.all([ - r - .table('ReflectPrompt') - .get(promptId) - .update({ - groupColor, - updatedAt: now - }) - .run(), - pg.updateTable('ReflectPrompt').set({groupColor}).where('id', '=', promptId).execute() - ]) + await pg.updateTable('ReflectPrompt').set({groupColor}).where('id', '=', promptId).execute() dataLoader.clearAll('reflectPrompts') const data = {promptId} publish( diff --git a/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts index c74b0b48b0a..71632eceb8f 100644 --- a/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts +++ b/packages/server/graphql/public/mutations/removeReflectTemplatePrompt.ts @@ -1,5 +1,5 @@ +import {sql} from 'kysely' 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' @@ -11,9 +11,7 @@ const removeReflectTemplatePrompt: MutationResolvers['removeReflectTemplatePromp {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) @@ -38,17 +36,11 @@ const removeReflectTemplatePrompt: MutationResolvers['removeReflectTemplatePromp } // 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() - ]) + await pg + .updateTable('ReflectPrompt') + .set({removedAt: sql`CURRENT_TIMESTAMP`}) + .where('id', '=', promptId) + .execute() dataLoader.clearAll('reflectPrompts') const data = {promptId, templateId} publish(SubscriptionChannel.TEAM, teamId, 'RemoveReflectTemplatePromptPayload', data, subOptions) diff --git a/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts b/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts index 426901b79e6..fc0805aeda6 100644 --- a/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts +++ b/packages/server/graphql/public/mutations/renameReflectTemplatePrompt.ts @@ -1,5 +1,4 @@ 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' @@ -11,9 +10,7 @@ const renameReflectTemplatePrompt: MutationResolvers['renameReflectTemplatePromp {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) @@ -39,21 +36,11 @@ const renameReflectTemplatePrompt: MutationResolvers['renameReflectTemplatePromp } // 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() - ]) + await pg + .updateTable('ReflectPrompt') + .set({question: normalizedQuestion}) + .where('id', '=', promptId) + .execute() dataLoader.clearAll('reflectPrompts') const data = {promptId} publish(SubscriptionChannel.TEAM, teamId, 'RenameReflectTemplatePromptPayload', data, subOptions) diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql index 1ff66c7adb6..7b59ea7a5a5 100644 --- a/packages/server/graphql/public/typeDefs/Mutation.graphql +++ b/packages/server/graphql/public/typeDefs/Mutation.graphql @@ -433,7 +433,7 @@ type Mutation { """ Move a reflect template """ - moveReflectTemplatePrompt(promptId: ID!, sortOrder: Float!): MoveReflectTemplatePromptPayload + moveReflectTemplatePrompt(promptId: ID!, sortOrder: String!): MoveReflectTemplatePromptPayload """ Move a team to a different org. Requires billing leader rights on both orgs! diff --git a/packages/server/graphql/public/typeDefs/Team.graphql b/packages/server/graphql/public/typeDefs/Team.graphql index 6d197a733d5..3e8ee51d148 100644 --- a/packages/server/graphql/public/typeDefs/Team.graphql +++ b/packages/server/graphql/public/typeDefs/Team.graphql @@ -66,7 +66,6 @@ type Team { The datetime the team was last updated """ updatedAt: DateTime - customPhaseItems: [ReflectPrompt] @deprecated(reason: "Field no longer needs to exist for now") """ The outstanding invitations to join the team diff --git a/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts index 59482f4f743..57687fda374 100644 --- a/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts +++ b/packages/server/graphql/public/types/AddReflectTemplatePromptPayload.ts @@ -8,7 +8,9 @@ export type AddReflectTemplatePromptPayloadSource = const AddReflectTemplatePromptPayload: AddReflectTemplatePromptPayloadResolvers = { prompt: (source, _args, {dataLoader}) => { - return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + return 'promptId' in source + ? dataLoader.get('reflectPrompts').loadNonNull(source.promptId) + : null } } diff --git a/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts index 87096f79f55..16cc05e208e 100644 --- a/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts +++ b/packages/server/graphql/public/types/MoveReflectTemplatePromptPayload.ts @@ -8,7 +8,9 @@ export type MoveReflectTemplatePromptPayloadSource = const MoveReflectTemplatePromptPayload: MoveReflectTemplatePromptPayloadResolvers = { prompt: (source, _args, {dataLoader}) => { - return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + return 'promptId' in source + ? dataLoader.get('reflectPrompts').loadNonNull(source.promptId) + : null } } diff --git a/packages/server/graphql/public/types/ReflectPhase.ts b/packages/server/graphql/public/types/ReflectPhase.ts index fb5bad8671e..8714fad14a0 100644 --- a/packages/server/graphql/public/types/ReflectPhase.ts +++ b/packages/server/graphql/public/types/ReflectPhase.ts @@ -5,7 +5,7 @@ const ReflectPhase: ReflectPhaseResolvers = { __isTypeOf: ({phaseType}) => phaseType === 'reflect', focusedPrompt: ({focusedPromptId}, _args, {dataLoader}) => { if (!focusedPromptId) return null - return dataLoader.get('reflectPrompts').load(focusedPromptId) + return dataLoader.get('reflectPrompts').loadNonNull(focusedPromptId) }, reflectPrompts: async ({meetingId}, _args, {dataLoader}) => { diff --git a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts index ab49737cd6e..defaec49b8d 100644 --- a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts +++ b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateDescriptionPayload.ts @@ -9,7 +9,9 @@ export type ReflectTemplatePromptUpdateDescriptionPayloadSource = const ReflectTemplatePromptUpdateDescriptionPayload: ReflectTemplatePromptUpdateDescriptionPayloadResolvers = { prompt: (source, _args, {dataLoader}) => { - return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + return 'promptId' in source + ? dataLoader.get('reflectPrompts').loadNonNull(source.promptId) + : null } } diff --git a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts index c964750561f..93b4d5e12a7 100644 --- a/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts +++ b/packages/server/graphql/public/types/ReflectTemplatePromptUpdateGroupColorPayload.ts @@ -9,7 +9,9 @@ export type ReflectTemplatePromptUpdateGroupColorPayloadSource = const ReflectTemplatePromptUpdateGroupColorPayload: ReflectTemplatePromptUpdateGroupColorPayloadResolvers = { prompt: (source, _args, {dataLoader}) => { - return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + return 'promptId' in source + ? dataLoader.get('reflectPrompts').loadNonNull(source.promptId) + : null } } diff --git a/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts index 8e81e655db0..012ef4d918e 100644 --- a/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts +++ b/packages/server/graphql/public/types/RemoveReflectTemplatePromptPayload.ts @@ -15,7 +15,9 @@ const RemoveReflectTemplatePromptPayload: RemoveReflectTemplatePromptPayloadReso }, prompt: (source, _args, {dataLoader}) => { - return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + return 'promptId' in source + ? dataLoader.get('reflectPrompts').loadNonNull(source.promptId) + : null } } diff --git a/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts b/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts index 980ad0ac390..30f33368213 100644 --- a/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts +++ b/packages/server/graphql/public/types/RenameReflectTemplatePromptPayload.ts @@ -8,7 +8,9 @@ export type RenameReflectTemplatePromptPayloadSource = const RenameReflectTemplatePromptPayload: RenameReflectTemplatePromptPayloadResolvers = { prompt: (source, _args, {dataLoader}) => { - return 'promptId' in source ? dataLoader.get('reflectPrompts').load(source.promptId) : null + return 'promptId' in source + ? dataLoader.get('reflectPrompts').loadNonNull(source.promptId) + : null } } diff --git a/packages/server/graphql/public/types/RetroReflection.ts b/packages/server/graphql/public/types/RetroReflection.ts index 07d6c6a0b04..80c967c6797 100644 --- a/packages/server/graphql/public/types/RetroReflection.ts +++ b/packages/server/graphql/public/types/RetroReflection.ts @@ -39,7 +39,7 @@ const RetroReflection: RetroReflectionResolvers = { }, prompt: ({promptId}, _args, {dataLoader}) => { - return dataLoader.get('reflectPrompts').load(promptId) + return dataLoader.get('reflectPrompts').loadNonNull(promptId) }, reactjis: ({reactjis, id}, _args, {authToken}) => { diff --git a/packages/server/graphql/public/types/RetroReflectionGroup.ts b/packages/server/graphql/public/types/RetroReflectionGroup.ts index d7a56fd4c16..2c0834b7635 100644 --- a/packages/server/graphql/public/types/RetroReflectionGroup.ts +++ b/packages/server/graphql/public/types/RetroReflectionGroup.ts @@ -12,7 +12,7 @@ const RetroReflectionGroup: RetroReflectionGroupResolvers = { return retroMeeting as MeetingRetrospective }, prompt: ({promptId}, _args, {dataLoader}) => { - return dataLoader.get('reflectPrompts').load(promptId) + return dataLoader.get('reflectPrompts').loadNonNull(promptId) }, reflections: async ({id: reflectionGroupId, meetingId}, _args, {dataLoader}) => { // use meetingId so we only hit the DB once instead of once per group diff --git a/packages/server/graphql/types/ReflectPrompt.ts b/packages/server/graphql/types/ReflectPrompt.ts deleted file mode 100644 index 9658eef5ebf..00000000000 --- a/packages/server/graphql/types/ReflectPrompt.ts +++ /dev/null @@ -1,70 +0,0 @@ -import {GraphQLFloat, GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' -import {resolveTeam} from '../resolvers' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import ReflectTemplate from './ReflectTemplate' -import Team from './Team' - -const ReflectPrompt: GraphQLObjectType = new GraphQLObjectType({ - name: 'ReflectPrompt', - description: - 'A team-specific reflection prompt. Usually 3 or 4 exist per team, eg Good/Bad/Change, 4Ls, etc.', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'shortid' - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type) - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'foreign key. use the team field' - }, - team: { - type: Team, - description: 'The team that owns this reflectPrompt', - resolve: resolveTeam - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type) - }, - sortOrder: { - type: new GraphQLNonNull(GraphQLFloat), - description: 'the order of the items in the template' - }, - templateId: { - type: new GraphQLNonNull(GraphQLID), - description: 'FK for template' - }, - template: { - type: new GraphQLNonNull(ReflectTemplate), - description: 'The template that this prompt belongs to', - resolve: ({templateId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('meetingTemplates').load(templateId) - } - }, - question: { - description: - 'The question to answer during the phase of the retrospective (eg What went well?)', - type: new GraphQLNonNull(GraphQLString) - }, - description: { - description: - 'The description to the question for further context. A long version of the question.', - type: new GraphQLNonNull(GraphQLString), - resolve: ({description}) => description || '' - }, - groupColor: { - description: 'The color used to visually group a phase item.', - type: new GraphQLNonNull(GraphQLString), - resolve: ({groupColor}) => groupColor || '#FFFFFF' - }, - removedAt: { - type: GraphQLISO8601Type, - description: 'The datetime that the prompt was removed. Null if it has not been removed.' - } - }) -}) - -export default ReflectPrompt diff --git a/packages/server/graphql/types/Team.ts b/packages/server/graphql/types/Team.ts index be6246317e1..385be345be2 100644 --- a/packages/server/graphql/types/Team.ts +++ b/packages/server/graphql/types/Team.ts @@ -24,7 +24,6 @@ import MassInvitation from './MassInvitation' import MeetingTypeEnum from './MeetingTypeEnum' import NewMeeting from './NewMeeting' import Organization from './Organization' -import ReflectPrompt from './ReflectPrompt' import {TaskConnection} from './Task' import TeamInvitation from './TeamInvitation' import TeamMeetingSettings from './TeamMeetingSettings' @@ -122,14 +121,6 @@ const Team: GraphQLObjectType = new GraphQLObjectType({ type: GraphQLISO8601Type, description: 'The datetime the team was last updated' }, - customPhaseItems: { - type: new GraphQLList(ReflectPrompt), - deprecationReason: 'Field no longer needs to exist for now', - resolve: () => { - // not useful for retros since there is no templateId filter - return [] - } - }, teamInvitations: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(TeamInvitation))), description: 'The outstanding invitations to join the team', From 21fc97c85650ce2c8cc41600a973beb0be0aeecc Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 10 Sep 2024 12:06:27 -0700 Subject: [PATCH 18/89] 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 } } From ec73789965ff62525a7fe44360772c0e944999a3 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 10 Sep 2024 12:21:53 -0700 Subject: [PATCH 19/89] fix bad old migrations referencing types Signed-off-by: Matt Krick --- .../migrations/1685721573097_addPrePostMortemTemplates.ts | 3 +-- .../migrations/1696443115482_addCustomerFeedbackTemplate.ts | 3 +-- .../migrations/1696616922662_addPeerReviewTemplates.ts | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/server/postgres/migrations/1685721573097_addPrePostMortemTemplates.ts b/packages/server/postgres/migrations/1685721573097_addPrePostMortemTemplates.ts index f034e4e35a6..86d6f9ce92e 100644 --- a/packages/server/postgres/migrations/1685721573097_addPrePostMortemTemplates.ts +++ b/packages/server/postgres/migrations/1685721573097_addPrePostMortemTemplates.ts @@ -2,7 +2,6 @@ import {PALETTE} from 'parabol-client/styles/paletteV3' import {Client} from 'pg' import {r} from 'rethinkdb-ts' import connectRethinkDB from '../../database/connectRethinkDB' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' import getPgConfig from '../getPgConfig' import getPgp from '../getPgp' @@ -1029,7 +1028,7 @@ type PromptInfo = { sortOrder: number } -const makePrompt = (promptInfo: PromptInfo, idx: number): RetrospectivePrompt => { +const makePrompt = (promptInfo: PromptInfo, idx: number) => { const {question, description, templateId, sortOrder} = promptInfo const paletteIdx = idx > promptColors.length - 1 ? idx % promptColors.length : idx const groupColor = promptColors[paletteIdx] diff --git a/packages/server/postgres/migrations/1696443115482_addCustomerFeedbackTemplate.ts b/packages/server/postgres/migrations/1696443115482_addCustomerFeedbackTemplate.ts index 976ef5295d4..f96e7641e37 100644 --- a/packages/server/postgres/migrations/1696443115482_addCustomerFeedbackTemplate.ts +++ b/packages/server/postgres/migrations/1696443115482_addCustomerFeedbackTemplate.ts @@ -2,7 +2,6 @@ import {PALETTE} from 'parabol-client/styles/paletteV3' import {Client} from 'pg' import {r} from 'rethinkdb-ts' import connectRethinkDB from '../../database/connectRethinkDB' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' import getPgConfig from '../getPgConfig' import getPgp from '../getPgp' @@ -95,7 +94,7 @@ type PromptInfo = { sortOrder: number } -const makePrompt = (promptInfo: PromptInfo, idx: number): RetrospectivePrompt => { +const makePrompt = (promptInfo: PromptInfo, idx: number) => { const {question, description, templateId, sortOrder} = promptInfo const paletteIdx = idx > promptColors.length - 1 ? idx % promptColors.length : idx const groupColor = promptColors[paletteIdx] diff --git a/packages/server/postgres/migrations/1696616922662_addPeerReviewTemplates.ts b/packages/server/postgres/migrations/1696616922662_addPeerReviewTemplates.ts index 5b363c57095..f32f9ce5262 100644 --- a/packages/server/postgres/migrations/1696616922662_addPeerReviewTemplates.ts +++ b/packages/server/postgres/migrations/1696616922662_addPeerReviewTemplates.ts @@ -2,7 +2,6 @@ import {PALETTE} from 'parabol-client/styles/paletteV3' import {Client} from 'pg' import {r} from 'rethinkdb-ts' import connectRethinkDB from '../../database/connectRethinkDB' -import RetrospectivePrompt from '../../database/types/RetrospectivePrompt' import getPgConfig from '../getPgConfig' import getPgp from '../getPgp' @@ -167,7 +166,7 @@ type PromptInfo = { sortOrder: number } -const makePrompt = (promptInfo: PromptInfo, idx: number): RetrospectivePrompt => { +const makePrompt = (promptInfo: PromptInfo, idx: number) => { const {question, description, templateId, sortOrder} = promptInfo const paletteIdx = idx > promptColors.length - 1 ? idx % promptColors.length : idx const groupColor = promptColors[paletteIdx] From 726f63242b395a1872e44b5f164ddc78935ad669 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 10 Sep 2024 13:07:13 -0700 Subject: [PATCH 20/89] chore: migrate PasswordResetRequest Signed-off-by: Matt Krick --- packages/server/__tests__/globalSetup.ts | 7 +-- packages/server/database/rethinkDriver.ts | 5 -- .../database/types/PasswordResetRequest.ts | 28 --------- .../graphql/mutations/emailPasswordReset.ts | 42 +++++++------ .../helpers/processEmailPasswordReset.ts | 29 +++++---- .../server/graphql/mutations/resetPassword.ts | 21 ++++--- .../1725996598345_PasswordResetRequest.ts | 62 +++++++++++++++++++ 7 files changed, 117 insertions(+), 77 deletions(-) delete mode 100644 packages/server/database/types/PasswordResetRequest.ts create mode 100644 packages/server/postgres/migrations/1725996598345_PasswordResetRequest.ts diff --git a/packages/server/__tests__/globalSetup.ts b/packages/server/__tests__/globalSetup.ts index 6cecd76a0f3..555249a35c2 100644 --- a/packages/server/__tests__/globalSetup.ts +++ b/packages/server/__tests__/globalSetup.ts @@ -1,12 +1,11 @@ +import {sql} from 'kysely' import '../../../scripts/webpack/utils/dotenv' -import getRethink from '../database/rethinkDriver' +import getKysely from '../postgres/getKysely' async function setup() { - const r = await getRethink() // The IP address is always localhost // so the safety checks will eventually fail if run too much - - await Promise.all([r.table('PasswordResetRequest').delete().run()]) + await sql`TRUNCATE TABLE "PasswordResetRequest"`.execute(getKysely()) } export default setup diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index d2ba61b2728..0a9e17ac7ce 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -14,7 +14,6 @@ import NotificationResponseReplied from './types/NotificationResponseReplied' import NotificationTaskInvolves from './types/NotificationTaskInvolves' import NotificationTeamArchived from './types/NotificationTeamArchived' import NotificationTeamInvitation from './types/NotificationTeamInvitation' -import PasswordResetRequest from './types/PasswordResetRequest' import PushInvitation from './types/PushInvitation' import Task from './types/Task' @@ -54,10 +53,6 @@ export type RethinkSchema = { | NotificationMentioned index: 'userId' } - PasswordResetRequest: { - type: PasswordResetRequest - index: 'email' | 'ip' | 'token' - } PushInvitation: { type: PushInvitation index: 'userId' diff --git a/packages/server/database/types/PasswordResetRequest.ts b/packages/server/database/types/PasswordResetRequest.ts deleted file mode 100644 index baa8187c571..00000000000 --- a/packages/server/database/types/PasswordResetRequest.ts +++ /dev/null @@ -1,28 +0,0 @@ -import generateUID from '../../generateUID' - -interface Input { - id?: string - ip: string - isValid?: boolean - email: string - token: string - time?: Date -} - -export default class PasswordResetRequest { - id: string - ip: string - email: string - time: Date - token: string - isValid: boolean - constructor(input: Input) { - const {id, email, ip, isValid, time, token} = input - this.id = id ?? generateUID() - this.email = email - this.ip = ip - this.time = time ?? new Date() - this.token = token - this.isValid = isValid ?? true - } -} diff --git a/packages/server/graphql/mutations/emailPasswordReset.ts b/packages/server/graphql/mutations/emailPasswordReset.ts index 65ab85d1a7c..43b711ecdf8 100644 --- a/packages/server/graphql/mutations/emailPasswordReset.ts +++ b/packages/server/graphql/mutations/emailPasswordReset.ts @@ -3,9 +3,8 @@ import ms from 'ms' import {AuthenticationError, Threshold} from 'parabol-client/types/constEnums' import {AuthIdentityTypeEnum} from '../../../client/types/constEnums' import getSSODomainFromEmail from '../../../client/utils/getSSODomainFromEmail' -import getRethink from '../../database/rethinkDriver' -import {RDatum} from '../../database/stricterR' import AuthIdentityLocal from '../../database/types/AuthIdentityLocal' +import getKysely from '../../postgres/getKysely' import {getUserByEmail} from '../../postgres/queries/getUsersByEmails' import {GQLContext} from '../graphql' import rateLimit from '../rateLimit' @@ -31,26 +30,33 @@ const emailPasswordReset = { return {error: {message: 'Resetting password is disabled'}} } const email = denormEmail.toLowerCase().trim() - const r = await getRethink() // we only wanna send like 2 emails/min or 5 per day to the same person const yesterday = new Date(Date.now() - ms('1d')) const user = await getUserByEmail(email) - const {failOnAccount, failOnTime} = await r({ - failOnAccount: r - .table('PasswordResetRequest') - .getAll(ip, {index: 'ip'}) - .filter({email}) - .filter((row: RDatum) => row('time').ge(yesterday)) - .count() - .ge(Threshold.MAX_ACCOUNT_DAILY_PASSWORD_RESETS) as unknown as boolean, - failOnTime: r - .table('PasswordResetRequest') - .getAll(ip, {index: 'ip'}) - .filter((row: RDatum) => row('time').ge(yesterday)) - .count() - .ge(Threshold.MAX_DAILY_PASSWORD_RESETS) as unknown as boolean - }).run() + const pg = getKysely() + const {failOnAccount, failOnTime} = await pg + .with('FailOnAccount', (qb) => + qb + .selectFrom('PasswordResetRequest') + .where('ip', '=', ip) + .where('email', '=', email) + .where('time', '>=', yesterday) + .select(({eb, fn}) => + eb(fn.count('id'), '>=', Threshold.MAX_ACCOUNT_DAILY_PASSWORD_RESETS).as( + 'failOnAccount' + ) + ) + ) + .selectFrom(['FailOnAccount', 'PasswordResetRequest']) + .where('ip', '=', ip) + .where('time', '>=', yesterday) + .select(({eb, fn}) => [ + 'FailOnAccount.failOnAccount', + eb(fn.count('id'), '>=', Threshold.MAX_DAILY_PASSWORD_RESETS).as('failOnTime') + ]) + .executeTakeFirstOrThrow() + if (failOnAccount || failOnTime) { return {error: {message: AuthenticationError.EXCEEDED_RESET_THRESHOLD}} } diff --git a/packages/server/graphql/mutations/helpers/processEmailPasswordReset.ts b/packages/server/graphql/mutations/helpers/processEmailPasswordReset.ts index 9ee0d7aef8e..f9b94215737 100644 --- a/packages/server/graphql/mutations/helpers/processEmailPasswordReset.ts +++ b/packages/server/graphql/mutations/helpers/processEmailPasswordReset.ts @@ -1,12 +1,11 @@ import base64url from 'base64url' import crypto from 'crypto' import {AuthenticationError} from 'parabol-client/types/constEnums' -import {r} from 'rethinkdb-ts' import util from 'util' import AuthIdentity from '../../../database/types/AuthIdentity' -import PasswordResetRequest from '../../../database/types/PasswordResetRequest' import getMailManager from '../../../email/getMailManager' import resetPasswordEmailCreator from '../../../email/resetPasswordEmailCreator' +import getKysely from '../../../postgres/getKysely' import updateUser from '../../../postgres/queries/updateUser' const randomBytes = util.promisify(crypto.randomBytes) @@ -17,19 +16,25 @@ const processEmailPasswordReset = async ( identities: AuthIdentity[], userId: string ) => { + const pg = getKysely() const tokenBuffer = await randomBytes(48) const resetPasswordToken = base64url.encode(tokenBuffer) // invalidate all other tokens for this email - await r - .table('PasswordResetRequest') - .getAll(email, {index: 'email'}) - .filter({isValid: true}) - .update({isValid: false}) - .run() - await r - .table('PasswordResetRequest') - .insert(new PasswordResetRequest({ip, email, token: resetPasswordToken})) - .run() + await pg + .with('InvalidateOtherTokens', (qb) => + qb + .updateTable('PasswordResetRequest') + .set({isValid: false}) + .where('email', '=', email) + .where('isValid', '=', true) + ) + .insertInto('PasswordResetRequest') + .values({ + ip, + email, + token: resetPasswordToken + }) + .execute() await updateUser({identities}, userId) diff --git a/packages/server/graphql/mutations/resetPassword.ts b/packages/server/graphql/mutations/resetPassword.ts index d90523d3cfc..89f82b8ac64 100644 --- a/packages/server/graphql/mutations/resetPassword.ts +++ b/packages/server/graphql/mutations/resetPassword.ts @@ -2,10 +2,8 @@ import bcrypt from 'bcryptjs' import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {Security, Threshold} from 'parabol-client/types/constEnums' import {AuthIdentityTypeEnum} from '../../../client/types/constEnums' -import getRethink from '../../database/rethinkDriver' import AuthIdentityLocal from '../../database/types/AuthIdentityLocal' import AuthToken from '../../database/types/AuthToken' -import PasswordResetRequest from '../../database/types/PasswordResetRequest' import getKysely from '../../postgres/getKysely' import {getUserByEmail} from '../../postgres/queries/getUsersByEmails' import updateUser from '../../postgres/queries/updateUser' @@ -39,13 +37,11 @@ const resetPassword = { return {error: {message: 'Resetting password is disabled'}} } const pg = getKysely() - const r = await getRethink() - const resetRequest = (await r - .table('PasswordResetRequest') - .getAll(token, {index: 'token'}) - .nth(0) - .default(null) - .run()) as PasswordResetRequest + const resetRequest = await pg + .selectFrom('PasswordResetRequest') + .selectAll() + .where('token', '=', token) + .executeTakeFirst() if (!resetRequest) { return {error: {message: 'Invalid reset token'}} @@ -69,7 +65,12 @@ const resetPassword = { if (!localIdentity) { return standardError(new Error(`User ${email} does not have a local identity`), {userId}) } - await r.table('PasswordResetRequest').get(resetRequestId).update({isValid: false}).run() + await pg + .updateTable('PasswordResetRequest') + .set({isValid: false}) + .where('id', '=', resetRequestId) + .execute() + // MUTATIVE localIdentity.hashedPassword = await bcrypt.hash(newPassword, Security.SALT_ROUNDS) localIdentity.isEmailVerified = true diff --git a/packages/server/postgres/migrations/1725996598345_PasswordResetRequest.ts b/packages/server/postgres/migrations/1725996598345_PasswordResetRequest.ts new file mode 100644 index 00000000000..b949a622405 --- /dev/null +++ b/packages/server/postgres/migrations/1725996598345_PasswordResetRequest.ts @@ -0,0 +1,62 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "PasswordResetRequest" ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "ip" cidr NOT NULL, + "email" "citext" NOT NULL, + "time" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "token" VARCHAR(64) NOT NULL, + "isValid" BOOLEAN NOT NULL DEFAULT TRUE + ); + CREATE INDEX IF NOT EXISTS "idx_PasswordResetRequest_ip" ON "PasswordResetRequest"("ip"); + CREATE INDEX IF NOT EXISTS "idx_PasswordResetRequest_email" ON "PasswordResetRequest"("email"); + CREATE INDEX IF NOT EXISTS "idx_PasswordResetRequest_token" ON "PasswordResetRequest"("token"); + END $$; +`.execute(pg) + + const rRequests = await r.table('PasswordResetRequest').coerceTo('array').run() + + await Promise.all( + rRequests.map(async (row) => { + const {ip, email, time, token, isValid} = row + try { + return await pg + .insertInto('PasswordResetRequest') + .values({ + ip, + email, + time, + token, + isValid + }) + .execute() + } catch (e) { + console.log(e, row) + } + }) + ) +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "PasswordResetRequest"; + ` /* Do undo magic */) + await client.end() +} From 9c7839de8f4d5a488fcee39c39a16733dd8d3113 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 10 Sep 2024 14:13:26 -0700 Subject: [PATCH 21/89] use 2 queries for resets Signed-off-by: Matt Krick --- .../graphql/mutations/emailPasswordReset.ts | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/packages/server/graphql/mutations/emailPasswordReset.ts b/packages/server/graphql/mutations/emailPasswordReset.ts index 43b711ecdf8..75a994d0a4d 100644 --- a/packages/server/graphql/mutations/emailPasswordReset.ts +++ b/packages/server/graphql/mutations/emailPasswordReset.ts @@ -35,29 +35,26 @@ const emailPasswordReset = { const yesterday = new Date(Date.now() - ms('1d')) const user = await getUserByEmail(email) const pg = getKysely() - const {failOnAccount, failOnTime} = await pg - .with('FailOnAccount', (qb) => - qb - .selectFrom('PasswordResetRequest') - .where('ip', '=', ip) - .where('email', '=', email) - .where('time', '>=', yesterday) - .select(({eb, fn}) => - eb(fn.count('id'), '>=', Threshold.MAX_ACCOUNT_DAILY_PASSWORD_RESETS).as( - 'failOnAccount' - ) - ) - ) - .selectFrom(['FailOnAccount', 'PasswordResetRequest']) - .where('ip', '=', ip) - .where('time', '>=', yesterday) - .select(({eb, fn}) => [ - 'FailOnAccount.failOnAccount', - eb(fn.count('id'), '>=', Threshold.MAX_DAILY_PASSWORD_RESETS).as('failOnTime') - ]) - .executeTakeFirstOrThrow() - - if (failOnAccount || failOnTime) { + const [failOnAccount, failOnTime] = await Promise.all([ + pg + .selectFrom('PasswordResetRequest') + .where('ip', '=', ip) + .where('email', '=', email) + .where('time', '>=', yesterday) + .select(({eb, fn}) => + eb(fn.count('id'), '>=', Threshold.MAX_ACCOUNT_DAILY_PASSWORD_RESETS).as('res') + ) + .executeTakeFirstOrThrow(), + pg + .selectFrom('PasswordResetRequest') + .where('ip', '=', ip) + .where('time', '>=', yesterday) + .select(({eb, fn}) => + eb(fn.count('id'), '>=', Threshold.MAX_DAILY_PASSWORD_RESETS).as('res') + ) + .executeTakeFirstOrThrow() + ]) + if (failOnAccount.res || failOnTime.res) { return {error: {message: AuthenticationError.EXCEEDED_RESET_THRESHOLD}} } const domain = getSSODomainFromEmail(email) From 89040dfd0d9b07e1a96908479ad70f39f424711b Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 10 Sep 2024 15:08:15 -0700 Subject: [PATCH 22/89] clean up clientside sortOrder Signed-off-by: Matt Krick --- .../components/GroupingKanbanColumn.tsx | 48 ++++--------------- .../meeting/components/AddTemplatePrompt.tsx | 6 +-- .../AddReflectTemplatePromptMutation.ts | 2 +- 3 files changed, 12 insertions(+), 44 deletions(-) diff --git a/packages/client/components/GroupingKanbanColumn.tsx b/packages/client/components/GroupingKanbanColumn.tsx index 575496872bd..cc44b48af09 100644 --- a/packages/client/components/GroupingKanbanColumn.tsx +++ b/packages/client/components/GroupingKanbanColumn.tsx @@ -8,46 +8,15 @@ import {GroupingKanbanColumn_reflectionGroups$key} from '~/__generated__/Groupin import {useCoverable} from '~/hooks/useControlBarCovers' import useDeepEqual from '~/hooks/useDeepEqual' import useSubColumns from '~/hooks/useSubColumns' -import makeMinWidthMediaQuery from '~/utils/makeMinWidthMediaQuery' import useAtmosphere from '../hooks/useAtmosphere' import useMutationProps from '../hooks/useMutationProps' import CreateReflectionMutation from '../mutations/CreateReflectionMutation' -import {PALETTE} from '../styles/paletteV3' -import { - BezierCurve, - Breakpoint, - DragAttribute, - ElementWidth, - MeetingControlBarEnum -} from '../types/constEnums' +import {BezierCurve, DragAttribute, ElementWidth, MeetingControlBarEnum} from '../types/constEnums' import getNextSortOrder from '../utils/getNextSortOrder' import {SwipeColumn} from './GroupingKanban' import GroupingKanbanColumnHeader from './GroupingKanbanColumnHeader' import ReflectionGroup from './ReflectionGroup/ReflectionGroup' -const Column = styled('div')<{ - isLengthExpanded: boolean - isFirstColumn: boolean - isLastColumn: boolean -}>(({isLengthExpanded, isFirstColumn, isLastColumn}) => ({ - alignContent: 'flex-start', - background: PALETTE.SLATE_300, - borderRadius: 8, - display: 'flex', - flex: 1, - flexDirection: 'column', - height: '100%', - minWidth: ElementWidth.REFLECTION_COLUMN, - padding: 0, - position: 'relative', - transition: `all 100ms ${BezierCurve.DECELERATE}`, - [makeMinWidthMediaQuery(Breakpoint.SINGLE_REFLECTION_COLUMN)]: { - height: isLengthExpanded ? '100%' : `calc(100% - ${MeetingControlBarEnum.HEIGHT}px)`, - margin: `0 ${isLastColumn ? 16 : 8}px 0px ${isFirstColumn ? 16 : 8}px`, - maxWidth: 'min-content' - } -})) - const ColumnScrollContainer = styled('div')({ display: 'flex', // must hide X on firefox v84 @@ -160,8 +129,6 @@ const GroupingKanbanColumn = (props: Props) => { const isLengthExpanded = useCoverable(promptId, columnRef, MeetingControlBarEnum.HEIGHT, phaseRef, columnsRef) || !!endedAt - const isFirstColumn = prompt.sortOrder === 0 - const isLastColumn = Math.round(prompt.sortOrder) === reflectPromptsCount - 1 const groups = useDeepEqual(reflectionGroups) // group may be undefined because relay could GC before useMemo in the Kanban recomputes >:-( const filteredReflectionGroups = useMemo( @@ -188,12 +155,13 @@ const GroupingKanbanColumn = (props: Props) => { submitMutation() CreateReflectionMutation(atmosphere, {input}, {onError, onCompleted}) } - return ( - @@ -240,7 +208,7 @@ const GroupingKanbanColumn = (props: Props) => { ) })} - + ) } diff --git a/packages/client/modules/meeting/components/AddTemplatePrompt.tsx b/packages/client/modules/meeting/components/AddTemplatePrompt.tsx index 1e97063dbe1..886dae9fdd2 100644 --- a/packages/client/modules/meeting/components/AddTemplatePrompt.tsx +++ b/packages/client/modules/meeting/components/AddTemplatePrompt.tsx @@ -8,7 +8,7 @@ import {Threshold} from '~/types/constEnums' import {AddTemplatePrompt_prompts$key} from '../../../__generated__/AddTemplatePrompt_prompts.graphql' import LinkButton from '../../../components/LinkButton' import AddReflectTemplatePromptMutation from '../../../mutations/AddReflectTemplatePromptMutation' -import dndNoise from '../../../utils/dndNoise' +import {positionAfter} from '../../../shared/sortOrder' import withMutationProps, {WithMutationProps} from '../../../utils/relay/withMutationProps' const AddPromptLink = styled(LinkButton)({ @@ -51,8 +51,8 @@ const AddTemplatePrompt = (props: Props) => { const {templateId, onError, onCompleted, submitMutation, submitting} = props if (submitting) return submitMutation() - const sortOrders = prompts.map(({sortOrder}) => sortOrder) - const sortOrder = Math.max(0, ...sortOrders) + 1 + dndNoise() + const lastPrompt = prompts.at(-1)! + const sortOrder = positionAfter(lastPrompt.sortOrder) const promptCount = prompts.length AddReflectTemplatePromptMutation( atmosphere, diff --git a/packages/client/mutations/AddReflectTemplatePromptMutation.ts b/packages/client/mutations/AddReflectTemplatePromptMutation.ts index 9daa5ec6a5f..5b92751cfdd 100644 --- a/packages/client/mutations/AddReflectTemplatePromptMutation.ts +++ b/packages/client/mutations/AddReflectTemplatePromptMutation.ts @@ -8,7 +8,7 @@ import handleAddReflectTemplatePrompt from './handlers/handleAddReflectTemplateP interface Handlers extends BaseLocalHandlers { promptCount: number - sortOrder: number + sortOrder: string } graphql` From 8abce476ac3cb9f87d4966a2e5367525ddc4f49d Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 11 Sep 2024 11:26:44 -0700 Subject: [PATCH 23/89] chore: migrate PushInvitation Signed-off-by: Matt Krick --- packages/server/database/rethinkDriver.ts | 5 -- .../server/database/types/PushInvitation.ts | 26 ------- .../graphql/mutations/denyPushInvitation.ts | 33 ++++---- .../graphql/mutations/pushInvitation.ts | 26 ++++--- .../private/mutations/hardDeleteUser.ts | 1 - .../1726078121329_PushInvitation.ts | 75 +++++++++++++++++++ .../safeMutations/acceptTeamInvitation.ts | 6 +- 7 files changed, 113 insertions(+), 59 deletions(-) delete mode 100644 packages/server/database/types/PushInvitation.ts create mode 100644 packages/server/postgres/migrations/1726078121329_PushInvitation.ts diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index 0a9e17ac7ce..0776e9efdad 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -14,7 +14,6 @@ import NotificationResponseReplied from './types/NotificationResponseReplied' import NotificationTaskInvolves from './types/NotificationTaskInvolves' import NotificationTeamArchived from './types/NotificationTeamArchived' import NotificationTeamInvitation from './types/NotificationTeamInvitation' -import PushInvitation from './types/PushInvitation' import Task from './types/Task' export type RethinkSchema = { @@ -53,10 +52,6 @@ export type RethinkSchema = { | NotificationMentioned index: 'userId' } - PushInvitation: { - type: PushInvitation - index: 'userId' - } Task: { type: Task index: diff --git a/packages/server/database/types/PushInvitation.ts b/packages/server/database/types/PushInvitation.ts deleted file mode 100644 index de742d0aafe..00000000000 --- a/packages/server/database/types/PushInvitation.ts +++ /dev/null @@ -1,26 +0,0 @@ -import generateUID from '../../generateUID' - -interface Input { - id?: string - userId: string - teamId: string - denialCount?: number - lastDenialAt?: Date -} - -export default class PushInvitation { - id: string - userId: string - teamId: string - denialCount: number - lastDenialAt?: Date - - constructor(input: Input) { - const {id, userId, teamId, denialCount, lastDenialAt} = input - this.id = id || generateUID() - this.userId = userId - this.teamId = teamId - this.denialCount = denialCount || 0 - this.lastDenialAt = lastDenialAt || undefined - } -} diff --git a/packages/server/graphql/mutations/denyPushInvitation.ts b/packages/server/graphql/mutations/denyPushInvitation.ts index f0868266627..aa3e7e3dea0 100644 --- a/packages/server/graphql/mutations/denyPushInvitation.ts +++ b/packages/server/graphql/mutations/denyPushInvitation.ts @@ -1,7 +1,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' +import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import PushInvitation from '../../database/types/PushInvitation' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -29,9 +29,8 @@ export default { {userId, teamId}: {userId: string; teamId: string}, {authToken, socketId: mutatorId}: GQLContext ) => { - const r = await getRethink() + const pg = getKysely() const viewerId = getUserId(authToken) - const now = new Date() // AUTH if (!isTeamMember(authToken, teamId)) { @@ -39,23 +38,27 @@ export default { } // VALIDATION - const teamBlacklist = (await r - .table('PushInvitation') - .getAll(userId, {index: 'userId'}) - .filter({teamId}) - .nth(0) - .run()) as PushInvitation | null + const teamBlacklist = await pg + .selectFrom('PushInvitation') + .selectAll() + .where('userId', '=', userId) + .where('teamId', '=', teamId) + .limit(1) + .executeTakeFirst() if (!teamBlacklist) { return standardError(new Error('User did not request push invitation'), {userId: viewerId}) } // RESOLUTION - await r - .table('PushInvitation') - .get(teamBlacklist.id) - .update({denialCount: teamBlacklist.denialCount + 1, lastDenialAt: now}) - .run() + await pg + .updateTable('PushInvitation') + .set((eb) => ({ + denialCount: eb('denialCount', '+', 1), + lastDenialAt: sql`CURRENT_TIMESTAMP` + })) + .where('id', '=', teamBlacklist.id) + .execute() const data = {teamId, userId} publish(SubscriptionChannel.TEAM, teamId, 'DenyPushInvitationPayload', data, {mutatorId}) diff --git a/packages/server/graphql/mutations/pushInvitation.ts b/packages/server/graphql/mutations/pushInvitation.ts index 3638b0f9a8d..967b7dcb55f 100644 --- a/packages/server/graphql/mutations/pushInvitation.ts +++ b/packages/server/graphql/mutations/pushInvitation.ts @@ -1,8 +1,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import ms from 'ms' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import PushInvitation from '../../database/types/PushInvitation' +import getKysely from '../../postgres/getKysely' import {getUserId, isAuthenticated} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -36,7 +35,7 @@ export default { {meetingId, teamId}: {meetingId?: string | null; teamId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { - const r = await getRethink() + const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -56,10 +55,12 @@ export default { if (approvalError instanceof Error) { return {error: {message: approvalError.message}} } - const pushInvitations = (await r - .table('PushInvitation') - .getAll(viewerId, {index: 'userId'}) - .run()) as PushInvitation[] + const pushInvitations = await pg + .selectFrom('PushInvitation') + .selectAll() + .where('userId', '=', viewerId) + .execute() + const teamPushInvitation = pushInvitations.find((row) => row.teamId === teamId) if (teamPushInvitation) { const {denialCount, lastDenialAt} = teamPushInvitation @@ -82,10 +83,13 @@ export default { // RESOLUTION if (!teamPushInvitation) { // create a row so we know there was a request so denials are substantiated - await r - .table('PushInvitation') - .insert(new PushInvitation({userId: viewerId, teamId})) - .run() + await pg + .insertInto('PushInvitation') + .values({ + userId: viewerId, + teamId + }) + .execute() } const data = {userId: viewerId, teamId, meetingId} diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 6c19edc395b..2c9bd2e58c0 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -93,7 +93,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .getAll(r.args(teamIds), {index: 'teamId'}) .filter((row: RValue) => row('createdBy').eq(userIdToDelete)) .delete(), - pushInvitation: r.table('PushInvitation').getAll(userIdToDelete, {index: 'userId'}).delete(), invitedByTeamInvitation: r .table('TeamInvitation') .getAll(r.args(teamIds), {index: 'teamId'}) diff --git a/packages/server/postgres/migrations/1726078121329_PushInvitation.ts b/packages/server/postgres/migrations/1726078121329_PushInvitation.ts new file mode 100644 index 00000000000..aedaeda9c87 --- /dev/null +++ b/packages/server/postgres/migrations/1726078121329_PushInvitation.ts @@ -0,0 +1,75 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "PushInvitation" ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "userId" VARCHAR(100) NOT NULL, + "teamId" VARCHAR(100) NOT NULL, + "denialCount" SMALLINT NOT NULL DEFAULT 0, + "lastDenialAt" TIMESTAMP WITH TIME ZONE, + CONSTRAINT "fk_userId" + FOREIGN KEY("userId") + REFERENCES "User"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE SET NULL + ); + CREATE INDEX IF NOT EXISTS "idx_PushInvitation_userId" ON "PushInvitation"("userId"); + CREATE INDEX IF NOT EXISTS "idx_PushInvitation_teamId" ON "PushInvitation"("teamId"); + END $$; +`.execute(pg) + + const rRequests = await r.table('PushInvitation').coerceTo('array').run() + + await Promise.all( + rRequests.map(async (row) => { + const {userId, teamId, denialCount, lastDenialAt} = row + try { + return await pg + .insertInto('PushInvitation') + .values({ + userId, + teamId, + denialCount, + lastDenialAt + }) + .execute() + } catch (e) { + if (e.constraint === 'fk_teamId') { + console.log(`Skipping ${row.id} because it has no valid teamId`) + return + } + if (e.constraint === 'fk_userId') { + console.log(`Skipping ${row.id} because it has no valid userId`) + return + } + console.log(e, row) + } + }) + ) +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "PushInvitation"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index ec91003e8c2..63171c108eb 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -130,7 +130,11 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data } // if accepted to team, don't count it towards the global denial count - await r.table('PushInvitation').getAll(userId, {index: 'userId'}).filter({teamId}).delete().run() + await pg + .deleteFrom('PushInvitation') + .where('userId', '=', userId) + .where('teamId', '=', teamId) + .execute() return { teamLeadUserIdWithNewActions, invitationNotificationIds: invitationNotificationIds as string[] From c30e68fee3a8a23af159e04ddef82314e1aa5553 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 11 Sep 2024 14:47:40 -0700 Subject: [PATCH 24/89] fix test Signed-off-by: Matt Krick --- .../__tests__/processRecurrence.test.ts | 376 +++++++++++------- .../server/database/types/DiscussPhase.ts | 3 +- .../server/database/types/ReflectPhase.ts | 3 +- .../types/TeamPromptResponsesPhase.ts | 3 +- .../1726079837618_NewMeeting-phase1.ts | 99 +++++ 5 files changed, 330 insertions(+), 154 deletions(-) create mode 100644 packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts diff --git a/packages/server/__tests__/processRecurrence.test.ts b/packages/server/__tests__/processRecurrence.test.ts index 948fc11b8fa..fb64965eafd 100644 --- a/packages/server/__tests__/processRecurrence.test.ts +++ b/packages/server/__tests__/processRecurrence.test.ts @@ -2,13 +2,11 @@ import dayjs from 'dayjs' import ms from 'ms' import TeamMemberId from 'parabol-client/shared/gqlIds/TeamMemberId' import {toDateTime} from '../../client/shared/rruleUtil' -import getRethink from '../database/rethinkDriver' import DiscussPhase from '../database/types/DiscussPhase' -import MeetingRetrospective from '../database/types/MeetingRetrospective' -import MeetingTeamPrompt from '../database/types/MeetingTeamPrompt' import ReflectPhase from '../database/types/ReflectPhase' import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' import generateUID from '../generateUID' +import getKysely from '../postgres/getKysely' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../postgres/queries/insertMeetingSeries' import {getUserTeams, sendIntranet, signUp} from './common' @@ -60,22 +58,28 @@ const assertIdempotency = async () => { } test('Should not end meetings that are not scheduled to end', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] const teamMemberId = TeamMemberId.join(teamId, userId) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?' - }) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: [phase], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -93,28 +97,38 @@ test('Should not end meetings that are not scheduled to end', async () => { await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeFalsy() }) test('Should not end meetings that are scheduled to end in the future', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] const teamMemberId = TeamMemberId.join(teamId, userId) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() + ms('5m')) - }) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: [phase], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + scheduledEndTime: new Date(Date.now() + ms('5m')), + facilitatorStageId: phase.stages[0]?.id + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -132,30 +146,39 @@ test('Should not end meetings that are scheduled to end in the future', async () await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeFalsy() - - await r.table('NewMeeting').get(meetingId).delete().run() + await pg.deleteFrom('NewMeeting').where('id', '=', meetingId).execute() }) test('Should end meetings that are scheduled to end in the past', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] const teamMemberId = TeamMemberId.join(teamId, userId) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('5m')) - }) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: [phase], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + scheduledEndTime: new Date(Date.now() - ms('5m')), + facilitatorStageId: phase.stages[0]?.id + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -173,12 +196,16 @@ test('Should end meetings that are scheduled to end in the past', async () => { await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }, 10000) test('Should end the current team prompt meeting and start a new meeting', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] const teamMemberId = TeamMemberId.join(teamId, userId) @@ -197,22 +224,27 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('5m')), - meetingSeriesId - }) - - // The last meeting in the series was created just over 24h ago, so the next one should start - // soon. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: [phase], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + scheduledEndTime: new Date(Date.now() - ms('5m')), + facilitatorStageId: phase.stages[0]?.id, + meetingSeriesId, + // The last meeting in the series was created just over 24h ago, so the next one should start + // soon. + createdAt: new Date(Date.now() - ms('25h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -230,15 +262,20 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() - const lastMeeting = await r - .table('NewMeeting') - .filter({meetingType: 'teamPrompt', meetingSeriesId}) - .orderBy(r.desc('createdAt')) - .nth(0) - .run() + const lastMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('meetingType', '=', 'teamPrompt') + .orderBy('createdAt desc') + .limit(1) + .executeTakeFirst() expect(lastMeeting).toMatchObject({ name: expect.stringMatching(/Daily Test Standup.*/), @@ -247,7 +284,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) test('Should end the current retro meeting and start a new meeting', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] @@ -267,26 +304,31 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingRetrospective({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new ReflectPhase(teamId, []), new DiscussPhase(undefined)], - facilitatorUserId: userId, - scheduledEndTime: new Date(Date.now() - ms('5m')), - meetingSeriesId, - templateId: 'startStopContinueTemplate', - disableAnonymity: false, - totalVotes: 5, - name: '', - maxVotesPerGroup: 5 - }) - - // The last meeting in the series was created just over 24h ago, so the next one should start - // soon. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - - await r.table('NewMeeting').insert(meeting).run() + const phases = [new ReflectPhase(teamId, []), new DiscussPhase(undefined)] + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases, + facilitatorUserId: userId, + meetingType: 'retrospective', + scheduledEndTime: new Date(Date.now() - ms('5m')), + facilitatorStageId: phases[0]?.stages[0]?.id, + meetingSeriesId, + templateId: 'startStopContinueTemplate', + disableAnonymity: false, + totalVotes: 5, + name: '', + maxVotesPerGroup: 5, + meetingPrompt: 'What are you working on today? Stuck on anything?', + // The last meeting in the series was created just over 24h ago, so the next one should start + // soon. + createdAt: new Date(Date.now() - ms('25h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -304,15 +346,20 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() - const lastMeeting = await r - .table('NewMeeting') - .filter({meetingType: 'retrospective', meetingSeriesId}) - .orderBy(r.desc('createdAt')) - .nth(0) - .run() + const lastMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('meetingType', '=', 'teamPrompt') + .orderBy('createdAt desc') + .limit(1) + .executeTakeFirst() expect(lastMeeting).toMatchObject({ meetingSeriesId @@ -320,7 +367,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) test('Should only start a new meeting if it would still be active', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] const teamMemberId = TeamMemberId.join(teamId, userId) @@ -340,23 +387,28 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('73h')), - meetingSeriesId: newMeetingSeriesId - }) - - // The last meeting in the series was created just over 72h ago, so 3 meetings should have started - // since then, but only 1 meeting should start as a result of the mutation. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('73h')) - meeting.endedAt = new Date(Date.now() - ms('49h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: [phase], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id, + scheduledEndTime: new Date(Date.now() - ms('73h')), + meetingSeriesId: newMeetingSeriesId, + // The last meeting in the series was created just over 72h ago, so 3 meetings should have started + // since then, but only 1 meeting should start as a result of the mutation. + createdAt: new Date(Date.now() - ms('73h')), + endedAt: new Date(Date.now() - ms('49h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -374,12 +426,16 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }, 10000) test('Should not start a new meeting if the rrule has not started', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] const teamMemberId = TeamMemberId.join(teamId, userId) @@ -399,23 +455,28 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('1h')), - meetingSeriesId: newMeetingSeriesId - }) - - // The last meeting in the series was created just over 24h ago, but the active rrule doesn't - // start until tomorrow. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - meeting.endedAt = new Date(Date.now() - ms('1h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: [phase], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id, + scheduledEndTime: new Date(Date.now() - ms('1h')), + meetingSeriesId: newMeetingSeriesId, + // The last meeting in the series was created just over 24h ago, but the active rrule doesn't + // start until tomorrow. + createdAt: new Date(Date.now() - ms('25h')), + endedAt: new Date(Date.now() - ms('1h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -433,12 +494,16 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }) test('Should not hang if the rrule interval is invalid', async () => { - const r = await getRethink() + const pg = getKysely() const {userId} = await signUp() const {id: teamId} = (await getUserTeams(userId))[0] const teamMemberId = TeamMemberId.join(teamId, userId) @@ -458,22 +523,27 @@ RRULE:FREQ=WEEKLY;INTERVAL=NaN;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('5m')), - meetingSeriesId: newMeetingSeriesId - }) - - // The last meeting in the series was created just over 24h ago, so the next one should start soon - // but the rrule is invalid, so it won't happen - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: [phase], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id, + scheduledEndTime: new Date(Date.now() - ms('5m')), + meetingSeriesId: newMeetingSeriesId, + // The last meeting in the series was created just over 24h ago, so the next one should start soon + // but the rrule is invalid, so it won't happen + createdAt: new Date(Date.now() - ms('25h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -491,6 +561,10 @@ RRULE:FREQ=WEEKLY;INTERVAL=NaN;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }) diff --git a/packages/server/database/types/DiscussPhase.ts b/packages/server/database/types/DiscussPhase.ts index b52bb537e3f..c37aa5f1019 100644 --- a/packages/server/database/types/DiscussPhase.ts +++ b/packages/server/database/types/DiscussPhase.ts @@ -2,7 +2,8 @@ import DiscussStage from './DiscussStage' import GenericMeetingPhase from './GenericMeetingPhase' export default class DiscussPhase extends GenericMeetingPhase { - stages: [DiscussStage, ...DiscussStage[]] + stages: [DiscussStage, ...DiscussStage[]]; + [k: string]: any constructor(durations: number[] | undefined) { super('discuss') this.stages = [new DiscussStage({sortOrder: 0, durations, reflectionGroupId: ''})] diff --git a/packages/server/database/types/ReflectPhase.ts b/packages/server/database/types/ReflectPhase.ts index 4de28d532c2..f5f04ad3399 100644 --- a/packages/server/database/types/ReflectPhase.ts +++ b/packages/server/database/types/ReflectPhase.ts @@ -4,7 +4,8 @@ import GenericMeetingStage from './GenericMeetingStage' export default class ReflectPhase extends GenericMeetingPhase { stages: [GenericMeetingStage, ...GenericMeetingStage[]] - focusedPromptId?: string + focusedPromptId?: string; + [k: string]: any constructor( public teamId: string, diff --git a/packages/server/database/types/TeamPromptResponsesPhase.ts b/packages/server/database/types/TeamPromptResponsesPhase.ts index c5d1e00ef0b..c1bc523f5b1 100644 --- a/packages/server/database/types/TeamPromptResponsesPhase.ts +++ b/packages/server/database/types/TeamPromptResponsesPhase.ts @@ -3,7 +3,8 @@ import TeamPromptResponseStage from './TeamPromptResponseStage' export default class TeamPromptResponsesPhase extends GenericMeetingPhase { stages: [TeamPromptResponseStage, ...TeamPromptResponseStage[]] - phaseType!: 'RESPONSES' + phaseType!: 'RESPONSES'; + [k: string]: any constructor(teamMemberIds: string[]) { super('RESPONSES') diff --git a/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts new file mode 100644 index 00000000000..2e5def71db7 --- /dev/null +++ b/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts @@ -0,0 +1,99 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + + // Notable changes + // - SlackTs is now a double precision + // - facilitatorUserId is nullable in the case of a user hard delete + // - hasScheduledEndTime index changed to scheduledEndTime + + await client.query(` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "NewMeeting" ( + "id" VARCHAR(100) PRIMARY KEY, + "isLegacy" BOOLEAN NOT NULL DEFAULT FALSE, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "createdBy" VARCHAR(100), + "endedAt" TIMESTAMP WITH TIME ZONE, + "facilitatorStageId" VARCHAR(100), + "facilitatorUserId" VARCHAR(100), + "meetingCount" INT NOT NULL, + "meetingNumber" INT NOT NULL, + "name" VARCHAR(100) NOT NULL, + "summarySentAt" TIMESTAMP WITH TIME ZONE, + "teamId" VARCHAR(100) NOT NULL, + "meetingType" "MeetingTypeEnum" NOT NULL, + "phases" JSONB NOT NULL, + "showConversionModal" BOOLEAN NOT NULL DEFAULT FALSE, + "meetingSeriesId" INT, + "scheduledEndTime" TIMESTAMP WITH TIME ZONE, + "summary" VARCHAR(10000), + "sentimentScore" DOUBLE PRECISION, + "usedReactjis" JSONB, + "slackTs" DOUBLE PRECISION, + "engagement" DOUBLE PRECISION, + "totalVotes" INT, + "maxVotesPerGroup" SMALLINT, + "disableAnonymity" BOOLEAN, + "commentCount" INT, + "taskCount" INT, + "agendaItemCount" INT, + "storyCount" INT, + "templateId" VARCHAR(100), + "topicCount" INT, + "reflectionCount" INT, + "transcription" JSONB, + "recallBotId" VARCHAR(255), + "videoMeetingURL" VARCHAR(2048), + "autogroupReflectionGroups" JSONB, + "resetReflectionGroups" JSONB, + "templateRefId" VARCHAR(25), + "meetingPrompt" VARCHAR(255), + CONSTRAINT "fk_createdBy" + FOREIGN KEY("createdBy") + REFERENCES "User"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_facilitatorUserId" + FOREIGN KEY("facilitatorUserId") + REFERENCES "User"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_meetingSeriesId" + FOREIGN KEY("meetingSeriesId") + REFERENCES "MeetingSeries"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_templateId" + FOREIGN KEY("templateId") + REFERENCES "MeetingTemplate"("id") + ON DELETE SET NULL + ); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_createdAt" ON "NewMeeting"("createdAt"); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_facilitatorUserId" ON "NewMeeting"("facilitatorUserId"); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_scheduledEndTime" ON "NewMeeting"("scheduledEndTime"); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_meetingSeriesId" ON "NewMeeting"("meetingSeriesId"); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_teamId" ON "NewMeeting"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_templateId" ON "NewMeeting"("templateId"); + DROP TRIGGER IF EXISTS "update_NewMeeting_updatedAt" ON "NewMeeting"; + CREATE TRIGGER "update_NewMeeting_updatedAt" BEFORE UPDATE ON "NewMeeting" FOR EACH ROW EXECUTE PROCEDURE "set_updatedAt"(); + END $$; +`) + + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "NewMeeting"; + ` /* Do undo magic */) + await client.end() +} From feb72b82f7e3d711a6b076cd77d9ff0b39e59269 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 12 Sep 2024 12:13:57 -0700 Subject: [PATCH 25/89] add types Signed-off-by: Matt Krick --- codegen.json | 10 +- .../modules/demo/ClientGraphQLServer.ts | 19 +- packages/client/modules/demo/initDB.ts | 2 +- packages/client/types/generics.ts | 4 + packages/client/utils/meetings/lookups.ts | 2 +- .../indexing/retrospectiveDiscussionTopic.ts | 3 +- .../helpers/publishSimilarRetroTopics.ts | 3 +- .../__tests__/processRecurrence.test.ts | 18 +- .../server/database/types/DiscussPhase.ts | 3 +- .../database/types/GenericMeetingStage.ts | 14 +- packages/server/database/types/Meeting.ts | 35 +-- .../server/database/types/MeetingAction.ts | 10 +- .../server/database/types/MeetingPoker.ts | 11 +- .../database/types/MeetingRetrospective.ts | 45 ++-- .../database/types/MeetingTeamPrompt.ts | 9 +- .../server/database/types/ReflectPhase.ts | 3 +- .../email/newMeetingSummaryEmailCreator.tsx | 4 +- .../server/graphql/meetingTypePredicates.ts | 13 -- .../server/graphql/mutations/endCheckIn.ts | 22 +- .../graphql/mutations/endRetrospective.ts | 4 +- .../graphql/mutations/endSprintPoker.ts | 18 +- .../addAgendaItemToActiveActionMeeting.ts | 3 +- .../mutations/helpers/addDiscussionTopics.ts | 4 +- .../mutations/helpers/calculateEngagement.ts | 7 +- .../mutations/helpers/collectReactjis.ts | 7 +- .../helpers/createNewMeetingPhases.ts | 5 +- .../endMeeting/sendNewMeetingSummary.ts | 4 +- .../mutations/helpers/gatherInsights.ts | 4 +- .../helpers/generateDiscussionSummary.ts | 6 +- .../mutations/helpers/generateGroups.ts | 3 +- .../helpers/generateStandupMeetingSummary.ts | 6 +- .../mutations/helpers/handleCompletedStage.ts | 9 +- .../helpers/notifications/MSTeamsNotifier.ts | 5 +- .../notifications/MattermostNotifier.ts | 15 +- .../NotificationIntegrationHelper.ts | 14 +- .../helpers/notifications/Notifier.ts | 12 +- .../helpers/notifications/SlackNotifier.ts | 29 ++- .../helpers/notifications/getSummaryText.ts | 31 +-- .../mutations/helpers/pushEstimateToGitHub.ts | 7 +- .../helpers/removeEmptyReflections.ts | 4 +- .../helpers/removeStagesFromMeetings.ts | 2 +- .../helpers/safeCreateRetrospective.ts | 7 +- .../mutations/helpers/safeCreateTeamPrompt.ts | 4 +- .../mutations/helpers/safeEndRetrospective.ts | 12 +- .../mutations/helpers/safeEndTeamPrompt.ts | 8 +- .../helpers/sendPokerMeetingRevoteEvent.ts | 4 +- .../removeReflectionFromGroup.ts | 13 +- .../server/graphql/mutations/joinMeeting.ts | 7 +- .../mutations/pokerAnnounceDeckHover.ts | 6 +- .../graphql/mutations/pokerResetDimension.ts | 6 +- .../graphql/mutations/pokerRevealVotes.ts | 6 +- .../resetRetroMeetingToGroupStage.ts | 7 +- .../graphql/mutations/setTaskEstimate.ts | 5 +- .../graphql/mutations/startSprintPoker.ts | 7 +- .../updateAzureDevOpsDimensionField.ts | 6 +- .../mutations/updateGitHubDimensionField.ts | 6 +- .../graphql/mutations/updatePokerScope.ts | 13 +- .../graphql/mutations/updateRetroMaxVotes.ts | 4 +- .../graphql/mutations/voteForPokerStory.ts | 11 +- .../mutations/voteForReflectionGroup.ts | 4 +- .../mutations/generateMeetingSummary.ts | 6 +- .../private/mutations/processRecurrence.ts | 13 +- .../private/mutations/runScheduledJobs.ts | 4 +- .../types/GenerateMeetingSummarySuccess.ts | 9 +- .../public/mutations/addTranscriptionBot.ts | 4 +- .../graphql/public/mutations/endTeamPrompt.ts | 4 +- .../public/mutations/helpers/getSummaries.ts | 4 +- .../public/mutations/helpers/getTopics.ts | 4 +- .../public/mutations/resetReflectionGroups.ts | 2 +- .../public/mutations/setTeamHealthVote.ts | 32 +++ .../graphql/public/mutations/startCheckIn.ts | 7 +- .../mutations/updateGitLabDimensionField.ts | 6 +- .../mutations/updateJiraDimensionField.ts | 6 +- .../updateJiraServerDimensionField.ts | 6 +- .../public/mutations/updateMeetingTemplate.ts | 4 +- .../mutations/updateRecurrenceSettings.ts | 6 +- .../types/AddTranscriptionBotSuccess.ts | 4 +- .../graphql/public/types/AutogroupSuccess.ts | 4 +- .../server/graphql/public/types/Discussion.ts | 8 +- .../public/types/EndTeamPromptSuccess.ts | 5 +- .../graphql/public/types/EstimateStage.ts | 10 +- .../public/types/GenerateGroupsSuccess.ts | 4 +- .../public/types/GenerateInsightSuccess.ts | 4 +- .../server/graphql/public/types/NewMeeting.ts | 2 +- .../public/types/NotifyResponseMentioned.ts | 4 +- .../public/types/NotifyResponseReplied.ts | 4 +- .../graphql/public/types/ReflectPhase.ts | 4 +- .../types/ResetReflectionGroupsSuccess.ts | 4 +- .../graphql/public/types/RetroDiscussStage.ts | 4 +- .../graphql/public/types/RetroReflection.ts | 4 +- .../public/types/RetroReflectionGroup.ts | 4 +- .../public/types/StartCheckInSuccess.ts | 7 +- .../public/types/StartRetrospectiveSuccess.ts | 4 +- .../public/types/StartTeamPromptSuccess.ts | 5 +- .../graphql/public/types/TeamPromptMeeting.ts | 6 +- .../types/UpdateDimensionFieldSuccess.ts | 4 +- .../types/UpdateMeetingPromptSuccess.ts | 5 +- .../types/UpdateRecurrenceSettingsSuccess.ts | 5 +- packages/server/graphql/resolvers.ts | 3 +- .../graphql/types/SetPhaseFocusPayload.ts | 4 +- .../1726079837618_NewMeeting-phase1.ts | 2 +- packages/server/postgres/select.ts | 54 ++++- packages/server/postgres/types/Meeting.d.ts | 95 +++++++- .../postgres/types/NewMeetingPhase.d.ts | 204 ++++++++++++++++++ packages/server/postgres/types/index.d.ts | 14 ++ .../server/utils/RecallAIServerManager.ts | 2 +- packages/server/utils/analytics/analytics.ts | 20 +- packages/server/utils/analytics/helpers.ts | 7 +- packages/server/utils/getPhase.ts | 31 +-- 109 files changed, 786 insertions(+), 437 deletions(-) create mode 100644 packages/server/postgres/types/NewMeetingPhase.d.ts diff --git a/codegen.json b/codegen.json index 39b03e6f474..14b74d7dc35 100644 --- a/codegen.json +++ b/codegen.json @@ -26,7 +26,7 @@ "PingableServices": "./types/PingableServices#PingableServicesSource", "ProcessRecurrenceSuccess": "./types/ProcessRecurrenceSuccess#ProcessRecurrenceSuccessSource", "RemoveAuthIdentitySuccess": "./types/RemoveAuthIdentitySuccess#RemoveAuthIdentitySuccessSource", - "RetrospectiveMeeting": "../../database/types/MeetingRetrospective#default", + "RetrospectiveMeeting": "../../postgres/types/Meeting#RetrospectiveMeeting", "SAML": "./types/SAML#SAMLSource", "SetIsFreeMeetingTemplateSuccess": "./types/SetIsFreeMeetingTemplateSuccess#SetIsFreeMeetingTemplateSuccessSource", "SignupsPayload": "./types/SignupsPayload#SignupsPayloadSource", @@ -73,7 +73,7 @@ "EndTeamPromptSuccess": "./types/EndTeamPromptSuccess#EndTeamPromptSuccessSource", "AcceptRequestToJoinDomainSuccess": "./types/AcceptRequestToJoinDomainSuccess#AcceptRequestToJoinDomainSuccessSource", "AcceptTeamInvitationPayload": "./types/AcceptTeamInvitationPayload#AcceptTeamInvitationPayloadSource", - "ActionMeeting": "../../database/types/MeetingAction#default", + "ActionMeeting": "../../postgres/types/Meeting#CheckInMeeting", "ActionMeetingMember": "../../database/types/ActionMeetingMember#default as ActionMeetingMemberDB", "AddApprovedOrganizationDomainsSuccess": "./types/AddApprovedOrganizationDomainsSuccess#AddApprovedOrganizationDomainsSuccessSource", "AddPokerTemplateSuccess": "./types/AddPokerTemplateSuccess#AddPokerTemplateSuccessSource", @@ -143,7 +143,7 @@ "Threadable": "./types/Threadable#ThreadableSource", "OrgIntegrationProviders": "./types/OrgIntegrationProviders#OrgIntegrationProvidersSource", "OrganizationUser": "../../postgres/types/index#OrganizationUser as OrganizationUserDB", - "PokerMeeting": "../../database/types/MeetingPoker#default as MeetingPoker", + "PokerMeeting": "../../postgres/types/Meeting#PokerMeeting", "PokerMeetingMember": "../../database/types/MeetingPokerMeetingMember#default as PokerMeetingMemberDB", "PokerTemplate": "../../database/types/PokerTemplate#default as PokerTemplateDB", "RRule": "rrule-rust#RRuleSet", @@ -159,7 +159,7 @@ "ResetReflectionGroupsSuccess": "./types/ResetReflectionGroupsSuccess#ResetReflectionGroupsSuccessSource", "RetroReflection": "../../postgres/types/index#RetroReflection as RetroReflectionDB", "RetroReflectionGroup": "./types/RetroReflectionGroup#RetroReflectionGroupSource", - "RetrospectiveMeeting": "../../database/types/MeetingRetrospective#default", + "RetrospectiveMeeting": "../../postgres/types/Meeting#RetrospectiveMeeting", "RetrospectiveMeetingMember": "../../database/types/RetroMeetingMember#default", "SAML": "./types/SAML#SAMLSource", "SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource", @@ -182,7 +182,7 @@ "TeamMemberIntegrationAuthOAuth1": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrations": "./types/TeamMemberIntegrations#TeamMemberIntegrationsSource", - "TeamPromptMeeting": "../../database/types/MeetingTeamPrompt#default as MeetingTeamPromptDB", + "TeamPromptMeeting": "../../postgres/types/Meeting#TeamPromptMeeting", "TeamPromptMeetingMember": "../../database/types/TeamPromptMeetingMember#default as TeamPromptMeetingMemberDB", "TeamPromptResponse": "../../postgres/types/index#TeamPromptResponse as TeamPromptResponseDB", "TemplateDimension": "../../postgres/types/index#TemplateDimension as TemplateDimensionDB", diff --git a/packages/client/modules/demo/ClientGraphQLServer.ts b/packages/client/modules/demo/ClientGraphQLServer.ts index e7ea44c8170..4be77ecd626 100644 --- a/packages/client/modules/demo/ClientGraphQLServer.ts +++ b/packages/client/modules/demo/ClientGraphQLServer.ts @@ -8,13 +8,15 @@ import stringSimilarity from 'string-similarity' import {ReactableEnum} from '~/__generated__/AddReactjiToReactableMutation.graphql' import {DragReflectionDropTargetTypeEnum} from '~/__generated__/EndDraggingReflectionMutation.graphql' import {PALETTE} from '~/styles/paletteV3' -import DiscussPhase from '../../../server/database/types/DiscussPhase' -import DiscussStage from '../../../server/database/types/DiscussStage' -import NewMeetingPhase from '../../../server/database/types/GenericMeetingPhase' -import NewMeetingStage from '../../../server/database/types/GenericMeetingStage' import GoogleAnalyzedEntity from '../../../server/database/types/GoogleAnalyzedEntity' import ReflectPhase from '../../../server/database/types/ReflectPhase' import ITask from '../../../server/database/types/Task' +import {NewMeetingStage} from '../../../server/graphql/private/resolverTypes' +import { + DiscussPhase, + DiscussStage, + NewMeetingPhase +} from '../../../server/postgres/types/NewMeetingPhase' import { ExternalLinks, MeetingSettingsThreshold, @@ -100,11 +102,7 @@ export type DemoReflectionGroup = { voterIds: string[] } -export type IDiscussPhase = Omit & { - readyToAdvance: any - startAt: string | Date - endAt: string | Date -} +export type IDiscussPhase = DiscussPhase export type IReflectPhase = Omit & { startAt: string | Date @@ -1048,7 +1046,6 @@ class ClientGraphQLServer extends (EventEmitter as GQLDemoEmitter) { reflectionGroupId: newReflectionGroupId, updatedAt: now }) - this.db.newMeeting.nextAutoGroupThreshold = null const nextTitle = getGroupSmartTitle([reflection as DemoReflection]) newReflectionGroup.smartTitle = nextTitle newReflectionGroup.title = nextTitle @@ -1523,7 +1520,7 @@ class ClientGraphQLServer extends (EventEmitter as GQLDemoEmitter) { }, EndRetrospectiveMutation: ({meetingId}: {meetingId: string}, userId: string) => { const phases = this.db.newMeeting.phases as INewMeetingPhase[] - const lastPhase = phases[phases.length - 1] as IDiscussPhase + const lastPhase = phases[phases.length - 1]! const currentStage = lastPhase.stages.find( (stage) => stage.startAt && !stage.endAt ) as IDiscussStage diff --git a/packages/client/modules/demo/initDB.ts b/packages/client/modules/demo/initDB.ts index aa9dfc85899..23e400d75f8 100644 --- a/packages/client/modules/demo/initDB.ts +++ b/packages/client/modules/demo/initDB.ts @@ -1,7 +1,7 @@ import {SlackNotificationEventEnum} from '~/__generated__/SlackNotificationList_viewer.graphql' import {PALETTE} from '~/styles/paletteV3' -import RetrospectiveMeeting from '../../../server/database/types/MeetingRetrospective' import ITask from '../../../server/database/types/Task' +import {RetrospectiveMeeting} from '../../../server/postgres/types/Meeting' import JiraProjectId from '../../shared/gqlIds/JiraProjectId' import demoUserAvatar from '../../styles/theme/images/avatar-user.svg' import {ExternalLinks, MeetingSettingsThreshold, RetroDemo} from '../../types/constEnums' diff --git a/packages/client/types/generics.ts b/packages/client/types/generics.ts index 755054dfa68..0148c9b35be 100644 --- a/packages/client/types/generics.ts +++ b/packages/client/types/generics.ts @@ -32,6 +32,10 @@ type DeepNonNullableObject = { [P in keyof T]-?: DeepNonNullable> } +export type NonNullableProps = { + [K in keyof T]: NonNullable +} + // export type DeepNullableObject = { // [P in keyof T]: T[P] extends Array // ? Array> | null diff --git a/packages/client/utils/meetings/lookups.ts b/packages/client/utils/meetings/lookups.ts index aa327d72d5b..b0a59f46588 100644 --- a/packages/client/utils/meetings/lookups.ts +++ b/packages/client/utils/meetings/lookups.ts @@ -1,6 +1,6 @@ import React from 'react' -import {MeetingTypeEnum} from '~/../server/postgres/types/Meeting' import {NewMeetingPhaseTypeEnum} from '~/__generated__/ActionMeetingSidebar_meeting.graphql' +import {MeetingTypeEnum} from '../../__generated__/SummarySheet_meeting.graphql' import CardsSVG from '../../components/CardsSVG' import {ACTION, POKER, RETROSPECTIVE, TEAM_PROMPT} from '../constants' diff --git a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts index 36eca506754..c962d3bcbdf 100644 --- a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts +++ b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts @@ -1,4 +1,3 @@ -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' @@ -73,7 +72,7 @@ export const createTextFromRetrospectiveDiscussionTopic = async ( dataLoader.get('retroReflectionGroups').load(reflectionGroupId), dataLoader.get('retroReflectionsByGroupId').load(reflectionGroupId) ]) - if (!isMeetingRetrospective(newMeeting)) throw new Error('Meeting is not a retro') + if (newMeeting.meetingType !== 'retrospective') throw new Error('Meeting is not a retro') // It should never be undefined, but our data integrity in RethinkDB is bad const templateId = newMeeting?.templateId ?? '' diff --git a/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts b/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts index 217ee63073b..9e43696e9f3 100644 --- a/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts +++ b/packages/embedder/workflows/helpers/publishSimilarRetroTopics.ts @@ -2,7 +2,6 @@ import {SubscriptionChannel} from '../../../client/types/constEnums' import makeAppURL from '../../../client/utils/makeAppURL' import appOrigin from '../../../server/appOrigin' import {DataLoaderInstance} from '../../../server/dataloader/RootDataLoader' -import {isRetroMeeting} from '../../../server/graphql/meetingTypePredicates' import { buildCommentContentBlock, createAIComment @@ -25,7 +24,7 @@ const makeSimilarDiscussionLink = async ( dataLoader.get('retroReflectionGroups').loadNonNull(reflectionGroupId) ]) - if (!meeting || !isRetroMeeting(meeting)) throw new Error('invalid meeting type') + if (!meeting || meeting.meetingType !== 'retrospective') throw new Error('invalid meeting type') const {phases, name: meetingName} = meeting const {title: topic} = reflectionGroup const discussPhase = getPhase(phases, 'discuss') diff --git a/packages/server/__tests__/processRecurrence.test.ts b/packages/server/__tests__/processRecurrence.test.ts index fb64965eafd..0d1e8e68f53 100644 --- a/packages/server/__tests__/processRecurrence.test.ts +++ b/packages/server/__tests__/processRecurrence.test.ts @@ -72,7 +72,7 @@ test('Should not end meetings that are not scheduled to end', async () => { teamId, meetingCount: 0, meetingNumber: 1, - phases: [phase], + phases: JSON.stringify([phase]), facilitatorUserId: userId, meetingPrompt: 'What are you working on today? Stuck on anything?', name: `Team Prompt #1`, @@ -120,7 +120,7 @@ test('Should not end meetings that are scheduled to end in the future', async () teamId, meetingCount: 0, meetingNumber: 1, - phases: [phase], + phases: JSON.stringify([phase]), facilitatorUserId: userId, meetingPrompt: 'What are you working on today? Stuck on anything?', name: `Team Prompt #1`, @@ -170,7 +170,7 @@ test('Should end meetings that are scheduled to end in the past', async () => { teamId, meetingCount: 0, meetingNumber: 1, - phases: [phase], + phases: JSON.stringify([phase]), facilitatorUserId: userId, meetingPrompt: 'What are you working on today? Stuck on anything?', name: `Team Prompt #1`, @@ -232,7 +232,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` teamId, meetingCount: 0, meetingNumber: 1, - phases: [phase], + phases: JSON.stringify([phase]), facilitatorUserId: userId, meetingPrompt: 'What are you working on today? Stuck on anything?', name: `Team Prompt #1`, @@ -312,11 +312,11 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` teamId, meetingCount: 0, meetingNumber: 1, - phases, + phases: JSON.stringify(phases), facilitatorUserId: userId, meetingType: 'retrospective', scheduledEndTime: new Date(Date.now() - ms('5m')), - facilitatorStageId: phases[0]?.stages[0]?.id, + facilitatorStageId: phases[0]!.stages[0]!.id, meetingSeriesId, templateId: 'startStopContinueTemplate', disableAnonymity: false, @@ -395,7 +395,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` teamId, meetingCount: 0, meetingNumber: 1, - phases: [phase], + phases: JSON.stringify([phase]), facilitatorUserId: userId, meetingPrompt: 'What are you working on today? Stuck on anything?', name: `Team Prompt #1`, @@ -463,7 +463,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` teamId, meetingCount: 0, meetingNumber: 1, - phases: [phase], + phases: JSON.stringify([phase]), facilitatorUserId: userId, meetingPrompt: 'What are you working on today? Stuck on anything?', name: `Team Prompt #1`, @@ -531,7 +531,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=NaN;BYDAY=MO,TU,WE,TH,FR,SA,SU` teamId, meetingCount: 0, meetingNumber: 1, - phases: [phase], + phases: JSON.stringify([phase]), facilitatorUserId: userId, meetingPrompt: 'What are you working on today? Stuck on anything?', name: `Team Prompt #1`, diff --git a/packages/server/database/types/DiscussPhase.ts b/packages/server/database/types/DiscussPhase.ts index c37aa5f1019..b52bb537e3f 100644 --- a/packages/server/database/types/DiscussPhase.ts +++ b/packages/server/database/types/DiscussPhase.ts @@ -2,8 +2,7 @@ import DiscussStage from './DiscussStage' import GenericMeetingPhase from './GenericMeetingPhase' export default class DiscussPhase extends GenericMeetingPhase { - stages: [DiscussStage, ...DiscussStage[]]; - [k: string]: any + stages: [DiscussStage, ...DiscussStage[]] constructor(durations: number[] | undefined) { super('discuss') this.stages = [new DiscussStage({sortOrder: 0, durations, reflectionGroupId: ''})] diff --git a/packages/server/database/types/GenericMeetingStage.ts b/packages/server/database/types/GenericMeetingStage.ts index 016e37d6b93..bfe7e2ae23d 100644 --- a/packages/server/database/types/GenericMeetingStage.ts +++ b/packages/server/database/types/GenericMeetingStage.ts @@ -36,17 +36,17 @@ export interface GenericMeetingStageInput { export default class GenericMeetingStage { id: string - isAsync: boolean | undefined | null + isAsync?: boolean | undefined | null isComplete = false isNavigable: boolean isNavigableByFacilitator: boolean - startAt: Date | undefined - endAt: Date | undefined = undefined - scheduledEndTime: Date | undefined | null - suggestedEndTime: Date | undefined - suggestedTimeLimit: number | undefined + startAt?: Date | undefined + endAt?: Date | undefined = undefined + scheduledEndTime?: Date | undefined | null + suggestedEndTime?: Date | undefined + suggestedTimeLimit?: number | undefined viewCount: number - readyToAdvance: string[] | undefined = [] + readyToAdvance?: string[] | undefined = [] phaseType: string constructor(input: GenericMeetingStageInput) { const {durations, phaseType, id, isNavigable, isNavigableByFacilitator, startAt, viewCount} = diff --git a/packages/server/database/types/Meeting.ts b/packages/server/database/types/Meeting.ts index c9ed6cbf778..503b945c153 100644 --- a/packages/server/database/types/Meeting.ts +++ b/packages/server/database/types/Meeting.ts @@ -1,21 +1,22 @@ import generateUID from '../../generateUID' import {MeetingTypeEnum} from '../../postgres/types/Meeting' +import {NewMeetingPhase} from '../../postgres/types/NewMeetingPhase' import GenericMeetingPhase from './GenericMeetingPhase' interface Input { - id?: string + id?: string | null teamId: string meetingType: MeetingTypeEnum meetingCount: number - name?: string + name?: string | null // Every meeting has at least one phase - phases: [GenericMeetingPhase, ...GenericMeetingPhase[]] + phases: [NewMeetingPhase, ...NewMeetingPhase[]] facilitatorUserId: string - showConversionModal?: boolean - meetingSeriesId?: number + showConversionModal?: boolean | null + meetingSeriesId?: number | null scheduledEndTime?: Date | null - summary?: string - sentimentScore?: number + summary?: string | null + sentimentScore?: number | null } const namePrefix = { @@ -29,23 +30,23 @@ export default abstract class Meeting { updatedAt = new Date() createdBy: string | null endedAt: Date | undefined | null = undefined - facilitatorStageId: string | undefined - facilitatorUserId: string + facilitatorStageId: string + facilitatorUserId: string | null meetingCount: number meetingNumber: number name: string - summarySentAt: Date | undefined = undefined + summarySentAt: Date | undefined | null = undefined teamId: string meetingType: MeetingTypeEnum phases: GenericMeetingPhase[] - showConversionModal?: boolean - meetingSeriesId?: number + showConversionModal?: boolean | null + meetingSeriesId?: number | null scheduledEndTime?: Date | null - summary?: string - sentimentScore?: number - usedReactjis?: Record - slackTs?: string - engagement?: number + summary?: string | null + sentimentScore?: number | null + usedReactjis?: Record | null + slackTs?: string | number | null + engagement?: number | null constructor(input: Input) { const { diff --git a/packages/server/database/types/MeetingAction.ts b/packages/server/database/types/MeetingAction.ts index fee9b580b8d..745df2cb0b3 100644 --- a/packages/server/database/types/MeetingAction.ts +++ b/packages/server/database/types/MeetingAction.ts @@ -1,10 +1,6 @@ -import AgendaItemsPhase from './AgendaItemsPhase' -import CheckInPhase from './CheckInPhase' -import GenericMeetingPhase from './GenericMeetingPhase' +import {CheckInMeetingPhase} from '../../postgres/types/NewMeetingPhase' import Meeting from './Meeting' -import UpdatesPhase from './UpdatesPhase' -type CheckInMeetingPhase = CheckInPhase | UpdatesPhase | GenericMeetingPhase | AgendaItemsPhase interface Input { id?: string teamId: string @@ -14,10 +10,6 @@ interface Input { facilitatorUserId: string } -export function isMeetingAction(meeting: Meeting): meeting is MeetingAction { - return meeting.meetingType === 'action' -} - export default class MeetingAction extends Meeting { meetingType!: 'action' taskCount?: number diff --git a/packages/server/database/types/MeetingPoker.ts b/packages/server/database/types/MeetingPoker.ts index 5883fd6d8ac..f8c4a19cea6 100644 --- a/packages/server/database/types/MeetingPoker.ts +++ b/packages/server/database/types/MeetingPoker.ts @@ -1,24 +1,17 @@ -import CheckInPhase from './CheckInPhase' -import EstimatePhase from './EstimatePhase' -import GenericMeetingPhase from './GenericMeetingPhase' +import {PokerMeetingPhase} from '../../postgres/types/NewMeetingPhase' import Meeting from './Meeting' -type PokerPhase = CheckInPhase | EstimatePhase | GenericMeetingPhase interface Input { id: string teamId: string meetingCount: number name: string - phases: [PokerPhase, ...PokerPhase[]] + phases: [PokerMeetingPhase, ...PokerMeetingPhase[]] facilitatorUserId: string templateId: string templateRefId: string } -export function isMeetingPoker(meeting: Meeting): meeting is MeetingPoker { - return meeting.meetingType === 'poker' -} - export default class MeetingPoker extends Meeting { meetingType!: 'poker' templateId: string diff --git a/packages/server/database/types/MeetingRetrospective.ts b/packages/server/database/types/MeetingRetrospective.ts index d727149bb78..4ee8d4e376d 100644 --- a/packages/server/database/types/MeetingRetrospective.ts +++ b/packages/server/database/types/MeetingRetrospective.ts @@ -1,44 +1,31 @@ -import GenericMeetingPhase from './GenericMeetingPhase' +import {AutogroupReflectionGroupType, TranscriptBlock} from '../../postgres/types' +import {RetroMeetingPhase} from '../../postgres/types/NewMeetingPhase' import Meeting from './Meeting' -export type AutogroupReflectionGroupType = { - groupTitle: string - reflectionIds: string[] -} - -export type TranscriptBlock = { - speaker: string - words: string -} - interface Input { - id?: string + id?: string | null teamId: string meetingCount: number name: string - phases: [GenericMeetingPhase, ...GenericMeetingPhase[]] + phases: [RetroMeetingPhase, ...RetroMeetingPhase[]] facilitatorUserId: string - showConversionModal?: boolean + showConversionModal?: boolean | null templateId: string totalVotes: number maxVotesPerGroup: number disableAnonymity: boolean - transcription?: TranscriptBlock[] - autogroupReflectionGroups?: AutogroupReflectionGroupType[] - resetReflectionGroups?: AutogroupReflectionGroupType[] + transcription?: TranscriptBlock[] | null + autogroupReflectionGroups?: AutogroupReflectionGroupType[] | null + resetReflectionGroups?: AutogroupReflectionGroupType[] | null recallBotId?: string - videoMeetingURL?: string - meetingSeriesId?: number + videoMeetingURL?: string | null + meetingSeriesId?: number | null scheduledEndTime?: Date | null } -export function isMeetingRetrospective(meeting: Meeting): meeting is MeetingRetrospective { - return meeting.meetingType === 'retrospective' -} - export default class MeetingRetrospective extends Meeting { meetingType!: 'retrospective' - showConversionModal?: boolean + showConversionModal?: boolean | null autoGroupThreshold?: number | null nextAutoGroupThreshold?: number | null totalVotes: number @@ -50,11 +37,11 @@ export default class MeetingRetrospective extends Meeting { templateId: string topicCount?: number reflectionCount?: number - transcription?: TranscriptBlock[] - recallBotId?: string - videoMeetingURL?: string - autogroupReflectionGroups?: AutogroupReflectionGroupType[] - resetReflectionGroups?: AutogroupReflectionGroupType[] + transcription?: TranscriptBlock[] | null + recallBotId?: string | null + videoMeetingURL?: string | null + autogroupReflectionGroups?: AutogroupReflectionGroupType[] | null + resetReflectionGroups?: AutogroupReflectionGroupType[] | null constructor(input: Input) { const { diff --git a/packages/server/database/types/MeetingTeamPrompt.ts b/packages/server/database/types/MeetingTeamPrompt.ts index bf57eb4ebaf..b781fbb2f55 100644 --- a/packages/server/database/types/MeetingTeamPrompt.ts +++ b/packages/server/database/types/MeetingTeamPrompt.ts @@ -1,8 +1,5 @@ -import GenericMeetingPhase from './GenericMeetingPhase' +import {TeamPromptPhase} from '../../postgres/types/NewMeetingPhase' import Meeting from './Meeting' -import TeamPromptResponsesPhase from './TeamPromptResponsesPhase' - -type TeamPromptPhase = TeamPromptResponsesPhase | GenericMeetingPhase interface Input { id?: string @@ -16,10 +13,6 @@ interface Input { scheduledEndTime?: Date } -export function isMeetingTeamPrompt(meeting: Meeting): meeting is MeetingTeamPrompt { - return meeting.meetingType === 'teamPrompt' -} - export default class MeetingTeamPrompt extends Meeting { meetingType!: 'teamPrompt' meetingPrompt: string diff --git a/packages/server/database/types/ReflectPhase.ts b/packages/server/database/types/ReflectPhase.ts index f5f04ad3399..4de28d532c2 100644 --- a/packages/server/database/types/ReflectPhase.ts +++ b/packages/server/database/types/ReflectPhase.ts @@ -4,8 +4,7 @@ import GenericMeetingStage from './GenericMeetingStage' export default class ReflectPhase extends GenericMeetingPhase { stages: [GenericMeetingStage, ...GenericMeetingStage[]] - focusedPromptId?: string; - [k: string]: any + focusedPromptId?: string constructor( public teamId: string, diff --git a/packages/server/email/newMeetingSummaryEmailCreator.tsx b/packages/server/email/newMeetingSummaryEmailCreator.tsx index 8387534b811..f0e065a2f33 100644 --- a/packages/server/email/newMeetingSummaryEmailCreator.tsx +++ b/packages/server/email/newMeetingSummaryEmailCreator.tsx @@ -19,9 +19,9 @@ const newMeetingSummaryEmailCreator = async (props: Props) => { const dataLoaderId = dataLoader.share() const newMeeting = await dataLoader.get('newMeetings').load(meetingId) - const facilitator = await dataLoader.get('users').loadNonNull(newMeeting.facilitatorUserId) + const facilitator = await dataLoader.get('users').loadNonNull(newMeeting.facilitatorUserId!) const {tms} = facilitator - const authToken = new AuthToken({sub: newMeeting.facilitatorUserId, tms, rol: 'impersonate'}) + const authToken = new AuthToken({sub: newMeeting.facilitatorUserId!, tms, rol: 'impersonate'}) const environment = new ServerEnvironment(authToken, dataLoaderId) // this depends on types, and those types are generated by created the schema, which must crawl the endMeeting file diff --git a/packages/server/graphql/meetingTypePredicates.ts b/packages/server/graphql/meetingTypePredicates.ts index 332acd71a87..bb4ed35985d 100644 --- a/packages/server/graphql/meetingTypePredicates.ts +++ b/packages/server/graphql/meetingTypePredicates.ts @@ -5,20 +5,7 @@ import EstimatePhase from '../database/types/EstimatePhase' import EstimateStage from '../database/types/EstimateStage' import GenericMeetingPhase from '../database/types/GenericMeetingPhase' import GenericMeetingStage from '../database/types/GenericMeetingStage' -import MeetingAction from '../database/types/MeetingAction' -import MeetingPoker from '../database/types/MeetingPoker' -import MeetingRetrospective from '../database/types/MeetingRetrospective' import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' -import {AnyMeeting} from '../postgres/types/Meeting' - -export const isRetroMeeting = (meeting: AnyMeeting): meeting is MeetingRetrospective => - meeting.meetingType === 'retrospective' - -export const isPokerMeeting = (meeting: AnyMeeting): meeting is MeetingPoker => - meeting.meetingType === 'poker' - -export const isActionMeeting = (meeting: AnyMeeting): meeting is MeetingAction => - meeting.meetingType === 'action' export const isEstimateStage = (stage: GenericMeetingStage): stage is EstimateStage => stage.phaseType === 'ESTIMATE' diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index 4ad865c40b0..ca676bbaeee 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -7,13 +7,13 @@ import {positionAfter} from '../../../client/shared/sortOrder' import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck' import getRethink from '../../database/rethinkDriver' import {RDatum} from '../../database/stricterR' -import MeetingAction from '../../database/types/MeetingAction' import Task from '../../database/types/Task' import TimelineEventCheckinComplete from '../../database/types/TimelineEventCheckinComplete' import {DataLoaderInstance} from '../../dataloader/RootDataLoader' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import {AgendaItem} from '../../postgres/types' +import {CheckInMeeting} from '../../postgres/types/Meeting' import archiveTasksForDB from '../../safeMutations/archiveTasksForDB' import removeSuggestedAction from '../../safeMutations/removeSuggestedAction' import {Logger} from '../../utils/Logger' @@ -101,7 +101,7 @@ const clonePinnedAgendaItems = async ( dataLoader.clearAll('agendaItems') } -const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataLoaderWorker) => { +const summarizeCheckInMeeting = async (meeting: CheckInMeeting, dataLoader: DataLoaderWorker) => { /* If isKill, no agenda items were processed so clear none of them. * Similarly, don't clone pins. the original ones will show up again. */ @@ -177,12 +177,11 @@ export default { const viewerId = getUserId(authToken) // AUTH - const meeting = (await r - .table('NewMeeting') - .get(meetingId) - .default(null) - .run()) as MeetingAction | null + const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + if (meeting.meetingType !== 'action') { + return standardError(new Error('Not a check-in meeting'), {userId: viewerId}) + } const {endedAt, facilitatorStageId, phases, teamId} = meeting // VALIDATION @@ -201,7 +200,7 @@ export default { const phase = getMeetingPhase(phases) const insights = await gatherInsights(meeting, dataLoader) - const completedCheckIn = (await r + const completedCheckIn = await r .table('NewMeeting') .get(meetingId) .update( @@ -213,7 +212,7 @@ export default { {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingAction + .run() if (!completedCheckIn) { return standardError(new Error('Completed check-in meeting does not exist'), { @@ -221,6 +220,11 @@ export default { }) } + if (completedCheckIn.meetingType === 'action') { + return standardError(new Error('Completed check-in meeting is not an action'), { + userId: viewerId + }) + } // remove any empty tasks const [meetingMembers, team, teamMembers, removedTaskIds] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), diff --git a/packages/server/graphql/mutations/endRetrospective.ts b/packages/server/graphql/mutations/endRetrospective.ts index 82136fa8f9d..1d2cc4e4699 100644 --- a/packages/server/graphql/mutations/endRetrospective.ts +++ b/packages/server/graphql/mutations/endRetrospective.ts @@ -1,6 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import getRethink from '../../database/rethinkDriver' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../utils/authorization' import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' @@ -27,7 +27,7 @@ export default { .table('NewMeeting') .get(meetingId) .default(null) - .run()) as MeetingRetrospective | null + .run()) as RetrospectiveMeeting | null if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {endedAt, teamId} = meeting diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 00db314a2ab..5927fcd34f4 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -4,8 +4,6 @@ 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 Meeting from '../../database/types/Meeting' -import MeetingPoker from '../../database/types/MeetingPoker' import TimelineEventPokerComplete from '../../database/types/TimelineEventPokerComplete' import getKysely from '../../postgres/getKysely' import {Logger} from '../../utils/Logger' @@ -42,12 +40,11 @@ export default { const viewerId = getUserId(authToken) // AUTH - const meeting = (await r - .table('NewMeeting') - .get(meetingId) - .default(null) - .run()) as Meeting | null + const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + if (meeting.meetingType !== 'poker') { + return standardError(new Error('Meeting is not a poker meeting'), {userId: viewerId}) + } const {endedAt, facilitatorStageId, phases, teamId} = meeting // VALIDATION @@ -82,7 +79,7 @@ export default { await dataLoader.get('commentCountByDiscussionId').loadMany(discussionIds) ).filter(isValid) const commentCount = commentCounts.reduce((cumSum, count) => cumSum + count, 0) - const completedMeeting = (await r + const completedMeeting = await r .table('NewMeeting') .get(meetingId) .update( @@ -96,12 +93,15 @@ export default { {returnChanges: true, nonAtomic: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingPoker + .run() if (!completedMeeting) { return standardError(new Error('Completed poker meeting does not exist'), { userId: viewerId }) } + if (completedMeeting.meetingType !== 'poker') { + return standardError(new Error('Meeting is not a poker meeting'), {userId: viewerId}) + } const {templateId} = completedMeeting const [meetingMembers, team, teamMembers, removedTaskIds, template] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), diff --git a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index 735ba4be831..69c0e8bca7e 100644 --- a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts +++ b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts @@ -1,6 +1,5 @@ import getRethink from '../../../database/rethinkDriver' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' -import MeetingAction from '../../../database/types/MeetingAction' import getKysely from '../../../postgres/getKysely' import getPhase from '../../../utils/getPhase' import {DataLoaderWorker} from '../../graphql' @@ -18,7 +17,7 @@ const addAgendaItemToActiveActionMeeting = async ( const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const actionMeeting = activeMeetings.find( (activeMeeting) => activeMeeting.meetingType === 'action' - ) as MeetingAction | undefined + ) if (!actionMeeting) return undefined const {id: meetingId, phases} = actionMeeting const agendaItemPhase = getPhase(phases, 'agendaitems') diff --git a/packages/server/graphql/mutations/helpers/addDiscussionTopics.ts b/packages/server/graphql/mutations/helpers/addDiscussionTopics.ts index ef05b73ae6d..a703837a169 100644 --- a/packages/server/graphql/mutations/helpers/addDiscussionTopics.ts +++ b/packages/server/graphql/mutations/helpers/addDiscussionTopics.ts @@ -1,11 +1,11 @@ import mapGroupsToStages from 'parabol-client/utils/makeGroupsToStages' import DiscussStage from '../../../database/types/DiscussStage' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import generateUID from '../../../generateUID' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import getPhase from '../../../utils/getPhase' import {DataLoaderWorker} from '../../graphql' -const addDiscussionTopics = async (meeting: MeetingRetrospective, dataLoader: DataLoaderWorker) => { +const addDiscussionTopics = async (meeting: RetrospectiveMeeting, dataLoader: DataLoaderWorker) => { const {id: meetingId, phases} = meeting const discussPhase = getPhase(phases, 'discuss') if (!discussPhase) return {discussPhaseStages: [], meetingId} diff --git a/packages/server/graphql/mutations/helpers/calculateEngagement.ts b/packages/server/graphql/mutations/helpers/calculateEngagement.ts index bed551a8130..78ebd67ddfc 100644 --- a/packages/server/graphql/mutations/helpers/calculateEngagement.ts +++ b/packages/server/graphql/mutations/helpers/calculateEngagement.ts @@ -1,6 +1,7 @@ import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId' import EstimatePhase from '../../../database/types/EstimatePhase' -import Meeting from '../../../database/types/Meeting' +import {AnyMeeting} from '../../../postgres/types/Meeting' +import {NewMeetingStages} from '../../../postgres/types/NewMeetingPhase' import getPhase from '../../../utils/getPhase' import {DataLoaderWorker} from '../../graphql' import isValid from '../../isValid' @@ -13,7 +14,7 @@ import isValid from '../../isValid' * **sprint poker**: meeting members facilitated, voted discussed or reacted / total meeting members * **standup**: replied, commented or reacted / all members */ -const calculateEngagement = async (meeting: Meeting, dataLoader: DataLoaderWorker) => { +const calculateEngagement = async (meeting: AnyMeeting, dataLoader: DataLoaderWorker) => { const {id: meetingId, phases, meetingType, facilitatorUserId} = meeting if (meetingType === 'action') return undefined @@ -78,7 +79,7 @@ const calculateEngagement = async (meeting: Meeting, dataLoader: DataLoaderWorke } // Discussions can happen in many different stage types: discuss, ESTIMATE, reflect, RESPONSES - const stages = phases.flatMap(({stages}) => stages) + const stages = phases.flatMap(({stages}) => stages as NewMeetingStages[]) const discussionIds = stages .map((stage) => 'discussionId' in stage && stage.discussionId) .filter(isValid) as string[] diff --git a/packages/server/graphql/mutations/helpers/collectReactjis.ts b/packages/server/graphql/mutations/helpers/collectReactjis.ts index 99c3206b63f..5e51b14d1a4 100644 --- a/packages/server/graphql/mutations/helpers/collectReactjis.ts +++ b/packages/server/graphql/mutations/helpers/collectReactjis.ts @@ -1,15 +1,16 @@ -import Meeting from '../../../database/types/Meeting' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' +import {AnyMeeting} from '../../../postgres/types/Meeting' +import {NewMeetingStages} from '../../../postgres/types/NewMeetingPhase' import {DataLoaderWorker} from '../../graphql' import isValid from '../../isValid' -const collectReactjis = async (meeting: Meeting, dataLoader: DataLoaderWorker) => { +const collectReactjis = async (meeting: AnyMeeting, dataLoader: DataLoaderWorker) => { const {id: meetingId, phases} = meeting const usedReactjis: Record = {} // Discussions can happen in many different stage types: discuss, ESTIMATE, reflect, RESPONSES - const stages = phases.flatMap(({stages}) => stages) + const stages = phases.flatMap(({stages}) => stages as NewMeetingStages[]) const discussionIds = stages .map((stage) => 'discussionId' in stage && stage.discussionId) .filter(isValid) as string[] diff --git a/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts b/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts index 098392fc6a6..44846ed791f 100644 --- a/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts +++ b/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts @@ -25,6 +25,7 @@ import UpdatesPhase from '../../../database/types/UpdatesPhase' import UpdatesStage from '../../../database/types/UpdatesStage' import getKysely from '../../../postgres/getKysely' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {NewMeetingPhase} from '../../../postgres/types/NewMeetingPhase' import isPhaseAvailable from '../../../utils/isPhaseAvailable' import {DataLoaderWorker} from '../../graphql' import {getFeatureTier} from '../../types/helpers/getFeatureTier' @@ -69,7 +70,7 @@ const getPastStageDurations = async (teamId: string) => { ) } -const createNewMeetingPhases = async ( +const createNewMeetingPhases = async ( facilitatorUserId: string, teamId: string, meetingId: string, @@ -162,7 +163,7 @@ const createNewMeetingPhases = async ( throw new Error(`Unhandled phaseType: ${phaseType}`) } }) - )) as [GenericMeetingPhase, ...GenericMeetingPhase[]] + )) as [T, ...T[]] primePhases(phases) await Promise.all(asyncSideEffects) return phases diff --git a/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts b/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts index 1b22cefe1b4..570f456e361 100644 --- a/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts +++ b/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts @@ -1,12 +1,12 @@ import getRethink from '../../../../database/rethinkDriver' -import Meeting from '../../../../database/types/Meeting' import getMailManager from '../../../../email/getMailManager' import newMeetingSummaryEmailCreator from '../../../../email/newMeetingSummaryEmailCreator' +import {AnyMeeting} from '../../../../postgres/types/Meeting' import {GQLContext} from '../../../graphql' import isValid from '../../../isValid' export default async function sendNewMeetingSummary( - newMeeting: Meeting, + newMeeting: AnyMeeting, context: Pick ) { const {id: meetingId, teamId, summarySentAt} = newMeeting diff --git a/packages/server/graphql/mutations/helpers/gatherInsights.ts b/packages/server/graphql/mutations/helpers/gatherInsights.ts index 159a7f75533..fed38a5f008 100644 --- a/packages/server/graphql/mutations/helpers/gatherInsights.ts +++ b/packages/server/graphql/mutations/helpers/gatherInsights.ts @@ -1,9 +1,9 @@ -import Meeting from '../../../database/types/Meeting' +import {AnyMeeting} from '../../../postgres/types/Meeting' import {DataLoaderWorker} from '../../graphql' import calculateEngagement from './calculateEngagement' import collectReactjis from './collectReactjis' -const gatherInsights = async (meeting: Meeting, dataLoader: DataLoaderWorker) => { +const gatherInsights = async (meeting: AnyMeeting, dataLoader: DataLoaderWorker) => { const [usedReactjis, engagement] = await Promise.all([ collectReactjis(meeting, dataLoader), calculateEngagement(meeting, dataLoader) diff --git a/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts b/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts index fe88bac549e..0c4943e6a7e 100644 --- a/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts +++ b/packages/server/graphql/mutations/helpers/generateDiscussionSummary.ts @@ -1,7 +1,7 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import {PARABOL_AI_USER_ID} from '../../../../client/utils/constants' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import updateDiscussions from '../../../postgres/queries/updateDiscussions' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import publish from '../../../utils/publish' import {DataLoaderWorker} from '../../graphql' @@ -9,12 +9,12 @@ import canAccessAISummary from './canAccessAISummary' const generateDiscussionSummary = async ( discussionId: string, - meeting: MeetingRetrospective, + meeting: RetrospectiveMeeting, dataLoader: DataLoaderWorker ) => { const {id: meetingId, endedAt, facilitatorUserId, teamId} = meeting const [facilitator, team] = await Promise.all([ - dataLoader.get('users').loadNonNull(facilitatorUserId), + dataLoader.get('users').loadNonNull(facilitatorUserId!), dataLoader.get('teams').loadNonNull(teamId) ]) const isAISummaryAccessible = await canAccessAISummary( diff --git a/packages/server/graphql/mutations/helpers/generateGroups.ts b/packages/server/graphql/mutations/helpers/generateGroups.ts index a37536bb97c..068e3b176d0 100644 --- a/packages/server/graphql/mutations/helpers/generateGroups.ts +++ b/packages/server/graphql/mutations/helpers/generateGroups.ts @@ -1,7 +1,6 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import {AutogroupReflectionGroupType} from '../../../database/types/MeetingRetrospective' -import {RetroReflection} from '../../../postgres/types' +import {AutogroupReflectionGroupType, RetroReflection} from '../../../postgres/types' import {Logger} from '../../../utils/Logger' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import {analytics} from '../../../utils/analytics/analytics' diff --git a/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts b/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts index ac20e9b106c..7b84ec54f00 100644 --- a/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts +++ b/packages/server/graphql/mutations/helpers/generateStandupMeetingSummary.ts @@ -1,15 +1,15 @@ -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' +import {TeamPromptMeeting} from '../../../postgres/types/Meeting' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import {DataLoaderWorker} from '../../graphql' import canAccessAISummary from './canAccessAISummary' const generateStandupMeetingSummary = async ( - meeting: MeetingTeamPrompt, + meeting: TeamPromptMeeting, dataLoader: DataLoaderWorker ) => { const [facilitator, team] = await Promise.all([ - dataLoader.get('users').loadNonNull(meeting.facilitatorUserId), + dataLoader.get('users').loadNonNull(meeting.facilitatorUserId!), dataLoader.get('teams').loadNonNull(meeting.teamId) ]) const isAISummaryAccessible = await canAccessAISummary( diff --git a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts index a6aa7267f88..398e0bc190b 100644 --- a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts +++ b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts @@ -4,9 +4,8 @@ import {r} from 'rethinkdb-ts' import groupReflections from '../../../../client/utils/smartGroup/groupReflections' import DiscussStage from '../../../database/types/DiscussStage' import GenericMeetingStage from '../../../database/types/GenericMeetingStage' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import getKysely from '../../../postgres/getKysely' -import {AnyMeeting} from '../../../postgres/types/Meeting' +import {AnyMeeting, RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {DataLoaderWorker} from '../../graphql' import addAIGeneratedContentToThreads from './addAIGeneratedContentToThreads' import addDiscussionTopics from './addDiscussionTopics' @@ -24,7 +23,7 @@ import removeEmptyReflections from './removeEmptyReflections' */ const handleCompletedRetrospectiveStage = async ( stage: GenericMeetingStage, - meeting: MeetingRetrospective, + meeting: RetrospectiveMeeting, dataLoader: DataLoaderWorker ) => { const pg = getKysely() @@ -72,7 +71,7 @@ const handleCompletedRetrospectiveStage = async ( .run() data.meeting = meeting // dont await for the OpenAI API response - generateDiscussionPrompt(meeting.id, teamId, dataLoader, facilitatorUserId) + generateDiscussionPrompt(meeting.id, teamId, dataLoader, facilitatorUserId!) } return {[stage.phaseType]: data} @@ -116,7 +115,7 @@ const handleCompletedStage = async ( dataLoader: DataLoaderWorker ) => { if (meeting.meetingType === 'retrospective') { - return handleCompletedRetrospectiveStage(stage, meeting as MeetingRetrospective, dataLoader) + return handleCompletedRetrospectiveStage(stage, meeting as RetrospectiveMeeting, dataLoader) } return {} } diff --git a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts index b96aec374fc..45459007256 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts @@ -3,11 +3,10 @@ import makeAppURL from 'parabol-client/utils/makeAppURL' import findStageById from 'parabol-client/utils/meetings/findStageById' import {phaseLabelLookup} from 'parabol-client/utils/meetings/lookups' import appOrigin from '../../../../appOrigin' -import Meeting from '../../../../database/types/Meeting' import {IntegrationProviderMSTeams as IIntegrationProviderMSTeams} from '../../../../postgres/queries/getIntegrationProvidersByIds' import {SlackNotification, Team} from '../../../../postgres/types' import IUser from '../../../../postgres/types/IUser' -import {MeetingTypeEnum} from '../../../../postgres/types/Meeting' +import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting' import MSTeamsServerManager from '../../../../utils/MSTeamsServerManager' import {analytics} from '../../../../utils/analytics/analytics' import sendToSentry from '../../../../utils/sendToSentry' @@ -360,7 +359,7 @@ function GenerateACMeetingTitle(meetingTitle: string) { return titleTextBlock } -function GenerateACMeetingAndTeamsDetails(team: Team, meeting: Meeting) { +function GenerateACMeetingAndTeamsDetails(team: Team, meeting: AnyMeeting) { const meetingDetailColumnSet = new AdaptiveCards.ColumnSet() const teamDetailColumn = new AdaptiveCards.Column() teamDetailColumn.width = 'stretch' diff --git a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts index 62eb866722c..857316161ff 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts @@ -4,11 +4,10 @@ import makeAppURL from 'parabol-client/utils/makeAppURL' import findStageById from 'parabol-client/utils/meetings/findStageById' import {phaseLabelLookup} from 'parabol-client/utils/meetings/lookups' import appOrigin from '../../../../appOrigin' -import Meeting from '../../../../database/types/Meeting' import {IntegrationProviderMattermost as IIntegrationProviderMattermost} from '../../../../postgres/queries/getIntegrationProvidersByIds' import {SlackNotification, Team} from '../../../../postgres/types' import IUser from '../../../../postgres/types/IUser' -import {MeetingTypeEnum} from '../../../../postgres/types/Meeting' +import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting' import MattermostServerManager from '../../../../utils/MattermostServerManager' import {analytics} from '../../../../utils/analytics/analytics' import {toEpochSeconds} from '../../../../utils/epochTime' @@ -47,7 +46,7 @@ const notifyMattermost = async ( return 'success' } -const makeEndMeetingButtons = (meeting: Meeting) => { +const makeEndMeetingButtons = (meeting: AnyMeeting) => { const {id: meetingId} = meeting const searchParams = { utm_source: 'mattermost summary', @@ -94,7 +93,7 @@ type MattermostNotificationAuth = IntegrationProviderMattermost & {userId: strin const makeTeamPromptStartMeetingNotification = ( team: Team, - meeting: Meeting, + meeting: AnyMeeting, meetingUrl: string ) => { return [ @@ -119,7 +118,11 @@ const makeTeamPromptStartMeetingNotification = ( ] } -const makeGenericStartMeetingNotification = (team: Team, meeting: Meeting, meetingUrl: string) => { +const makeGenericStartMeetingNotification = ( + team: Team, + meeting: AnyMeeting, + meetingUrl: string +) => { return [ makeFieldsAttachment( [ @@ -149,7 +152,7 @@ const makeGenericStartMeetingNotification = (team: Team, meeting: Meeting, meeti const makeStartMeetingNotificationLookup: Record< MeetingTypeEnum, - (team: Team, meeting: Meeting, meetingUrl: string) => ReturnType[] + (team: Team, meeting: AnyMeeting, meetingUrl: string) => ReturnType[] > = { teamPrompt: makeTeamPromptStartMeetingNotification, action: makeGenericStartMeetingNotification, diff --git a/packages/server/graphql/mutations/helpers/notifications/NotificationIntegrationHelper.ts b/packages/server/graphql/mutations/helpers/notifications/NotificationIntegrationHelper.ts index e1bd2c9287c..6223b769ba8 100644 --- a/packages/server/graphql/mutations/helpers/notifications/NotificationIntegrationHelper.ts +++ b/packages/server/graphql/mutations/helpers/notifications/NotificationIntegrationHelper.ts @@ -1,6 +1,6 @@ -import Meeting from '../../../../database/types/Meeting' import {Team, TeamPromptResponse} from '../../../../postgres/types' import User from '../../../../postgres/types/IUser' +import {AnyMeeting} from '../../../../postgres/types/Meeting' export type NotifyResponse = | 'success' | { @@ -10,24 +10,24 @@ export type NotifyResponse = } export type NotificationIntegration = { - startMeeting(meeting: Meeting, team: Team, user: User): Promise - updateMeeting?(meeting: Meeting, team: Team, user: User): Promise + startMeeting(meeting: AnyMeeting, team: Team, user: User): Promise + updateMeeting?(meeting: AnyMeeting, team: Team, user: User): Promise endMeeting( - meeting: Meeting, + meeting: AnyMeeting, team: Team, user: User, standupResponses: {user: User; response: TeamPromptResponse}[] | null ): Promise startTimeLimit( scheduledEndTime: Date, - meeting: Meeting, + meeting: AnyMeeting, team: Team, user: User ): Promise - endTimeLimit(meeting: Meeting, team: Team, user: User): Promise + endTimeLimit(meeting: AnyMeeting, team: Team, user: User): Promise integrationUpdated(user: User): Promise standupResponseSubmitted( - meeting: Meeting, + meeting: AnyMeeting, team: Team, user: User, response: TeamPromptResponse diff --git a/packages/server/graphql/mutations/helpers/notifications/Notifier.ts b/packages/server/graphql/mutations/helpers/notifications/Notifier.ts index 6ba69226bed..e01c3e88709 100644 --- a/packages/server/graphql/mutations/helpers/notifications/Notifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/Notifier.ts @@ -61,14 +61,14 @@ export const createNotifier = (loader: NotificationIntegrationLoader): Notifier async startMeeting(dataLoader: DataLoaderWorker, meetingId: string, teamId: string) { const {meeting, team, user} = await loadMeetingTeam(dataLoader, meetingId, teamId) if (!meeting || !team || !user) return - const notifiers = await loader(dataLoader, team.id, meeting.facilitatorUserId, 'meetingStart') + const notifiers = await loader(dataLoader, team.id, meeting.facilitatorUserId!, 'meetingStart') notifiers.forEach((notifier) => notifier.startMeeting(meeting, team, user)) }, async updateMeeting(dataLoader: DataLoaderWorker, meetingId: string, teamId: string) { const {meeting, team, user} = await loadMeetingTeam(dataLoader, meetingId, teamId) if (!meeting || !team || !user) return - const notifiers = await loader(dataLoader, team.id, meeting.facilitatorUserId, 'meetingStart') + const notifiers = await loader(dataLoader, team.id, meeting.facilitatorUserId!, 'meetingStart') notifiers.forEach((notifier) => notifier.updateMeeting?.(meeting, team, user)) }, @@ -85,7 +85,7 @@ export const createNotifier = (loader: NotificationIntegrationLoader): Notifier } }) ) - const notifiers = await loader(dataLoader, team.id, meeting.facilitatorUserId, 'meetingEnd') + const notifiers = await loader(dataLoader, team.id, meeting.facilitatorUserId!, 'meetingEnd') notifiers.forEach((notifier) => notifier.endMeeting(meeting, team, user, standupResponses)) }, @@ -100,7 +100,7 @@ export const createNotifier = (loader: NotificationIntegrationLoader): Notifier const notifiers = await loader( dataLoader, team.id, - meeting.facilitatorUserId, + meeting.facilitatorUserId!, 'MEETING_STAGE_TIME_LIMIT_START' ) notifiers.forEach((notifier) => notifier.startTimeLimit(scheduledEndTime, meeting, team, user)) @@ -112,7 +112,7 @@ export const createNotifier = (loader: NotificationIntegrationLoader): Notifier const notifiers = await loader( dataLoader, team.id, - meeting.facilitatorUserId, + meeting.facilitatorUserId!, 'MEETING_STAGE_TIME_LIMIT_END' ) notifiers.forEach((notifier) => notifier.endTimeLimit(meeting, team, user)) @@ -141,7 +141,7 @@ export const createNotifier = (loader: NotificationIntegrationLoader): Notifier const notifiers = await loader( dataLoader, team.id, - meeting.facilitatorUserId, + meeting.facilitatorUserId!, 'STANDUP_RESPONSE_SUBMITTED' ) notifiers.forEach((notifier) => diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts index 457220ef98a..5a10f159e0e 100644 --- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts @@ -7,7 +7,6 @@ import TeamPromptResponseId from '../../../../../client/shared/gqlIds/TeamPrompt import {ErrorResponse, PostMessageResponse} from '../../../../../client/utils/SlackManager' import appOrigin from '../../../../appOrigin' import getRethink, {RethinkSchema} from '../../../../database/rethinkDriver' -import Meeting from '../../../../database/types/Meeting' import SlackAuth from '../../../../database/types/SlackAuth' import {SlackNotificationAuth} from '../../../../dataloader/integrationAuthLoaders' import getKysely from '../../../../postgres/getKysely' @@ -91,7 +90,7 @@ const notifySlack = async ( return res } -const makeEndMeetingButtons = (meeting: Meeting) => { +const makeEndMeetingButtons = (meeting: AnyMeeting) => { const {id: meetingId} = meeting const searchParams = { utm_source: 'slack summary', @@ -136,11 +135,11 @@ const makeEndMeetingButtons = (meeting: Meeting) => { const createTeamSectionContent = (team: Team) => `*Team:*\n${team.name}` -const createMeetingSectionContent = (meeting: Meeting) => `*Meeting:*\n${meeting.name}` +const createMeetingSectionContent = (meeting: AnyMeeting) => `*Meeting:*\n${meeting.name}` const makeTeamPromptStartMeetingNotification = ( team: Team, - meeting: Meeting, + meeting: AnyMeeting, meetingUrl: string ): SlackNotificationMessage => { const title = `*${meeting.name}* is open :speech_balloon: ` @@ -155,7 +154,7 @@ const makeTeamPromptStartMeetingNotification = ( const makeGenericStartMeetingNotification = ( team: Team, - meeting: Meeting, + meeting: AnyMeeting, meetingUrl: string ): SlackNotificationMessage => { const title = 'Meeting started :wave: ' @@ -170,7 +169,7 @@ const makeGenericStartMeetingNotification = ( const makeStartMeetingNotificationLookup: Record< MeetingTypeEnum, - (team: Team, meeting: Meeting, meetingUrl: string) => SlackNotificationMessage + (team: Team, meeting: AnyMeeting, meetingUrl: string) => SlackNotificationMessage > = { teamPrompt: makeTeamPromptStartMeetingNotification, action: makeGenericStartMeetingNotification, @@ -183,7 +182,7 @@ const addStandupResponsesToThread = async ( standupResponses: Array<{user: User; response: TeamPromptResponse}> | null, team: Team, user: User, - meeting: Meeting, + meeting: AnyMeeting, notificationChannel: NotificationChannel ) => { if (!standupResponses || standupResponses.length === 0) { @@ -361,7 +360,11 @@ export const SlackSingleChannelNotifier: NotificationIntegrationHelper { // Order of slack auth is: diff --git a/packages/server/graphql/mutations/helpers/notifications/getSummaryText.ts b/packages/server/graphql/mutations/helpers/notifications/getSummaryText.ts index d3522fce222..49ea47b1c85 100644 --- a/packages/server/graphql/mutations/helpers/notifications/getSummaryText.ts +++ b/packages/server/graphql/mutations/helpers/notifications/getSummaryText.ts @@ -1,16 +1,15 @@ import relativeDate from 'parabol-client/utils/date/relativeDate' import plural from 'parabol-client/utils/plural' -import Meeting from '../../../../database/types/Meeting' -import {isMeetingAction} from '../../../../database/types/MeetingAction' -import {isMeetingPoker} from '../../../../database/types/MeetingPoker' -import {isMeetingRetrospective} from '../../../../database/types/MeetingRetrospective' -import {isMeetingTeamPrompt} from '../../../../database/types/MeetingTeamPrompt' import {getTeamPromptResponsesByMeetingId} from '../../../../postgres/queries/getTeamPromptResponsesByMeetingIds' +import {AnyMeeting} from '../../../../postgres/types/Meeting' import sendToSentry from '../../../../utils/sendToSentry' -const getSummaryText = async (meeting: Meeting) => { - if (isMeetingRetrospective(meeting)) { - const {commentCount = 0, reflectionCount = 0, topicCount = 0, taskCount = 0} = meeting +const getSummaryText = async (meeting: AnyMeeting) => { + if (meeting.meetingType === 'retrospective') { + const commentCount = meeting.commentCount || 0 + const reflectionCount = meeting.reflectionCount || 0 + const topicCount = meeting.topicCount || 0 + const taskCount = meeting.taskCount || 0 const hasNonZeroStat = commentCount || reflectionCount || topicCount || taskCount if (!hasNonZeroStat && meeting.summary) { sendToSentry(new Error('No stats found for meeting'), { @@ -24,8 +23,11 @@ const getSummaryText = async (meeting: Meeting) => { commentCount, 'comment' )} and created ${taskCount} ${plural(taskCount, 'task')}.` - } else if (isMeetingAction(meeting)) { - const {createdAt, endedAt, agendaItemCount = 0, commentCount = 0, taskCount = 0} = meeting + } else if (meeting.meetingType === 'action') { + const agendaItemCount = meeting.agendaItemCount || 0 + const commentCount = meeting.commentCount || 0 + const taskCount = meeting.taskCount || 0 + const {createdAt, endedAt} = meeting const meetingDuration = relativeDate(createdAt, { now: endedAt, max: 2, @@ -39,21 +41,22 @@ const getSummaryText = async (meeting: Meeting) => { commentCount, 'comment' )}.` - } else if (isMeetingTeamPrompt(meeting)) { + } else if (meeting.meetingType === 'teamPrompt') { const responseCount = (await getTeamPromptResponsesByMeetingId(meeting.id)).filter( (response) => !!response.plaintextContent ).length // :TODO: (jmtaber129): Add additional stats here. return `Your team shared ${responseCount} ${plural(responseCount, 'response', 'responses')}.` - } else if (isMeetingPoker(meeting)) { - const {storyCount = 0, commentCount = 0} = meeting + } else if (meeting.meetingType === 'poker') { + const storyCount = meeting.storyCount || 0 + const commentCount = meeting.commentCount || 0 return `You voted on ${storyCount} ${plural( storyCount, 'story', 'stories' )} and added ${commentCount} ${plural(commentCount, 'comment')}.` } else { - throw new Error(`Meeting type not supported ${meeting.meetingType}`) + throw new Error(`Meeting type not supported ${(meeting as any).meetingType}`) } } diff --git a/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts b/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts index 039c216bd81..52cef841b7f 100644 --- a/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts +++ b/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts @@ -6,7 +6,6 @@ import {SprintPokerDefaults} from 'parabol-client/types/constEnums' import makeAppURL from 'parabol-client/utils/makeAppURL' import {isNotNull} from 'parabol-client/utils/predicates' import appOrigin from '../../../appOrigin' -import MeetingPoker from '../../../database/types/MeetingPoker' import { AddCommentMutation, AddCommentMutationVariables, @@ -50,6 +49,9 @@ const pushEstimateToGitHub = async ( return new Error('Meeting does not exist') } + if (meeting.meetingType !== 'poker') { + return new Error('Not a poker meeting') + } const githubIntegration = task.integration as Extract< typeof task.integration, {service: 'github'} @@ -92,6 +94,7 @@ const pushEstimateToGitHub = async ( const {id: issueId} = issue const {name: meetingName, phases} = meeting + const estimatePhase = getPhase(phases, 'ESTIMATE') const {stages} = estimatePhase const stageIdx = stages.findIndex((stage) => stage.id === stageId) @@ -150,7 +153,7 @@ const pushEstimateToGitHub = async ( if (!matchingLabel) { let color = PALETTE.GRAPE_500.slice(1) if (meeting) { - const {templateRefId} = meeting as MeetingPoker + const {templateRefId} = meeting const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) const {dimensions} = templateRef const dimensionRef = dimensions.find((dimension) => dimension.name === dimensionName) diff --git a/packages/server/graphql/mutations/helpers/removeEmptyReflections.ts b/packages/server/graphql/mutations/helpers/removeEmptyReflections.ts index e11080a9bb6..cdccfb40f76 100644 --- a/packages/server/graphql/mutations/helpers/removeEmptyReflections.ts +++ b/packages/server/graphql/mutations/helpers/removeEmptyReflections.ts @@ -1,9 +1,9 @@ import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' -import Meeting from '../../../database/types/Meeting' import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' +import {AnyMeeting} from '../../../postgres/types/Meeting' -const removeEmptyReflections = async (meeting: Meeting, dataLoader: DataLoaderInstance) => { +const removeEmptyReflections = async (meeting: AnyMeeting, dataLoader: DataLoaderInstance) => { const pg = getKysely() const {id: meetingId} = meeting const reflections = await dataLoader.get('retroReflectionsByMeetingId').load(meetingId) diff --git a/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts b/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts index 64f1dda66a0..39c5546998e 100644 --- a/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts +++ b/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts @@ -40,7 +40,7 @@ const removeStagesFromMeetings = async ( nextStage.viewCount = nextStage.viewCount ? nextStage.viewCount + 1 : 1 nextStage.isNavigable = true } - const stageIdx = stages.indexOf(stage) + const stageIdx = (stages as any).indexOf(stage) stages.splice(stageIdx, 1) } } diff --git a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts index fa01659f21f..8a9f2e8ef0e 100644 --- a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts @@ -1,6 +1,7 @@ import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import generateUID from '../../../generateUID' -import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {MeetingTypeEnum, RetrospectiveMeeting} from '../../../postgres/types/Meeting' +import {RetroMeetingPhase} from '../../../postgres/types/NewMeetingPhase' import {DataLoaderWorker} from '../../graphql' import createNewMeetingPhases from './createNewMeetingPhases' @@ -30,7 +31,7 @@ const safeCreateRetrospective = async ( const {showConversionModal} = organization const meetingId = generateUID() - const phases = await createNewMeetingPhases( + const phases = await createNewMeetingPhases( facilitatorUserId, teamId, meetingId, @@ -46,7 +47,7 @@ const safeCreateRetrospective = async ( showConversionModal, ...meetingSettings, name - }) + }) as RetrospectiveMeeting } export default safeCreateRetrospective diff --git a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts index 7416d9d78dc..7c702494396 100644 --- a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts @@ -3,7 +3,7 @@ import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import TeamPromptResponsesPhase from '../../../database/types/TeamPromptResponsesPhase' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' -import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {MeetingTypeEnum, TeamPromptMeeting} from '../../../postgres/types/Meeting' import {DataLoaderWorker} from '../../graphql' import {primePhases} from './createNewMeetingPhases' @@ -52,7 +52,7 @@ const safeCreateTeamPrompt = async ( facilitatorUserId: facilitatorId, meetingPrompt: DEFAULT_PROMPT, // :TODO: (jmtaber129): Get this from meeting settings. ...meetingOverrideProps - }) + }) as TeamPromptMeeting } export default safeCreateTeamPrompt diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index 1af261de253..aa7b8f3b161 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -4,9 +4,9 @@ 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 MeetingRetrospective from '../../../database/types/MeetingRetrospective' import TimelineEventRetroComplete from '../../../database/types/TimelineEventRetroComplete' import getKysely from '../../../postgres/getKysely' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import removeSuggestedAction from '../../../safeMutations/removeSuggestedAction' import {Logger} from '../../../utils/Logger' import RecallAIServerManager from '../../../utils/RecallAIServerManager' @@ -33,14 +33,14 @@ const getTranscription = async (recallBotId?: string | null) => { return await manager.getBotTranscript(recallBotId) } -const summarizeRetroMeeting = async (meeting: MeetingRetrospective, context: InternalContext) => { +const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: InternalContext) => { const {dataLoader} = context const {id: meetingId, phases, facilitatorUserId, teamId, recallBotId} = meeting const r = await getRethink() const [reflectionGroups, reflections, sentimentScore] = await Promise.all([ dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId), dataLoader.get('retroReflectionsByMeetingId').load(meetingId), - generateWholeMeetingSentimentScore(meetingId, facilitatorUserId, dataLoader) + generateWholeMeetingSentimentScore(meetingId, facilitatorUserId!, dataLoader) ]) const discussPhase = getPhase(phases, 'discuss') const {stages} = discussPhase @@ -48,7 +48,7 @@ const summarizeRetroMeeting = async (meeting: MeetingRetrospective, context: Int const reflectionGroupIds = reflectionGroups.map(({id}) => id) const [summary, transcription] = await Promise.all([ - generateWholeMeetingSummary(discussionIds, meetingId, teamId, facilitatorUserId, dataLoader), + generateWholeMeetingSummary(discussionIds, meetingId, teamId, facilitatorUserId!, dataLoader), getTranscription(recallBotId) ]) const commentCounts = ( @@ -93,7 +93,7 @@ const safeEndRetrospective = async ({ context, now }: { - meeting: MeetingRetrospective + meeting: RetrospectiveMeeting context: InternalContext now: Date }) => { @@ -127,7 +127,7 @@ const safeEndRetrospective = async ({ {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingRetrospective + .run()) as unknown as RetrospectiveMeeting if (!completedRetrospective) { return standardError(new Error('Completed retrospective meeting does not exist'), { diff --git a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts index e88ed41daf1..db8fb9d1c0c 100644 --- a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts @@ -1,10 +1,10 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' import getRethink, {ParabolR} from '../../../database/rethinkDriver' -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import TimelineEventTeamPromptComplete from '../../../database/types/TimelineEventTeamPromptComplete' import getKysely from '../../../postgres/getKysely' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' +import {TeamPromptMeeting} from '../../../postgres/types/Meeting' import {Logger} from '../../../utils/Logger' import {analytics} from '../../../utils/analytics/analytics' import publish, {SubOptions} from '../../../utils/publish' @@ -17,7 +17,7 @@ import {IntegrationNotifier} from './notifications/IntegrationNotifier' import updateQualAIMeetingsCount from './updateQualAIMeetingsCount' import updateTeamInsights from './updateTeamInsights' -const summarizeTeamPrompt = async (meeting: MeetingTeamPrompt, context: InternalContext) => { +const summarizeTeamPrompt = async (meeting: TeamPromptMeeting, context: InternalContext) => { const {dataLoader} = context const r = await getRethink() @@ -51,7 +51,7 @@ const safeEndTeamPrompt = async ({ context, subOptions }: { - meeting: MeetingTeamPrompt + meeting: TeamPromptMeeting now: Date viewerId?: string r: ParabolR @@ -77,7 +77,7 @@ const safeEndTeamPrompt = async ({ {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingTeamPrompt + .run()) as unknown as TeamPromptMeeting if (!completedTeamPrompt) { return standardError(new Error('Completed team prompt meeting does not exist'), { diff --git a/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts b/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts index 9db1d61ab85..88c74280788 100644 --- a/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts +++ b/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts @@ -1,11 +1,11 @@ -import Meeting from '../../../database/types/Meeting' import MeetingMember from '../../../database/types/MeetingMember' import {TeamMember} from '../../../postgres/types' +import {AnyMeeting} from '../../../postgres/types/Meeting' import {analytics} from '../../../utils/analytics/analytics' import {DataLoaderWorker} from '../../graphql' const sendPokerMeetingRevoteEvent = async ( - meeting: Meeting, + meeting: AnyMeeting, teamMembers: TeamMember[], meetingMembers: MeetingMember[], dataLoader: DataLoaderWorker diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts index 389ea399d3d..d82fb1c99f0 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts @@ -1,21 +1,17 @@ import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTitle' import dndNoise from '../../../../../client/utils/dndNoise' -import getRethink from '../../../../database/rethinkDriver' -import MeetingRetrospective from '../../../../database/types/MeetingRetrospective' import ReflectionGroup from '../../../../database/types/ReflectionGroup' import getKysely from '../../../../postgres/getKysely' import {GQLContext} from '../../../graphql' import updateSmartGroupTitle from './updateSmartGroupTitle' const removeReflectionFromGroup = async (reflectionId: string, {dataLoader}: GQLContext) => { - const r = await getRethink() const pg = getKysely() const reflection = await dataLoader.get('retroReflections').load(reflectionId) if (!reflection) throw new Error('Reflection not found') const {reflectionGroupId: oldReflectionGroupId, meetingId, promptId} = reflection - const [meetingReflectionGroups, meeting] = await Promise.all([ - dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId), - dataLoader.get('newMeetings').load(meetingId) + const [meetingReflectionGroups] = await Promise.all([ + dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId) ]) dataLoader.get('retroReflectionGroupsByMeetingId').clear(meetingId) dataLoader.get('retroReflectionGroups').clearAll() @@ -52,14 +48,11 @@ const removeReflectionFromGroup = async (reflectionId: string, {dataLoader}: GQL reflectionGroupId }) .where('id', '=', reflectionId) - .execute(), - r.table('NewMeeting').get(meetingId).update({nextAutoGroupThreshold: null}).run() + .execute() ]) // mutates the dataloader response reflection.sortOrder = 0 reflection.reflectionGroupId = reflectionGroupId - const retroMeeting = meeting as MeetingRetrospective - retroMeeting.nextAutoGroupThreshold = null const oldReflections = await dataLoader .get('retroReflectionsByGroupId') .load(oldReflectionGroupId) diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index c95582aeea1..456e7eadf13 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -6,8 +6,6 @@ import getRethink from '../../database/rethinkDriver' import ActionMeetingMember from '../../database/types/ActionMeetingMember' import CheckInStage from '../../database/types/CheckInStage' import {NewMeetingPhaseTypeEnum} from '../../database/types/GenericMeetingPhase' -import Meeting from '../../database/types/Meeting' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' import PokerMeetingMember from '../../database/types/PokerMeetingMember' import RetroMeetingMember from '../../database/types/RetroMeetingMember' import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember' @@ -15,6 +13,7 @@ import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStag import UpdatesStage from '../../database/types/UpdatesStage' import getKysely from '../../postgres/getKysely' import {TeamMember} from '../../postgres/types' +import {AnyMeeting, RetrospectiveMeeting} from '../../postgres/types/Meeting' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -22,13 +21,13 @@ import publish from '../../utils/publish' import {GQLContext} from '../graphql' import JoinMeetingPayload from '../types/JoinMeetingPayload' -const createMeetingMember = (meeting: Meeting, teamMember: TeamMember) => { +const createMeetingMember = (meeting: AnyMeeting, teamMember: TeamMember) => { const {userId, teamId, isSpectatingPoker} = teamMember switch (meeting.meetingType) { case 'action': return new ActionMeetingMember({teamId, userId, meetingId: meeting.id}) case 'retrospective': - const {id: meetingId, totalVotes} = meeting as MeetingRetrospective + const {id: meetingId, totalVotes} = meeting as RetrospectiveMeeting return new RetroMeetingMember({ teamId, userId, diff --git a/packages/server/graphql/mutations/pokerAnnounceDeckHover.ts b/packages/server/graphql/mutations/pokerAnnounceDeckHover.ts index d4f09a698a6..e0b4dcbc794 100644 --- a/packages/server/graphql/mutations/pokerAnnounceDeckHover.ts +++ b/packages/server/graphql/mutations/pokerAnnounceDeckHover.ts @@ -2,7 +2,6 @@ import {GraphQLBoolean, GraphQLID, GraphQLNonNull} from 'graphql' import ms from 'ms' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' -import MeetingPoker from '../../database/types/MeetingPoker' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import getRedis, {RedisPipelineResponse} from '../../utils/getRedis' @@ -35,10 +34,13 @@ const pokerAnnounceDeckHover = { const subOptions = {mutatorId, operationId} // AUTH - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingPoker + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) { return {error: {message: 'Meeting not found'}} } + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } const {endedAt, phases, meetingType, teamId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on the team'}} diff --git a/packages/server/graphql/mutations/pokerResetDimension.ts b/packages/server/graphql/mutations/pokerResetDimension.ts index d728eaa4b2f..c25a460db2c 100644 --- a/packages/server/graphql/mutations/pokerResetDimension.ts +++ b/packages/server/graphql/mutations/pokerResetDimension.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import {RValue} from '../../database/stricterR' -import MeetingPoker from '../../database/types/MeetingPoker' import updateStage from '../../database/updateStage' import removeMeetingTaskEstimates from '../../postgres/queries/removeMeetingTaskEstimates' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -33,10 +32,13 @@ const pokerResetDimension = { const subOptions = {mutatorId, operationId} //AUTH - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingPoker + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) { return {error: {message: 'Meeting not found'}} } + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } const {endedAt, phases, meetingType, teamId, createdBy, facilitatorUserId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on the team'}} diff --git a/packages/server/graphql/mutations/pokerRevealVotes.ts b/packages/server/graphql/mutations/pokerRevealVotes.ts index c640ae4964b..50c9620df76 100644 --- a/packages/server/graphql/mutations/pokerRevealVotes.ts +++ b/packages/server/graphql/mutations/pokerRevealVotes.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {PokerCards, SubscriptionChannel} from 'parabol-client/types/constEnums' import {RValue} from '../../database/stricterR' import EstimateUserScore from '../../database/types/EstimateUserScore' -import MeetingPoker from '../../database/types/MeetingPoker' import PokerMeetingMember from '../../database/types/PokerMeetingMember' import updateStage from '../../database/updateStage' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -36,12 +35,15 @@ const pokerRevealVotes = { // fetch meetingMembers up here to reduce chance of race condition that a vote gets cast in between now & when we update the scores const [meetingMembers, meeting] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), - dataLoader.get('newMeetings').load(meetingId) as Promise + dataLoader.get('newMeetings').load(meetingId) ]) if (!meeting) { return {error: {message: 'Meeting not found'}} } + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } const {endedAt, phases, meetingType, teamId, createdBy, facilitatorUserId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on the team'}} diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index 259a838413a..2e7786b27dc 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -4,8 +4,9 @@ import {CHECKIN, DISCUSS, GROUP, REFLECT, VOTE} from '../../../client/utils/cons import getRethink from '../../database/rethinkDriver' import DiscussPhase from '../../database/types/DiscussPhase' import GenericMeetingPhase from '../../database/types/GenericMeetingPhase' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' import getKysely from '../../postgres/getKysely' +import {RetrospectiveMeeting} from '../../postgres/types/Meeting' +import {RetroMeetingPhase} from '../../postgres/types/NewMeetingPhase' import {getUserId} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -34,7 +35,7 @@ const resetRetroMeetingToGroupStage = { // AUTH const viewerId = getUserId(authToken) - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {createdBy, facilitatorUserId, phases, meetingType} = meeting if (meetingType !== 'retrospective') { @@ -91,7 +92,7 @@ const resetRetroMeetingToGroupStage = { default: throw new Error(`Unhandled phaseType: ${phase.phaseType}`) } - }) + }) as RetroMeetingPhase[] primePhases(newPhases, resetToPhaseIndex) meeting.phases = newPhases diff --git a/packages/server/graphql/mutations/setTaskEstimate.ts b/packages/server/graphql/mutations/setTaskEstimate.ts index 08c2fa49895..1f2bdaea1fa 100644 --- a/packages/server/graphql/mutations/setTaskEstimate.ts +++ b/packages/server/graphql/mutations/setTaskEstimate.ts @@ -3,7 +3,6 @@ import {SprintPokerDefaults, SubscriptionChannel, Threshold} from 'parabol-clien import makeAppURL from 'parabol-client/utils/makeAppURL' import JiraProjectKeyId from '../../../client/shared/gqlIds/JiraProjectKeyId' import appOrigin from '../../appOrigin' -import MeetingPoker from '../../database/types/MeetingPoker' import TaskIntegrationJiraServer from '../../database/types/TaskIntegrationJiraServer' import JiraServerRestManager from '../../integrations/jiraServer/JiraServerRestManager' import {IntegrationProviderJiraServer} from '../../postgres/queries/getIntegrationProvidersByIds' @@ -69,10 +68,10 @@ const setTaskEstimate = { return {error: {message: 'Invalid dimension name'}} } - const {phases, meetingType, templateRefId, name: meetingName} = meeting as MeetingPoker - if (meetingType !== 'poker') { + if (meeting.meetingType !== 'poker') { return {error: {message: 'Invalid poker meeting'}} } + const {phases, templateRefId, name: meetingName} = meeting const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) const {dimensions} = templateRef const dimensionRefIdx = dimensions.findIndex((dimension) => dimension.name === dimensionName) diff --git a/packages/server/graphql/mutations/startSprintPoker.ts b/packages/server/graphql/mutations/startSprintPoker.ts index 0fd4ecd04f9..ece86c98986 100644 --- a/packages/server/graphql/mutations/startSprintPoker.ts +++ b/packages/server/graphql/mutations/startSprintPoker.ts @@ -8,7 +8,8 @@ import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import updateMeetingTemplateLastUsedAt from '../../postgres/queries/updateMeetingTemplateLastUsedAt' import updateTeamByTeamId from '../../postgres/queries/updateTeamByTeamId' -import {MeetingTypeEnum} from '../../postgres/types/Meeting' +import {MeetingTypeEnum, PokerMeeting} from '../../postgres/types/Meeting' +import {PokerMeetingPhase} from '../../postgres/types/NewMeetingPhase' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import getHashAndJSON from '../../utils/getHashAndJSON' @@ -123,7 +124,7 @@ export default { .default(0) .run() - const phases = await createNewMeetingPhases( + const phases = await createNewMeetingPhases( viewerId, teamId, meetingId, @@ -149,7 +150,7 @@ export default { facilitatorUserId: viewerId, templateId: selectedTemplateId, templateRefId - }) + }) as PokerMeeting const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) await Promise.all([ diff --git a/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts b/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts index b8a5fd2245c..fda78fffe3c 100644 --- a/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts +++ b/packages/server/graphql/mutations/updateAzureDevOpsDimensionField.ts @@ -1,6 +1,5 @@ import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import MeetingPoker from '../../database/types/MeetingPoker' import upsertAzureDevOpsDimensionFieldMap, { AzureDevOpsFieldMapProps } from '../../postgres/queries/upsertAzureDevOpsDimensionFieldMap' @@ -67,7 +66,10 @@ const updateAzureDevOpsDimensionField = { if (!meeting) { return {error: {message: 'Invalid meetingId'}} } - const {teamId, templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } + const {teamId, templateRefId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on team'}} } diff --git a/packages/server/graphql/mutations/updateGitHubDimensionField.ts b/packages/server/graphql/mutations/updateGitHubDimensionField.ts index ded249edf3f..f6c18e70f53 100644 --- a/packages/server/graphql/mutations/updateGitHubDimensionField.ts +++ b/packages/server/graphql/mutations/updateGitHubDimensionField.ts @@ -1,6 +1,5 @@ import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import MeetingPoker from '../../database/types/MeetingPoker' import upsertGitHubDimensionFieldMap from '../../postgres/queries/upsertGitHubDimensionFieldMap' import {Logger} from '../../utils/Logger' import {isTeamMember} from '../../utils/authorization' @@ -49,7 +48,10 @@ const updateGitHubDimensionField = { if (!meeting) { return {error: {message: 'Invalid meetingId'}} } - const {teamId, templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } + const {teamId, templateRefId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on team'}} } diff --git a/packages/server/graphql/mutations/updatePokerScope.ts b/packages/server/graphql/mutations/updatePokerScope.ts index 823a6381383..0e59ab72161 100644 --- a/packages/server/graphql/mutations/updatePokerScope.ts +++ b/packages/server/graphql/mutations/updatePokerScope.ts @@ -4,7 +4,6 @@ import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' import {ESTIMATE_TASK_SORT_ORDER} from '../../../client/utils/constants' import getRethink from '../../database/rethinkDriver' import EstimateStage from '../../database/types/EstimateStage' -import MeetingPoker from '../../database/types/MeetingPoker' import {TaskServiceEnum} from '../../database/types/Task' import getKysely from '../../postgres/getKysely' import {Discussion} from '../../postgres/pg' @@ -56,12 +55,14 @@ const updatePokerScope = { // Wrap everything in try catch to ensure the lock is released try { //AUTH - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingPoker + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) { return {error: {message: `Meeting not found`}} } - - const {endedAt, teamId, phases, meetingType, templateRefId, facilitatorStageId} = meeting + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } + const {endedAt, teamId, phases, templateRefId, facilitatorStageId} = meeting if (!isTeamMember(authToken, teamId)) { // bad actors could be naughty & just lock meetings that they don't own. Limit bad actors to team members return {error: {message: `Not on team`}} @@ -70,10 +71,6 @@ const updatePokerScope = { return {error: {message: `Meeting already ended`}} } - if (meetingType !== 'poker') { - return {error: {message: 'Not a poker meeting'}} - } - // RESOLUTION const estimatePhase = getPhase(phases, 'ESTIMATE') diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index db5c0beca24..7ad488253c9 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -4,8 +4,8 @@ import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import mode from 'parabol-client/utils/mode' import getRethink from '../../database/rethinkDriver' import {RValue} from '../../database/stricterR' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' import getKysely from '../../postgres/getKysely' +import {RetrospectiveMeeting} from '../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -44,7 +44,7 @@ const updateRetroMaxVotes = { const subOptions = {mutatorId, operationId} //AUTH - const meeting = (await r.table('NewMeeting').get(meetingId).run()) as MeetingRetrospective + const meeting = (await r.table('NewMeeting').get(meetingId).run()) as RetrospectiveMeeting if (!meeting) { return {error: {message: 'Meeting not found'}} diff --git a/packages/server/graphql/mutations/voteForPokerStory.ts b/packages/server/graphql/mutations/voteForPokerStory.ts index 76330da4d8a..15705e31292 100644 --- a/packages/server/graphql/mutations/voteForPokerStory.ts +++ b/packages/server/graphql/mutations/voteForPokerStory.ts @@ -3,7 +3,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import {RValue} from '../../database/stricterR' import EstimateUserScore from '../../database/types/EstimateUserScore' -import MeetingPoker from '../../database/types/MeetingPoker' import updateStage from '../../database/updateStage' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -71,20 +70,20 @@ const voteForPokerStory = { const subOptions = {mutatorId, operationId} //AUTH - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingPoker + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) { return {error: {message: 'Meeting not found'}} } - const {endedAt, phases, meetingType, teamId, templateRefId} = meeting + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } + const {endedAt, phases, teamId, templateRefId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on the team'}} } if (endedAt) { return {error: {message: 'Meeting has ended'}} } - if (meetingType !== 'poker') { - return {error: {message: 'Not a poker meeting'}} - } // No need to check for now (https://github.com/ParabolInc/parabol/issues/7191) // if (isPhaseComplete('ESTIMATE', phases)) { // return {error: {message: 'Estimate phase is already complete'}} diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index fe193c618c9..e8dd4e83135 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -3,7 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {VOTE} from 'parabol-client/utils/constants' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import getRethink from '../../database/rethinkDriver' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -43,7 +43,7 @@ export default { }) } const {meetingId} = reflectionGroup - const meeting = (await r.table('NewMeeting').get(meetingId).run()) as MeetingRetrospective + const meeting = (await r.table('NewMeeting').get(meetingId).run()) as RetrospectiveMeeting const {endedAt, phases, maxVotesPerGroup, teamId} = meeting if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 8dbb9b5a071..88f1ae6d604 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -1,7 +1,7 @@ import yaml from 'js-yaml' import getRethink from '../../../database/rethinkDriver' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import getKysely from '../../../postgres/getKysely' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import getPhase from '../../../utils/getPhase' import {MutationResolvers} from '../resolverTypes' @@ -32,7 +32,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) ) - .run()) as MeetingRetrospective[] + .run()) as RetrospectiveMeeting[] const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] @@ -87,7 +87,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn return comments } - const getMeetingsContent = async (meeting: MeetingRetrospective) => { + const getMeetingsContent = async (meeting: RetrospectiveMeeting) => { const pg = getKysely() const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting const rawReflectionGroups = await dataLoader diff --git a/packages/server/graphql/private/mutations/processRecurrence.ts b/packages/server/graphql/private/mutations/processRecurrence.ts index 32670a43866..eb6a8a2c362 100644 --- a/packages/server/graphql/private/mutations/processRecurrence.ts +++ b/packages/server/graphql/private/mutations/processRecurrence.ts @@ -5,11 +5,8 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DateTime, RRuleSet} from 'rrule-rust' import {fromDateTime, toDateTime} from '../../../../client/shared/rruleUtil' import getRethink from '../../../database/rethinkDriver' -import MeetingRetrospective, { - isMeetingRetrospective -} from '../../../database/types/MeetingRetrospective' -import MeetingTeamPrompt, {isMeetingTeamPrompt} from '../../../database/types/MeetingTeamPrompt' import {getActiveMeetingSeries} from '../../../postgres/queries/getActiveMeetingSeries' +import {RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting' import {MeetingSeries} from '../../../postgres/types/MeetingSeries' import {analytics} from '../../../utils/analytics/analytics' import {getNextRRuleDate} from '../../../utils/getNextRRuleDate' @@ -52,7 +49,7 @@ const startRecurringMeeting = async ( const meetingName = createMeetingSeriesTitle(meetingSeries.title, startTime, rrule.tzid) const meeting = await (async () => { if (meetingSeries.meetingType === 'teamPrompt') { - const teamPromptMeeting = lastMeeting as MeetingTeamPrompt | null + const teamPromptMeeting = lastMeeting as TeamPromptMeeting | null const meeting = await safeCreateTeamPrompt( meetingName, teamId, @@ -71,7 +68,7 @@ const startRecurringMeeting = async ( return meeting } else if (meetingSeries.meetingType === 'retrospective') { const {totalVotes, maxVotesPerGroup, disableAnonymity, templateId} = - (lastMeeting as MeetingRetrospective) ?? { + (lastMeeting as RetrospectiveMeeting) ?? { templateId: meetingSettings.selectedTemplateId, ...meetingSettings } @@ -130,9 +127,9 @@ const processRecurrence: MutationResolvers['processRecurrence'] = async ( const res = await tracer.trace('processRecurrence.endMeetings', async () => Promise.all( meetingsToEnd.map((meeting) => { - if (isMeetingTeamPrompt(meeting)) { + if (meeting.meetingType === 'teamPrompt') { return safeEndTeamPrompt({meeting, now, context, r, subOptions}) - } else if (isMeetingRetrospective(meeting)) { + } else if (meeting.meetingType === 'retrospective') { return safeEndRetrospective({meeting, now, context}) } else { return standardError(new Error('Unhandled recurring meeting type'), { diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts index 2b1c3b70575..bc93f4f84f8 100644 --- a/packages/server/graphql/private/mutations/runScheduledJobs.ts +++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts @@ -31,11 +31,11 @@ const processMeetingStageTimeLimits = async ( const notification = new NotificationMeetingStageTimeLimitEnd({ meetingId, - userId: facilitatorUserId + userId: facilitatorUserId! }) const r = await getRethink() await r.table('Notification').insert(notification).run() - publish(SubscriptionChannel.NOTIFICATION, facilitatorUserId, 'MeetingStageTimeLimitPayload', { + publish(SubscriptionChannel.NOTIFICATION, facilitatorUserId!, 'MeetingStageTimeLimitPayload', { notification }) } diff --git a/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts b/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts index 8a572dec1e0..9e7fee37513 100644 --- a/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts +++ b/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts @@ -1,4 +1,5 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' +import isValid from '../../isValid' import {GenerateMeetingSummarySuccessResolvers} from '../resolverTypes' export type GenerateMeetingSummarySuccessSource = { @@ -7,10 +8,8 @@ export type GenerateMeetingSummarySuccessSource = { const GenerateMeetingSummarySuccess: GenerateMeetingSummarySuccessResolvers = { meetings: async ({meetingIds}, _args, {dataLoader}) => { - const meetings = (await dataLoader - .get('newMeetings') - .loadMany(meetingIds)) as MeetingRetrospective[] - return meetings + const meetings = (await dataLoader.get('newMeetings').loadMany(meetingIds)).filter(isValid) + return meetings.filter((m) => m.meetingType === 'retrospective') as RetrospectiveMeeting[] } } diff --git a/packages/server/graphql/public/mutations/addTranscriptionBot.ts b/packages/server/graphql/public/mutations/addTranscriptionBot.ts index 6862a934f00..fb93454a901 100644 --- a/packages/server/graphql/public/mutations/addTranscriptionBot.ts +++ b/packages/server/graphql/public/mutations/addTranscriptionBot.ts @@ -1,5 +1,5 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../../utils/authorization' import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' @@ -14,7 +14,7 @@ const addTranscriptionBot: MutationResolvers['addTranscriptionBot'] = async ( const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting if (!meeting) { return standardError(new Error('Meeting not found'), {userId: viewerId}) } diff --git a/packages/server/graphql/public/mutations/endTeamPrompt.ts b/packages/server/graphql/public/mutations/endTeamPrompt.ts index 692ee81d339..fbc3efe7cb4 100644 --- a/packages/server/graphql/public/mutations/endTeamPrompt.ts +++ b/packages/server/graphql/public/mutations/endTeamPrompt.ts @@ -1,5 +1,5 @@ import getRethink from '../../../database/rethinkDriver' -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' +import {TeamPromptMeeting} from '../../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../../utils/authorization' import standardError from '../../../utils/standardError' import safeEndTeamPrompt from '../../mutations/helpers/safeEndTeamPrompt' @@ -14,7 +14,7 @@ const endTeamPrompt: MutationResolvers['endTeamPrompt'] = async (_source, {meeti const subOptions = {mutatorId, operationId} // AUTH - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingTeamPrompt | null + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as TeamPromptMeeting | null if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {teamId} = meeting diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index c80339a30f9..66e553ff231 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -1,6 +1,6 @@ import yaml from 'js-yaml' import getRethink from '../../../../database/rethinkDriver' -import MeetingRetrospective from '../../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../../postgres/types/Meeting' import OpenAIServerManager from '../../../../utils/OpenAIServerManager' import standardError from '../../../../utils/standardError' @@ -27,7 +27,7 @@ export const getSummaries = async ( .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) .and(row.hasFields('summary')) ) - .run()) as MeetingRetrospective[] + .run()) as RetrospectiveMeeting[] if (!rawMeetings.length) { return standardError(new Error('No meetings found')) diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index 4cb74174c5b..61d5366db17 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -1,7 +1,7 @@ import yaml from 'js-yaml' import getRethink from '../../../../database/rethinkDriver' -import MeetingRetrospective from '../../../../database/types/MeetingRetrospective' import getKysely from '../../../../postgres/getKysely' +import {RetrospectiveMeeting} from '../../../../postgres/types/Meeting' import OpenAIServerManager from '../../../../utils/OpenAIServerManager' import sendToSentry from '../../../../utils/sendToSentry' import standardError from '../../../../utils/standardError' @@ -134,7 +134,7 @@ export const getTopics = async ( disableAnonymity, name: meetingName, createdAt: meetingDate - } = meeting as MeetingRetrospective + } = meeting as RetrospectiveMeeting const rawReflectionGroups = await dataLoader .get('retroReflectionGroupsByMeetingId') .load(meetingId) diff --git a/packages/server/graphql/public/mutations/resetReflectionGroups.ts b/packages/server/graphql/public/mutations/resetReflectionGroups.ts index c52fc4361a3..4f574390d0f 100644 --- a/packages/server/graphql/public/mutations/resetReflectionGroups.ts +++ b/packages/server/graphql/public/mutations/resetReflectionGroups.ts @@ -75,7 +75,7 @@ const resetReflectionGroups: MutationResolvers['resetReflectionGroups'] = async .get(meetingId) .replace(r.row.without('resetReflectionGroups') as any) .run() - meeting.resetReflectionGroups = undefined + meeting.resetReflectionGroups = null analytics.resetGroupsClicked(viewer, meetingId, teamId) const data = {meetingId} publish(SubscriptionChannel.MEETING, meetingId, 'ResetReflectionGroupsSuccess', data, subOptions) diff --git a/packages/server/graphql/public/mutations/setTeamHealthVote.ts b/packages/server/graphql/public/mutations/setTeamHealthVote.ts index d186c7940af..7f3fe36af53 100644 --- a/packages/server/graphql/public/mutations/setTeamHealthVote.ts +++ b/packages/server/graphql/public/mutations/setTeamHealthVote.ts @@ -3,12 +3,44 @@ import getRethink from '../../../database/rethinkDriver' import {RValue} from '../../../database/stricterR' import TeamHealthVote from '../../../database/types/TeamHealthVote' import updateStage from '../../../database/updateStage' +import getKysely from '../../../postgres/getKysely' +import {NewMeetingPhase} from '../../../postgres/types/NewMeetingPhase.d' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' import {MutationResolvers} from '../resolverTypes' const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealthVote) => { + const pg = getKysely() + await pg.transaction().execute(async (trx) => { + // console.log('start transaction', newVote) + const meeting = await trx + .selectFrom('NewMeeting') + .select(({fn}) => fn('to_json', ['phases']).as('phases')) + .where('id', '=', meetingId) + .forUpdate() + // NewMeeting: add OrThrow in phase 3 + .executeTakeFirst() + if (!meeting) return + // console.log('got lock', newVote) + const {phases} = meeting + const phase = getPhase(phases, 'TEAM_HEALTH') + const {stages} = phase + const [stage] = stages + const {votes} = stage + const existingVote = votes.find((vote) => vote.userId === newVote.userId) + if (existingVote) { + existingVote.vote = newVote.vote + } else { + votes.push(newVote) + } + await trx + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + // console.log('wrote update, commit', newVote) + }) const r = await getRethink() const updater = (stage: RValue) => stage.merge({ diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index b90ffd146a7..62086ac17ba 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -5,7 +5,8 @@ import MeetingAction from '../../../database/types/MeetingAction' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' -import {MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {CheckInMeeting, MeetingTypeEnum} from '../../../postgres/types/Meeting' +import {CheckInPhase} from '../../../postgres/types/NewMeetingPhase' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -48,7 +49,7 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( .run() const meetingId = generateUID() - const phases = await createNewMeetingPhases( + const phases = await createNewMeetingPhases( viewerId, teamId, meetingId, @@ -64,7 +65,7 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( meetingCount, phases, facilitatorUserId: viewerId - }) + }) as CheckInMeeting await r.table('NewMeeting').insert(meeting).run() // Disallow 2 active check-in meetings diff --git a/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts b/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts index 63540c4507b..217bde46903 100644 --- a/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts +++ b/packages/server/graphql/public/mutations/updateGitLabDimensionField.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import MeetingPoker from '../../../database/types/MeetingPoker' import upsertGitLabDimensionFieldMap from '../../../postgres/queries/upsertGitLabDimensionFieldMap' import {Logger} from '../../../utils/Logger' import {isTeamMember} from '../../../utils/authorization' @@ -20,7 +19,10 @@ const updateGitLabDimensionField: MutationResolvers['updateGitLabDimensionField' if (!meeting) { return {error: {message: 'Invalid meetingId'}} } - const {teamId, templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } + const {teamId, templateRefId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on team'}} } diff --git a/packages/server/graphql/public/mutations/updateJiraDimensionField.ts b/packages/server/graphql/public/mutations/updateJiraDimensionField.ts index fd750b7c978..b99f44c49be 100644 --- a/packages/server/graphql/public/mutations/updateJiraDimensionField.ts +++ b/packages/server/graphql/public/mutations/updateJiraDimensionField.ts @@ -1,6 +1,5 @@ import {SprintPokerDefaults, SubscriptionChannel} from 'parabol-client/types/constEnums' import JiraProjectKeyId from '../../../../client/shared/gqlIds/JiraProjectKeyId' -import MeetingPoker from '../../../database/types/MeetingPoker' import {JiraIssue} from '../../../dataloader/atlassianLoaders' import upsertJiraDimensionFieldMap from '../../../postgres/queries/upsertJiraDimensionFieldMap' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -38,7 +37,10 @@ const updateJiraDimensionField: MutationResolvers['updateJiraDimensionField'] = if (!meeting) { return {error: {message: 'Invalid meetingId'}} } - const {teamId, templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } + const {teamId, templateRefId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on team'}} } diff --git a/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts b/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts index e37cde5efb5..2443adc0121 100644 --- a/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts +++ b/packages/server/graphql/public/mutations/updateJiraServerDimensionField.ts @@ -1,5 +1,4 @@ import {SprintPokerDefaults, SubscriptionChannel} from 'parabol-client/types/constEnums' -import MeetingPoker from '../../../database/types/MeetingPoker' import JiraServerRestManager from '../../../integrations/jiraServer/JiraServerRestManager' import {IntegrationProviderJiraServer} from '../../../postgres/queries/getIntegrationProvidersByIds' import upsertJiraServerDimensionFieldMap from '../../../postgres/queries/upsertJiraServerDimensionFieldMap' @@ -21,7 +20,10 @@ const updateJiraServerDimensionField: MutationResolvers['updateJiraServerDimensi if (!meeting) { return {error: {message: 'Invalid meetingId'}} } - const {teamId, templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } + const {teamId, templateRefId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on team'}} } diff --git a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts index a0be265f8de..b81fb77f628 100644 --- a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts +++ b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts @@ -1,6 +1,6 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' @@ -16,7 +16,7 @@ const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) if (!isTeamMember(authToken, meeting.teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index a41aea04e18..18f93e412b1 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -22,7 +22,7 @@ export const startNewMeetingSeries = async ( teamId: string meetingType: MeetingTypeEnum name: string - facilitatorUserId: string + facilitatorUserId: string | null }, recurrenceRule: RRuleSet, meetingSeriesName?: string | null @@ -35,7 +35,9 @@ export const startNewMeetingSeries = async ( facilitatorUserId: facilitatorId } = meeting const r = await getRethink() - + if (!facilitatorId) { + throw new Error('No facilitatorId') + } const newMeetingSeriesParams = { meetingType, title: meetingSeriesName || meetingName.split('-')[0]!.trim(), // if no name is provided, we use the name of the first meeting without the date diff --git a/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts index 880fb7e8538..1b2d3073bcc 100644 --- a/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts +++ b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {AddTranscriptionBotSuccessResolvers} from '../resolverTypes' export type AddTranscriptionBotSuccessSource = { @@ -8,7 +8,7 @@ export type AddTranscriptionBotSuccessSource = { const AddTranscriptionBotSuccess: AddTranscriptionBotSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingRetrospective + return meeting as RetrospectiveMeeting } } diff --git a/packages/server/graphql/public/types/AutogroupSuccess.ts b/packages/server/graphql/public/types/AutogroupSuccess.ts index 9c62aced1b3..61a474883ff 100644 --- a/packages/server/graphql/public/types/AutogroupSuccess.ts +++ b/packages/server/graphql/public/types/AutogroupSuccess.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {AutogroupSuccessResolvers} from '../resolverTypes' export type AutogroupSuccessSource = { @@ -8,7 +8,7 @@ export type AutogroupSuccessSource = { const AutogroupSuccess: AutogroupSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingRetrospective + return meeting as RetrospectiveMeeting } } diff --git a/packages/server/graphql/public/types/Discussion.ts b/packages/server/graphql/public/types/Discussion.ts index 765d795215d..bf3f5908851 100644 --- a/packages/server/graphql/public/types/Discussion.ts +++ b/packages/server/graphql/public/types/Discussion.ts @@ -46,7 +46,9 @@ const Discussion: DiscussionResolvers = { return null } const {stages} = phase - const dbStage = stages.find((stage) => stage.reflectionGroupId === discussionTopicId) + const dbStage = stages.find( + (stage) => 'reflectionGroupId' in stage && stage.reflectionGroupId === discussionTopicId + ) return dbStage ? augmentDBStage(dbStage, meetingId, DISCUSS, teamId) : null } @@ -56,7 +58,9 @@ const Discussion: DiscussionResolvers = { return null } const {stages} = phase - const dbStage = stages.find((stage) => stage.taskId === discussionTopicId) + const dbStage = stages.find( + (stage) => 'taskId' in stage && stage.taskId === discussionTopicId + ) return dbStage ? augmentDBStage(dbStage, meetingId, 'ESTIMATE', teamId) : null } diff --git a/packages/server/graphql/public/types/EndTeamPromptSuccess.ts b/packages/server/graphql/public/types/EndTeamPromptSuccess.ts index a802a25f5aa..2452403289d 100644 --- a/packages/server/graphql/public/types/EndTeamPromptSuccess.ts +++ b/packages/server/graphql/public/types/EndTeamPromptSuccess.ts @@ -1,4 +1,3 @@ -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {EndTeamPromptSuccessResolvers} from '../resolverTypes' export type EndTeamPromptSuccessSource = { @@ -9,7 +8,9 @@ export type EndTeamPromptSuccessSource = { const EndTeamPromptSuccess: EndTeamPromptSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - return (await dataLoader.get('newMeetings').load(meetingId)) as MeetingTeamPrompt + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (meeting.meetingType !== 'teamPrompt') throw new Error('Meeting is not a team prompt') + return meeting }, team: async ({teamId}, _args, {dataLoader}) => { return await dataLoader.get('teams').loadNonNull(teamId) diff --git a/packages/server/graphql/public/types/EstimateStage.ts b/packages/server/graphql/public/types/EstimateStage.ts index 112e2c91bc5..9e949ac0de7 100644 --- a/packages/server/graphql/public/types/EstimateStage.ts +++ b/packages/server/graphql/public/types/EstimateStage.ts @@ -1,6 +1,5 @@ import JiraProjectKeyId from '../../../../client/shared/gqlIds/JiraProjectKeyId' import {SprintPokerDefaults} from '../../../../client/types/constEnums' -import MeetingPoker from '../../../database/types/MeetingPoker' import TaskIntegrationAzureDevOps from '../../../database/types/TaskIntegrationAzureDevOps' import TaskIntegrationJiraServer from '../../../database/types/TaskIntegrationJiraServer' import GitLabServerManager from '../../../integrations/gitlab/GitLabServerManager' @@ -23,7 +22,8 @@ const EstimateStage: EstimateStageResolvers = { const {service} = integration const getDimensionName = async (meetingId: string) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - const {templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') throw new Error('Meeting is not a poker meeting') + const {templateRefId} = meeting const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) const {dimensions} = templateRef const dimensionRef = dimensions[dimensionRefIdx]! @@ -176,7 +176,8 @@ const EstimateStage: EstimateStageResolvers = { dimensionRef: async ({meetingId, dimensionRefIdx}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - const {templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') return null + const {templateRefId} = meeting const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) const {dimensions} = templateRef const {name, scaleRefId} = dimensions[dimensionRefIdx]! @@ -193,7 +194,8 @@ const EstimateStage: EstimateStageResolvers = { dataLoader.get('newMeetings').load(meetingId), dataLoader.get('meetingTaskEstimates').load({taskId, meetingId}) ]) - const {templateRefId} = meeting as MeetingPoker + if (meeting.meetingType !== 'poker') return null + const {templateRefId} = meeting const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) const {dimensions} = templateRef const dimensionRef = dimensions[dimensionRefIdx]! diff --git a/packages/server/graphql/public/types/GenerateGroupsSuccess.ts b/packages/server/graphql/public/types/GenerateGroupsSuccess.ts index 421d0142de3..b08a5c03810 100644 --- a/packages/server/graphql/public/types/GenerateGroupsSuccess.ts +++ b/packages/server/graphql/public/types/GenerateGroupsSuccess.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {GenerateGroupsSuccessResolvers} from '../resolverTypes' export type GenerateGroupsSuccessSource = { @@ -8,7 +8,7 @@ export type GenerateGroupsSuccessSource = { const GenerateGroupsSuccess: GenerateGroupsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingRetrospective + return meeting as RetrospectiveMeeting } } diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts index ac099aa2295..4b8c3fa989b 100644 --- a/packages/server/graphql/public/types/GenerateInsightSuccess.ts +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {GenerateInsightSuccessResolvers} from '../resolverTypes' export type GenerateInsightSuccessSource = { @@ -13,7 +13,7 @@ const GenerateInsightSuccess: GenerateInsightSuccessResolvers = { meetings: async ({meetingIds}, _args, {dataLoader}) => { const meetings = (await dataLoader .get('newMeetings') - .loadMany(meetingIds)) as MeetingRetrospective[] + .loadMany(meetingIds)) as RetrospectiveMeeting[] return meetings } } diff --git a/packages/server/graphql/public/types/NewMeeting.ts b/packages/server/graphql/public/types/NewMeeting.ts index d75bc19bceb..941809c0297 100644 --- a/packages/server/graphql/public/types/NewMeeting.ts +++ b/packages/server/graphql/public/types/NewMeeting.ts @@ -19,7 +19,7 @@ const NewMeeting: NewMeetingResolvers = { return dataLoader.get('users').loadNonNull(createdBy) }, facilitator: ({facilitatorUserId, teamId}, _args, {dataLoader}) => { - const teamMemberId = toTeamMemberId(teamId, facilitatorUserId) + const teamMemberId = toTeamMemberId(teamId, facilitatorUserId!) return dataLoader.get('teamMembers').loadNonNull(teamMemberId) }, locked: async ({endedAt, teamId}, _args, {authToken, dataLoader}) => { diff --git a/packages/server/graphql/public/types/NotifyResponseMentioned.ts b/packages/server/graphql/public/types/NotifyResponseMentioned.ts index 0a8b86906a5..a9bc5d31501 100644 --- a/packages/server/graphql/public/types/NotifyResponseMentioned.ts +++ b/packages/server/graphql/public/types/NotifyResponseMentioned.ts @@ -1,12 +1,12 @@ import TeamPromptResponseId from '../../../../client/shared/gqlIds/TeamPromptResponseId' -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {NotifyResponseMentionedResolvers} from '../resolverTypes' const NotifyResponseMentioned: NotifyResponseMentionedResolvers = { __isTypeOf: ({type}) => type === 'RESPONSE_MENTIONED', meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingTeamPrompt + if (meeting.meetingType !== 'teamPrompt') throw new Error('Meeting is not a team prompt') + return meeting }, response: ({responseId}, _args, {dataLoader}) => { // Hack, in a perfect world, this notification would have the numeric DB ID saved on it diff --git a/packages/server/graphql/public/types/NotifyResponseReplied.ts b/packages/server/graphql/public/types/NotifyResponseReplied.ts index 883a71d93bb..87ff6af7198 100644 --- a/packages/server/graphql/public/types/NotifyResponseReplied.ts +++ b/packages/server/graphql/public/types/NotifyResponseReplied.ts @@ -1,4 +1,3 @@ -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' import {NotifyResponseRepliedResolvers} from '../resolverTypes' @@ -6,7 +5,8 @@ const NotifyResponseReplied: NotifyResponseRepliedResolvers = { __isTypeOf: ({type}) => type === 'RESPONSE_REPLIED', meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingTeamPrompt + if (meeting.meetingType !== 'teamPrompt') throw new Error('Meeting is not a team prompt') + return meeting }, response: async ({userId, meetingId}) => { // TODO: implement getTeamPromptResponsesByMeetingIdAndUserId diff --git a/packages/server/graphql/public/types/ReflectPhase.ts b/packages/server/graphql/public/types/ReflectPhase.ts index 8714fad14a0..d78d857ed2f 100644 --- a/packages/server/graphql/public/types/ReflectPhase.ts +++ b/packages/server/graphql/public/types/ReflectPhase.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {ReflectPhaseResolvers} from '../resolverTypes' const ReflectPhase: ReflectPhaseResolvers = { @@ -9,7 +9,7 @@ const ReflectPhase: ReflectPhaseResolvers = { }, reflectPrompts: async ({meetingId}, _args, {dataLoader}) => { - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(meeting.templateId) // only show prompts that were created before the meeting and // either have not been removed or they were removed after the meeting was created diff --git a/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts b/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts index e15430efc9d..ad5e3d30dbe 100644 --- a/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts +++ b/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {ResetReflectionGroupsSuccessResolvers} from '../resolverTypes' export type ResetReflectionGroupsSuccessSource = { @@ -8,7 +8,7 @@ export type ResetReflectionGroupsSuccessSource = { const ResetReflectionGroupsSuccess: ResetReflectionGroupsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingRetrospective + return meeting as RetrospectiveMeeting } } diff --git a/packages/server/graphql/public/types/RetroDiscussStage.ts b/packages/server/graphql/public/types/RetroDiscussStage.ts index cd98f071fb3..0fb5a5dc6a2 100644 --- a/packages/server/graphql/public/types/RetroDiscussStage.ts +++ b/packages/server/graphql/public/types/RetroDiscussStage.ts @@ -1,5 +1,5 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import ReflectionGroup from '../../../database/types/ReflectionGroup' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {RetroDiscussStageResolvers} from '../resolverTypes' // note: this is the GraphQL type, not source DB type @@ -27,7 +27,7 @@ const RetroDiscussStage: RetroDiscussStageResolvers = { reflectionGroup: async ({reflectionGroupId, meetingId}, _args, {dataLoader}) => { if (!reflectionGroupId) { - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(meeting.templateId) return new ReflectionGroup({ id: `${meetingId}:dummyGroup`, diff --git a/packages/server/graphql/public/types/RetroReflection.ts b/packages/server/graphql/public/types/RetroReflection.ts index 80c967c6797..4a7fd7fbd1d 100644 --- a/packages/server/graphql/public/types/RetroReflection.ts +++ b/packages/server/graphql/public/types/RetroReflection.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId, isSuperUser} from '../../../utils/authorization' import getGroupedReactjis from '../../../utils/getGroupedReactjis' import {RetroReflectionResolvers} from '../resolverTypes' @@ -35,7 +35,7 @@ const RetroReflection: RetroReflectionResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingRetrospective + return meeting as RetrospectiveMeeting }, prompt: ({promptId}, _args, {dataLoader}) => { diff --git a/packages/server/graphql/public/types/RetroReflectionGroup.ts b/packages/server/graphql/public/types/RetroReflectionGroup.ts index 2c0834b7635..a49f128271d 100644 --- a/packages/server/graphql/public/types/RetroReflectionGroup.ts +++ b/packages/server/graphql/public/types/RetroReflectionGroup.ts @@ -1,6 +1,6 @@ import {Selectable} from 'kysely' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import {RetroReflectionGroup as TRetroReflectionGroup} from '../../../postgres/pg' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId} from '../../../utils/authorization' import {RetroReflectionGroupResolvers} from '../resolverTypes' @@ -9,7 +9,7 @@ export interface RetroReflectionGroupSource extends Selectable { const retroMeeting = await dataLoader.get('newMeetings').load(meetingId) - return retroMeeting as MeetingRetrospective + return retroMeeting as RetrospectiveMeeting }, prompt: ({promptId}, _args, {dataLoader}) => { return dataLoader.get('reflectPrompts').loadNonNull(promptId) diff --git a/packages/server/graphql/public/types/StartCheckInSuccess.ts b/packages/server/graphql/public/types/StartCheckInSuccess.ts index 0dedaf9905c..7b44a4004ce 100644 --- a/packages/server/graphql/public/types/StartCheckInSuccess.ts +++ b/packages/server/graphql/public/types/StartCheckInSuccess.ts @@ -1,4 +1,3 @@ -import MeetingAction from '../../../database/types/MeetingAction' import {StartCheckInSuccessResolvers} from '../resolverTypes' export type StartCheckInSuccessSource = { @@ -7,8 +6,10 @@ export type StartCheckInSuccessSource = { } const StartCheckInSuccess: StartCheckInSuccessResolvers = { - meeting: ({meetingId}, _args, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) as Promise + meeting: async ({meetingId}, _args, {dataLoader}) => { + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (meeting.meetingType !== 'action') throw new Error('Not a check-in meeting') + return meeting }, team: ({teamId}, _args, {dataLoader}) => { return dataLoader.get('teams').loadNonNull(teamId) diff --git a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts index 4ba4baef9be..c5c60bc715d 100644 --- a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts +++ b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {StartRetrospectiveSuccessResolvers} from '../resolverTypes' export type StartRetrospectiveSuccessSource = { @@ -9,7 +9,7 @@ export type StartRetrospectiveSuccessSource = { const StartRetrospectiveSuccess: StartRetrospectiveSuccessResolvers = { meeting: ({meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) as Promise + return dataLoader.get('newMeetings').load(meetingId) as Promise }, team: ({teamId}, _args: unknown, {dataLoader}) => { return dataLoader.get('teams').loadNonNull(teamId) diff --git a/packages/server/graphql/public/types/StartTeamPromptSuccess.ts b/packages/server/graphql/public/types/StartTeamPromptSuccess.ts index 15f9fcff241..017c7b63180 100644 --- a/packages/server/graphql/public/types/StartTeamPromptSuccess.ts +++ b/packages/server/graphql/public/types/StartTeamPromptSuccess.ts @@ -1,4 +1,3 @@ -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {StartTeamPromptSuccessResolvers} from '../resolverTypes' export type StartTeamPromptSuccessSource = { @@ -8,7 +7,9 @@ export type StartTeamPromptSuccessSource = { const StartTeamPromptSuccess: StartTeamPromptSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) as Promise + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (meeting.meetingType !== 'teamPrompt') throw new Error('Not a team prompt meeting') + return meeting }, team: async ({teamId}, _args, {dataLoader}) => { return dataLoader.get('teams').loadNonNull(teamId) diff --git a/packages/server/graphql/public/types/TeamPromptMeeting.ts b/packages/server/graphql/public/types/TeamPromptMeeting.ts index 1bf595b202d..97e89b35548 100644 --- a/packages/server/graphql/public/types/TeamPromptMeeting.ts +++ b/packages/server/graphql/public/types/TeamPromptMeeting.ts @@ -1,7 +1,7 @@ import getRethink from '../../../database/rethinkDriver' import {RValue} from '../../../database/stricterR' -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' +import {TeamPromptMeeting} from '../../../postgres/types/Meeting' import {getUserId} from '../../../utils/authorization' import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' import getPhase from '../../../utils/getPhase' @@ -28,7 +28,7 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { .limit(1) .run() - return meetings[0] as MeetingTeamPrompt + return meetings[0] as TeamPromptMeeting }, nextMeeting: async ({meetingSeriesId, createdAt}, _args, {dataLoader}) => { if (!meetingSeriesId) return null @@ -48,7 +48,7 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { .limit(1) .run() - return meetings[0] as MeetingTeamPrompt + return meetings[0] as TeamPromptMeeting }, tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) diff --git a/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts b/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts index d74d4f47c02..a6e28d189a1 100644 --- a/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts +++ b/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts @@ -1,4 +1,3 @@ -import MeetingPoker from '../../../database/types/MeetingPoker' import {UpdateDimensionFieldSuccessResolvers} from '../resolverTypes' export type UpdateDimensionFieldSuccessSource = { @@ -10,7 +9,8 @@ const UpdateDimensionFieldSuccess: UpdateDimensionFieldSuccessResolvers = { team: ({teamId}, _args, {dataLoader}) => dataLoader.get('teams').loadNonNull(teamId), meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as MeetingPoker + if (meeting.meetingType !== 'poker') throw new Error('Not a poker meeting') + return meeting } } diff --git a/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts b/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts index 50d348e1e46..0873d892c7d 100644 --- a/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts +++ b/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts @@ -1,4 +1,3 @@ -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {UpdateMeetingPromptSuccessResolvers} from '../resolverTypes' export type UpdateMeetingPromptSuccessSource = { @@ -8,7 +7,9 @@ export type UpdateMeetingPromptSuccessSource = { const UpdateMeetingPromptSuccess: UpdateMeetingPromptSuccessResolvers = { meeting: async (source, _args, {dataLoader}) => { const {meetingId} = source - return dataLoader.get('newMeetings').load(meetingId) as Promise + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (meeting.meetingType !== 'teamPrompt') throw new Error('Not a team prompt meeting') + return meeting } } diff --git a/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts b/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts index b94a914ca27..d9e4f566501 100644 --- a/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts +++ b/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts @@ -1,4 +1,3 @@ -import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {UpdateRecurrenceSettingsSuccessResolvers} from '../resolverTypes' export type UpdateRecurrenceSettingsSuccessSource = { @@ -7,7 +6,9 @@ export type UpdateRecurrenceSettingsSuccessSource = { const UpdateRecurrenceSettingsSuccess: UpdateRecurrenceSettingsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) as Promise + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (meeting.meetingType !== 'teamPrompt') throw new Error('Not a team prompt') + return meeting } } diff --git a/packages/server/graphql/resolvers.ts b/packages/server/graphql/resolvers.ts index b02819208d0..09c762ab07a 100644 --- a/packages/server/graphql/resolvers.ts +++ b/packages/server/graphql/resolvers.ts @@ -3,7 +3,6 @@ import nullIfEmpty from 'parabol-client/utils/nullIfEmpty' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {NewMeetingPhaseTypeEnum} from '../database/types/GenericMeetingPhase' import GenericMeetingStage from '../database/types/GenericMeetingStage' -import Meeting from '../database/types/Meeting' import Organization from '../database/types/Organization' import Task from '../database/types/Task' import User from '../database/types/User' @@ -116,7 +115,7 @@ export const resolveTeamMembers = ( : teamMembers } -export const resolveGQLStageFromId = (stageId: string | undefined, meeting: Meeting) => { +export const resolveGQLStageFromId = (stageId: string | undefined, meeting: AnyMeeting) => { const {id: meetingId, phases} = meeting const stageRes = findStageById(phases, stageId) if (!stageRes) return undefined diff --git a/packages/server/graphql/types/SetPhaseFocusPayload.ts b/packages/server/graphql/types/SetPhaseFocusPayload.ts index 3bb578e72fc..7f6147a522d 100644 --- a/packages/server/graphql/types/SetPhaseFocusPayload.ts +++ b/packages/server/graphql/types/SetPhaseFocusPayload.ts @@ -1,6 +1,6 @@ import {GraphQLNonNull, GraphQLObjectType} from 'graphql' import {REFLECT} from 'parabol-client/utils/constants' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' +import {RetrospectiveMeeting as RetrospectiveMeetingSource} from '../../postgres/types/Meeting' import {GQLContext} from '../graphql' import {resolveNewMeeting} from '../resolvers' import ReflectPhase from './ReflectPhase' @@ -26,7 +26,7 @@ const SetPhaseFocusPayload = new GraphQLObjectType({ ) => { const meeting = (await dataLoader .get('newMeetings') - .load(meetingId)) as MeetingRetrospective + .load(meetingId)) as RetrospectiveMeetingSource return meeting.phases.find((phase) => phase.phaseType === REFLECT) } } diff --git a/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts index 2e5def71db7..357686f5068 100644 --- a/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts +++ b/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts @@ -20,7 +20,7 @@ export async function up() { "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), "createdBy" VARCHAR(100), "endedAt" TIMESTAMP WITH TIME ZONE, - "facilitatorStageId" VARCHAR(100), + "facilitatorStageId" VARCHAR(100) NOT NULL, "facilitatorUserId" VARCHAR(100), "meetingCount" INT NOT NULL, "meetingNumber" INT NOT NULL, diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index affc2e06e00..d0eacbffb2e 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -2,8 +2,8 @@ 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' - +import {AutogroupReflectionGroupType, ReactjiDB, TranscriptBlock, UsedReactjis} from './types' +import type {NewMeetingPhase} from './types/NewMeetingPhase' export const selectTimelineEvent = () => { return getKysely().selectFrom('TimelineEvent').selectAll().$narrowType< | { @@ -230,3 +230,53 @@ export const selectComments = () => .select(({fn}) => [fn('to_json', ['reactjis']).as('reactjis')]) export const selectReflectPrompts = () => getKysely().selectFrom('ReflectPrompt').selectAll() + +export const selectNewMeetings = () => + getKysely() + .selectFrom('NewMeeting') + .select(({fn}) => [ + 'id', + 'isLegacy', + 'createdAt', + 'updatedAt', + 'createdBy', + 'endedAt', + 'facilitatorStageId', + 'facilitatorUserId', + 'meetingCount', + 'meetingNumber', + 'name', + 'summarySentAt', + 'teamId', + 'meetingType', + 'showConversionModal', + 'meetingSeriesId', + 'scheduledEndTime', + 'summary', + 'sentimentScore', + 'slackTs', + 'engagement', + 'totalVotes', + 'maxVotesPerGroup', + 'disableAnonymity', + 'commentCount', + 'taskCount', + 'agendaItemCount', + 'storyCount', + 'templateId', + 'topicCount', + 'reflectionCount', + 'recallBotId', + 'videoMeetingURL', + 'templateRefId', + 'meetingPrompt', + fn('to_json', ['phases']).as('phases'), + fn('to_json', ['usedReactjis']).as('usedReactjis'), + fn('to_json', ['transcription']).as('transcription'), + fn('to_json', ['autogroupReflectionGroups']).as( + 'autogroupReflectionGroups' + ), + fn('to_json', ['resetReflectionGroups']).as( + 'resetReflectionGroups' + ) + ]) diff --git a/packages/server/postgres/types/Meeting.d.ts b/packages/server/postgres/types/Meeting.d.ts index 65d7995ce18..b4d38c02e0e 100644 --- a/packages/server/postgres/types/Meeting.d.ts +++ b/packages/server/postgres/types/Meeting.d.ts @@ -1,16 +1,97 @@ +import {NonNullableProps} from '../../../client/types/generics' import ActionMeetingMember from '../../database/types/ActionMeetingMember' -import MeetingAction from '../../database/types/MeetingAction' -import MeetingPoker from '../../database/types/MeetingPoker' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' -import MeetingTeamPrompt from '../../database/types/MeetingTeamPrompt' import PokerMeetingMember from '../../database/types/PokerMeetingMember' import RetroMeetingMember from '../../database/types/RetroMeetingMember' import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember' -import {MeetingTypeEnum} from '../queries/generated/insertTeamQuery' +import {NewMeeting as NewMeetingDB} from '../pg' +import {NewMeeting} from './index.d' -export {MeetingTypeEnum} +import {Insertable} from 'kysely' +import { + CheckInMeetingPhase, + NewMeetingPhase, + PokerMeetingPhase, + RetroMeetingPhase, + TeamPromptPhase +} from './NewMeetingPhase' -export type AnyMeeting = MeetingRetrospective | MeetingPoker | MeetingAction | MeetingTeamPrompt +export type MeetingTypeEnum = NewMeeting['meetingType'] + +type BaseNewMeeting = Pick< + NewMeeting, + | 'id' + | 'isLegacy' + | 'createdAt' + | 'updatedAt' + | 'createdBy' + | 'endedAt' + | 'facilitatorStageId' + | 'facilitatorUserId' + | 'meetingCount' + | 'meetingNumber' + | 'name' + | 'summarySentAt' + | 'teamId' + | 'meetingType' + | 'showConversionModal' + | 'meetingSeriesId' + | 'scheduledEndTime' + | 'summary' + | 'sentimentScore' + | 'usedReactjis' + | 'slackTs' + | 'engagement' +> & {phases: NewMeetingPhase[]} + +type InsertableRetrospectiveMeeting = Insertable & { + meetingType: 'retrospective' + phases: RetroMeetingPhase[] + totalVotes: number + maxVotesPerGroup: number + disableAnonymity: boolean + templateId: string +} + +export type RetrospectiveMeeting = BaseNewMeeting & + NonNullableProps< + Pick + > & + Pick< + NewMeeting, + | 'commentCount' + | 'taskCount' + | 'topicCount' + | 'reflectionCount' + | 'transcription' + | 'recallBotId' + | 'videoMeetingURL' + | 'autogroupReflectionGroups' + | 'resetReflectionGroups' + > & { + meetingType: 'retrospective' + phases: RetroMeetingPhase[] + } + +export type PokerMeeting = BaseNewMeeting & + NonNullableProps> & + Pick & { + meetingType: 'poker' + phases: PokerMeetingPhase[] + } + +export type CheckInMeeting = BaseNewMeeting & + Pick & { + meetingType: 'action' + phases: CheckInMeetingPhase[] + } + +export type TeamPromptMeeting = BaseNewMeeting & + NonNullableProps> & { + meetingType: 'teamPrompt' + phases: TeamPromptPhase[] + } + +export type AnyMeeting = RetrospectiveMeeting | PokerMeeting | CheckInMeeting | TeamPromptMeeting export type AnyMeetingTeamMember = | PokerMeetingMember diff --git a/packages/server/postgres/types/NewMeetingPhase.d.ts b/packages/server/postgres/types/NewMeetingPhase.d.ts new file mode 100644 index 00000000000..6569deba9db --- /dev/null +++ b/packages/server/postgres/types/NewMeetingPhase.d.ts @@ -0,0 +1,204 @@ +interface GenericMeetingStage { + id: string + isAsync?: boolean | null + isComplete: boolean + isNavigable: boolean + isNavigableByFacilitator: boolean + startAt?: Date + endAt?: Date + scheduledEndTime?: Date | null + suggestedEndTime?: Date + suggestedTimeLimit?: number + viewCount: number + readyToAdvance?: string[] + phaseType: string +} + +interface AgendaItemStage extends GenericMeetingStage { + phaseType: 'agendaitems' + agendaItemId: string + discussionId: string +} + +interface CheckInStage extends GenericMeetingStage { + phaseType: 'checkin' + teamMemberId: string + durations?: number[] +} + +interface DiscussStage extends GenericMeetingStage { + phaseType: 'discuss' + reflectionGroupId: string + discussionId: string + sortOrder: number +} + +interface EstimateStage extends GenericMeetingStage { + phaseType: 'ESTIMATE' + creatorUserId: string + serviceTaskId: string + taskId: string + sortOrder: number + dimensionRefIdx: number + finalScore?: number + scores: { + userId: string + label: string + }[] + isVoting: boolean + discussionId: string +} + +interface ReflectStage extends GenericMeetingStage { + phaseType: 'reflect' +} + +interface TeamHealthStage extends GenericMeetingStage { + phaseType: 'TEAM_HEALTH' + votes: { + userId: string + vote: number + }[] + isRevealed: boolean + question: string + labels: string[] + durations?: number[] +} + +interface TeamPromptResponseStage extends GenericMeetingStage { + phaseType: 'RESPONSES' + teamMemberId: string + discussionId: string +} + +interface UpdatesStage extends GenericMeetingStage { + phaseType: 'updates' + teamMemberId: string + durations?: number[] +} + +interface FirstCallStage extends GenericMeetingStage { + phaseType: 'firstcall' +} + +interface LastCallStage extends GenericMeetingStage { + phaseType: 'lastcall' +} + +interface GroupStage extends GenericMeetingStage { + phaseType: 'group' +} + +interface VoteStage extends GenericMeetingStage { + phaseType: 'vote' +} + +interface ScopeStage extends GenericMeetingStage { + phaseType: 'SCOPE' +} + +interface GenericMeetingPhase { + id: string +} + +interface FirstCallPhase extends GenericMeetingPhase { + phaseType: 'firstcall' + stages: [FirstCallStage] +} + +interface LastCallPhase extends GenericMeetingPhase { + phaseType: 'lastcall' + stages: [LastCallStage] +} + +interface GroupPhase extends GenericMeetingPhase { + phaseType: 'group' + stages: [GroupStage] +} + +interface VotePhase extends GenericMeetingPhase { + phaseType: 'vote' + stages: [VoteStage] +} + +interface ScopePhase extends GenericMeetingPhase { + phaseType: 'SCOPE' + stages: [ScopeStage] +} + +interface AgendaItemPhase extends GenericMeetingPhase { + phaseType: 'agendaitems' + stages: AgendaItemStage[] +} + +const a: AgendaItemPhase + +interface CheckInPhase extends GenericMeetingPhase { + phaseType: 'checkin' + stages: [CheckInStage, ...CheckInStage[]] + checkInGreeting: {content: string; language: string} + checkInQuestion: string +} + +interface DiscussPhase extends GenericMeetingPhase { + phaseType: 'discuss' + stages: [DiscussStage, ...DiscussStage[]] +} + +interface EstimatePhase extends GenericMeetingPhase { + phaseType: 'ESTIMATE' + stages: EstimateStage[] +} + +interface ReflectPhase extends GenericMeetingPhase { + phaseType: 'reflect' + stages: [ReflectStage] + teamId: string + focusedPromptId?: string +} + +interface TeamHealthPhase extends GenericMeetingPhase { + phaseType: 'TEAM_HEALTH' + isRevealed: boolean + stages: [TeamHealthStage] +} + +interface TeamPromptResponsesPhase extends GenericMeetingPhase { + phaseType: 'RESPONSES' + stages: [TeamPromptResponseStage, ...TeamPromptResponseStage[]] +} + +interface UpdatesPhase extends GenericMeetingPhase { + phaseType: 'updates' + + stages: [UpdatesStage, ...UpdatesStage[]] +} + +export type RetroMeetingPhase = + | CheckInPhase + | TeamHealthPhase + | ReflectPhase + | GroupPhase + | VotePhase + | DiscussPhase + +export type PokerMeetingPhase = CheckInPhase | TeamHealthPhase | ScopePhase | EstimatePhase + +export type CheckInMeetingPhase = + | CheckInPhase + | TeamHealthPhase + | UpdatesPhase + | FirstCallPhase + | LastCallPhase + | AgendaItemPhase + +export type TeamPromptPhase = TeamPromptResponsesPhase + +export type NewMeetingPhase = + | RetroMeetingPhase + | PokerMeetingPhase + | CheckInMeetingPhase + | TeamPromptPhase + +type TupleToArray = T extends (infer U)[] ? U : never +export type NewMeetingStages = TupleToArray diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index affb35215d4..2186bdc5ee3 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -8,6 +8,7 @@ import { selectAgendaItems, selectComments, selectMeetingSettings, + selectNewMeetings, selectOrganizations, selectReflectPrompts, selectRetroReflections, @@ -26,6 +27,17 @@ type ExtractTypeFromQueryBuilderSelect any> = export type Discussion = Selectable export type ReactjiDB = {id: string; userId: string} +export type UsedReactjis = Record +export type TranscriptBlock = { + speaker: string + words: string +} + +export type AutogroupReflectionGroupType = { + groupTitle: string + reflectionIds: string[] +} + export interface Organization extends ExtractTypeFromQueryBuilderSelect {} export type OrganizationUser = Selectable @@ -58,3 +70,5 @@ export type SlackNotification = ExtractTypeFromQueryBuilderSelect export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect + +export type NewMeeting = ExtractTypeFromQueryBuilderSelect diff --git a/packages/server/utils/RecallAIServerManager.ts b/packages/server/utils/RecallAIServerManager.ts index 920337d662e..87d5857ec7f 100644 --- a/packages/server/utils/RecallAIServerManager.ts +++ b/packages/server/utils/RecallAIServerManager.ts @@ -2,7 +2,7 @@ import api from 'api' import axios from 'axios' import {ExternalLinks} from '../../client/types/constEnums' import appOrigin from '../appOrigin' -import {TranscriptBlock} from '../database/types/MeetingRetrospective' +import {TranscriptBlock} from '../postgres/types' import {Logger} from './Logger' import sendToSentry from './sendToSentry' diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 00b37a3fbc3..568cda427e8 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -3,16 +3,14 @@ import type {UpgradeCTALocationEnumType} from '../../../client/shared/UpgradeCTA import TeamPromptResponseId from '../../../client/shared/gqlIds/TeamPromptResponseId' import {PARABOL_AI_USER_ID} from '../../../client/utils/constants' import {TeamLimitsEmailType} from '../../billing/helpers/sendTeamsLimitEmail' -import Meeting from '../../database/types/Meeting' import MeetingMember from '../../database/types/MeetingMember' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' import MeetingTemplate from '../../database/types/MeetingTemplate' import {TaskServiceEnum} from '../../database/types/Task' import {DataLoaderWorker} from '../../graphql/graphql' import {ModifyType, ReactableEnum} from '../../graphql/public/resolverTypes' import {IntegrationProviderServiceEnumType} from '../../graphql/types/IntegrationProviderServiceEnum' import {SlackNotification, TeamPromptResponse, TemplateScale} from '../../postgres/types' -import {MeetingTypeEnum} from '../../postgres/types/Meeting' +import {AnyMeeting, MeetingTypeEnum, RetrospectiveMeeting} from '../../postgres/types/Meeting' import {MeetingSeries} from '../../postgres/types/MeetingSeries' import {AmplitudeAnalytics} from './amplitude/AmplitudeAnalytics' import {createMeetingProperties} from './helpers' @@ -193,7 +191,7 @@ class Analytics { // meeting teamPromptEnd = async ( - completedMeeting: Meeting, + completedMeeting: AnyMeeting, meetingMembers: MeetingMember[], responses: TeamPromptResponse[], dataLoader: DataLoaderWorker @@ -220,7 +218,7 @@ class Analytics { } checkInEnd = async ( - completedMeeting: Meeting, + completedMeeting: AnyMeeting, meetingMembers: MeetingMember[], dataLoader: DataLoaderWorker ) => @@ -238,7 +236,7 @@ class Analytics { ) retrospectiveEnd = async ( - completedMeeting: MeetingRetrospective, + completedMeeting: RetrospectiveMeeting, meetingMembers: MeetingMember[], template: MeetingTemplate, dataLoader: DataLoaderWorker @@ -261,7 +259,7 @@ class Analytics { } sprintPokerEnd = ( - completedMeeting: Meeting, + completedMeeting: AnyMeeting, meetingMembers: MeetingMember[], template: MeetingTemplate, dataLoader: DataLoaderWorker @@ -282,7 +280,7 @@ class Analytics { private meetingEnd = async ( dataloader: DataLoaderWorker, userId: string, - completedMeeting: Meeting, + completedMeeting: AnyMeeting, meetingMembers: MeetingMember[], template?: MeetingTemplate, meetingSpecificProperties?: any @@ -295,7 +293,7 @@ class Analytics { }) } - meetingStarted = (user: AnalyticsUser, meeting: Meeting, template?: MeetingTemplate) => { + meetingStarted = (user: AnalyticsUser, meeting: AnyMeeting, template?: MeetingTemplate) => { this.track(user, 'Meeting Started', createMeetingProperties(meeting, undefined, template)) } @@ -307,7 +305,7 @@ class Analytics { this.track(user, 'Meeting Recurrence Stopped', meetingSeries) } - meetingJoined = (user: AnalyticsUser, meeting: Meeting) => { + meetingJoined = (user: AnalyticsUser, meeting: AnyMeeting) => { this.track(user, 'Meeting Joined', createMeetingProperties(meeting, undefined, undefined)) } @@ -326,7 +324,7 @@ class Analytics { commentAdded = ( user: AnalyticsUser, - meeting: Meeting, + meeting: AnyMeeting, isAnonymous: boolean, isAsync: boolean, isReply: boolean diff --git a/packages/server/utils/analytics/helpers.ts b/packages/server/utils/analytics/helpers.ts index fb3b90cddeb..59f51eaff77 100644 --- a/packages/server/utils/analytics/helpers.ts +++ b/packages/server/utils/analytics/helpers.ts @@ -1,11 +1,10 @@ import {CHECKIN} from '../../../client/utils/constants' -import Meeting from '../../database/types/Meeting' import MeetingMember from '../../database/types/MeetingMember' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' import MeetingTemplate from '../../database/types/MeetingTemplate' +import {AnyMeeting, RetrospectiveMeeting} from '../../postgres/types/Meeting' export const createMeetingProperties = ( - meeting: Meeting, + meeting: AnyMeeting, meetingMembers?: MeetingMember[], template?: MeetingTemplate ) => { @@ -29,7 +28,7 @@ export const createMeetingProperties = ( meetingSeriesId: meeting.meetingSeriesId, disableAnonymity: meetingType === 'retrospective' - ? (meeting as MeetingRetrospective).disableAnonymity ?? false + ? (meeting as RetrospectiveMeeting).disableAnonymity ?? false : undefined } } diff --git a/packages/server/utils/getPhase.ts b/packages/server/utils/getPhase.ts index 94a825bac70..8c7936e36ab 100644 --- a/packages/server/utils/getPhase.ts +++ b/packages/server/utils/getPhase.ts @@ -1,26 +1,13 @@ -import AgendaItemsPhase from '../database/types/AgendaItemsPhase' -import CheckInPhase from '../database/types/CheckInPhase' -import DiscussPhase from '../database/types/DiscussPhase' -import EstimatePhase from '../database/types/EstimatePhase' -import GenericMeetingPhase from '../database/types/GenericMeetingPhase' -import ReflectPhase from '../database/types/ReflectPhase' -import TeamHealthPhase from '../database/types/TeamHealthPhase' -import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' -import UpdatesPhase from '../database/types/UpdatesPhase' +import {NewMeetingPhase} from '../postgres/types/NewMeetingPhase' -interface PhaseTypeLookup { - agendaitems: AgendaItemsPhase - checkin: CheckInPhase - discuss: DiscussPhase - ESTIMATE: EstimatePhase - reflect: ReflectPhase - updates: UpdatesPhase - RESPONSES: TeamPromptResponsesPhase - TEAM_HEALTH: TeamHealthPhase -} - -const getPhase = (phases: GenericMeetingPhase[], phaseType: T) => { - return phases.find((phase) => phase.phaseType === phaseType) as unknown as PhaseTypeLookup[T] +const getPhase = ( + phases: NewMeetingPhase[], + phaseType: T +) => { + return phases.find((phase) => phase.phaseType === phaseType) as Extract< + NewMeetingPhase, + {phaseType: T} + > } export default getPhase From 7d84391806b82ef43f113013f0a30a7aa2ec96cf Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 12 Sep 2024 14:31:58 -0700 Subject: [PATCH 26/89] fix name conflict --- packages/server/graphql/public/types/TeamPromptMeeting.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/graphql/public/types/TeamPromptMeeting.ts b/packages/server/graphql/public/types/TeamPromptMeeting.ts index 97e89b35548..870dd09843b 100644 --- a/packages/server/graphql/public/types/TeamPromptMeeting.ts +++ b/packages/server/graphql/public/types/TeamPromptMeeting.ts @@ -1,7 +1,7 @@ import getRethink from '../../../database/rethinkDriver' import {RValue} from '../../../database/stricterR' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' -import {TeamPromptMeeting} from '../../../postgres/types/Meeting' +import {TeamPromptMeeting as TeamPromptMeetingSource} from '../../../postgres/types/Meeting' import {getUserId} from '../../../utils/authorization' import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' import getPhase from '../../../utils/getPhase' @@ -28,7 +28,7 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { .limit(1) .run() - return meetings[0] as TeamPromptMeeting + return meetings[0] as TeamPromptMeetingSource }, nextMeeting: async ({meetingSeriesId, createdAt}, _args, {dataLoader}) => { if (!meetingSeriesId) return null @@ -48,7 +48,7 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { .limit(1) .run() - return meetings[0] as TeamPromptMeeting + return meetings[0] as TeamPromptMeetingSource }, tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) From a33d00ff2f12913d4260f16e0a0eb4df1d191010 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 12 Sep 2024 15:10:28 -0700 Subject: [PATCH 27/89] remove as much coercion as possible --- .../database/types/TeamPromptResponsesPhase.ts | 3 +-- .../server/graphql/mutations/endRetrospective.ts | 10 ++++------ .../mutations/helpers/handleCompletedStage.ts | 2 +- .../mutations/helpers/pushEstimateToGitHub.ts | 1 - .../mutations/helpers/safeEndRetrospective.ts | 10 +++++++--- .../graphql/mutations/helpers/safeEndTeamPrompt.ts | 8 ++++++-- packages/server/graphql/mutations/joinMeeting.ts | 4 ++-- .../mutations/resetRetroMeetingToGroupStage.ts | 3 +-- .../server/graphql/mutations/updateRetroMaxVotes.ts | 6 ++++-- .../graphql/mutations/voteForReflectionGroup.ts | 6 ++++-- .../private/mutations/generateMeetingSummary.ts | 5 +++-- .../private/types/GenerateMeetingSummarySuccess.ts | 6 +++--- .../graphql/public/mutations/addTranscriptionBot.ts | 6 ++++-- .../server/graphql/public/mutations/endTeamPrompt.ts | 6 ++++-- .../graphql/public/mutations/helpers/getSummaries.ts | 5 ++--- .../graphql/public/mutations/helpers/getTopics.ts | 12 +++--------- .../public/mutations/updateMeetingTemplate.ts | 4 ++-- .../public/types/AddTranscriptionBotSuccess.ts | 5 +++-- .../server/graphql/public/types/AutogroupSuccess.ts | 4 ++-- .../graphql/public/types/GenerateGroupsSuccess.ts | 5 +++-- .../graphql/public/types/GenerateInsightSuccess.ts | 8 +++----- packages/server/graphql/public/types/ReflectPhase.ts | 4 ++-- .../public/types/ResetReflectionGroupsSuccess.ts | 4 ++-- .../server/graphql/public/types/RetroDiscussStage.ts | 4 ++-- .../server/graphql/public/types/RetroReflection.ts | 4 ++-- .../graphql/public/types/RetroReflectionGroup.ts | 4 ++-- .../public/types/StartRetrospectiveSuccess.ts | 7 ++++--- packages/server/utils/analytics/helpers.ts | 6 ++---- 28 files changed, 78 insertions(+), 74 deletions(-) diff --git a/packages/server/database/types/TeamPromptResponsesPhase.ts b/packages/server/database/types/TeamPromptResponsesPhase.ts index c1bc523f5b1..c5d1e00ef0b 100644 --- a/packages/server/database/types/TeamPromptResponsesPhase.ts +++ b/packages/server/database/types/TeamPromptResponsesPhase.ts @@ -3,8 +3,7 @@ import TeamPromptResponseStage from './TeamPromptResponseStage' export default class TeamPromptResponsesPhase extends GenericMeetingPhase { stages: [TeamPromptResponseStage, ...TeamPromptResponseStage[]] - phaseType!: 'RESPONSES'; - [k: string]: any + phaseType!: 'RESPONSES' constructor(teamMemberIds: string[]) { super('RESPONSES') diff --git a/packages/server/graphql/mutations/endRetrospective.ts b/packages/server/graphql/mutations/endRetrospective.ts index 1d2cc4e4699..322a6ee1c67 100644 --- a/packages/server/graphql/mutations/endRetrospective.ts +++ b/packages/server/graphql/mutations/endRetrospective.ts @@ -1,6 +1,5 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import getRethink from '../../database/rethinkDriver' -import {RetrospectiveMeeting} from '../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../utils/authorization' import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' @@ -23,12 +22,11 @@ export default { const viewerId = getUserId(authToken) // AUTH - const meeting = (await r - .table('NewMeeting') - .get(meetingId) - .default(null) - .run()) as RetrospectiveMeeting | null + const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + if (meeting.meetingType !== 'retrospective') { + return standardError(new Error('Meeting not found'), {userId: viewerId}) + } const {endedAt, teamId} = meeting // VALIDATION diff --git a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts index 398e0bc190b..0fec6c43d1d 100644 --- a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts +++ b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts @@ -115,7 +115,7 @@ const handleCompletedStage = async ( dataLoader: DataLoaderWorker ) => { if (meeting.meetingType === 'retrospective') { - return handleCompletedRetrospectiveStage(stage, meeting as RetrospectiveMeeting, dataLoader) + return handleCompletedRetrospectiveStage(stage, meeting, dataLoader) } return {} } diff --git a/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts b/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts index 52cef841b7f..891895d7a71 100644 --- a/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts +++ b/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts @@ -94,7 +94,6 @@ const pushEstimateToGitHub = async ( const {id: issueId} = issue const {name: meetingName, phases} = meeting - const estimatePhase = getPhase(phases, 'ESTIMATE') const {stages} = estimatePhase const stageIdx = stages.findIndex((stage) => stage.id === stageId) diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index aa7b8f3b161..58ca5c03b34 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -115,7 +115,7 @@ const safeEndRetrospective = async ({ const phase = getMeetingPhase(phases) const insights = await gatherInsights(meeting, dataLoader) - const completedRetrospective = (await r + const completedRetrospective = await r .table('NewMeeting') .get(meetingId) .update( @@ -127,14 +127,18 @@ const safeEndRetrospective = async ({ {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as RetrospectiveMeeting + .run() if (!completedRetrospective) { return standardError(new Error('Completed retrospective meeting does not exist'), { userId: viewerId }) } - + if (completedRetrospective.meetingType !== 'retrospective') { + return standardError(new Error('Meeting type is not retrospective'), { + userId: viewerId + }) + } // remove any empty tasks const {templateId} = completedRetrospective const [meetingMembers, team, teamMembers, removedTaskIds, template] = await Promise.all([ diff --git a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts index db8fb9d1c0c..e44d2ee81d2 100644 --- a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts @@ -66,7 +66,7 @@ const safeEndTeamPrompt = async ({ // RESOLUTION const insights = await gatherInsights(meeting, dataLoader) - const completedTeamPrompt = (await r + const completedTeamPrompt = await r .table('NewMeeting') .get(meetingId) .update( @@ -77,7 +77,7 @@ const safeEndTeamPrompt = async ({ {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as TeamPromptMeeting + .run() if (!completedTeamPrompt) { return standardError(new Error('Completed team prompt meeting does not exist'), { @@ -85,6 +85,10 @@ const safeEndTeamPrompt = async ({ }) } + if (completedTeamPrompt.meetingType !== 'teamPrompt') { + return standardError(new Error('Meeting is not a team prompt'), {userId: viewerId}) + } + const [meetingMembers, team, teamMembers, responses] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index 456e7eadf13..99f2e3a61ab 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -13,7 +13,7 @@ import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStag import UpdatesStage from '../../database/types/UpdatesStage' import getKysely from '../../postgres/getKysely' import {TeamMember} from '../../postgres/types' -import {AnyMeeting, RetrospectiveMeeting} from '../../postgres/types/Meeting' +import {AnyMeeting} from '../../postgres/types/Meeting' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -27,7 +27,7 @@ const createMeetingMember = (meeting: AnyMeeting, teamMember: TeamMember) => { case 'action': return new ActionMeetingMember({teamId, userId, meetingId: meeting.id}) case 'retrospective': - const {id: meetingId, totalVotes} = meeting as RetrospectiveMeeting + const {id: meetingId, totalVotes} = meeting return new RetroMeetingMember({ teamId, userId, diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index 2e7786b27dc..f9c202d981d 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -5,7 +5,6 @@ import getRethink from '../../database/rethinkDriver' import DiscussPhase from '../../database/types/DiscussPhase' import GenericMeetingPhase from '../../database/types/GenericMeetingPhase' import getKysely from '../../postgres/getKysely' -import {RetrospectiveMeeting} from '../../postgres/types/Meeting' import {RetroMeetingPhase} from '../../postgres/types/NewMeetingPhase' import {getUserId} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -35,7 +34,7 @@ const resetRetroMeetingToGroupStage = { // AUTH const viewerId = getUserId(authToken) - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {createdBy, facilitatorUserId, phases, meetingType} = meeting if (meetingType !== 'retrospective') { diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index 7ad488253c9..be2c7749cb6 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -5,7 +5,6 @@ import mode from 'parabol-client/utils/mode' import getRethink from '../../database/rethinkDriver' import {RValue} from '../../database/stricterR' import getKysely from '../../postgres/getKysely' -import {RetrospectiveMeeting} from '../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -44,12 +43,15 @@ const updateRetroMaxVotes = { const subOptions = {mutatorId, operationId} //AUTH - const meeting = (await r.table('NewMeeting').get(meetingId).run()) as RetrospectiveMeeting + const meeting = await r.table('NewMeeting').get(meetingId).run() if (!meeting) { return {error: {message: 'Meeting not found'}} } + if (meeting.meetingType !== 'retrospective') { + return {error: {message: `Meeting not retrospective`}} + } const { endedAt, meetingType, diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index e8dd4e83135..171cf1f451e 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -3,7 +3,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {VOTE} from 'parabol-client/utils/constants' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import getRethink from '../../database/rethinkDriver' -import {RetrospectiveMeeting} from '../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -43,7 +42,10 @@ export default { }) } const {meetingId} = reflectionGroup - const meeting = (await r.table('NewMeeting').get(meetingId).run()) as RetrospectiveMeeting + const meeting = await r.table('NewMeeting').get(meetingId).run() + if (meeting.meetingType !== 'retrospective') { + return {error: {message: 'Meeting type is not retrospective'}} + } const {endedAt, phases, maxVotesPerGroup, teamId} = meeting if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 88f1ae6d604..3640fe0541e 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -20,7 +20,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const twoYearsAgo = new Date() twoYearsAgo.setFullYear(endDate.getFullYear() - 2) - const rawMeetings = (await r + const rawMeetings = await r .table('NewMeeting') .getAll(r.args(teamIds), {index: 'teamId'}) .filter((row: any) => @@ -32,7 +32,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) ) - .run()) as RetrospectiveMeeting[] + .run() const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] @@ -157,6 +157,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const updatedMeetingIds = await Promise.all( rawMeetings.map(async (meeting) => { + if (meeting.meetingType !== 'retrospective') return null const meetingsContent = await getMeetingsContent(meeting) if (!meetingsContent || meetingsContent.length === 0) { return null diff --git a/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts b/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts index 9e7fee37513..ad29fab70d5 100644 --- a/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts +++ b/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import isValid from '../../isValid' import {GenerateMeetingSummarySuccessResolvers} from '../resolverTypes' @@ -8,8 +7,9 @@ export type GenerateMeetingSummarySuccessSource = { const GenerateMeetingSummarySuccess: GenerateMeetingSummarySuccessResolvers = { meetings: async ({meetingIds}, _args, {dataLoader}) => { - const meetings = (await dataLoader.get('newMeetings').loadMany(meetingIds)).filter(isValid) - return meetings.filter((m) => m.meetingType === 'retrospective') as RetrospectiveMeeting[] + return (await dataLoader.get('newMeetings').loadMany(meetingIds)) + .filter(isValid) + .filter((m) => m.meetingType === 'retrospective') } } diff --git a/packages/server/graphql/public/mutations/addTranscriptionBot.ts b/packages/server/graphql/public/mutations/addTranscriptionBot.ts index fb93454a901..6b78c0bace5 100644 --- a/packages/server/graphql/public/mutations/addTranscriptionBot.ts +++ b/packages/server/graphql/public/mutations/addTranscriptionBot.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../../utils/authorization' import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' @@ -14,10 +13,13 @@ const addTranscriptionBot: MutationResolvers['addTranscriptionBot'] = async ( const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) { return standardError(new Error('Meeting not found'), {userId: viewerId}) } + if (meeting.meetingType !== 'retrospective') { + return {error: {message: 'Meeting type is not retrospective'}} + } const {teamId} = meeting if (!isTeamMember(authToken, teamId)) { const error = new Error('Not on team') diff --git a/packages/server/graphql/public/mutations/endTeamPrompt.ts b/packages/server/graphql/public/mutations/endTeamPrompt.ts index fbc3efe7cb4..b98a70f1261 100644 --- a/packages/server/graphql/public/mutations/endTeamPrompt.ts +++ b/packages/server/graphql/public/mutations/endTeamPrompt.ts @@ -1,5 +1,4 @@ import getRethink from '../../../database/rethinkDriver' -import {TeamPromptMeeting} from '../../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../../utils/authorization' import standardError from '../../../utils/standardError' import safeEndTeamPrompt from '../../mutations/helpers/safeEndTeamPrompt' @@ -14,8 +13,11 @@ const endTeamPrompt: MutationResolvers['endTeamPrompt'] = async (_source, {meeti const subOptions = {mutatorId, operationId} // AUTH - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as TeamPromptMeeting | null + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + if (meeting.meetingType !== 'teamPrompt') { + return {error: {message: 'Meeting type is not teamPrompt'}} + } const {teamId} = meeting // VALIDATION diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 66e553ff231..30ff97c9f9d 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -1,6 +1,5 @@ import yaml from 'js-yaml' import getRethink from '../../../../database/rethinkDriver' -import {RetrospectiveMeeting} from '../../../../postgres/types/Meeting' import OpenAIServerManager from '../../../../utils/OpenAIServerManager' import standardError from '../../../../utils/standardError' @@ -14,7 +13,7 @@ export const getSummaries = async ( const MIN_MILLISECONDS = 60 * 1000 // 1 minute const MIN_REFLECTION_COUNT = 3 - const rawMeetings = (await r + const rawMeetings = await r .table('NewMeeting') .getAll(teamId, {index: 'teamId'}) .filter((row: any) => @@ -27,7 +26,7 @@ export const getSummaries = async ( .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) .and(row.hasFields('summary')) ) - .run()) as RetrospectiveMeeting[] + .run() if (!rawMeetings.length) { return standardError(new Error('No meetings found')) diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index 61d5366db17..47782e177f9 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -1,7 +1,6 @@ import yaml from 'js-yaml' import getRethink from '../../../../database/rethinkDriver' import getKysely from '../../../../postgres/getKysely' -import {RetrospectiveMeeting} from '../../../../postgres/types/Meeting' import OpenAIServerManager from '../../../../utils/OpenAIServerManager' import sendToSentry from '../../../../utils/sendToSentry' import standardError from '../../../../utils/standardError' @@ -113,7 +112,7 @@ export const getTopics = async ( const r = await getRethink() const MIN_REFLECTION_COUNT = 3 const MIN_MILLISECONDS = 60 * 1000 // 1 minute - const rawMeetings = await r + const rawAnyMeetings = await r .table('NewMeeting') .getAll(teamId, {index: 'teamId'}) .filter((row: any) => @@ -126,15 +125,10 @@ export const getTopics = async ( .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) ) .run() - + const rawMeetings = rawAnyMeetings.filter((m) => m.meetingType === 'retrospective') const meetings = await Promise.all( rawMeetings.map(async (meeting) => { - const { - id: meetingId, - disableAnonymity, - name: meetingName, - createdAt: meetingDate - } = meeting as RetrospectiveMeeting + const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting const rawReflectionGroups = await dataLoader .get('retroReflectionGroupsByMeetingId') .load(meetingId) diff --git a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts index b81fb77f628..d355a6062a2 100644 --- a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts +++ b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts @@ -1,6 +1,5 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' @@ -16,8 +15,9 @@ const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + if (!('templateId' in meeting)) return {error: {message: 'Meeting has no template'}} if (!isTeamMember(authToken, meeting.teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) } diff --git a/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts index 1b2d3073bcc..44839c75df1 100644 --- a/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts +++ b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {AddTranscriptionBotSuccessResolvers} from '../resolverTypes' export type AddTranscriptionBotSuccessSource = { @@ -8,7 +7,9 @@ export type AddTranscriptionBotSuccessSource = { const AddTranscriptionBotSuccess: AddTranscriptionBotSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as RetrospectiveMeeting + if (meeting.meetingType !== 'retrospective') + throw new Error('Meeting type is not retrospective') + return meeting } } diff --git a/packages/server/graphql/public/types/AutogroupSuccess.ts b/packages/server/graphql/public/types/AutogroupSuccess.ts index 61a474883ff..1c86eb2efce 100644 --- a/packages/server/graphql/public/types/AutogroupSuccess.ts +++ b/packages/server/graphql/public/types/AutogroupSuccess.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {AutogroupSuccessResolvers} from '../resolverTypes' export type AutogroupSuccessSource = { @@ -8,7 +7,8 @@ export type AutogroupSuccessSource = { const AutogroupSuccess: AutogroupSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as RetrospectiveMeeting + if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') + return meeting } } diff --git a/packages/server/graphql/public/types/GenerateGroupsSuccess.ts b/packages/server/graphql/public/types/GenerateGroupsSuccess.ts index b08a5c03810..a15db891039 100644 --- a/packages/server/graphql/public/types/GenerateGroupsSuccess.ts +++ b/packages/server/graphql/public/types/GenerateGroupsSuccess.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {GenerateGroupsSuccessResolvers} from '../resolverTypes' export type GenerateGroupsSuccessSource = { @@ -8,7 +7,9 @@ export type GenerateGroupsSuccessSource = { const GenerateGroupsSuccess: GenerateGroupsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as RetrospectiveMeeting + if (meeting.meetingType !== 'retrospective') + throw new Error('Meeting type is not retrospective') + return meeting } } diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts index 4b8c3fa989b..a22c5a7e1cd 100644 --- a/packages/server/graphql/public/types/GenerateInsightSuccess.ts +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -1,4 +1,4 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' +import isValid from '../../isValid' import {GenerateInsightSuccessResolvers} from '../resolverTypes' export type GenerateInsightSuccessSource = { @@ -11,10 +11,8 @@ const GenerateInsightSuccess: GenerateInsightSuccessResolvers = { wins: ({wins}) => wins, challenges: ({challenges}) => challenges, meetings: async ({meetingIds}, _args, {dataLoader}) => { - const meetings = (await dataLoader - .get('newMeetings') - .loadMany(meetingIds)) as RetrospectiveMeeting[] - return meetings + const meetings = await dataLoader.get('newMeetings').loadMany(meetingIds) + return meetings.filter(isValid).filter((m) => m.meetingType === 'retrospective') } } diff --git a/packages/server/graphql/public/types/ReflectPhase.ts b/packages/server/graphql/public/types/ReflectPhase.ts index d78d857ed2f..e11232d3cc6 100644 --- a/packages/server/graphql/public/types/ReflectPhase.ts +++ b/packages/server/graphql/public/types/ReflectPhase.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {ReflectPhaseResolvers} from '../resolverTypes' const ReflectPhase: ReflectPhaseResolvers = { @@ -9,7 +8,8 @@ const ReflectPhase: ReflectPhaseResolvers = { }, reflectPrompts: async ({meetingId}, _args, {dataLoader}) => { - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (!('templateId' in meeting)) return [] const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(meeting.templateId) // only show prompts that were created before the meeting and // either have not been removed or they were removed after the meeting was created diff --git a/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts b/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts index ad5e3d30dbe..be7d98e9b8c 100644 --- a/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts +++ b/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {ResetReflectionGroupsSuccessResolvers} from '../resolverTypes' export type ResetReflectionGroupsSuccessSource = { @@ -8,7 +7,8 @@ export type ResetReflectionGroupsSuccessSource = { const ResetReflectionGroupsSuccess: ResetReflectionGroupsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as RetrospectiveMeeting + if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') + return meeting } } diff --git a/packages/server/graphql/public/types/RetroDiscussStage.ts b/packages/server/graphql/public/types/RetroDiscussStage.ts index 0fb5a5dc6a2..3d4aa611c20 100644 --- a/packages/server/graphql/public/types/RetroDiscussStage.ts +++ b/packages/server/graphql/public/types/RetroDiscussStage.ts @@ -1,5 +1,4 @@ import ReflectionGroup from '../../../database/types/ReflectionGroup' -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {RetroDiscussStageResolvers} from '../resolverTypes' // note: this is the GraphQL type, not source DB type @@ -27,7 +26,8 @@ const RetroDiscussStage: RetroDiscussStageResolvers = { reflectionGroup: async ({reflectionGroupId, meetingId}, _args, {dataLoader}) => { if (!reflectionGroupId) { - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as RetrospectiveMeeting + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (!('templateId' in meeting)) throw new Error('Meeting has no template') const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(meeting.templateId) return new ReflectionGroup({ id: `${meetingId}:dummyGroup`, diff --git a/packages/server/graphql/public/types/RetroReflection.ts b/packages/server/graphql/public/types/RetroReflection.ts index 4a7fd7fbd1d..1443aba8d35 100644 --- a/packages/server/graphql/public/types/RetroReflection.ts +++ b/packages/server/graphql/public/types/RetroReflection.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId, isSuperUser} from '../../../utils/authorization' import getGroupedReactjis from '../../../utils/getGroupedReactjis' import {RetroReflectionResolvers} from '../resolverTypes' @@ -35,7 +34,8 @@ const RetroReflection: RetroReflectionResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { const meeting = await dataLoader.get('newMeetings').load(meetingId) - return meeting as RetrospectiveMeeting + if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') + return meeting }, prompt: ({promptId}, _args, {dataLoader}) => { diff --git a/packages/server/graphql/public/types/RetroReflectionGroup.ts b/packages/server/graphql/public/types/RetroReflectionGroup.ts index a49f128271d..c5435b3a00e 100644 --- a/packages/server/graphql/public/types/RetroReflectionGroup.ts +++ b/packages/server/graphql/public/types/RetroReflectionGroup.ts @@ -1,6 +1,5 @@ import {Selectable} from 'kysely' import {RetroReflectionGroup as TRetroReflectionGroup} from '../../../postgres/pg' -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {getUserId} from '../../../utils/authorization' import {RetroReflectionGroupResolvers} from '../resolverTypes' @@ -9,7 +8,8 @@ export interface RetroReflectionGroupSource extends Selectable { const retroMeeting = await dataLoader.get('newMeetings').load(meetingId) - return retroMeeting as RetrospectiveMeeting + if (retroMeeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') + return retroMeeting }, prompt: ({promptId}, _args, {dataLoader}) => { return dataLoader.get('reflectPrompts').loadNonNull(promptId) diff --git a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts index c5c60bc715d..261edb42fec 100644 --- a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts +++ b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts @@ -1,4 +1,3 @@ -import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {StartRetrospectiveSuccessResolvers} from '../resolverTypes' export type StartRetrospectiveSuccessSource = { @@ -8,8 +7,10 @@ export type StartRetrospectiveSuccessSource = { } const StartRetrospectiveSuccess: StartRetrospectiveSuccessResolvers = { - meeting: ({meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) as Promise + meeting: async ({meetingId}, _args: unknown, {dataLoader}) => { + const meeting = await dataLoader.get('newMeetings').load(meetingId) + if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') + return meeting }, team: ({teamId}, _args: unknown, {dataLoader}) => { return dataLoader.get('teams').loadNonNull(teamId) diff --git a/packages/server/utils/analytics/helpers.ts b/packages/server/utils/analytics/helpers.ts index 59f51eaff77..1e9f965b50c 100644 --- a/packages/server/utils/analytics/helpers.ts +++ b/packages/server/utils/analytics/helpers.ts @@ -1,7 +1,7 @@ import {CHECKIN} from '../../../client/utils/constants' import MeetingMember from '../../database/types/MeetingMember' import MeetingTemplate from '../../database/types/MeetingTemplate' -import {AnyMeeting, RetrospectiveMeeting} from '../../postgres/types/Meeting' +import {AnyMeeting} from '../../postgres/types/Meeting' export const createMeetingProperties = ( meeting: AnyMeeting, @@ -27,8 +27,6 @@ export const createMeetingProperties = ( meetingTemplateCategory: template?.mainCategory, meetingSeriesId: meeting.meetingSeriesId, disableAnonymity: - meetingType === 'retrospective' - ? (meeting as RetrospectiveMeeting).disableAnonymity ?? false - : undefined + meetingType === 'retrospective' ? meeting.disableAnonymity ?? false : undefined } } From cdc0a02635ae39027bce0a73ef09faa1a85bf147 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 12 Sep 2024 15:29:02 -0700 Subject: [PATCH 28/89] fix migration name Signed-off-by: Matt Krick --- ...Meeting-phase1.ts => 1726174453131_NewMeeting-phase1.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename packages/server/postgres/migrations/{1726079837618_NewMeeting-phase1.ts => 1726174453131_NewMeeting-phase1.ts} (94%) diff --git a/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts similarity index 94% rename from packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts rename to packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts index 357686f5068..6bd308d9849 100644 --- a/packages/server/postgres/migrations/1726079837618_NewMeeting-phase1.ts +++ b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts @@ -77,10 +77,10 @@ export async function up() { ); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_createdAt" ON "NewMeeting"("createdAt"); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_facilitatorUserId" ON "NewMeeting"("facilitatorUserId"); - CREATE INDEX IF NOT EXISTS "idx_NewMeeting_scheduledEndTime" ON "NewMeeting"("scheduledEndTime"); - CREATE INDEX IF NOT EXISTS "idx_NewMeeting_meetingSeriesId" ON "NewMeeting"("meetingSeriesId"); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_scheduledEndTime" ON "NewMeeting"("scheduledEndTime") WHERE "scheduledEndTime" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_meetingSeriesId" ON "NewMeeting"("meetingSeriesId") WHERE "meetingSeriesId" IS NOT NULL; CREATE INDEX IF NOT EXISTS "idx_NewMeeting_teamId" ON "NewMeeting"("teamId"); - CREATE INDEX IF NOT EXISTS "idx_NewMeeting_templateId" ON "NewMeeting"("templateId"); + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_templateId" ON "NewMeeting"("templateId") WHERE "templateId" IS NOT NULL; DROP TRIGGER IF EXISTS "update_NewMeeting_updatedAt" ON "NewMeeting"; CREATE TRIGGER "update_NewMeeting_updatedAt" BEFORE UPDATE ON "NewMeeting" FOR EACH ROW EXECUTE PROCEDURE "set_updatedAt"(); END $$; From 5a155ca4b660ea6c38e32b71b74cf67cc933f67d Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 13 Sep 2024 10:04:24 -0700 Subject: [PATCH 29/89] revert processRecurrence test Signed-off-by: Matt Krick --- .../__tests__/processRecurrence.test.ts | 380 +++++++----------- 1 file changed, 155 insertions(+), 225 deletions(-) diff --git a/packages/server/__tests__/processRecurrence.test.ts b/packages/server/__tests__/processRecurrence.test.ts index adfe4632193..86edc1b87fb 100644 --- a/packages/server/__tests__/processRecurrence.test.ts +++ b/packages/server/__tests__/processRecurrence.test.ts @@ -2,12 +2,15 @@ import dayjs from 'dayjs' import ms from 'ms' import TeamMemberId from 'parabol-client/shared/gqlIds/TeamMemberId' import {toDateTime} from '../../client/shared/rruleUtil' +import getRethink from '../database/rethinkDriver' import DiscussPhase from '../database/types/DiscussPhase' +import MeetingRetrospective from '../database/types/MeetingRetrospective' +import MeetingTeamPrompt from '../database/types/MeetingTeamPrompt' import ReflectPhase from '../database/types/ReflectPhase' import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' import generateUID from '../generateUID' -import getKysely from '../postgres/getKysely' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../postgres/queries/insertMeetingSeries' +import {RetroMeetingPhase} from '../postgres/types/NewMeetingPhase' import {getUserTeams, sendIntranet, signUp} from './common' const PROCESS_RECURRENCE = ` @@ -74,25 +77,19 @@ beforeAll(async () => { }) test('Should not end meetings that are not scheduled to end', async () => { - const pg = getKysely() + const r = await getRethink() const meetingId = generateUID() - const phase = new TeamPromptResponsesPhase([teamMemberId]) - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify([phase]), - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - name: `Team Prompt #1`, - meetingType: 'teamPrompt', - facilitatorStageId: phase.stages[0]?.id - }) - .execute() + const meeting = new MeetingTeamPrompt({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [new TeamPromptResponsesPhase([teamMemberId])], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?' + }) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -110,35 +107,25 @@ test('Should not end meetings that are not scheduled to end', async () => { await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeFalsy() }) test('Should not end meetings that are scheduled to end in the future', async () => { - const pg = getKysely() + const r = await getRethink() const meetingId = generateUID() - const phase = new TeamPromptResponsesPhase([teamMemberId]) - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify([phase]), - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - name: `Team Prompt #1`, - meetingType: 'teamPrompt', - scheduledEndTime: new Date(Date.now() + ms('5m')), - facilitatorStageId: phase.stages[0]?.id - }) - .execute() + const meeting = new MeetingTeamPrompt({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [new TeamPromptResponsesPhase([teamMemberId])], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + scheduledEndTime: new Date(Date.now() + ms('5m')) + }) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -156,36 +143,27 @@ test('Should not end meetings that are scheduled to end in the future', async () await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeFalsy() - await pg.deleteFrom('NewMeeting').where('id', '=', meetingId).execute() + + await r.table('NewMeeting').get(meetingId).delete().run() }) test('Should end meetings that are scheduled to end in the past', async () => { - const pg = getKysely() + const r = await getRethink() const meetingId = generateUID() - const phase = new TeamPromptResponsesPhase([teamMemberId]) - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify([phase]), - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - name: `Team Prompt #1`, - meetingType: 'teamPrompt', - scheduledEndTime: new Date(Date.now() - ms('5m')), - facilitatorStageId: phase.stages[0]?.id - }) - .execute() + const meeting = new MeetingTeamPrompt({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [new TeamPromptResponsesPhase([teamMemberId])], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + scheduledEndTime: new Date(Date.now() - ms('5m')) + }) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -203,16 +181,12 @@ test('Should end meetings that are scheduled to end in the past', async () => { await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeTruthy() }, 10000) test('Should end the current team prompt meeting and start a new meeting', async () => { - const pg = getKysely() + const r = await getRethink() const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') const recurrenceRule = `DTSTART:${dateTime} @@ -228,27 +202,22 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const phase = new TeamPromptResponsesPhase([teamMemberId]) - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify([phase]), - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - name: `Team Prompt #1`, - meetingType: 'teamPrompt', - scheduledEndTime: new Date(Date.now() - ms('5m')), - facilitatorStageId: phase.stages[0]?.id, - meetingSeriesId, - // The last meeting in the series was created just over 24h ago, so the next one should start - // soon. - createdAt: new Date(Date.now() - ms('25h')) - }) - .execute() + const meeting = new MeetingTeamPrompt({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [new TeamPromptResponsesPhase([teamMemberId])], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + scheduledEndTime: new Date(Date.now() - ms('5m')), + meetingSeriesId + }) + + // The last meeting in the series was created just over 24h ago, so the next one should start + // soon. + meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -266,20 +235,15 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeTruthy() - const lastMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('meetingType', '=', 'teamPrompt') - .orderBy('createdAt desc') - .limit(1) - .executeTakeFirst() + const lastMeeting = await r + .table('NewMeeting') + .filter({meetingType: 'teamPrompt', meetingSeriesId}) + .orderBy(r.desc('createdAt')) + .nth(0) + .run() expect(lastMeeting).toMatchObject({ name: expect.stringMatching(/Daily Test Standup.*/), @@ -288,7 +252,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) test('Should end the current retro meeting and start a new meeting', async () => { - const pg = getKysely() + const r = await getRethink() // Create a meeting series that's been going on for a few days, and happens daily at 9a UTC. const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) @@ -306,31 +270,29 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const phases = [new ReflectPhase(teamId, []), new DiscussPhase(undefined)] - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify(phases), - facilitatorUserId: userId, - meetingType: 'retrospective', - scheduledEndTime: new Date(Date.now() - ms('5m')), - facilitatorStageId: phases[0]!.stages[0]!.id, - meetingSeriesId, - templateId: 'startStopContinueTemplate', - disableAnonymity: false, - totalVotes: 5, - name: '', - maxVotesPerGroup: 5, - meetingPrompt: 'What are you working on today? Stuck on anything?', - // The last meeting in the series was created just over 24h ago, so the next one should start - // soon. - createdAt: new Date(Date.now() - ms('25h')) - }) - .execute() + const meeting = new MeetingRetrospective({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [ + new ReflectPhase(teamId, []) as RetroMeetingPhase, + new DiscussPhase(undefined) as RetroMeetingPhase + ], + facilitatorUserId: userId, + scheduledEndTime: new Date(Date.now() - ms('5m')), + meetingSeriesId, + templateId: 'startStopContinueTemplate', + disableAnonymity: false, + totalVotes: 5, + name: '', + maxVotesPerGroup: 5 + }) + + // The last meeting in the series was created just over 24h ago, so the next one should start + // soon. + meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -348,20 +310,15 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeTruthy() - const lastMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('meetingType', '=', 'teamPrompt') - .orderBy('createdAt desc') - .limit(1) - .executeTakeFirst() + const lastMeeting = await r + .table('NewMeeting') + .filter({meetingType: 'retrospective', meetingSeriesId}) + .orderBy(r.desc('createdAt')) + .nth(0) + .run() expect(lastMeeting).toMatchObject({ meetingSeriesId @@ -369,7 +326,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) test('Should only start a new meeting if it would still be active', async () => { - const pg = getKysely() + const r = await getRethink() const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') @@ -386,28 +343,23 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const phase = new TeamPromptResponsesPhase([teamMemberId]) - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify([phase]), - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - name: `Team Prompt #1`, - meetingType: 'teamPrompt', - facilitatorStageId: phase.stages[0]?.id, - scheduledEndTime: new Date(Date.now() - ms('73h')), - meetingSeriesId: newMeetingSeriesId, - // The last meeting in the series was created just over 72h ago, so 3 meetings should have started - // since then, but only 1 meeting should start as a result of the mutation. - createdAt: new Date(Date.now() - ms('73h')), - endedAt: new Date(Date.now() - ms('49h')) - }) - .execute() + const meeting = new MeetingTeamPrompt({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [new TeamPromptResponsesPhase([teamMemberId])], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + scheduledEndTime: new Date(Date.now() - ms('73h')), + meetingSeriesId: newMeetingSeriesId + }) + + // The last meeting in the series was created just over 72h ago, so 3 meetings should have started + // since then, but only 1 meeting should start as a result of the mutation. + meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('73h')) + meeting.endedAt = new Date(Date.now() - ms('49h')) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -425,16 +377,12 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeTruthy() }, 10000) test('Should not start a new meeting if the rrule has not started', async () => { - const pg = getKysely() + const r = await getRethink() const startDate = dayjs().utc().add(1, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') @@ -451,28 +399,23 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const phase = new TeamPromptResponsesPhase([teamMemberId]) - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify([phase]), - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - name: `Team Prompt #1`, - meetingType: 'teamPrompt', - facilitatorStageId: phase.stages[0]?.id, - scheduledEndTime: new Date(Date.now() - ms('1h')), - meetingSeriesId: newMeetingSeriesId, - // The last meeting in the series was created just over 24h ago, but the active rrule doesn't - // start until tomorrow. - createdAt: new Date(Date.now() - ms('25h')), - endedAt: new Date(Date.now() - ms('1h')) - }) - .execute() + const meeting = new MeetingTeamPrompt({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [new TeamPromptResponsesPhase([teamMemberId])], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + scheduledEndTime: new Date(Date.now() - ms('1h')), + meetingSeriesId: newMeetingSeriesId + }) + + // The last meeting in the series was created just over 24h ago, but the active rrule doesn't + // start until tomorrow. + meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) + meeting.endedAt = new Date(Date.now() - ms('1h')) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -490,16 +433,12 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeTruthy() }) test('Should not hang if the rrule interval is invalid', async () => { - const pg = getKysely() + const r = await getRethink() const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') @@ -516,27 +455,22 @@ RRULE:FREQ=WEEKLY;INTERVAL=NaN;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const phase = new TeamPromptResponsesPhase([teamMemberId]) - await pg - .insertInto('NewMeeting') - .values({ - id: meetingId, - teamId, - meetingCount: 0, - meetingNumber: 1, - phases: JSON.stringify([phase]), - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - name: `Team Prompt #1`, - meetingType: 'teamPrompt', - facilitatorStageId: phase.stages[0]?.id, - scheduledEndTime: new Date(Date.now() - ms('5m')), - meetingSeriesId: newMeetingSeriesId, - // The last meeting in the series was created just over 24h ago, so the next one should start soon - // but the rrule is invalid, so it won't happen - createdAt: new Date(Date.now() - ms('25h')) - }) - .execute() + const meeting = new MeetingTeamPrompt({ + id: meetingId, + teamId, + meetingCount: 0, + phases: [new TeamPromptResponsesPhase([teamMemberId])], + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + scheduledEndTime: new Date(Date.now() - ms('5m')), + meetingSeriesId: newMeetingSeriesId + }) + + // The last meeting in the series was created just over 24h ago, so the next one should start soon + // but the rrule is invalid, so it won't happen + meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) + + await r.table('NewMeeting').insert(meeting).run() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -554,10 +488,6 @@ RRULE:FREQ=WEEKLY;INTERVAL=NaN;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await pg - .selectFrom('NewMeeting') - .selectAll() - .where('id', '=', meetingId) - .executeTakeFirstOrThrow() + const actualMeeting = await r.table('NewMeeting').get(meetingId).run() expect(actualMeeting.endedAt).toBeTruthy() }) From 3a0d5303cbf89e03466d4c286b2e588f18dce40c Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Sat, 14 Sep 2024 19:09:07 -0700 Subject: [PATCH 30/89] progress --- packages/client/components/TeamHealth.tsx | 1 + .../server/dataloader/customLoaderMakers.ts | 102 +++++++++++++++++- .../dataloader/foreignKeyLoaderMakers.ts | 24 +++++ .../dataloader/primaryKeyLoaderMakers.ts | 5 + .../graphql/mutations/createReflection.ts | 8 +- .../graphql/mutations/dragDiscussionTopic.ts | 11 +- .../graphql/mutations/dragEstimatingTask.ts | 11 +- .../server/graphql/mutations/endCheckIn.ts | 23 +++- .../graphql/mutations/endRetrospective.ts | 6 +- .../graphql/mutations/endSprintPoker.ts | 15 ++- .../graphql/mutations/flagReadyToAdvance.ts | 10 +- .../mutations/helpers/safeEndRetrospective.ts | 12 ++- .../server/graphql/mutations/joinMeeting.ts | 35 +++++- .../graphql/mutations/navigateMeeting.ts | 13 ++- packages/server/graphql/mutations/payLater.ts | 9 +- .../graphql/mutations/pokerResetDimension.ts | 19 +++- .../graphql/mutations/pokerRevealVotes.ts | 20 +++- .../mutations/promoteNewMeetingFacilitator.ts | 10 +- .../graphql/mutations/removeReflection.ts | 5 + .../server/graphql/mutations/renameMeeting.ts | 3 +- .../resetRetroMeetingToGroupStage.ts | 25 ++++- .../server/graphql/mutations/setPhaseFocus.ts | 9 +- .../server/graphql/mutations/setStageTimer.ts | 7 +- .../graphql/mutations/startSprintPoker.ts | 17 ++- .../mutations/updateNewCheckInQuestion.ts | 11 +- .../graphql/mutations/updatePokerScope.ts | 11 ++ .../graphql/mutations/updateRetroMaxVotes.ts | 10 +- .../graphql/mutations/voteForPokerStory.ts | 49 +++++++++ .../public/mutations/revealTeamHealthVotes.ts | 13 +++ .../public/mutations/setTeamHealthVote.ts | 6 +- .../graphql/public/mutations/startCheckIn.ts | 10 +- .../public/mutations/startRetrospective.ts | 7 +- .../1726251201860_NewMeeting-uniq.ts | 45 ++++++++ .../postgres/types/NewMeetingPhase.d.ts | 1 + 34 files changed, 507 insertions(+), 56 deletions(-) create mode 100644 packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts diff --git a/packages/client/components/TeamHealth.tsx b/packages/client/components/TeamHealth.tsx index 2f746c182cf..ba2b5bd1274 100644 --- a/packages/client/components/TeamHealth.tsx +++ b/packages/client/components/TeamHealth.tsx @@ -61,6 +61,7 @@ const TeamHealth = (props: Props) => { const isFacilitator = facilitatorUserId === viewerId const canVote = !isRevealed && !endedAt + console.log({canVote, isRevealed, endedAt}) const canReveal = isFacilitator && votedUserIds && votedUserIds.length > 0 && canVote const onVote = (label: string) => { diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 00ef82e4031..3214c255f42 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -25,7 +25,7 @@ import getLatestTaskEstimates from '../postgres/queries/getLatestTaskEstimates' import getMeetingTaskEstimates, { MeetingTaskEstimatesResult } from '../postgres/queries/getMeetingTaskEstimates' -import {selectMeetingSettings, selectTeams} from '../postgres/select' +import {selectMeetingSettings, selectNewMeetings, selectTeams} from '../postgres/select' import {MeetingSettings, OrganizationUser, Team} from '../postgres/types' import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting' import {Logger} from '../utils/Logger' @@ -510,6 +510,37 @@ export const meetingStatsByOrgId = (parent: RootDataLoader, dependsOn: RegisterD ) } +export const _pgmeetingStatsByOrgId = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { + dependsOn('newMeetings') + return new DataLoader( + async (orgIds) => { + const pg = getKysely() + const r = await getRethink() + const meetingStatsByOrgId = await Promise.all( + orgIds.map(async (orgId) => { + // note: does not include archived teams! + const teams = await parent.get('teamsByOrgIds').load(orgId) + const teamIds = teams.map(({id}) => id) + const stats = await pg + .selectFrom('NewMeeting') + .select(['createdAt', 'meetingType']) + .where('teamId', 'in', teamIds) + .execute() + return stats.map((stat) => ({ + createdAt: stat.createdAt, + meetingType: stat.meetingType, + id: `ms${stat.createdAt.getTime()}` + })) + }) + ) + return meetingStatsByOrgId + }, + { + ...parent.dataLoaderOptions + } + ) +} + export const teamStatsByOrgId = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { dependsOn('teams') return new DataLoader( @@ -595,6 +626,27 @@ export const activeMeetingsByMeetingSeriesId = ( ) } +export const _pgactiveMeetingsByMeetingSeriesId = ( + parent: RootDataLoader, + dependsOn: RegisterDependsOn +) => { + dependsOn('newMeetings') + return new DataLoader( + async (keys) => { + const res = await selectNewMeetings() + .where('meetingSeriesId', 'in', keys) + .where('endedAt', 'is', null) + .orderBy('createdAt') + .$narrowType() + .execute() + return normalizeArrayResults(keys, res, 'meetingSeriesId') + }, + { + ...parent.dataLoaderOptions + } + ) +} + export const lastMeetingByMeetingSeriesId = ( parent: RootDataLoader, dependsOn: RegisterDependsOn @@ -623,6 +675,31 @@ export const lastMeetingByMeetingSeriesId = ( ) } +export const _pglastMeetingByMeetingSeriesId = ( + parent: RootDataLoader, + dependsOn: RegisterDependsOn +) => { + dependsOn('newMeetings') + return new DataLoader( + async (keys) => { + return await Promise.all( + keys.map(async (key) => { + const latestMeeting = await selectNewMeetings() + .where('meetingSeriesId', '=', key) + .orderBy('createdAt desc') + .limit(1) + .$narrowType() + .executeTakeFirst() + return latestMeeting || null + }) + ) + }, + { + ...parent.dataLoaderOptions + } + ) +} + export const billingLeadersIdsByOrgId = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { dependsOn('organizationUsers') return new DataLoader( @@ -842,3 +919,26 @@ export const meetingCount = (parent: RootDataLoader, dependsOn: RegisterDependsO } ) } + +export const _pgmeetingCount = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { + dependsOn('newMeetings') + return new DataLoader<{teamId: string; meetingType: MeetingTypeEnum}, number, string>( + async (keys) => { + return await Promise.all( + keys.map(async ({teamId, meetingType}) => { + const row = await getKysely() + .selectFrom('NewMeeting') + .select(({fn}) => fn.count('id').as('count')) + .where('teamId', '=', teamId) + .where('meetingType', '=', meetingType) + .executeTakeFirstOrThrow() + return Number(row.count) + }) + ) + }, + { + ...parent.dataLoaderOptions, + cacheKeyFn: (key) => `${key.teamId}:${key.meetingType}` + } + ) +} diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 77b010bc58e..5b0e9d2eb2e 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -3,6 +3,7 @@ import {getTeamPromptResponsesByMeetingIds} from '../postgres/queries/getTeamPro import { selectAgendaItems, selectComments, + selectNewMeetings, selectOrganizations, selectReflectPrompts, selectRetroReflections, @@ -227,3 +228,26 @@ export const reflectPromptsByTemplateId = foreignKeyLoaderMaker( .execute() } ) + +export const _pgactiveMeetingsByTeamId = foreignKeyLoaderMaker( + '_pgnewMeetings', + 'teamId', + async (teamIds) => { + return selectNewMeetings() + .where('teamId', 'in', teamIds) + .where('endedAt', 'is', null) + .orderBy('createdAt desc') + .execute() + } +) +export const _pgcompletedMeetingsByTeamId = foreignKeyLoaderMaker( + '_pgnewMeetings', + 'teamId', + async (teamIds) => { + return selectNewMeetings() + .where('teamId', 'in', teamIds) + .where('endedAt', 'is not', null) + .orderBy('endedAt desc') + .execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 24a45d0b660..842397eb089 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -9,6 +9,7 @@ import { selectAgendaItems, selectComments, selectMeetingSettings, + selectNewMeetings, selectOrganizations, selectReflectPrompts, selectRetroReflections, @@ -115,3 +116,7 @@ export const comments = primaryKeyLoaderMaker((ids: readonly string[]) => { export const reflectPrompts = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectReflectPrompts().where('id', 'in', ids).execute() }) + +export const _pgnewMeetings = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectNewMeetings().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/createReflection.ts b/packages/server/graphql/mutations/createReflection.ts index de7171d3705..7a20208c72d 100644 --- a/packages/server/graphql/mutations/createReflection.ts +++ b/packages/server/graphql/mutations/createReflection.ts @@ -43,7 +43,7 @@ export default { const viewerId = getUserId(authToken) const [reflectPrompt, meeting, viewer] = await Promise.all([ dataLoader.get('reflectPrompts').load(promptId), - r.table('NewMeeting').get(meetingId).default(null).run(), + dataLoader.get('newMeetings').load(meetingId), dataLoader.get('users').loadNonNull(viewerId) ]) if (!reflectPrompt) { @@ -119,6 +119,12 @@ export default { phases }) .run() + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') } analytics.reflectionAdded(viewer, teamId, meetingId) const data = { diff --git a/packages/server/graphql/mutations/dragDiscussionTopic.ts b/packages/server/graphql/mutations/dragDiscussionTopic.ts index 3ed3f0acd65..472c314197c 100644 --- a/packages/server/graphql/mutations/dragDiscussionTopic.ts +++ b/packages/server/graphql/mutations/dragDiscussionTopic.ts @@ -1,6 +1,7 @@ import {GraphQLFloat, 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 getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -27,13 +28,14 @@ export default { {meetingId, stageId, sortOrder}: {meetingId: string; stageId: string; sortOrder: number}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) // AUTH - const meeting = await r.table('NewMeeting').get(meetingId).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {endedAt, phases, teamId} = meeting if (!isTeamMember(authToken, teamId)) { @@ -63,7 +65,12 @@ export default { phases }) .run() - + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') const data = { meetingId, stageId diff --git a/packages/server/graphql/mutations/dragEstimatingTask.ts b/packages/server/graphql/mutations/dragEstimatingTask.ts index 4034619b38c..5539ab1667b 100644 --- a/packages/server/graphql/mutations/dragEstimatingTask.ts +++ b/packages/server/graphql/mutations/dragEstimatingTask.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLInt, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {ESTIMATE_TASK_SORT_ORDER} from '../../../client/utils/constants' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -34,13 +35,14 @@ export default { }: {meetingId: string; taskId: string; newPositionIndex: number}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) // AUTH - const meeting = await r.table('NewMeeting').get(meetingId).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {endedAt, phases, teamId} = meeting if (!isTeamMember(authToken, teamId)) { @@ -92,7 +94,12 @@ export default { phases }) .run() - + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') const data = { meetingId, stageIds diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index ca676bbaeee..c366417261f 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -107,6 +107,7 @@ const summarizeCheckInMeeting = async (meeting: CheckInMeeting, dataLoader: Data */ const {id: meetingId, teamId, phases} = meeting + const pg = getKysely() const r = await getRethink() const [meetingMembers, tasks, doneTasks, activeAgendaItems] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), @@ -142,6 +143,15 @@ const summarizeCheckInMeeting = async (meeting: CheckInMeeting, dataLoader: Data isKill ? undefined : archiveTasksForDB(doneTasks, meetingId), isKill ? undefined : clonePinnedAgendaItems(pinnedAgendaItems, dataLoader), updateTaskSortOrders(userIds, tasks), + pg + .updateTable('NewMeeting') + .set({ + agendaItemCount: activeAgendaItems.length, + commentCount, + taskCount: tasks.length + }) + .where('id', '=', meetingId) + .execute(), r .table('NewMeeting') .get(meetingId) @@ -155,7 +165,7 @@ const summarizeCheckInMeeting = async (meeting: CheckInMeeting, dataLoader: Data ) .run() ]) - + dataLoader.clearAll('newMeetings') return {updatedTaskIds: [...tasks, ...doneTasks].map(({id}) => id)} } @@ -170,6 +180,7 @@ export default { }, async resolve(_source: unknown, {meetingId}: {meetingId: string}, context: GQLContext) { const {authToken, socketId: mutatorId, dataLoader} = context + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -177,7 +188,7 @@ export default { const viewerId = getUserId(authToken) // AUTH - const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) if (meeting.meetingType !== 'action') { return standardError(new Error('Not a check-in meeting'), {userId: viewerId}) @@ -213,7 +224,12 @@ export default { )('changes')(0)('new_val') .default(null) .run() - + await pg + .updateTable('NewMeeting') + .set({endedAt: now, phases: JSON.stringify(phases), ...insights}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') if (!completedCheckIn) { return standardError(new Error('Completed check-in meeting does not exist'), { userId: viewerId @@ -252,7 +268,6 @@ export default { }) ) const timelineEventId = events[0]!.id - const pg = getKysely() await pg.insertInto('TimelineEvent').values(events).execute() if (team.isOnboardTeam) { const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) diff --git a/packages/server/graphql/mutations/endRetrospective.ts b/packages/server/graphql/mutations/endRetrospective.ts index 322a6ee1c67..4d2e36bfd99 100644 --- a/packages/server/graphql/mutations/endRetrospective.ts +++ b/packages/server/graphql/mutations/endRetrospective.ts @@ -1,5 +1,4 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' -import getRethink from '../../database/rethinkDriver' import {getUserId, isTeamMember} from '../../utils/authorization' import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' @@ -16,13 +15,12 @@ export default { } }, async resolve(_source: unknown, {meetingId}: {meetingId: string}, context: GQLContext) { - const {authToken} = context - const r = await getRethink() + const {authToken, dataLoader} = context const now = new Date() const viewerId = getUserId(authToken) // AUTH - const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) if (meeting.meetingType !== 'retrospective') { return standardError(new Error('Meeting not found'), {userId: viewerId}) diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 5927fcd34f4..1f6e8b5a1ad 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -1,4 +1,5 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' +import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' @@ -40,7 +41,7 @@ export default { const viewerId = getUserId(authToken) // AUTH - const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) if (meeting.meetingType !== 'poker') { return standardError(new Error('Meeting is not a poker meeting'), {userId: viewerId}) @@ -94,6 +95,18 @@ export default { )('changes')(0)('new_val') .default(null) .run() + await getKysely() + .updateTable('NewMeeting') + .set({ + endedAt: sql`CURRENT_TIMESTAMP`, + phases: JSON.stringify(phases), + commentCount, + storyCount, + ...insights + }) + .where('id', '=', meetingId) + .executeTakeFirst() + dataLoader.clearAll('newMeetings') if (!completedMeeting) { return standardError(new Error('Completed poker meeting does not exist'), { userId: viewerId diff --git a/packages/server/graphql/mutations/flagReadyToAdvance.ts b/packages/server/graphql/mutations/flagReadyToAdvance.ts index da1ba3c08af..24a07512bf5 100644 --- a/packages/server/graphql/mutations/flagReadyToAdvance.ts +++ b/packages/server/graphql/mutations/flagReadyToAdvance.ts @@ -3,6 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import findStageById from 'parabol-client/utils/meetings/findStageById' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' import {GQLContext} from '../graphql' @@ -29,6 +30,7 @@ const flagReadyToAdvance = { {meetingId, stageId, isReady}: {meetingId: string; stageId: string; isReady: boolean}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { + const pg = getKysely() const r = await getRethink() const viewerId = getUserId(authToken) const now = new Date() @@ -38,7 +40,7 @@ const flagReadyToAdvance = { //AUTH const meetingMemberId = toTeamMemberId(meetingId, viewerId) const [meeting, viewerMeetingMember] = await Promise.all([ - r.table('NewMeeting').get(meetingId).run(), + dataLoader.get('newMeetings').load(meetingId), dataLoader.get('meetingMembers').load(meetingMemberId) ]) if (!meeting) { @@ -81,6 +83,12 @@ const flagReadyToAdvance = { // RESOLUTION // TODO there's enough evidence showing that we should probably worry about atomicity await r.table('NewMeeting').get(meetingId).update({phases, updatedAt: now}).run() + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') const data = {meetingId, stageId} publish(SubscriptionChannel.MEETING, meetingId, 'FlagReadyToAdvanceSuccess', data, subOptions) return data diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index 58ca5c03b34..beb8d39a719 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -1,3 +1,4 @@ +import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DISCUSS} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' @@ -128,7 +129,16 @@ const safeEndRetrospective = async ({ )('changes')(0)('new_val') .default(null) .run() - + await getKysely() + .updateTable('NewMeeting') + .set({ + endedAt: sql`CURRENT_TIMESTAMP`, + phases: JSON.stringify(phases), + ...insights + }) + .where('id', '=', meetingId) + .executeTakeFirst() + dataLoader.clearAll('newMeetings') if (!completedRetrospective) { return standardError(new Error('Completed retrospective meeting does not exist'), { userId: viewerId diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index 99f2e3a61ab..e830e298608 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -5,7 +5,6 @@ import rMapIf from '../../database/rMapIf' import getRethink from '../../database/rethinkDriver' import ActionMeetingMember from '../../database/types/ActionMeetingMember' import CheckInStage from '../../database/types/CheckInStage' -import {NewMeetingPhaseTypeEnum} from '../../database/types/GenericMeetingPhase' import PokerMeetingMember from '../../database/types/PokerMeetingMember' import RetroMeetingMember from '../../database/types/RetroMeetingMember' import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember' @@ -14,6 +13,7 @@ import UpdatesStage from '../../database/types/UpdatesStage' import getKysely from '../../postgres/getKysely' import {TeamMember} from '../../postgres/types' import {AnyMeeting} from '../../postgres/types/Meeting' +import {NewMeetingPhase, NewMeetingStages} from '../../postgres/types/NewMeetingPhase' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -91,10 +91,37 @@ const joinMeeting = { const mapIf = rMapIf(r) - const addStageToPhase = ( + const addStageToPhase = async ( stage: CheckInStage | UpdatesStage | TeamPromptResponseStage, - phaseType: NewMeetingPhaseTypeEnum + phaseType: NewMeetingPhase['phaseType'] ) => { + await getKysely() + .transaction() + .execute(async (trx) => { + const meeting = await trx + .selectFrom('NewMeeting') + .select(({fn}) => fn('to_json', ['phases']).as('phases')) + .where('id', '=', meetingId) + .forUpdate() + // NewMeeting: add OrThrow in phase 3 + .executeTakeFirst() + if (!meeting) return + const {phases} = meeting + const phase = getPhase(phases, phaseType) + const stages = phase.stages as NewMeetingStages[] + stages.push({ + ...stage, + isNavigable: true, + isNavigableByFacilitator: true, + // the stage is complete if all other stages are complete & there's at least 1 + isComplete: stages.length >= 1 && stages.every((stage) => stage.isComplete) + }) + await trx + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + }) return r .table('NewMeeting') .get(meetingId) @@ -160,7 +187,7 @@ const joinMeeting = { // effort is taken here to run both at the same time // so e.g.the 5th person in check-in is the 5th person in updates await Promise.all([appendToCheckin(), appendToUpdate(), appendToTeamPromptResponses()]) - dataLoader.get('newMeetings').clear(meetingId) + dataLoader.clearAll('newMeetings') const data = {meetingId} publish(SubscriptionChannel.MEETING, meetingId, 'JoinMeetingSuccess', data, subOptions) diff --git a/packages/server/graphql/mutations/navigateMeeting.ts b/packages/server/graphql/mutations/navigateMeeting.ts index be704dedc4a..8563af871b1 100644 --- a/packages/server/graphql/mutations/navigateMeeting.ts +++ b/packages/server/graphql/mutations/navigateMeeting.ts @@ -4,6 +4,7 @@ import findStageById from 'parabol-client/utils/meetings/findStageById' import startStage_ from 'parabol-client/utils/startStage_' import unlockNextStages from 'parabol-client/utils/unlockNextStages' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {Logger} from '../../utils/Logger' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' @@ -46,7 +47,7 @@ export default { // AUTH const viewerId = getUserId(authToken) - const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {createdBy, endedAt, facilitatorUserId, phases, teamId, meetingType} = meeting if (endedAt) { @@ -123,7 +124,15 @@ export default { )('changes')(0)('old_val')('facilitatorStageId') .default(null) .run() - + await getKysely() + .updateTable('NewMeeting') + .set({ + facilitatorStageId: facilitatorStageId ?? undefined, + phases: JSON.stringify(phases) + }) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') if (!oldFacilitatorStageId) { return {error: {message: 'Stage already advanced'}} } diff --git a/packages/server/graphql/mutations/payLater.ts b/packages/server/graphql/mutations/payLater.ts index 18ec12d6af9..fa4c0189a89 100644 --- a/packages/server/graphql/mutations/payLater.ts +++ b/packages/server/graphql/mutations/payLater.ts @@ -32,7 +32,7 @@ export default { // AUTH const viewerId = getUserId(authToken) const [meeting, viewer] = await Promise.all([ - r.table('NewMeeting').get(meetingId).run(), + dataLoader.get('newMeetings').load(meetingId), dataLoader.get('users').loadNonNull(viewerId) ]) if (!meeting) { @@ -63,7 +63,12 @@ export default { showConversionModal: false }) .run() - + await getKysely() + .updateTable('NewMeeting') + .set({showConversionModal: false}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') await incrementUserPayLaterClickCountQuery.run({id: viewerId}, getPg()) analytics.conversionModalPayLaterClicked(viewer) diff --git a/packages/server/graphql/mutations/pokerResetDimension.ts b/packages/server/graphql/mutations/pokerResetDimension.ts index c25a460db2c..5dafb36786b 100644 --- a/packages/server/graphql/mutations/pokerResetDimension.ts +++ b/packages/server/graphql/mutations/pokerResetDimension.ts @@ -1,8 +1,10 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' +import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import {RValue} from '../../database/stricterR' import updateStage from '../../database/updateStage' +import getKysely from '../../postgres/getKysely' import removeMeetingTaskEstimates from '../../postgres/queries/removeMeetingTaskEstimates' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -27,6 +29,7 @@ const pokerResetDimension = { {meetingId, stageId}: {meetingId: string; stageId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { + const pg = getKysely() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -65,8 +68,10 @@ const pokerResetDimension = { // VALIDATION const estimatePhase = getPhase(phases, 'ESTIMATE') + const estimatePhaseIdx = phases.indexOf(estimatePhase) const {stages} = estimatePhase - const stage = stages.find((stage) => stage.id === stageId) + const stageIdx = stages.findIndex((stage) => stage.id === stageId) + const stage = stages[stageIdx] if (!stage) { return {error: {message: 'Invalid stageId provided'}} } @@ -77,14 +82,26 @@ const pokerResetDimension = { scores: [] } // mutate the cached meeting + Object.assign(stage, updates) const updater = (estimateStage: RValue) => estimateStage.merge(updates) const [meetingMembers, teamMembers] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teamMembersByTeamId').load(teamId), updateStage(meetingId, stageId, 'ESTIMATE', updater), + pg + .updateTable('NewMeeting') + .set({ + phases: sql`jsonb_set( + jsonb_set(phases, ${sql.lit(`{${estimatePhaseIdx},stages,${stageIdx},"scores"}`)}, '[]'::jsonb, false), + ${sql.lit(`{${estimatePhaseIdx},stages,${stageIdx},"isVoting"}`)}, 'true'::jsonb, false + )` + }) + .where('id', '=', meetingId) + .execute(), removeMeetingTaskEstimates(meetingId, stageId) ]) + dataLoader.clearAll('newMeetings') const data = {meetingId, stageId} sendPokerMeetingRevoteEvent(meeting, teamMembers, meetingMembers, dataLoader) diff --git a/packages/server/graphql/mutations/pokerRevealVotes.ts b/packages/server/graphql/mutations/pokerRevealVotes.ts index 50c9620df76..c0f900244e0 100644 --- a/packages/server/graphql/mutations/pokerRevealVotes.ts +++ b/packages/server/graphql/mutations/pokerRevealVotes.ts @@ -1,9 +1,11 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' +import {sql} from 'kysely' import {PokerCards, SubscriptionChannel} from 'parabol-client/types/constEnums' import {RValue} from '../../database/stricterR' import EstimateUserScore from '../../database/types/EstimateUserScore' import PokerMeetingMember from '../../database/types/PokerMeetingMember' import updateStage from '../../database/updateStage' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -26,6 +28,7 @@ const pokerRevealVotes = { {meetingId, stageId}: {meetingId: string; stageId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { + const pg = getKysely() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -67,8 +70,10 @@ const pokerRevealVotes = { // VALIDATION const estimatePhase = getPhase(phases, 'ESTIMATE') + const estimatePhaseIdx = phases.indexOf(estimatePhase) const {stages} = estimatePhase - const stage = stages.find((stage) => stage.id === stageId) + const stageIdx = stages.findIndex((stage) => stage.id === stageId) + const stage = stages[stageIdx] if (!stage) { return {error: {message: 'Invalid stageId provided'}} } @@ -93,7 +98,18 @@ const pokerRevealVotes = { // note that a race condition exists here. it's possible that i cast my vote after the meeting is fetched but before this update & that'll be overwritten scores }) - await updateStage(meetingId, stageId, 'ESTIMATE', updater) + await pg + .updateTable('NewMeeting') + .set({ + phases: sql`jsonb_set( + jsonb_set(phases, ${sql.lit(`{${estimatePhaseIdx},stages,${stageIdx},"scores"}`)}, ${JSON.stringify(scores)}::jsonb, false), + ${sql.lit(`{${estimatePhaseIdx},stages,${stageIdx},"isVoting"}`)}, 'false'::jsonb, false + )` + }) + .where('id', '=', meetingId) + .execute(), + await updateStage(meetingId, stageId, 'ESTIMATE', updater) + dataLoader.clearAll('newMeetings') const data = {meetingId, stageId} publish(SubscriptionChannel.MEETING, meetingId, 'PokerRevealVotesSuccess', data, subOptions) return data diff --git a/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts b/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts index 1d93c486ad1..ce3a30827f9 100644 --- a/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts +++ b/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts @@ -1,6 +1,7 @@ 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' @@ -31,7 +32,7 @@ export default { const viewerId = getUserId(authToken) // AUTH - const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {facilitatorUserId: oldFacilitatorUserId, teamId, endedAt} = meeting if (!isTeamMember(authToken, teamId)) { @@ -59,7 +60,12 @@ export default { updatedAt: now }) .run() - + await getKysely() + .updateTable('NewMeeting') + .set({facilitatorUserId}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') const data = {meetingId, oldFacilitatorUserId} publish( SubscriptionChannel.MEETING, diff --git a/packages/server/graphql/mutations/removeReflection.ts b/packages/server/graphql/mutations/removeReflection.ts index 83460fccc5d..b864cfff51a 100644 --- a/packages/server/graphql/mutations/removeReflection.ts +++ b/packages/server/graphql/mutations/removeReflection.ts @@ -68,6 +68,11 @@ export default { phases }) .run() + await getKysely() + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() } const data = {meetingId, reflectionId, unlockedStageIds} publish(SubscriptionChannel.MEETING, meetingId, 'RemoveReflectionPayload', data, subOptions) diff --git a/packages/server/graphql/mutations/renameMeeting.ts b/packages/server/graphql/mutations/renameMeeting.ts index 09c207b70f2..a4d2263145b 100644 --- a/packages/server/graphql/mutations/renameMeeting.ts +++ b/packages/server/graphql/mutations/renameMeeting.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import linkify from 'parabol-client/utils/linkify' 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' @@ -61,7 +62,7 @@ const renameMeeting = { name }) .run() - + await getKysely().updateTable('NewMeeting').set({name}).where('id', '=', meetingId).execute() const data = {meetingId} IntegrationNotifier.updateMeeting?.(dataLoader, meetingId, teamId) publish(SubscriptionChannel.TEAM, teamId, 'RenameMeetingSuccess', data, subOptions) diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index f9c202d981d..6985db0c274 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -104,18 +104,33 @@ const resetRetroMeetingToGroupStage = { reflectionGroups.forEach((rg) => (rg.voterIds = [])) await Promise.all([ - pg.deleteFrom('Comment').where('discussionId', 'in', discussionIdsToDelete).execute(), - r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run(), pg - .updateTable('RetroReflectionGroup') - .set({voterIds: [], discussionPromptQuestion: null}) - .where('id', 'in', reflectionGroupIds) + .with('DeleteComments', (qb) => + qb.deleteFrom('Comment').where('discussionId', 'in', discussionIdsToDelete) + ) + .with('ResetGroups', (qb) => + qb + .updateTable('RetroReflectionGroup') + .set({voterIds: [], discussionPromptQuestion: null}) + .where('id', 'in', reflectionGroupIds) + ) + .updateTable('NewMeeting') + .set({phases: JSON.stringify(newPhases)}) + .where('id', '=', meetingId) .execute(), + r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run(), r.table('NewMeeting').get(meetingId).update({phases: newPhases}).run(), (r.table('MeetingMember').getAll(meetingId, {index: 'meetingId'}) as any) .update({votesRemaining: meeting.totalVotes}) .run() ]) + dataLoader.clearAll([ + 'newMeetings', + 'comments', + 'retroReflectionGroups', + 'tasks', + 'meetingMembers' + ]) const data = { meetingId } diff --git a/packages/server/graphql/mutations/setPhaseFocus.ts b/packages/server/graphql/mutations/setPhaseFocus.ts index dcd45ac0b33..301502c2aa6 100644 --- a/packages/server/graphql/mutations/setPhaseFocus.ts +++ b/packages/server/graphql/mutations/setPhaseFocus.ts @@ -3,6 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {GROUP} from 'parabol-client/utils/constants' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -33,7 +34,7 @@ const setPhaseFocus = { // AUTH const viewerId = getUserId(authToken) - const meeting = await r.table('NewMeeting').get(meetingId).default(null).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {endedAt, facilitatorUserId, phases} = meeting if (endedAt) return standardError(new Error('Meeting already completed'), {userId: viewerId}) @@ -52,6 +53,12 @@ const setPhaseFocus = { // mutative reflectPhase.focusedPromptId = focusedPromptId ?? undefined await r.table('NewMeeting').get(meetingId).update(meeting).run() + await getKysely() + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') const data = {meetingId} publish(SubscriptionChannel.MEETING, meetingId, 'SetPhaseFocusPayload', data, subOptions) return data diff --git a/packages/server/graphql/mutations/setStageTimer.ts b/packages/server/graphql/mutations/setStageTimer.ts index 79c8bfc1593..6e93cdd39ff 100644 --- a/packages/server/graphql/mutations/setStageTimer.ts +++ b/packages/server/graphql/mutations/setStageTimer.ts @@ -45,6 +45,7 @@ export default { }: {scheduledEndTime: Date | null; meetingId: string; timeRemaining: number | null}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -115,7 +116,11 @@ export default { updatedAt: now }) .run() - + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() const data = {meetingId, stageId: facilitatorStageId} const {isAsync, phaseType, startAt, viewCount} = stage const stoppedOrStarted = newScheduledEndTime ? `Meeting Timer Started` : `Meeting Timer Stopped` diff --git a/packages/server/graphql/mutations/startSprintPoker.ts b/packages/server/graphql/mutations/startSprintPoker.ts index ece86c98986..abb727757e7 100644 --- a/packages/server/graphql/mutations/startSprintPoker.ts +++ b/packages/server/graphql/mutations/startSprintPoker.ts @@ -97,6 +97,7 @@ export default { }: {teamId: string; name: string | null | undefined; gcalInput?: CreateGcalEventInputType}, {authToken, socketId: mutatorId, dataLoader}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -116,13 +117,7 @@ export default { // RESOLUTION const meetingId = generateUID() - const meetingCount = await r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType}) - .count() - .default(0) - .run() + const meetingCount = await dataLoader.get('meetingCount').load({teamId, meetingType}) const phases = await createNewMeetingPhases( viewerId, @@ -153,11 +148,15 @@ export default { }) as PokerMeeting const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) - await Promise.all([ + await Promise.allSettled([ + pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(phases)}) + .execute(), r.table('NewMeeting').insert(meeting).run(), updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) ]) - + dataLoader.clearAll('newMeetings') // Disallow accidental starts (2 meetings within 2 seconds) const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { diff --git a/packages/server/graphql/mutations/updateNewCheckInQuestion.ts b/packages/server/graphql/mutations/updateNewCheckInQuestion.ts index 5a5935be3f8..0446229d527 100644 --- a/packages/server/graphql/mutations/updateNewCheckInQuestion.ts +++ b/packages/server/graphql/mutations/updateNewCheckInQuestion.ts @@ -4,6 +4,7 @@ import convertToTaskContent from 'parabol-client/utils/draftjs/convertToTaskCont import {makeCheckinQuestion} from 'parabol-client/utils/makeCheckinGreeting' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -29,6 +30,7 @@ export default { {meetingId, checkInQuestion}: {meetingId: string; checkInQuestion: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -36,7 +38,7 @@ export default { const viewerId = getUserId(authToken) // AUTH - const meeting = await r.table('NewMeeting').get(meetingId).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {endedAt, phases, teamId} = meeting if (!isTeamMember(authToken, teamId)) { @@ -64,7 +66,12 @@ export default { updatedAt: now }) .run() - + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + dataLoader.clearAll('newMeetings') const data = {meetingId} publish( SubscriptionChannel.MEETING, diff --git a/packages/server/graphql/mutations/updatePokerScope.ts b/packages/server/graphql/mutations/updatePokerScope.ts index 0e59ab72161..f104dc07282 100644 --- a/packages/server/graphql/mutations/updatePokerScope.ts +++ b/packages/server/graphql/mutations/updatePokerScope.ts @@ -42,6 +42,7 @@ const updatePokerScope = { {meetingId, updates}: {meetingId: string; updates: TUpdatePokerScopeItemInput[]}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { + const pg = getKysely() const r = await getRethink() const redis = getRedis() const viewerId = getUserId(authToken) @@ -156,6 +157,15 @@ const updatePokerScope = { if (stages.length > Threshold.MAX_POKER_STORIES * dimensions.length) { return {error: {message: 'Story limit reached'}} } + + await pg + .updateTable('NewMeeting') + .set({ + facilitatorStageId: meeting.facilitatorStageId, + phases: JSON.stringify(phases) + }) + .where('id', '=', meetingId) + .execute() await r .table('NewMeeting') .get(meetingId) @@ -168,6 +178,7 @@ const updatePokerScope = { if (newDiscussions.length > 0) { await getKysely().insertInto('Discussion').values(newDiscussions).execute() } + dataLoader.clearAll(['newMeetings']) const data = {meetingId, newStageIds} publish(SubscriptionChannel.MEETING, meetingId, 'UpdatePokerScopeSuccess', data, subOptions) return data diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index be2c7749cb6..329ce6646fd 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -43,7 +43,7 @@ const updateRetroMaxVotes = { const subOptions = {mutatorId, operationId} //AUTH - const meeting = await r.table('NewMeeting').get(meetingId).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) { return {error: {message: 'Meeting not found'}} @@ -140,6 +140,12 @@ const updateRetroMaxVotes = { // RESOLUTION await Promise.all([ getKysely() + .with('MeetingUpdates', (qb) => + qb + .updateTable('NewMeeting') + .set({totalVotes, maxVotesPerGroup}) + .where('id', '=', meetingId) + ) .updateTable('MeetingSettings') .set({ totalVotes, @@ -157,7 +163,7 @@ const updateRetroMaxVotes = { }) .run() ]) - + dataLoader.get('newMeetings').clear(meetingId) const data = {meetingId} publish(SubscriptionChannel.MEETING, meetingId, 'UpdateRetroMaxVotesSuccess', data, subOptions) return data diff --git a/packages/server/graphql/mutations/voteForPokerStory.ts b/packages/server/graphql/mutations/voteForPokerStory.ts index 15705e31292..79e4012c8a4 100644 --- a/packages/server/graphql/mutations/voteForPokerStory.ts +++ b/packages/server/graphql/mutations/voteForPokerStory.ts @@ -4,6 +4,8 @@ import getRethink from '../../database/rethinkDriver' import {RValue} from '../../database/stricterR' import EstimateUserScore from '../../database/types/EstimateUserScore' import updateStage from '../../database/updateStage' +import getKysely from '../../postgres/getKysely' +import {NewMeetingPhase} from '../../postgres/types/NewMeetingPhase' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -11,6 +13,29 @@ import {GQLContext} from '../graphql' import VoteForPokerStoryPayload from '../types/VoteForPokerStoryPayload' export const removeVoteForUserId = async (userId: string, stageId: string, meetingId: string) => { + await getKysely() + .transaction() + .execute(async (trx) => { + const meeting = await trx + .selectFrom('NewMeeting') + .select(({fn}) => fn('to_json', ['phases']).as('phases')) + .where('id', '=', meetingId) + .forUpdate() + // NewMeeting: add OrThrow in phase 3 + .executeTakeFirst() + if (!meeting) return + const {phases} = meeting + const phase = getPhase(phases, 'ESTIMATE') + const {stages} = phase + const stage = stages.find((stage) => stage.id === stageId)! + const {scores} = stage + stage.scores = scores.filter((score) => score.userId !== userId) + await trx + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + }) const updater = (estimateStage: RValue) => estimateStage.merge({ scores: estimateStage('scores').deleteAt( @@ -23,6 +48,30 @@ export const removeVoteForUserId = async (userId: string, stageId: string, meeti } const upsertVote = async (vote: EstimateUserScore, stageId: string, meetingId: string) => { + await getKysely() + .transaction() + .execute(async (trx) => { + const meeting = await trx + .selectFrom('NewMeeting') + .select(({fn}) => fn('to_json', ['phases']).as('phases')) + .where('id', '=', meetingId) + .forUpdate() + // NewMeeting: add OrThrow in phase 3 + .executeTakeFirst() + if (!meeting) return + const {phases} = meeting + const phase = getPhase(phases, 'ESTIMATE') + const {stages} = phase + const stage = stages.find((stage) => stage.id === stageId)! + const {scores} = stage + stage.scores = [...scores.filter((score) => score.userId !== vote.userId), vote] + await trx + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + }) + const r = await getRethink() const updater = (estimateStage: RValue) => estimateStage.merge({ diff --git a/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts b/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts index ba88c567f35..a7193367d3e 100644 --- a/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts +++ b/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts @@ -1,5 +1,7 @@ +import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import updateStage from '../../../database/updateStage' +import getKysely from '../../../postgres/getKysely' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' @@ -10,6 +12,7 @@ const revealTeamHealthVotes: MutationResolvers['revealTeamHealthVotes'] = async {meetingId, stageId}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -42,6 +45,9 @@ const revealTeamHealthVotes: MutationResolvers['revealTeamHealthVotes'] = async const teamHealthPhase = getPhase(phases, 'TEAM_HEALTH') const {stages} = teamHealthPhase const stage = stages.find((stage) => stage.id === stageId) + const phaseIdx = phases.indexOf(teamHealthPhase) + const stageIdx = stages.findIndex((stage) => stage.id === stageId) + if (!stage || stage.phaseType !== 'TEAM_HEALTH') { return {error: {message: 'Invalid stageId provided'}} } @@ -49,6 +55,13 @@ const revealTeamHealthVotes: MutationResolvers['revealTeamHealthVotes'] = async return {error: {message: 'Votes are already revealed'}} } + await pg + .updateTable('NewMeeting') + .set({ + phases: sql`jsonb_set(phases, ${sql.lit(`{${phaseIdx},stages,${stageIdx},"isRevealed"}`)}, 'true'::jsonb, false)` + }) + .where('id', '=', meetingId) + .execute() updateStage(meetingId, stageId, 'TEAM_HEALTH', (stage) => stage.merge({isRevealed: true})) stage.isRevealed = true diff --git a/packages/server/graphql/public/mutations/setTeamHealthVote.ts b/packages/server/graphql/public/mutations/setTeamHealthVote.ts index 7f3fe36af53..f9484791312 100644 --- a/packages/server/graphql/public/mutations/setTeamHealthVote.ts +++ b/packages/server/graphql/public/mutations/setTeamHealthVote.ts @@ -13,7 +13,7 @@ import {MutationResolvers} from '../resolverTypes' const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealthVote) => { const pg = getKysely() await pg.transaction().execute(async (trx) => { - // console.log('start transaction', newVote) + console.log('start transaction', newVote) const meeting = await trx .selectFrom('NewMeeting') .select(({fn}) => fn('to_json', ['phases']).as('phases')) @@ -22,7 +22,7 @@ const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealt // NewMeeting: add OrThrow in phase 3 .executeTakeFirst() if (!meeting) return - // console.log('got lock', newVote) + console.log('got lock', newVote) const {phases} = meeting const phase = getPhase(phases, 'TEAM_HEALTH') const {stages} = phase @@ -39,7 +39,7 @@ const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealt .set({phases: JSON.stringify(phases)}) .where('id', '=', meetingId) .execute() - // console.log('wrote update, commit', newVote) + console.log('wrote update, commit', newVote) }) const r = await getRethink() const updater = (stage: RValue) => diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index 62086ac17ba..70442b50723 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -22,6 +22,7 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( {teamId, name, gcalInput}, context ) => { + const pg = getKysely() const r = await getRethink() const {authToken, socketId: mutatorId, dataLoader} = context const operationId = dataLoader.share() @@ -67,7 +68,14 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( facilitatorUserId: viewerId }) as CheckInMeeting await r.table('NewMeeting').insert(meeting).run() - + try { + await pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(phases)}) + .executeTakeFirst() + } catch (e) { + return standardError(new Error('Failed to create meeting'), {userId: viewerId}) + } // Disallow 2 active check-in meetings const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index 541e983f79b..a27d4cb4b0d 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -74,8 +74,12 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( const meetingId = meeting.id const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) - await Promise.all([ + await Promise.allSettled([ r.table('NewMeeting').insert(meeting).run(), + pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(meeting.phases)}) + .execute(), updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) ]) @@ -87,6 +91,7 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( return createdAt.getTime() > Date.now() - DUPLICATE_THRESHOLD }) if (otherActiveMeeting) { + // trigger exists in PG to prevent this await r.table('NewMeeting').get(meetingId).delete().run() return {error: {message: 'Meeting already started'}} } diff --git a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts new file mode 100644 index 00000000000..49c0f53c670 --- /dev/null +++ b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts @@ -0,0 +1,45 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + // Doing this as a trigger instead of making unique teamId/meetingType/createdAt because rounding createdAt to the nearest 5 seconds felt bad + sql` + CREATE OR REPLACE FUNCTION prevent_meeting_overlap() + RETURNS TRIGGER AS $$ + BEGIN + -- Check if a meeting exists within a 2-second window of the new start_time + IF EXISTS ( + SELECT 1 FROM "NewMeeting" + WHERE "teamId" = NEW."teamId" + AND "meetingType" = NEW."meetingType" + AND ABS(EXTRACT(EPOCH FROM (NEW.start_time - start_time))) < 2 + ) THEN + RAISE EXCEPTION 'Cannot insert meeting. A meeting exists within a 2-second window.'; + END IF; + -- If no conflict, allow the insert + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + DROP TRIGGER IF EXISTS check_meeting_overlap; + CREATE TRIGGER check_meeting_overlap + BEFORE INSERT ON "NewMeeting" + FOR EACH ROW + EXECUTE FUNCTION prevent_meeting_overlap(); + `.execute(pg) +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`DROP TRIGGER IF EXISTS check_meeting_overlap;`.execute(pg) +} diff --git a/packages/server/postgres/types/NewMeetingPhase.d.ts b/packages/server/postgres/types/NewMeetingPhase.d.ts index 6569deba9db..54ad380c972 100644 --- a/packages/server/postgres/types/NewMeetingPhase.d.ts +++ b/packages/server/postgres/types/NewMeetingPhase.d.ts @@ -202,3 +202,4 @@ export type NewMeetingPhase = type TupleToArray = T extends (infer U)[] ? U : never export type NewMeetingStages = TupleToArray +// export type NewMeetingPhaseType = NewMeetingPhase['phaseType'] From 30c1ab3b3be45c6cf8f84348fad6faf8b7f021dc Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 17 Sep 2024 10:09:22 -0600 Subject: [PATCH 31/89] finish adding writes to PG Signed-off-by: Matt Krick --- .../server/dataloader/customLoaderMakers.ts | 1 - .../server/graphql/mutations/endCheckIn.ts | 9 ++- .../graphql/mutations/endSprintPoker.ts | 5 +- .../addAgendaItemToActiveActionMeeting.ts | 6 ++ .../graphql/mutations/helpers/addRecallBot.ts | 6 ++ .../endMeeting/sendNewMeetingSummary.ts | 10 +++- .../mutations/helpers/generateGroups.ts | 6 ++ .../mutations/helpers/handleCompletedStage.ts | 5 ++ .../mutations/helpers/hideConversionModal.ts | 8 ++- .../helpers/notifications/SlackNotifier.ts | 10 ++++ .../mutations/helpers/removeEmptyTasks.ts | 3 +- .../helpers/removeStagesFromMeetings.ts | 9 ++- .../mutations/helpers/removeTeamMember.ts | 5 ++ .../helpers/removeUserFromMeetingStages.ts | 9 ++- .../mutations/helpers/safeCreateTeamPrompt.ts | 10 +--- .../mutations/helpers/safeEndRetrospective.ts | 33 ++++++++--- .../mutations/helpers/safeEndTeamPrompt.ts | 16 +++++- .../mutations/voteForReflectionGroup.ts | 2 +- .../mutations/generateMeetingSummary.ts | 5 ++ .../private/mutations/hardDeleteUser.ts | 56 +++++++++++++------ .../private/mutations/processRecurrence.ts | 27 +++++---- .../graphql/public/mutations/autogroup.ts | 9 ++- .../public/mutations/modifyCheckInQuestion.ts | 4 +- .../public/mutations/resetReflectionGroups.ts | 7 +++ .../public/mutations/setTeamHealthVote.ts | 3 - .../graphql/public/mutations/startCheckIn.ts | 9 +-- .../public/mutations/startTeamPrompt.ts | 18 +++--- .../public/mutations/updateAgendaItem.ts | 5 ++ .../public/mutations/updateMeetingPrompt.ts | 7 +++ .../public/mutations/updateMeetingTemplate.ts | 4 +- .../mutations/updateRecurrenceSettings.ts | 53 ++++++++++++++---- 31 files changed, 263 insertions(+), 97 deletions(-) diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 3214c255f42..a809dae9d0a 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -515,7 +515,6 @@ export const _pgmeetingStatsByOrgId = (parent: RootDataLoader, dependsOn: Regist return new DataLoader( async (orgIds) => { const pg = getKysely() - const r = await getRethink() const meetingStatsByOrgId = await Promise.all( orgIds.map(async (orgId) => { // note: does not include archived teams! diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index c366417261f..c28b73a9fad 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -226,7 +226,12 @@ export default { .run() await pg .updateTable('NewMeeting') - .set({endedAt: now, phases: JSON.stringify(phases), ...insights}) + .set({ + endedAt: now, + phases: JSON.stringify(phases), + usedReactjis: JSON.stringify(insights.usedReactjis), + engagement: insights.engagement + }) .where('id', '=', meetingId) .execute() dataLoader.clearAll('newMeetings') @@ -246,7 +251,7 @@ export default { dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('teamMembersByTeamId').load(teamId), - removeEmptyTasks(meetingId), + removeEmptyTasks(meetingId, teamId), updateTeamInsights(teamId, dataLoader) ]) // need to wait for removeEmptyTasks before finishing the meeting diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 1f6e8b5a1ad..f057e37a57c 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -102,7 +102,8 @@ export default { phases: JSON.stringify(phases), commentCount, storyCount, - ...insights + usedReactjis: JSON.stringify(insights.usedReactjis), + engagement: insights.engagement }) .where('id', '=', meetingId) .executeTakeFirst() @@ -120,7 +121,7 @@ export default { dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('teamMembersByTeamId').load(teamId), - removeEmptyTasks(meetingId), + removeEmptyTasks(meetingId, teamId), // technically, this template could have mutated while the meeting was going on. but in practice, probably not dataLoader.get('meetingTemplates').loadNonNull(templateId), updateTeamInsights(teamId, dataLoader) diff --git a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index 69c0e8bca7e..b7a34bc7b6f 100644 --- a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts +++ b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts @@ -46,6 +46,12 @@ const addAgendaItemToActiveActionMeeting = async ( }) .run(), getKysely() + .with('UpdatePhases', (qb) => + qb + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + ) .with('InsertDiscussion', (qb) => qb.insertInto('Discussion').values({ id: discussionId, diff --git a/packages/server/graphql/mutations/helpers/addRecallBot.ts b/packages/server/graphql/mutations/helpers/addRecallBot.ts index fde576b504a..3a16b97c0d2 100644 --- a/packages/server/graphql/mutations/helpers/addRecallBot.ts +++ b/packages/server/graphql/mutations/helpers/addRecallBot.ts @@ -1,4 +1,5 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import RecallAIServerManager from '../../../utils/RecallAIServerManager' const getBotId = async (videoMeetingURL: string) => { @@ -11,6 +12,11 @@ const addRecallBot = async (meetingId: string, videoMeetingURL: string) => { const r = await getRethink() const recallBotId = (await getBotId(videoMeetingURL)) ?? undefined await r.table('NewMeeting').get(meetingId).update({recallBotId, videoMeetingURL}).run() + await getKysely() + .updateTable('NewMeeting') + .set({recallBotId, videoMeetingURL}) + .where('id', '=', meetingId) + .execute() } export default addRecallBot diff --git a/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts b/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts index 570f456e361..bc4aa19f9a4 100644 --- a/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts +++ b/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts @@ -1,6 +1,8 @@ +import {sql} from 'kysely' import getRethink from '../../../../database/rethinkDriver' import getMailManager from '../../../../email/getMailManager' import newMeetingSummaryEmailCreator from '../../../../email/newMeetingSummaryEmailCreator' +import getKysely from '../../../../postgres/getKysely' import {AnyMeeting} from '../../../../postgres/types/Meeting' import {GQLContext} from '../../../graphql' import isValid from '../../../isValid' @@ -11,13 +13,19 @@ export default async function sendNewMeetingSummary( ) { const {id: meetingId, teamId, summarySentAt} = newMeeting if (summarySentAt) return + const pg = getKysely() const now = new Date() const r = await getRethink() const {dataLoader} = context const [teamMembers, team] = await Promise.all([ dataLoader.get('teamMembersByTeamId').load(teamId), dataLoader.get('teams').loadNonNull(teamId), - r.table('NewMeeting').get(meetingId).update({summarySentAt: now}).run() + r.table('NewMeeting').get(meetingId).update({summarySentAt: now}).run(), + pg + .updateTable('NewMeeting') + .set({summarySentAt: sql`CURRENT_TIMESTAMP`}) + .where('id', '=', meetingId) + .execute() ]) const {name: teamName, orgId} = team const userIds = teamMembers.map(({userId}) => userId) diff --git a/packages/server/graphql/mutations/helpers/generateGroups.ts b/packages/server/graphql/mutations/helpers/generateGroups.ts index 068e3b176d0..df1d1ea0427 100644 --- a/packages/server/graphql/mutations/helpers/generateGroups.ts +++ b/packages/server/graphql/mutations/helpers/generateGroups.ts @@ -1,5 +1,6 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {AutogroupReflectionGroupType, RetroReflection} from '../../../postgres/types' import {Logger} from '../../../utils/Logger' import OpenAIServerManager from '../../../utils/OpenAIServerManager' @@ -53,6 +54,11 @@ const generateGroups = async ( } const r = await getRethink() + await getKysely() + .updateTable('NewMeeting') + .set({autogroupReflectionGroups: JSON.stringify(autogroupReflectionGroups)}) + .where('id', '=', meetingId) + .execute() const meetingRes = await r .table('NewMeeting') .get(meetingId) diff --git a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts index 0fec6c43d1d..5a88154ce8b 100644 --- a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts +++ b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts @@ -62,6 +62,11 @@ const handleCompletedRetrospectiveStage = async ( } else if (stage.phaseType === GROUP) { const {facilitatorUserId, phases, teamId} = meeting unlockAllStagesForPhase(phases, 'discuss', true) + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meeting.id) + .execute() await r .table('NewMeeting') .get(meeting.id) diff --git a/packages/server/graphql/mutations/helpers/hideConversionModal.ts b/packages/server/graphql/mutations/helpers/hideConversionModal.ts index df578c85a2c..b36acb05a09 100644 --- a/packages/server/graphql/mutations/helpers/hideConversionModal.ts +++ b/packages/server/graphql/mutations/helpers/hideConversionModal.ts @@ -8,7 +8,8 @@ const hideConversionModal = async (orgId: string, dataLoader: DataLoaderWorker) const {showConversionModal} = organization if (showConversionModal) { const r = await getRethink() - await getKysely() + const pg = getKysely() + await pg .updateTable('Organization') .set({showConversionModal: false}) .where('id', '=', orgId) @@ -25,6 +26,11 @@ const hideConversionModal = async (orgId: string, dataLoader: DataLoaderWorker) meeting.showConversionModal = false }) const meetingIds = activeMeetings.map(({id}) => id) + await pg + .updateTable('NewMeeting') + .set({showConversionModal: false}) + .where('id', 'in', meetingIds) + .execute() await r .table('NewMeeting') .getAll(r.args(meetingIds)) diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts index 5a10f159e0e..d38ed8dffc4 100644 --- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts @@ -360,6 +360,11 @@ export const SlackSingleChannelNotifier: NotificationIntegrationHelper { +const removeEmptyTasks = async (meetingId: string, teamId: string) => { const r = await getRethink() - const teamId = await r.table('NewMeeting').get(meetingId)('teamId').run() const createdTasks = await r .table('Task') .getAll(teamId, {index: 'teamId'}) diff --git a/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts b/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts index 39c5546998e..1fa3d45a48c 100644 --- a/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts +++ b/packages/server/graphql/mutations/helpers/removeStagesFromMeetings.ts @@ -1,4 +1,5 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {DataLoaderWorker} from '../../graphql' import getNextFacilitatorStageAfterStageRemoved from './getNextFacilitatorStageAfterStageRemoved' @@ -11,6 +12,7 @@ const removeStagesFromMeetings = async ( teamId: string, dataLoader: DataLoaderWorker ) => { + const pg = getKysely() const now = new Date() const r = await getRethink() const [activeMeetings, completedMeetings] = await Promise.all([ @@ -20,7 +22,7 @@ const removeStagesFromMeetings = async ( const meetings = activeMeetings.concat(completedMeetings) await Promise.all( - meetings.map((meeting) => { + meetings.map(async (meeting) => { const {id: meetingId, phases} = meeting phases.forEach((phase) => { // do this inside the loop since it's mutative @@ -45,6 +47,11 @@ const removeStagesFromMeetings = async ( } } }) + await pg + .updateTable('NewMeeting') + .set({facilitatorStageId: meeting.facilitatorStageId, phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() return r .table('NewMeeting') .get(meetingId) diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 8088ee60491..9a9452ba27e 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -166,6 +166,11 @@ const removeTeamMember = async ( // member. return } + await pg + .updateTable('NewMeeting') + .set({facilitatorUserId: newFacilitator.userId}) + .where('id', '=', newFacilitator.meetingId) + .execute() await r .table('NewMeeting') .get(newFacilitator.meetingId) diff --git a/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts b/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts index db8324e6865..dff4349cf30 100644 --- a/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts +++ b/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts @@ -1,4 +1,5 @@ import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {DataLoaderWorker} from '../../graphql' import {isEstimateStage} from '../../meetingTypePredicates' @@ -12,6 +13,7 @@ const removeUserFromMeetingStages = async ( teamId: string, dataLoader: DataLoaderWorker ) => { + const pg = getKysely() const now = new Date() const r = await getRethink() const [activeMeetings, completedMeetings] = await Promise.all([ @@ -21,7 +23,7 @@ const removeUserFromMeetingStages = async ( const meetings = activeMeetings.concat(completedMeetings) await Promise.all( - meetings.map((meeting) => { + meetings.map(async (meeting) => { const {id: meetingId, phases} = meeting let isChanged = false phases.forEach((phase) => { @@ -45,6 +47,11 @@ const removeUserFromMeetingStages = async ( } }) if (!isChanged) return Promise.resolve(undefined) + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() return r .table('NewMeeting') .get(meetingId) diff --git a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts index 7c702494396..7bb25288232 100644 --- a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts @@ -1,4 +1,3 @@ -import {ParabolR} from '../../../database/rethinkDriver' import MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import TeamPromptResponsesPhase from '../../../database/types/TeamPromptResponsesPhase' import generateUID from '../../../generateUID' @@ -13,18 +12,11 @@ const safeCreateTeamPrompt = async ( name: string, teamId: string, facilitatorId: string, - r: ParabolR, dataLoader: DataLoaderWorker, meetingOverrideProps = {} ) => { const meetingType: MeetingTypeEnum = 'teamPrompt' - const meetingCount = await r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType}) - .count() - .default(0) - .run() + const meetingCount = await dataLoader.get('meetingCount').load({teamId, meetingType}) const meetingId = generateUID() const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) const teamMemberIds = teamMembers.map(({id}) => id) diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index beb8d39a719..6a0b1e21689 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -37,6 +37,7 @@ const getTranscription = async (recallBotId?: string | null) => { const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: InternalContext) => { const {dataLoader} = context const {id: meetingId, phases, facilitatorUserId, teamId, recallBotId} = meeting + const pg = getKysely() const r = await getRethink() const [reflectionGroups, reflections, sentimentScore] = await Promise.all([ dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId), @@ -56,17 +57,32 @@ const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: Int await dataLoader.get('commentCountByDiscussionId').loadMany(discussionIds) ).filter(isValid) const commentCount = commentCounts.reduce((cumSum, count) => cumSum + count, 0) + const taskCount = await r + .table('Task') + .getAll(r.args(discussionIds), {index: 'discussionId'}) + .count() + .default(0) + .run() + await pg + .updateTable('NewMeeting') + .set({ + commentCount, + taskCount, + topicCount: reflectionGroupIds.length, + reflectionCount: reflections.length, + sentimentScore, + summary, + transcription + }) + .where('id', '=', meetingId) + .execute() await r .table('NewMeeting') .get(meetingId) .update( { commentCount, - taskCount: r - .table('Task') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .count() - .default(0) as unknown as number, + taskCount, topicCount: reflectionGroupIds.length, reflectionCount: reflections.length, sentimentScore, @@ -77,7 +93,7 @@ const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: Int ) .run() - dataLoader.get('newMeetings').clear(meetingId) + dataLoader.clearAll('newMeetings') // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount sendNewMeetingSummary(meeting, context).catch(Logger.log) updateQualAIMeetingsCount(meetingId, teamId, dataLoader) @@ -134,7 +150,8 @@ const safeEndRetrospective = async ({ .set({ endedAt: sql`CURRENT_TIMESTAMP`, phases: JSON.stringify(phases), - ...insights + usedReactjis: JSON.stringify(insights.usedReactjis), + engagement: insights.engagement }) .where('id', '=', meetingId) .executeTakeFirst() @@ -155,7 +172,7 @@ const safeEndRetrospective = async ({ dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('teamMembersByTeamId').load(teamId), - removeEmptyTasks(meetingId), + removeEmptyTasks(meetingId, teamId), dataLoader.get('meetingTemplates').loadNonNull(templateId), updateTeamInsights(teamId, dataLoader) ]) diff --git a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts index e44d2ee81d2..33364edf2fb 100644 --- a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts @@ -1,3 +1,4 @@ +import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' import getRethink, {ParabolR} from '../../../database/rethinkDriver' @@ -19,10 +20,11 @@ import updateTeamInsights from './updateTeamInsights' const summarizeTeamPrompt = async (meeting: TeamPromptMeeting, context: InternalContext) => { const {dataLoader} = context + const pg = getKysely() const r = await getRethink() const summary = await generateStandupMeetingSummary(meeting, dataLoader) - + await pg.updateTable('NewMeeting').set({summary}).where('id', '=', meeting.id).execute() await r .table('NewMeeting') .get(meeting.id) @@ -31,7 +33,7 @@ const summarizeTeamPrompt = async (meeting: TeamPromptMeeting, context: Internal }) .run() - dataLoader.get('newMeetings').clear(meeting.id) + dataLoader.clearAll('newMeetings') // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount sendNewMeetingSummary(meeting, context).catch(Logger.log) updateQualAIMeetingsCount(meeting.id, meeting.teamId, dataLoader) @@ -58,6 +60,7 @@ const safeEndTeamPrompt = async ({ context: InternalContext subOptions: SubOptions }) => { + const pg = getKysely() const {dataLoader} = context const {endedAt, id: meetingId, teamId} = meeting @@ -66,6 +69,14 @@ const safeEndTeamPrompt = async ({ // RESOLUTION const insights = await gatherInsights(meeting, dataLoader) + await pg + .updateTable('NewMeeting') + .set({ + endedAt: sql`CURRENT_TIMESTAMP`, + usedReactjis: JSON.stringify(insights.usedReactjis), + engagement: insights.engagement + }) + .execute() const completedTeamPrompt = await r .table('NewMeeting') .get(meetingId) @@ -107,7 +118,6 @@ const safeEndTeamPrompt = async ({ }) ) const timelineEventId = events[0]!.id - const pg = getKysely() await pg.insertInto('TimelineEvent').values(events).execute() summarizeTeamPrompt(meeting, context) analytics.teamPromptEnd(completedTeamPrompt, meetingMembers, responses, dataLoader) diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index 171cf1f451e..6afe994dee6 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -42,7 +42,7 @@ export default { }) } const {meetingId} = reflectionGroup - const meeting = await r.table('NewMeeting').get(meetingId).run() + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (meeting.meetingType !== 'retrospective') { return {error: {message: 'Meeting type is not retrospective'}} } diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 3640fe0541e..0d5d21ede25 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -169,6 +169,11 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn if (!newSummary) return null const now = new Date() + await getKysely() + .updateTable('NewMeeting') + .set({summary: newSummary}) + .where('id', '=', meeting.id) + .execute() await r .table('NewMeeting') .get(meeting.id) diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 2c9bd2e58c0..27995849eea 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -1,7 +1,7 @@ import getRethink from '../../../database/rethinkDriver' import {RValue} from '../../../database/stricterR' import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' -import getPg from '../../../postgres/getPg' +import getKysely from '../../../postgres/getKysely' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' import {getUserById} from '../../../postgres/queries/getUsersByIds' import blacklistJWT from '../../../utils/blacklistJWT' @@ -15,6 +15,7 @@ const setFacilitatedUserIdOrDelete = async ( teamIds: string[], dataLoader: DataLoaderInstance ) => { + const pg = getKysely() const r = await getRethink() const facilitatedMeetings = await r .table('NewMeeting') @@ -26,6 +27,11 @@ const setFacilitatedUserIdOrDelete = async ( const meetingMembers = await dataLoader.get('meetingMembersByMeetingId').load(meetingId) const otherMember = meetingMembers.find(({userId}) => userId !== userIdToDelete) if (otherMember) { + await pg + .updateTable('NewMeeting') + .set({facilitatorUserId: otherMember.userId}) + .where('id', '=', meetingId) + .execute() await r .table('NewMeeting') .get(meetingId) @@ -34,6 +40,7 @@ const setFacilitatedUserIdOrDelete = async ( }) .run() } else { + await pg.deleteFrom('NewMeeting').where('id', '=', meetingId).execute() // single-person meeting must be deleted because facilitatorUserId must be non-null await r.table('NewMeeting').get(meetingId).delete().run() } @@ -53,7 +60,7 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( return {error: {message: 'Provide a userId or email'}} } const r = await getRethink() - const pg = getPg() + const pg = getKysely() const user = userId ? await getUserById(userId) : email ? await getUserByEmail(email) : null if (!user) { @@ -69,16 +76,24 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( const teamIds = teamMembers.map(({teamId}) => teamId) const meetingIds = meetingMembers.map(({meetingId}) => meetingId) - const discussions = await pg.query(`SELECT "id" FROM "Discussion" WHERE "teamId" = ANY ($1);`, [ - teamIds - ]) - const teamDiscussionIds = discussions.rows.map(({id}) => id) + const discussions = await pg + .selectFrom('Discussion') + .select('id') + .where('id', 'in', teamIds) + .execute() + const teamDiscussionIds = discussions.map(({id}) => id) // soft delete first for side effects await softDeleteUser(userIdToDelete, dataLoader) // all other writes await setFacilitatedUserIdOrDelete(userIdToDelete, teamIds, dataLoader) + await pg + .updateTable('NewMeeting') + .set({createdBy: null}) + .where('teamId', 'in', teamIds) + .where('createdBy', '=', userIdToDelete) + .execute() await r({ nullifyCreatedBy: r .table('NewMeeting') @@ -107,24 +122,29 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( // now postgres, after FKs are added then triggers should take care of children // TODO when we're done migrating to PG, these should have constraints that ON DELETE CASCADE - await Promise.all([ - pg.query(`DELETE FROM "AtlassianAuth" WHERE "userId" = $1`, [userIdToDelete]), - pg.query(`DELETE FROM "GitHubAuth" WHERE "userId" = $1`, [userIdToDelete]), - pg.query( - `DELETE FROM "TaskEstimate" WHERE "meetingId" = ANY($1::varchar[]) AND "userId" = $2`, - [meetingIds, userIdToDelete] - ), - pg.query( - `DELETE FROM "Poll" WHERE "discussionId" = ANY($1::varchar[]) AND "createdById" = $2`, - [teamDiscussionIds, userIdToDelete] + await pg + .with('AtlassianAuthDelete', (qb) => + qb.deleteFrom('AtlassianAuth').where('userId', '=', userIdToDelete) ) - ]) + .with('GitHubAuthDelete', (qb) => + qb.deleteFrom('GitHubAuth').where('userId', '=', userIdToDelete) + ) + .with('TaskEstimateDelete', (qb) => + qb + .deleteFrom('TaskEstimate') + .where('userId', '=', userIdToDelete) + .where('meetingId', 'in', meetingIds) + ) + .deleteFrom('Poll') + .where('discussionId', 'in', teamDiscussionIds) + .where('createdById', '=', userIdToDelete) + .execute() // Send metrics to HubSpot before the user is really deleted in DB await sendAccountRemovedEvent(userIdToDelete, user.email, reasonText ?? '') // User needs to be deleted after children - await pg.query(`DELETE FROM "User" WHERE "id" = $1`, [userIdToDelete]) + await pg.deleteFrom('User').where('id', '=', userIdToDelete).execute() await blacklistJWT(userIdToDelete, toEpochSeconds(new Date())) return {} diff --git a/packages/server/graphql/private/mutations/processRecurrence.ts b/packages/server/graphql/private/mutations/processRecurrence.ts index eb6a8a2c362..f30e8db8b3c 100644 --- a/packages/server/graphql/private/mutations/processRecurrence.ts +++ b/packages/server/graphql/private/mutations/processRecurrence.ts @@ -5,6 +5,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DateTime, RRuleSet} from 'rrule-rust' import {fromDateTime, toDateTime} from '../../../../client/shared/rruleUtil' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {getActiveMeetingSeries} from '../../../postgres/queries/getActiveMeetingSeries' import {RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting' import {MeetingSeries} from '../../../postgres/types/MeetingSeries' @@ -28,6 +29,7 @@ const startRecurringMeeting = async ( dataLoader: DataLoaderWorker, subOptions: SubOptions ) => { + const pg = getKysely() const r = await getRethink() const {id: meetingSeriesId, teamId, facilitatorId, meetingType} = meetingSeries @@ -50,18 +52,15 @@ const startRecurringMeeting = async ( const meeting = await (async () => { if (meetingSeries.meetingType === 'teamPrompt') { const teamPromptMeeting = lastMeeting as TeamPromptMeeting | null - const meeting = await safeCreateTeamPrompt( - meetingName, - teamId, - facilitatorId, - r, - dataLoader, - { - scheduledEndTime, - meetingSeriesId: meetingSeries.id, - meetingPrompt: teamPromptMeeting?.meetingPrompt ?? DEFAULT_PROMPT - } - ) + const meeting = await safeCreateTeamPrompt(meetingName, teamId, facilitatorId, dataLoader, { + scheduledEndTime, + meetingSeriesId: meetingSeries.id, + meetingPrompt: teamPromptMeeting?.meetingPrompt ?? DEFAULT_PROMPT + }) + await pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(meeting.phases)}) + .execute() await r.table('NewMeeting').insert(meeting).run() const data = {teamId, meetingId: meeting.id} publish(SubscriptionChannel.TEAM, teamId, 'StartTeamPromptSuccess', data, subOptions) @@ -88,6 +87,10 @@ const startRecurringMeeting = async ( dataLoader ) await r.table('NewMeeting').insert(meeting).run() + await pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(meeting.phases)}) + .execute() const data = {teamId, meetingId: meeting.id} publish(SubscriptionChannel.TEAM, teamId, 'StartRetrospectiveSuccess', data, subOptions) return meeting diff --git a/packages/server/graphql/public/mutations/autogroup.ts b/packages/server/graphql/public/mutations/autogroup.ts index 182b6910563..21db1e150a7 100644 --- a/packages/server/graphql/public/mutations/autogroup.ts +++ b/packages/server/graphql/public/mutations/autogroup.ts @@ -1,5 +1,6 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -13,6 +14,7 @@ const autogroup: MutationResolvers['autogroup'] = async ( {meetingId}: {meetingId: string}, context: GQLContext ) => { + const pg = getKysely() const r = await getRethink() const {authToken, dataLoader, socketId: mutatorId} = context const viewerId = getUserId(authToken) @@ -70,7 +72,12 @@ const autogroup: MutationResolvers['autogroup'] = async ( ) ) }), - r.table('NewMeeting').get(meetingId).update({resetReflectionGroups}).run() + r.table('NewMeeting').get(meetingId).update({resetReflectionGroups}).run(), + pg + .updateTable('NewMeeting') + .set({resetReflectionGroups: JSON.stringify(resetReflectionGroups)}) + .where('id', '=', meetingId) + .execute() ]) meeting.resetReflectionGroups = resetReflectionGroups analytics.suggestGroupsClicked(viewer, meetingId, teamId) diff --git a/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts index 18149f77431..ec00936901a 100644 --- a/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts +++ b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts @@ -3,7 +3,6 @@ import {getUserId, isTeamMember} from '../../../utils/authorization' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import publish from '../../../utils/publish' -import getRethink from '../../../database/rethinkDriver' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import {analytics} from '../../../utils/analytics/analytics' import standardError from '../../../utils/standardError' @@ -14,14 +13,13 @@ const modifyCheckInQuestion: MutationResolvers['modifyCheckInQuestion'] = async {meetingId, checkInQuestion, modifyType}, {authToken, dataLoader, socketId: mutatorId} ) => { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) // AUTH const [meeting, viewer] = await Promise.all([ - r.table('NewMeeting').get(meetingId).run(), + dataLoader.get('newMeetings').load(meetingId), dataLoader.get('users').loadNonNull(viewerId) ]) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) diff --git a/packages/server/graphql/public/mutations/resetReflectionGroups.ts b/packages/server/graphql/public/mutations/resetReflectionGroups.ts index 4f574390d0f..991eea4ddda 100644 --- a/packages/server/graphql/public/mutations/resetReflectionGroups.ts +++ b/packages/server/graphql/public/mutations/resetReflectionGroups.ts @@ -1,5 +1,6 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -21,6 +22,7 @@ const resetReflectionGroups: MutationResolvers['resetReflectionGroups'] = async {meetingId}: {meetingId: string}, context: GQLContext ) => { + const pg = getKysely() const {authToken, dataLoader, socketId: mutatorId} = context const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -75,6 +77,11 @@ const resetReflectionGroups: MutationResolvers['resetReflectionGroups'] = async .get(meetingId) .replace(r.row.without('resetReflectionGroups') as any) .run() + await pg + .updateTable('NewMeeting') + .set({resetReflectionGroups: null}) + .where('id', '=', meetingId) + .execute() meeting.resetReflectionGroups = null analytics.resetGroupsClicked(viewer, meetingId, teamId) const data = {meetingId} diff --git a/packages/server/graphql/public/mutations/setTeamHealthVote.ts b/packages/server/graphql/public/mutations/setTeamHealthVote.ts index f9484791312..68198ba59cf 100644 --- a/packages/server/graphql/public/mutations/setTeamHealthVote.ts +++ b/packages/server/graphql/public/mutations/setTeamHealthVote.ts @@ -13,7 +13,6 @@ import {MutationResolvers} from '../resolverTypes' const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealthVote) => { const pg = getKysely() await pg.transaction().execute(async (trx) => { - console.log('start transaction', newVote) const meeting = await trx .selectFrom('NewMeeting') .select(({fn}) => fn('to_json', ['phases']).as('phases')) @@ -22,7 +21,6 @@ const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealt // NewMeeting: add OrThrow in phase 3 .executeTakeFirst() if (!meeting) return - console.log('got lock', newVote) const {phases} = meeting const phase = getPhase(phases, 'TEAM_HEALTH') const {stages} = phase @@ -39,7 +37,6 @@ const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealt .set({phases: JSON.stringify(phases)}) .where('id', '=', meetingId) .execute() - console.log('wrote update, commit', newVote) }) const r = await getRethink() const updater = (stage: RValue) => diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index 70442b50723..08999d8d5c3 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -41,13 +41,7 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( const meetingType: MeetingTypeEnum = 'action' // RESOLUTION - const meetingCount = await r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType}) - .count() - .default(0) - .run() + const meetingCount = await dataLoader.get('meetingCount').load({teamId, meetingType}) const meetingId = generateUID() const phases = await createNewMeetingPhases( @@ -87,6 +81,7 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( await r.table('NewMeeting').get(meetingId).delete().run() return {error: {message: 'Meeting already started'}} } + dataLoader.clearAll('newMeetings') const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) const agendaItemIds = agendaItems.map(({id}) => id) diff --git a/packages/server/graphql/public/mutations/startTeamPrompt.ts b/packages/server/graphql/public/mutations/startTeamPrompt.ts index 322ee844986..0f22ea40ab6 100644 --- a/packages/server/graphql/public/mutations/startTeamPrompt.ts +++ b/packages/server/graphql/public/mutations/startTeamPrompt.ts @@ -1,7 +1,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' -import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' import RedisLockQueue from '../../../utils/RedisLockQueue' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -22,6 +21,7 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async ( {teamId, name, rrule, gcalInput}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -49,16 +49,18 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async ( //TODO: use client timezone here (requires sending it from the client and passing it via gql context most likely) const meetingName = createMeetingSeriesTitle(name || 'Standup', new Date(), 'UTC') const eventName = rrule ? name || 'Standup' : meetingName - const meeting = await safeCreateTeamPrompt(meetingName, teamId, viewerId, r, dataLoader) + const meeting = await safeCreateTeamPrompt(meetingName, teamId, viewerId, dataLoader) await Promise.all([ r.table('NewMeeting').insert(meeting).run(), - updateTeamByTeamId( - { - lastMeetingType: 'teamPrompt' - }, - teamId - ) + pg + .with('NewMeetingInsert', (qb) => + qb.insertInto('NewMeeting').values({...meeting, phases: JSON.stringify(meeting.phases)}) + ) + .updateTable('Team') + .set({lastMeetingType: 'teamPrompt'}) + .where('id', '=', teamId) + .execute() ]) const {id: meetingId} = meeting diff --git a/packages/server/graphql/public/mutations/updateAgendaItem.ts b/packages/server/graphql/public/mutations/updateAgendaItem.ts index 51c506e8fdb..6acc7e5efd2 100644 --- a/packages/server/graphql/public/mutations/updateAgendaItem.ts +++ b/packages/server/graphql/public/mutations/updateAgendaItem.ts @@ -57,6 +57,11 @@ const updateAgendaItem: MutationResolvers['updateAgendaItem'] = async ( return (agendaItem && agendaItem.sortOrder) || 0 } stages.sort((a, b) => (getSortOrder(a) > getSortOrder(b) ? 1 : -1)) + await pg + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() await r .table('NewMeeting') .get(meetingId) diff --git a/packages/server/graphql/public/mutations/updateMeetingPrompt.ts b/packages/server/graphql/public/mutations/updateMeetingPrompt.ts index 98d2986eb96..edc59a97d9c 100644 --- a/packages/server/graphql/public/mutations/updateMeetingPrompt.ts +++ b/packages/server/graphql/public/mutations/updateMeetingPrompt.ts @@ -1,5 +1,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' 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' @@ -10,6 +11,7 @@ const updateMeetingPrompt: MutationResolvers['updateMeetingPrompt'] = async ( {meetingId, newPrompt}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() @@ -36,6 +38,11 @@ const updateMeetingPrompt: MutationResolvers['updateMeetingPrompt'] = async ( } // RESOLUTION + await pg + .updateTable('NewMeeting') + .set({meetingPrompt: newPrompt}) + .where('id', '=', meetingId) + .execute() await r .table('NewMeeting') .get(meetingId) diff --git a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts index d355a6062a2..de34566210a 100644 --- a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts +++ b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts @@ -1,5 +1,6 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' @@ -11,6 +12,7 @@ const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async {meetingId, templateId}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const viewerId = getUserId(authToken) const r = await getRethink() const operationId = dataLoader.share() @@ -37,7 +39,7 @@ const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async } ) } - + await pg.updateTable('NewMeeting').set({templateId}).where('id', '=', meetingId).execute() await r.table('NewMeeting').get(meetingId).update({templateId}).run() meeting.templateId = templateId diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 18f93e412b1..8b0626ab92c 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -1,8 +1,10 @@ import dayjs from 'dayjs' +import {sql} from 'kysely' import {toDateTime} from 'parabol-client/shared/rruleUtil' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DateTime, RRuleSet} from 'rrule-rust' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../../../postgres/queries/insertMeetingSeries' import restartMeetingSeries from '../../../postgres/queries/restartMeetingSeries' import updateMeetingSeriesQuery from '../../../postgres/queries/updateMeetingSeries' @@ -34,6 +36,7 @@ export const startNewMeetingSeries = async ( name: meetingName, facilitatorUserId: facilitatorId } = meeting + const pg = getKysely() const r = await getRethink() if (!facilitatorId) { throw new Error('No facilitatorId') @@ -58,7 +61,11 @@ export const startNewMeetingSeries = async ( scheduledEndTime: nextMeetingStartDate }) .run() - + await pg + .updateTable('NewMeeting') + .set({meetingSeriesId: newMeetingSeriesId, scheduledEndTime: nextMeetingStartDate}) + .where('id', '=', meetingId) + .execute() return { id: newMeetingSeriesId, ...newMeetingSeriesParams @@ -66,6 +73,7 @@ export const startNewMeetingSeries = async ( } const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRule: RRuleSet) => { + const pg = getKysely() const r = await getRethink() const {id: meetingSeriesId} = meetingSeries @@ -78,22 +86,43 @@ const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRu .getAll(meetingSeriesId, {index: 'meetingSeriesId'}) .filter({endedAt: null}, {default: true}) .run() - const updates = activeMeetings.map((meeting) => - r - .table('NewMeeting') - .get(meeting.id) - .update({ - scheduledEndTime: getNextRRuleDate(newRecurrenceRule) - }) - .run() - ) - await Promise.all(updates) + if (activeMeetings.length > 0) { + const meetingIds = activeMeetings.map(({id}) => id) + const scheduledEndTime = getNextRRuleDate(newRecurrenceRule) + await pg + .updateTable('NewMeeting') + .set({scheduledEndTime}) + .where('id', 'in', meetingIds) + .execute() + const updates = activeMeetings.map((meeting) => + r + .table('NewMeeting') + .get(meeting.id) + .update({ + scheduledEndTime + }) + .run() + ) + await Promise.all(updates) + } } const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { + const pg = getKysely() const r = await getRethink() const now = new Date() - + await pg + .with('NewMeetingUpdateEnd', (qb) => + qb + .updateTable('NewMeeting') + .set({scheduledEndTime: null}) + .where('meetingSeriesId', '=', meetingSeries.id) + .where('endedAt', 'is', null) + ) + .updateTable('MeetingSeries') + .set({cancelledAt: sql`CURRENT_TIMESTAMP`}) + .where('id', '=', meetingSeries.id) + .execute() await updateMeetingSeriesQuery({cancelledAt: now}, meetingSeries.id) await r .table('NewMeeting') From e2f8b30ad7c79d2d897a5be1159a34c91bff1190 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 17 Sep 2024 10:25:02 -0600 Subject: [PATCH 32/89] fix downmigration Signed-off-by: Matt Krick --- .../server/postgres/migrations/1726251201860_NewMeeting-uniq.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts index 49c0f53c670..71cd878a294 100644 --- a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts +++ b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts @@ -41,5 +41,5 @@ export async function down() { pool: getPg() }) }) - await sql`DROP TRIGGER IF EXISTS check_meeting_overlap;`.execute(pg) + await sql`DROP TRIGGER IF EXISTS "check_meeting_overlap" ON "NewMeeting";`.execute(pg) } From 2eb448235ee7c30104d0c80e998c81f308a49344 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 17 Sep 2024 12:43:06 -0600 Subject: [PATCH 33/89] self review Signed-off-by: Matt Krick --- packages/client/components/TeamHealth.tsx | 1 - .../public/mutations/revealTeamHealthVotes.ts | 4 ++-- .../graphql/public/mutations/startCheckIn.ts | 12 ++++------- .../mutations/updateRecurrenceSettings.ts | 14 ++++++------- .../1726251201860_NewMeeting-uniq.ts | 4 ++-- .../src/updateMeetingSeriesByIdQuery.sql | 10 ---------- .../postgres/queries/updateMeetingSeries.ts | 20 ------------------- .../postgres/types/NewMeetingPhase.d.ts | 1 - 8 files changed, 15 insertions(+), 51 deletions(-) delete mode 100644 packages/server/postgres/queries/src/updateMeetingSeriesByIdQuery.sql delete mode 100644 packages/server/postgres/queries/updateMeetingSeries.ts diff --git a/packages/client/components/TeamHealth.tsx b/packages/client/components/TeamHealth.tsx index ba2b5bd1274..2f746c182cf 100644 --- a/packages/client/components/TeamHealth.tsx +++ b/packages/client/components/TeamHealth.tsx @@ -61,7 +61,6 @@ const TeamHealth = (props: Props) => { const isFacilitator = facilitatorUserId === viewerId const canVote = !isRevealed && !endedAt - console.log({canVote, isRevealed, endedAt}) const canReveal = isFacilitator && votedUserIds && votedUserIds.length > 0 && canVote const onVote = (label: string) => { diff --git a/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts b/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts index a7193367d3e..86bbbb99744 100644 --- a/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts +++ b/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts @@ -44,9 +44,9 @@ const revealTeamHealthVotes: MutationResolvers['revealTeamHealthVotes'] = async // VALIDATION const teamHealthPhase = getPhase(phases, 'TEAM_HEALTH') const {stages} = teamHealthPhase - const stage = stages.find((stage) => stage.id === stageId) - const phaseIdx = phases.indexOf(teamHealthPhase) const stageIdx = stages.findIndex((stage) => stage.id === stageId) + const phaseIdx = phases.indexOf(teamHealthPhase) + const stage = stages[stageIdx] if (!stage || stage.phaseType !== 'TEAM_HEALTH') { return {error: {message: 'Invalid stageId provided'}} diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index 08999d8d5c3..3714e6a2042 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -62,14 +62,10 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( facilitatorUserId: viewerId }) as CheckInMeeting await r.table('NewMeeting').insert(meeting).run() - try { - await pg - .insertInto('NewMeeting') - .values({...meeting, phases: JSON.stringify(phases)}) - .executeTakeFirst() - } catch (e) { - return standardError(new Error('Failed to create meeting'), {userId: viewerId}) - } + await pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(phases)}) + .execute() // Disallow 2 active check-in meetings const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 8b0626ab92c..4b46db14d93 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -7,7 +7,6 @@ import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../../../postgres/queries/insertMeetingSeries' import restartMeetingSeries from '../../../postgres/queries/restartMeetingSeries' -import updateMeetingSeriesQuery from '../../../postgres/queries/updateMeetingSeries' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' import {MeetingSeries} from '../../../postgres/types/MeetingSeries' import {analytics} from '../../../utils/analytics/analytics' @@ -110,7 +109,6 @@ const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRu const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { const pg = getKysely() const r = await getRethink() - const now = new Date() await pg .with('NewMeetingUpdateEnd', (qb) => qb @@ -123,7 +121,6 @@ const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { .set({cancelledAt: sql`CURRENT_TIMESTAMP`}) .where('id', '=', meetingSeries.id) .execute() - await updateMeetingSeriesQuery({cancelledAt: now}, meetingSeries.id) await r .table('NewMeeting') .getAll(meetingSeries.id, {index: 'meetingSeriesId'}) @@ -148,6 +145,7 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = {meetingId, name, rrule}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -193,10 +191,12 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = } if (name) { - await updateMeetingSeriesQuery({title: name}, meetingSeries.id) + await pg + .updateTable('MeetingSeries') + .set({title: name}) + .where('id', '=', meetingSeries.id) + .execute() } - - dataLoader.get('meetingSeries').clear(meetingSeries.id) } else { if (!rrule) { return standardError( @@ -209,7 +209,7 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = analytics.recurrenceStarted(viewer, newMeetingSeries) } - dataLoader.get('newMeetings').clear(meetingId) + dataLoader.clearAll(['newMeetings', 'meetingSeries']) // RESOLUTION const data = {meetingId} diff --git a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts index 71cd878a294..829f053409e 100644 --- a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts +++ b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts @@ -27,8 +27,8 @@ export async function up() { RETURN NEW; END; $$ LANGUAGE plpgsql; - DROP TRIGGER IF EXISTS check_meeting_overlap; - CREATE TRIGGER check_meeting_overlap + DROP TRIGGER IF EXISTS "check_meeting_overlap" ON "NewMeeting"; + CREATE TRIGGER "check_meeting_overlap" BEFORE INSERT ON "NewMeeting" FOR EACH ROW EXECUTE FUNCTION prevent_meeting_overlap(); diff --git a/packages/server/postgres/queries/src/updateMeetingSeriesByIdQuery.sql b/packages/server/postgres/queries/src/updateMeetingSeriesByIdQuery.sql deleted file mode 100644 index bc8bd35dde6..00000000000 --- a/packages/server/postgres/queries/src/updateMeetingSeriesByIdQuery.sql +++ /dev/null @@ -1,10 +0,0 @@ -/* - @name updateMeetingSeriesByIdQuery -*/ -UPDATE "MeetingSeries" SET - "meetingType" = COALESCE(:meetingType, "meetingType"), - "title" = COALESCE(:title, "title"), - "recurrenceRule" = COALESCE(:recurrenceRule, "recurrenceRule"), - "duration" = COALESCE(:duration, "duration"), - "cancelledAt" = COALESCE(:cancelledAt, "cancelledAt") -WHERE id = :id; diff --git a/packages/server/postgres/queries/updateMeetingSeries.ts b/packages/server/postgres/queries/updateMeetingSeries.ts deleted file mode 100644 index e3ff1f91435..00000000000 --- a/packages/server/postgres/queries/updateMeetingSeries.ts +++ /dev/null @@ -1,20 +0,0 @@ -import getPg from '../getPg' -import { - IUpdateMeetingSeriesByIdQueryParams, - updateMeetingSeriesByIdQuery -} from './generated/updateMeetingSeriesByIdQuery' - -const updateMeetingSeries = async ( - update: Partial, - id: number -) => { - return updateMeetingSeriesByIdQuery.run( - { - ...update, - id - } as any, - getPg() - ) -} - -export default updateMeetingSeries diff --git a/packages/server/postgres/types/NewMeetingPhase.d.ts b/packages/server/postgres/types/NewMeetingPhase.d.ts index 54ad380c972..6569deba9db 100644 --- a/packages/server/postgres/types/NewMeetingPhase.d.ts +++ b/packages/server/postgres/types/NewMeetingPhase.d.ts @@ -202,4 +202,3 @@ export type NewMeetingPhase = type TupleToArray = T extends (infer U)[] ? U : never export type NewMeetingStages = TupleToArray -// export type NewMeetingPhaseType = NewMeetingPhase['phaseType'] From 7f256a2cbc500253eb0e3dd1e7685351e1844b8a Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 23 Sep 2024 10:54:06 -0700 Subject: [PATCH 34/89] first pass Signed-off-by: Matt Krick --- .../1726174453131_NewMeeting-phase1.ts | 4 +- .../1726602922665_NewMeeting-phase2.ts | 163 ++++++++++++++++++ 2 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts diff --git a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts index 6bd308d9849..ecaa2a94cf3 100644 --- a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts +++ b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts @@ -65,7 +65,7 @@ export async function up() { CONSTRAINT "fk_teamId" FOREIGN KEY("teamId") REFERENCES "Team"("id") - ON DELETE SET NULL, + ON DELETE CASCADE, CONSTRAINT "fk_meetingSeriesId" FOREIGN KEY("meetingSeriesId") REFERENCES "MeetingSeries"("id") @@ -73,7 +73,7 @@ export async function up() { CONSTRAINT "fk_templateId" FOREIGN KEY("templateId") REFERENCES "MeetingTemplate"("id") - ON DELETE SET NULL + ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_createdAt" ON "NewMeeting"("createdAt"); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_facilitatorUserId" ON "NewMeeting"("facilitatorUserId"); diff --git a/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts new file mode 100644 index 00000000000..80d6e5b84d5 --- /dev/null +++ b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts @@ -0,0 +1,163 @@ +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('NewMeeting') + .indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')]) + .run() + await r.table('NewMeeting').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'isLegacy', + 'createdAt', + 'updatedAt', + 'createdBy', + 'endedAt', + 'facilitatorStageId', + 'facilitatorUserId', + 'meetingCount', + 'meetingNumber', + 'name', + 'summarySentAt', + 'teamId', + 'meetingType', + 'phases', + 'showConversionModal', + 'meetingSeriesId', + 'scheduledEndTime', + 'summary', + 'sentimentScore', + 'usedReactjis', + 'slackTs', + 'engagement', + 'totalVotes', + 'maxVotesPerGroup', + 'disableAnonymity', + 'commentCount', + 'taskCount', + 'agendaItemCount', + 'storyCount', + 'templateId', + 'topicCount', + 'reflectionCount', + 'transcription', + 'recallBotId', + 'videoMeetingURL', + 'autogroupReflectionGroups', + 'resetReflectionGroups', + 'templateRefId', + 'meetingPrompt' + ] as const + type NewMeeting = { + [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 + + const insertRow = async (row) => { + try { + await pg + .insertInto('NewMeeting') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_createdBy') { + return insertRow({...row, createdBy: null}) + } + if (e.constraint === 'fk_facilitatorUserId') { + return insertRow({...row, facilitatorUserId: null}) + } + if (e.constraint === 'fk_teamId') { + console.log('Meeting has no team, skipping insert', row.id) + return + } + if (e.constraint === 'fk_meetingSeriesId') { + return insertRow({...row, meetingSeriesId: null}) + } + if (e.constraint === 'fk_templateId') { + console.log('Meeting has no template, skipping insert', row.id) + return + } + throw e + } + } + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('NewMeeting') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as NewMeeting[] + + const rowsToInsert = rawRowsToInsert.map((row) => { + const { + phases, + name, + summary, + usedReactjis, + slackTs, + transcription, + autogroupReflectionGroups, + resetReflectionGroups, + meetingPrompt, + + ...rest + } = row as any + return { + ...rest, + phases: JSON.stringify(phases), + name: name.slice(0, 100), + summary: summary ? summary.slice(0, 10000) : null, + usedReactjis: JSON.stringify(usedReactjis), + slackTs: isNaN(Number(slackTs)) ? null : Number(slackTs), + transcription: JSON.stringify(transcription), + autogroupReflectionGroups: JSON.stringify(autogroupReflectionGroups), + resetReflectionGroups: JSON.stringify(resetReflectionGroups), + meetingPrompt: meetingPrompt ? meetingPrompt.slice(0, 255) : null + } + }) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.updatedAt + curId = lastRow.id + await Promise.all(rowsToInsert.map(async (row) => insertRow(row))) + } +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "NewMeeting" CASCADE`.execute(pg) +} From 36fe2f892f1cd37197481a675af1756ba64d2e24 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 09:45:05 -0700 Subject: [PATCH 35/89] chore: add default value for meetinCount --- .../postgres/migrations/1726602922665_NewMeeting-phase2.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts index 80d6e5b84d5..ee42e8525fe 100644 --- a/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts +++ b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts @@ -128,7 +128,7 @@ export async function up() { autogroupReflectionGroups, resetReflectionGroups, meetingPrompt, - + meetingCount, ...rest } = row as any return { @@ -141,7 +141,8 @@ export async function up() { transcription: JSON.stringify(transcription), autogroupReflectionGroups: JSON.stringify(autogroupReflectionGroups), resetReflectionGroups: JSON.stringify(resetReflectionGroups), - meetingPrompt: meetingPrompt ? meetingPrompt.slice(0, 255) : null + meetingPrompt: meetingPrompt ? meetingPrompt.slice(0, 255) : null, + meetingCount: meetingCount || 0 } }) From 9e3c1bcd07962ef5dd909b0304e89cc6a1d2b7ec Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 12:30:09 -0700 Subject: [PATCH 36/89] equality checker first pass --- .../mutations/checkRethinkPgEquality.ts | 63 ++++++++++++++----- .../1726602922665_NewMeeting-phase2.ts | 4 ++ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts b/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts index 927cf3685fe..79381f9b0bd 100644 --- a/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts +++ b/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts @@ -1,12 +1,15 @@ import getRethink from '../../../database/rethinkDriver' import getFileStoreManager from '../../../fileStorage/getFileStoreManager' -import getKysely from '../../../postgres/getKysely' +import {selectNewMeetings} from '../../../postgres/select' import {checkRowCount, checkTableEq} from '../../../postgres/utils/checkEqBase' import { compareDateAlmostEqual, + compareRealNumber, + compareRValStringAsNumber, compareRValUndefinedAsFalse, compareRValUndefinedAsNull, compareRValUndefinedAsNullAndTruncateRVal, + compareRValUndefinedAsZero, defaultEqFn } from '../../../postgres/utils/rethinkEqualityFns' import {MutationResolvers} from '../resolverTypes' @@ -33,12 +36,12 @@ const checkRethinkPgEquality: MutationResolvers['checkRethinkPgEquality'] = asyn ) => { const r = await getRethink() - if (tableName === 'TeamMember') { + if (tableName === 'NewMeting') { const rowCountResult = await checkRowCount(tableName) - const rethinkQuery = (joinedAt: Date, id: string | number) => { + const rethinkQuery = (updatedAt: Date, id: string | number) => { return r - .table('TeamMember' as any) - .between([joinedAt, id], [r.maxval, r.maxval], { + .table('NewMeting' as any) + .between([updatedAt, id], [r.maxval, r.maxval], { index: 'updatedAtId', leftBound: 'open', rightBound: 'closed' @@ -46,24 +49,52 @@ const checkRethinkPgEquality: MutationResolvers['checkRethinkPgEquality'] = asyn .orderBy({index: 'updatedAtId'}) as any } const pgQuery = async (ids: string[]) => { - return getKysely().selectFrom('TeamMember').selectAll().where('id', 'in', ids).execute() + return selectNewMeetings().where('id', 'in', ids).execute() } const errors = await checkTableEq( rethinkQuery, pgQuery, { id: defaultEqFn, - isNotRemoved: compareRValUndefinedAsFalse, - isLead: compareRValUndefinedAsFalse, - isSpectatingPoker: compareRValUndefinedAsFalse, - email: defaultEqFn, - openDrawer: compareRValUndefinedAsNull, - picture: defaultEqFn, - preferredName: compareRValUndefinedAsNullAndTruncateRVal(100), - teamId: defaultEqFn, - userId: defaultEqFn, + isLegacy: compareRValUndefinedAsFalse, createdAt: compareDateAlmostEqual, - updatedAt: compareDateAlmostEqual + updatedAt: compareDateAlmostEqual, + createdBy: defaultEqFn, + endedAt: compareDateAlmostEqual, + facilitatorStageId: defaultEqFn, + facilitatorUserId: defaultEqFn, + meetingCount: compareRValUndefinedAsZero, + meetingNumber: compareRValUndefinedAsZero, + name: compareRValUndefinedAsNullAndTruncateRVal(100), + summarySentAt: compareDateAlmostEqual, + teamId: defaultEqFn, + meetingType: defaultEqFn, + phases: defaultEqFn, + showConversionModal: compareRValUndefinedAsFalse, + meetingSeriesId: defaultEqFn, + scheduledEndTime: compareDateAlmostEqual, + summary: compareRValUndefinedAsNullAndTruncateRVal(10000), + sentimentScore: compareRealNumber, + usedReactjis: defaultEqFn, + slackTs: compareRValStringAsNumber, + engagement: compareRealNumber, + totalVotes: compareRealNumber, + maxVotesPerGroup: compareRealNumber, + disableAnonymity: compareRValUndefinedAsFalse, + commentCount: compareRealNumber, + taskCount: compareRealNumber, + agendaItemCount: compareRealNumber, + storyCount: compareRealNumber, + templateId: defaultEqFn, + topicCount: compareRealNumber, + reflectionCount: compareRealNumber, + transcription: defaultEqFn, + recallBotId: compareRValUndefinedAsNull, + videoMeetingURL: compareRValUndefinedAsNull, + autogroupReflectionGroups: defaultEqFn, + resetReflectionGroups: defaultEqFn, + templateRefId: compareRValUndefinedAsNull, + meetingPrompt: compareRValUndefinedAsNull }, maxErrors ) diff --git a/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts index ee42e8525fe..44b3e3d3690 100644 --- a/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts +++ b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts @@ -76,6 +76,10 @@ export async function up() { let curId = r.minval const insertRow = async (row) => { + if (!row.facilitatorStageId) { + console.log('Meeting has no facilitatorId, skipping insert', row.id, row.teamId) + return + } try { await pg .insertInto('NewMeeting') From 7eebaba56909f2f0b649ab16c63101c258fac50f Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 12:35:31 -0700 Subject: [PATCH 37/89] on delete cascade --- .../postgres/migrations/1726174453131_NewMeeting-phase1.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts index 6bd308d9849..ecaa2a94cf3 100644 --- a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts +++ b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts @@ -65,7 +65,7 @@ export async function up() { CONSTRAINT "fk_teamId" FOREIGN KEY("teamId") REFERENCES "Team"("id") - ON DELETE SET NULL, + ON DELETE CASCADE, CONSTRAINT "fk_meetingSeriesId" FOREIGN KEY("meetingSeriesId") REFERENCES "MeetingSeries"("id") @@ -73,7 +73,7 @@ export async function up() { CONSTRAINT "fk_templateId" FOREIGN KEY("templateId") REFERENCES "MeetingTemplate"("id") - ON DELETE SET NULL + ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_createdAt" ON "NewMeeting"("createdAt"); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_facilitatorUserId" ON "NewMeeting"("facilitatorUserId"); From c2acd2250c8e9ce07bc7c2cd14ed9c8a728da21d Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 15:28:56 -0700 Subject: [PATCH 38/89] fixup equality checker Signed-off-by: Matt Krick --- .../mutations/checkRethinkPgEquality.ts | 45 +++++++++---------- .../mutations/updateRecurrenceSettings.ts | 8 ---- packages/server/postgres/utils/checkEqBase.ts | 2 +- .../postgres/utils/rethinkEqualityFns.ts | 28 ++++++++++-- 4 files changed, 48 insertions(+), 35 deletions(-) diff --git a/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts b/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts index 79381f9b0bd..03ff9c53399 100644 --- a/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts +++ b/packages/server/graphql/private/mutations/checkRethinkPgEquality.ts @@ -4,7 +4,6 @@ import {selectNewMeetings} from '../../../postgres/select' import {checkRowCount, checkTableEq} from '../../../postgres/utils/checkEqBase' import { compareDateAlmostEqual, - compareRealNumber, compareRValStringAsNumber, compareRValUndefinedAsFalse, compareRValUndefinedAsNull, @@ -36,11 +35,11 @@ const checkRethinkPgEquality: MutationResolvers['checkRethinkPgEquality'] = asyn ) => { const r = await getRethink() - if (tableName === 'NewMeting') { + if (tableName === 'NewMeeting') { const rowCountResult = await checkRowCount(tableName) const rethinkQuery = (updatedAt: Date, id: string | number) => { return r - .table('NewMeting' as any) + .table('NewMeeting' as any) .between([updatedAt, id], [r.maxval, r.maxval], { index: 'updatedAtId', leftBound: 'open', @@ -60,39 +59,39 @@ const checkRethinkPgEquality: MutationResolvers['checkRethinkPgEquality'] = asyn createdAt: compareDateAlmostEqual, updatedAt: compareDateAlmostEqual, createdBy: defaultEqFn, - endedAt: compareDateAlmostEqual, + endedAt: compareRValUndefinedAsNull, facilitatorStageId: defaultEqFn, facilitatorUserId: defaultEqFn, meetingCount: compareRValUndefinedAsZero, meetingNumber: compareRValUndefinedAsZero, name: compareRValUndefinedAsNullAndTruncateRVal(100), - summarySentAt: compareDateAlmostEqual, + summarySentAt: compareRValUndefinedAsNull, teamId: defaultEqFn, meetingType: defaultEqFn, phases: defaultEqFn, showConversionModal: compareRValUndefinedAsFalse, - meetingSeriesId: defaultEqFn, - scheduledEndTime: compareDateAlmostEqual, + meetingSeriesId: compareRValUndefinedAsNull, + scheduledEndTime: compareRValUndefinedAsNull, summary: compareRValUndefinedAsNullAndTruncateRVal(10000), - sentimentScore: compareRealNumber, - usedReactjis: defaultEqFn, + sentimentScore: compareRValUndefinedAsNull, + usedReactjis: compareRValUndefinedAsNull, slackTs: compareRValStringAsNumber, - engagement: compareRealNumber, - totalVotes: compareRealNumber, - maxVotesPerGroup: compareRealNumber, - disableAnonymity: compareRValUndefinedAsFalse, - commentCount: compareRealNumber, - taskCount: compareRealNumber, - agendaItemCount: compareRealNumber, - storyCount: compareRealNumber, - templateId: defaultEqFn, - topicCount: compareRealNumber, - reflectionCount: compareRealNumber, - transcription: defaultEqFn, + engagement: compareRValUndefinedAsNull, + totalVotes: compareRValUndefinedAsNull, + maxVotesPerGroup: compareRValUndefinedAsNull, + disableAnonymity: compareRValUndefinedAsNull, + commentCount: compareRValUndefinedAsNull, + taskCount: compareRValUndefinedAsNull, + agendaItemCount: compareRValUndefinedAsNull, + storyCount: compareRValUndefinedAsNull, + templateId: compareRValUndefinedAsNull, + topicCount: compareRValUndefinedAsNull, + reflectionCount: compareRValUndefinedAsNull, + transcription: compareRValUndefinedAsNull, recallBotId: compareRValUndefinedAsNull, videoMeetingURL: compareRValUndefinedAsNull, - autogroupReflectionGroups: defaultEqFn, - resetReflectionGroups: defaultEqFn, + autogroupReflectionGroups: compareRValUndefinedAsNull, + resetReflectionGroups: compareRValUndefinedAsNull, templateRefId: compareRValUndefinedAsNull, meetingPrompt: compareRValUndefinedAsNull }, diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 2c65125dede..5d4913b0751 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -120,14 +120,6 @@ export const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { .set({cancelledAt: sql`CURRENT_TIMESTAMP`}) .where('id', '=', meetingSeries.id) .execute() - await r - .table('NewMeeting') - .getAll(meetingSeries.id, {index: 'meetingSeriesId'}) - .filter({endedAt: null}, {default: true}) - .update({ - scheduledEndTime: null - }) - .run() } const updateGCalRecurrenceRule = (oldRule: RRuleSet, newRule: RRuleSet | null | undefined) => { diff --git a/packages/server/postgres/utils/checkEqBase.ts b/packages/server/postgres/utils/checkEqBase.ts index 175d20a57a5..8d483673c6e 100644 --- a/packages/server/postgres/utils/checkEqBase.ts +++ b/packages/server/postgres/utils/checkEqBase.ts @@ -65,7 +65,7 @@ export async function checkTableEq( const pgRow = pgRowsById[id] if (!pgRow) { - errors.push({id, prop: id, rVal: id, pgVal: null}) + errors.push({id, prop: '', rVal: null, pgVal: null}) if (errors.length >= maxErrors) return errors continue } diff --git a/packages/server/postgres/utils/rethinkEqualityFns.ts b/packages/server/postgres/utils/rethinkEqualityFns.ts index 201afa28e9c..0c003efbd57 100644 --- a/packages/server/postgres/utils/rethinkEqualityFns.ts +++ b/packages/server/postgres/utils/rethinkEqualityFns.ts @@ -1,10 +1,32 @@ import isValidDate from 'parabol-client/utils/isValidDate' import stringSimilarity from 'string-similarity' +function sortObjectKeys(obj: any): any { + if (Array.isArray(obj)) { + // If it's an array, recurse into each element + return obj.map(sortObjectKeys) + } else if (obj !== null && typeof obj === 'object') { + if (obj instanceof Date) return obj + // If it's an object, sort the keys and recurse on each value + const sortedObj: {[key: string]: any} = {} + Object.keys(obj) + .sort() + .forEach((key) => { + sortedObj[key] = sortObjectKeys(obj[key]) + }) + return sortedObj + } else { + // If it's a primitive value, just return it + return obj + } +} + export const defaultEqFn = (a: unknown, b: unknown) => { if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime() - if (Array.isArray(a) && Array.isArray(b)) return JSON.stringify(a) === JSON.stringify(b) - if (typeof a === 'object' && typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b) + if (Array.isArray(a) && Array.isArray(b)) + return JSON.stringify(sortObjectKeys(a)) === JSON.stringify(sortObjectKeys(b)) + if (typeof a === 'object' && typeof b === 'object') + return JSON.stringify(sortObjectKeys(a)) === JSON.stringify(sortObjectKeys(b)) return a === b } export const compareDateAlmostEqual = (rVal: unknown, pgVal: unknown) => { @@ -47,7 +69,7 @@ export const compareRValUndefinedAsEmptyArray = (rVal: unknown, pgVal: unknown) } export const compareRValStringAsNumber = (rVal: unknown, pgVal: unknown) => { - const normalizedRVal = Number(rVal) + const normalizedRVal = rVal ? Number(rVal) : null return defaultEqFn(normalizedRVal, pgVal) } From 57790ac031590485429aad3b9ddf6e7bb1affb71 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 15:29:42 -0700 Subject: [PATCH 39/89] fix stopMeetingSeries Signed-off-by: Matt Krick --- .../graphql/public/mutations/updateRecurrenceSettings.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 5d4913b0751..2692015636f 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -108,6 +108,7 @@ const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRu export const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { const pg = getKysely() + const r = await getRethink() await pg .with('NewMeetingUpdateEnd', (qb) => qb @@ -120,6 +121,14 @@ export const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { .set({cancelledAt: sql`CURRENT_TIMESTAMP`}) .where('id', '=', meetingSeries.id) .execute() + await r + .table('NewMeeting') + .getAll(meetingSeries.id, {index: 'meetingSeriesId'}) + .filter({endedAt: null}, {default: true}) + .update({ + scheduledEndTime: null + }) + .run() } const updateGCalRecurrenceRule = (oldRule: RRuleSet, newRule: RRuleSet | null | undefined) => { From 6a232050321035cfb86b8b6d0ab11077eaf57df3 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 16:39:40 -0700 Subject: [PATCH 40/89] fix trigger --- .../postgres/migrations/1726251201860_NewMeeting-uniq.ts | 4 ++-- .../postgres/migrations/1726602922665_NewMeeting-phase2.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts index 829f053409e..5bdf1e5a27d 100644 --- a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts +++ b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts @@ -14,12 +14,12 @@ export async function up() { CREATE OR REPLACE FUNCTION prevent_meeting_overlap() RETURNS TRIGGER AS $$ BEGIN - -- Check if a meeting exists within a 2-second window of the new start_time + -- Check if a meeting exists within a 2-second window of the new createdAt IF EXISTS ( SELECT 1 FROM "NewMeeting" WHERE "teamId" = NEW."teamId" AND "meetingType" = NEW."meetingType" - AND ABS(EXTRACT(EPOCH FROM (NEW.start_time - start_time))) < 2 + AND ABS(EXTRACT(EPOCH FROM (NEW."createdAt" - "createdAt"))) < 2 ) THEN RAISE EXCEPTION 'Cannot insert meeting. A meeting exists within a 2-second window.'; END IF; diff --git a/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts index 44b3e3d3690..cdb45ed0b34 100644 --- a/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts +++ b/packages/server/postgres/migrations/1726602922665_NewMeeting-phase2.ts @@ -24,6 +24,7 @@ export async function up() { console.log('Adding index complete') + await sql`ALTER TABLE "NewMeeting" DISABLE TRIGGER "check_meeting_overlap"`.execute(pg) const MAX_PG_PARAMS = 65545 const PG_COLS = [ 'id', @@ -156,6 +157,7 @@ export async function up() { curId = lastRow.id await Promise.all(rowsToInsert.map(async (row) => insertRow(row))) } + await sql`ALTER TABLE "NewMeeting" ENABLE TRIGGER "check_meeting_overlap"`.execute(pg) } export async function down() { From feb3e4b24da2c8345f23d1f2464aafc90ef109d6 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 16:40:21 -0700 Subject: [PATCH 41/89] fix trigger --- .../postgres/migrations/1726251201860_NewMeeting-uniq.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts index 829f053409e..5bdf1e5a27d 100644 --- a/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts +++ b/packages/server/postgres/migrations/1726251201860_NewMeeting-uniq.ts @@ -14,12 +14,12 @@ export async function up() { CREATE OR REPLACE FUNCTION prevent_meeting_overlap() RETURNS TRIGGER AS $$ BEGIN - -- Check if a meeting exists within a 2-second window of the new start_time + -- Check if a meeting exists within a 2-second window of the new createdAt IF EXISTS ( SELECT 1 FROM "NewMeeting" WHERE "teamId" = NEW."teamId" AND "meetingType" = NEW."meetingType" - AND ABS(EXTRACT(EPOCH FROM (NEW.start_time - start_time))) < 2 + AND ABS(EXTRACT(EPOCH FROM (NEW."createdAt" - "createdAt"))) < 2 ) THEN RAISE EXCEPTION 'Cannot insert meeting. A meeting exists within a 2-second window.'; END IF; From dd6c1bfcf2fe80918616c6c6b6cc020e86ef53a9 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 16:56:53 -0700 Subject: [PATCH 42/89] init --- ...MetadataForRetrospectiveDiscussionTopic.ts | 17 +- .../__tests__/processRecurrence.test.ts | 380 +++++++++++------- packages/server/database/rMapIf.ts | 12 - packages/server/database/updateStage.ts | 33 -- .../graphql/mutations/pokerResetDimension.ts | 4 - .../graphql/mutations/pokerRevealVotes.ts | 11 +- .../graphql/mutations/voteForPokerStory.ts | 32 -- .../public/mutations/revealTeamHealthVotes.ts | 2 - .../public/mutations/setTeamHealthVote.ts | 26 +- 9 files changed, 236 insertions(+), 281 deletions(-) delete mode 100644 packages/server/database/rMapIf.ts delete mode 100644 packages/server/database/updateStage.ts diff --git a/packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts b/packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts index 72bd3519779..8c4f5b6cd10 100644 --- a/packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts +++ b/packages/embedder/addEmbeddingsMetadataForRetrospectiveDiscussionTopic.ts @@ -1,6 +1,4 @@ import {ExpressionOrFactory, SqlBool, sql} from 'kysely' -import getRethink from 'parabol-server/database/rethinkDriver' -import {RDatum} from 'parabol-server/database/stricterR' import {DB} from 'parabol-server/postgres/pg' import {Logger} from 'parabol-server/utils/Logger' import getKysely from '../server/postgres/getKysely' @@ -14,16 +12,17 @@ export interface DiscussionMeta { } const validateDiscussions = async (discussions: (DiscussionMeta & {meetingId: string})[]) => { - const r = await getRethink() + const pg = getKysely() if (discussions.length === 0) return discussions // Exclude discussions that belong to an unfinished meeting const meetingIds = [...new Set(discussions.map(({meetingId}) => meetingId))] - const endedMeetingIds = await r - .table('NewMeeting') - .getAll(r.args(meetingIds), {index: 'id'}) - .filter((row: RDatum) => row('endedAt').default(null).ne(null))('id') - .distinct() - .run() + const endedMeetings = await pg + .selectFrom('NewMeeting') + .select('id') + .where('id', 'in', meetingIds) + .where('endedAt', 'is', null) + .execute() + const endedMeetingIds = endedMeetings.map(({id}) => id) const endedMeetingIdsSet = new Set(endedMeetingIds) return discussions.filter(({meetingId}) => endedMeetingIdsSet.has(meetingId)) } diff --git a/packages/server/__tests__/processRecurrence.test.ts b/packages/server/__tests__/processRecurrence.test.ts index 86edc1b87fb..adfe4632193 100644 --- a/packages/server/__tests__/processRecurrence.test.ts +++ b/packages/server/__tests__/processRecurrence.test.ts @@ -2,15 +2,12 @@ import dayjs from 'dayjs' import ms from 'ms' import TeamMemberId from 'parabol-client/shared/gqlIds/TeamMemberId' import {toDateTime} from '../../client/shared/rruleUtil' -import getRethink from '../database/rethinkDriver' import DiscussPhase from '../database/types/DiscussPhase' -import MeetingRetrospective from '../database/types/MeetingRetrospective' -import MeetingTeamPrompt from '../database/types/MeetingTeamPrompt' import ReflectPhase from '../database/types/ReflectPhase' import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' import generateUID from '../generateUID' +import getKysely from '../postgres/getKysely' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../postgres/queries/insertMeetingSeries' -import {RetroMeetingPhase} from '../postgres/types/NewMeetingPhase' import {getUserTeams, sendIntranet, signUp} from './common' const PROCESS_RECURRENCE = ` @@ -77,19 +74,25 @@ beforeAll(async () => { }) test('Should not end meetings that are not scheduled to end', async () => { - const r = await getRethink() + const pg = getKysely() const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?' - }) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify([phase]), + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -107,25 +110,35 @@ test('Should not end meetings that are not scheduled to end', async () => { await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeFalsy() }) test('Should not end meetings that are scheduled to end in the future', async () => { - const r = await getRethink() + const pg = getKysely() const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() + ms('5m')) - }) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify([phase]), + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + scheduledEndTime: new Date(Date.now() + ms('5m')), + facilitatorStageId: phase.stages[0]?.id + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -143,27 +156,36 @@ test('Should not end meetings that are scheduled to end in the future', async () await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeFalsy() - - await r.table('NewMeeting').get(meetingId).delete().run() + await pg.deleteFrom('NewMeeting').where('id', '=', meetingId).execute() }) test('Should end meetings that are scheduled to end in the past', async () => { - const r = await getRethink() + const pg = getKysely() const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('5m')) - }) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify([phase]), + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + scheduledEndTime: new Date(Date.now() - ms('5m')), + facilitatorStageId: phase.stages[0]?.id + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -181,12 +203,16 @@ test('Should end meetings that are scheduled to end in the past', async () => { await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }, 10000) test('Should end the current team prompt meeting and start a new meeting', async () => { - const r = await getRethink() + const pg = getKysely() const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') const recurrenceRule = `DTSTART:${dateTime} @@ -202,22 +228,27 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('5m')), - meetingSeriesId - }) - - // The last meeting in the series was created just over 24h ago, so the next one should start - // soon. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify([phase]), + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + scheduledEndTime: new Date(Date.now() - ms('5m')), + facilitatorStageId: phase.stages[0]?.id, + meetingSeriesId, + // The last meeting in the series was created just over 24h ago, so the next one should start + // soon. + createdAt: new Date(Date.now() - ms('25h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -235,15 +266,20 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() - const lastMeeting = await r - .table('NewMeeting') - .filter({meetingType: 'teamPrompt', meetingSeriesId}) - .orderBy(r.desc('createdAt')) - .nth(0) - .run() + const lastMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('meetingType', '=', 'teamPrompt') + .orderBy('createdAt desc') + .limit(1) + .executeTakeFirst() expect(lastMeeting).toMatchObject({ name: expect.stringMatching(/Daily Test Standup.*/), @@ -252,7 +288,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) test('Should end the current retro meeting and start a new meeting', async () => { - const r = await getRethink() + const pg = getKysely() // Create a meeting series that's been going on for a few days, and happens daily at 9a UTC. const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) @@ -270,29 +306,31 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingRetrospective({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [ - new ReflectPhase(teamId, []) as RetroMeetingPhase, - new DiscussPhase(undefined) as RetroMeetingPhase - ], - facilitatorUserId: userId, - scheduledEndTime: new Date(Date.now() - ms('5m')), - meetingSeriesId, - templateId: 'startStopContinueTemplate', - disableAnonymity: false, - totalVotes: 5, - name: '', - maxVotesPerGroup: 5 - }) - - // The last meeting in the series was created just over 24h ago, so the next one should start - // soon. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - - await r.table('NewMeeting').insert(meeting).run() + const phases = [new ReflectPhase(teamId, []), new DiscussPhase(undefined)] + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify(phases), + facilitatorUserId: userId, + meetingType: 'retrospective', + scheduledEndTime: new Date(Date.now() - ms('5m')), + facilitatorStageId: phases[0]!.stages[0]!.id, + meetingSeriesId, + templateId: 'startStopContinueTemplate', + disableAnonymity: false, + totalVotes: 5, + name: '', + maxVotesPerGroup: 5, + meetingPrompt: 'What are you working on today? Stuck on anything?', + // The last meeting in the series was created just over 24h ago, so the next one should start + // soon. + createdAt: new Date(Date.now() - ms('25h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -310,15 +348,20 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() - const lastMeeting = await r - .table('NewMeeting') - .filter({meetingType: 'retrospective', meetingSeriesId}) - .orderBy(r.desc('createdAt')) - .nth(0) - .run() + const lastMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('meetingType', '=', 'teamPrompt') + .orderBy('createdAt desc') + .limit(1) + .executeTakeFirst() expect(lastMeeting).toMatchObject({ meetingSeriesId @@ -326,7 +369,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) test('Should only start a new meeting if it would still be active', async () => { - const r = await getRethink() + const pg = getKysely() const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') @@ -343,23 +386,28 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('73h')), - meetingSeriesId: newMeetingSeriesId - }) - - // The last meeting in the series was created just over 72h ago, so 3 meetings should have started - // since then, but only 1 meeting should start as a result of the mutation. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('73h')) - meeting.endedAt = new Date(Date.now() - ms('49h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify([phase]), + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id, + scheduledEndTime: new Date(Date.now() - ms('73h')), + meetingSeriesId: newMeetingSeriesId, + // The last meeting in the series was created just over 72h ago, so 3 meetings should have started + // since then, but only 1 meeting should start as a result of the mutation. + createdAt: new Date(Date.now() - ms('73h')), + endedAt: new Date(Date.now() - ms('49h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -377,12 +425,16 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }, 10000) test('Should not start a new meeting if the rrule has not started', async () => { - const r = await getRethink() + const pg = getKysely() const startDate = dayjs().utc().add(1, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') @@ -399,23 +451,28 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('1h')), - meetingSeriesId: newMeetingSeriesId - }) - - // The last meeting in the series was created just over 24h ago, but the active rrule doesn't - // start until tomorrow. - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - meeting.endedAt = new Date(Date.now() - ms('1h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify([phase]), + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id, + scheduledEndTime: new Date(Date.now() - ms('1h')), + meetingSeriesId: newMeetingSeriesId, + // The last meeting in the series was created just over 24h ago, but the active rrule doesn't + // start until tomorrow. + createdAt: new Date(Date.now() - ms('25h')), + endedAt: new Date(Date.now() - ms('1h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -433,12 +490,16 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }) test('Should not hang if the rrule interval is invalid', async () => { - const r = await getRethink() + const pg = getKysely() const startDate = dayjs().utc().subtract(2, 'day').set('hour', 9) const dateTime = toDateTime(startDate, 'UTC') @@ -455,22 +516,27 @@ RRULE:FREQ=WEEKLY;INTERVAL=NaN;BYDAY=MO,TU,WE,TH,FR,SA,SU` }) const meetingId = generateUID() - const meeting = new MeetingTeamPrompt({ - id: meetingId, - teamId, - meetingCount: 0, - phases: [new TeamPromptResponsesPhase([teamMemberId])], - facilitatorUserId: userId, - meetingPrompt: 'What are you working on today? Stuck on anything?', - scheduledEndTime: new Date(Date.now() - ms('5m')), - meetingSeriesId: newMeetingSeriesId - }) - - // The last meeting in the series was created just over 24h ago, so the next one should start soon - // but the rrule is invalid, so it won't happen - meeting.createdAt = new Date(meeting.createdAt.getTime() - ms('25h')) - - await r.table('NewMeeting').insert(meeting).run() + const phase = new TeamPromptResponsesPhase([teamMemberId]) + await pg + .insertInto('NewMeeting') + .values({ + id: meetingId, + teamId, + meetingCount: 0, + meetingNumber: 1, + phases: JSON.stringify([phase]), + facilitatorUserId: userId, + meetingPrompt: 'What are you working on today? Stuck on anything?', + name: `Team Prompt #1`, + meetingType: 'teamPrompt', + facilitatorStageId: phase.stages[0]?.id, + scheduledEndTime: new Date(Date.now() - ms('5m')), + meetingSeriesId: newMeetingSeriesId, + // The last meeting in the series was created just over 24h ago, so the next one should start soon + // but the rrule is invalid, so it won't happen + createdAt: new Date(Date.now() - ms('25h')) + }) + .execute() const update = await sendIntranet({ query: PROCESS_RECURRENCE, @@ -488,6 +554,10 @@ RRULE:FREQ=WEEKLY;INTERVAL=NaN;BYDAY=MO,TU,WE,TH,FR,SA,SU` await assertIdempotency() - const actualMeeting = await r.table('NewMeeting').get(meetingId).run() + const actualMeeting = await pg + .selectFrom('NewMeeting') + .selectAll() + .where('id', '=', meetingId) + .executeTakeFirstOrThrow() expect(actualMeeting.endedAt).toBeTruthy() }) diff --git a/packages/server/database/rMapIf.ts b/packages/server/database/rMapIf.ts deleted file mode 100644 index a976d40a89b..00000000000 --- a/packages/server/database/rMapIf.ts +++ /dev/null @@ -1,12 +0,0 @@ -// mapIf is a repeatable pattern for updating 1 or more values inside an arbitrarily deep nested array on a RethinkDB document -// rArr is the array -// test should return true if you want to update that specific value inside the array -// f is the updater - -import {ParabolR} from './rethinkDriver' - -const rMapIf = (r: ParabolR) => (rArr: unknown[], test: any, f: (x: unknown) => void) => { - return rArr.map((x) => r.branch(test(x), f(x), x)) -} - -export default rMapIf diff --git a/packages/server/database/updateStage.ts b/packages/server/database/updateStage.ts deleted file mode 100644 index 51d8505d5ac..00000000000 --- a/packages/server/database/updateStage.ts +++ /dev/null @@ -1,33 +0,0 @@ -import getRethink from './rethinkDriver' -import rMapIf from './rMapIf' -import {NewMeetingPhaseTypeEnum} from './types/GenericMeetingPhase' - -// this is a uesful function for updating a stage inside a meeting object -// it is superior to mutating the `phases` object in JS and then pushing the whole object -// because it eliminates the chance of a race - -const updateStage = async ( - meetingId: string, - stageId: string, - phaseType: NewMeetingPhaseTypeEnum, - updater: (stage: any) => any -) => { - const r = await getRethink() - const mapIf = rMapIf(r) - return r - .table('NewMeeting') - .get(meetingId) - .update((meeting: any) => ({ - phases: mapIf( - meeting('phases'), - (phase: any) => phase('phaseType').eq(phaseType), - (phase: any) => - phase.merge({ - stages: mapIf(phase('stages'), (stage: any) => stage('id').eq(stageId), updater) - }) - ) - })) - .run() -} - -export default updateStage diff --git a/packages/server/graphql/mutations/pokerResetDimension.ts b/packages/server/graphql/mutations/pokerResetDimension.ts index 5dafb36786b..24b118742c5 100644 --- a/packages/server/graphql/mutations/pokerResetDimension.ts +++ b/packages/server/graphql/mutations/pokerResetDimension.ts @@ -2,8 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' -import {RValue} from '../../database/stricterR' -import updateStage from '../../database/updateStage' import getKysely from '../../postgres/getKysely' import removeMeetingTaskEstimates from '../../postgres/queries/removeMeetingTaskEstimates' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -84,11 +82,9 @@ const pokerResetDimension = { // mutate the cached meeting Object.assign(stage, updates) - const updater = (estimateStage: RValue) => estimateStage.merge(updates) const [meetingMembers, teamMembers] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teamMembersByTeamId').load(teamId), - updateStage(meetingId, stageId, 'ESTIMATE', updater), pg .updateTable('NewMeeting') .set({ diff --git a/packages/server/graphql/mutations/pokerRevealVotes.ts b/packages/server/graphql/mutations/pokerRevealVotes.ts index c0f900244e0..63d23dccfbb 100644 --- a/packages/server/graphql/mutations/pokerRevealVotes.ts +++ b/packages/server/graphql/mutations/pokerRevealVotes.ts @@ -1,10 +1,8 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {sql} from 'kysely' import {PokerCards, SubscriptionChannel} from 'parabol-client/types/constEnums' -import {RValue} from '../../database/stricterR' import EstimateUserScore from '../../database/types/EstimateUserScore' import PokerMeetingMember from '../../database/types/PokerMeetingMember' -import updateStage from '../../database/updateStage' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -92,12 +90,6 @@ const pokerRevealVotes = { }) stage.isVoting = false - const updater = (estimateStage: RValue) => - estimateStage.merge({ - isVoting: false, - // note that a race condition exists here. it's possible that i cast my vote after the meeting is fetched but before this update & that'll be overwritten - scores - }) await pg .updateTable('NewMeeting') .set({ @@ -107,8 +99,7 @@ const pokerRevealVotes = { )` }) .where('id', '=', meetingId) - .execute(), - await updateStage(meetingId, stageId, 'ESTIMATE', updater) + .execute() dataLoader.clearAll('newMeetings') const data = {meetingId, stageId} publish(SubscriptionChannel.MEETING, meetingId, 'PokerRevealVotesSuccess', data, subOptions) diff --git a/packages/server/graphql/mutations/voteForPokerStory.ts b/packages/server/graphql/mutations/voteForPokerStory.ts index 79e4012c8a4..d0c26ffd2bc 100644 --- a/packages/server/graphql/mutations/voteForPokerStory.ts +++ b/packages/server/graphql/mutations/voteForPokerStory.ts @@ -1,9 +1,6 @@ import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import {RValue} from '../../database/stricterR' import EstimateUserScore from '../../database/types/EstimateUserScore' -import updateStage from '../../database/updateStage' import getKysely from '../../postgres/getKysely' import {NewMeetingPhase} from '../../postgres/types/NewMeetingPhase' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -36,15 +33,6 @@ export const removeVoteForUserId = async (userId: string, stageId: string, meeti .where('id', '=', meetingId) .execute() }) - const updater = (estimateStage: RValue) => - estimateStage.merge({ - scores: estimateStage('scores').deleteAt( - estimateStage('scores') - .offsetsOf((score: RValue) => score('userId').eq(userId)) - .nth(0) - ) - }) - return updateStage(meetingId, stageId, 'ESTIMATE', updater) } const upsertVote = async (vote: EstimateUserScore, stageId: string, meetingId: string) => { @@ -71,26 +59,6 @@ const upsertVote = async (vote: EstimateUserScore, stageId: string, meetingId: s .where('id', '=', meetingId) .execute() }) - - const r = await getRethink() - const updater = (estimateStage: RValue) => - estimateStage.merge({ - scores: r.branch( - estimateStage('scores') - .offsetsOf((score: RValue) => score('userId').eq(vote.userId)) - .nth(0) - .default(-1) - .eq(-1), - estimateStage('scores').append(vote), - estimateStage('scores').changeAt( - estimateStage('scores') - .offsetsOf((score: RValue) => score('userId').eq(vote.userId)) - .nth(0), - vote - ) - ) - }) - return updateStage(meetingId, stageId, 'ESTIMATE', updater) } const voteForPokerStory = { diff --git a/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts b/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts index 86bbbb99744..d7f52b510cc 100644 --- a/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts +++ b/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts @@ -1,6 +1,5 @@ import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import updateStage from '../../../database/updateStage' import getKysely from '../../../postgres/getKysely' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' @@ -62,7 +61,6 @@ const revealTeamHealthVotes: MutationResolvers['revealTeamHealthVotes'] = async }) .where('id', '=', meetingId) .execute() - updateStage(meetingId, stageId, 'TEAM_HEALTH', (stage) => stage.merge({isRevealed: true})) stage.isRevealed = true const data = { diff --git a/packages/server/graphql/public/mutations/setTeamHealthVote.ts b/packages/server/graphql/public/mutations/setTeamHealthVote.ts index 68198ba59cf..689561177ac 100644 --- a/packages/server/graphql/public/mutations/setTeamHealthVote.ts +++ b/packages/server/graphql/public/mutations/setTeamHealthVote.ts @@ -1,8 +1,5 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import TeamHealthVote from '../../../database/types/TeamHealthVote' -import updateStage from '../../../database/updateStage' import getKysely from '../../../postgres/getKysely' import {NewMeetingPhase} from '../../../postgres/types/NewMeetingPhase.d' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -10,7 +7,7 @@ import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' import {MutationResolvers} from '../resolverTypes' -const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealthVote) => { +const upsertVote = async (meetingId: string, newVote: TeamHealthVote) => { const pg = getKysely() await pg.transaction().execute(async (trx) => { const meeting = await trx @@ -38,25 +35,6 @@ const upsertVote = async (meetingId: string, stageId: string, newVote: TeamHealt .where('id', '=', meetingId) .execute() }) - const r = await getRethink() - const updater = (stage: RValue) => - stage.merge({ - votes: r.branch( - stage('votes') - .offsetsOf((oldVote: RValue) => oldVote('userId').eq(newVote.userId)) - .nth(0) - .default(-1) - .eq(-1), - stage('votes').append(newVote), - stage('votes').changeAt( - stage('votes') - .offsetsOf((oldVote: RValue) => oldVote('userId').eq(newVote.userId)) - .nth(0), - newVote - ) - ) - }) - return updateStage(meetingId, stageId, 'TEAM_HEALTH', updater) } const setTeamHealthVote: MutationResolvers['setTeamHealthVote'] = async ( @@ -96,7 +74,7 @@ const setTeamHealthVote: MutationResolvers['setTeamHealthVote'] = async ( if (vote === -1) { return {error: {message: 'Invalid label provided'}} } - await upsertVote(meetingId, stageId, {userId: viewerId, vote}) + await upsertVote(meetingId, {userId: viewerId, vote}) // update dataloader const existingVote = stage.votes.find((vote) => vote.userId === viewerId) if (existingVote) { From 2a28f191590a15fc02226dd1ee99a2eecab46761 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 17:00:34 -0700 Subject: [PATCH 43/89] remove comma after execute() --- packages/server/graphql/mutations/pokerRevealVotes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/graphql/mutations/pokerRevealVotes.ts b/packages/server/graphql/mutations/pokerRevealVotes.ts index c0f900244e0..1c0f22fc51d 100644 --- a/packages/server/graphql/mutations/pokerRevealVotes.ts +++ b/packages/server/graphql/mutations/pokerRevealVotes.ts @@ -107,8 +107,8 @@ const pokerRevealVotes = { )` }) .where('id', '=', meetingId) - .execute(), - await updateStage(meetingId, stageId, 'ESTIMATE', updater) + .execute() + await updateStage(meetingId, stageId, 'ESTIMATE', updater) dataLoader.clearAll('newMeetings') const data = {meetingId, stageId} publish(SubscriptionChannel.MEETING, meetingId, 'PokerRevealVotesSuccess', data, subOptions) From 11eeec04c58489d54921c44af7383f9d0b744989 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 17:29:10 -0700 Subject: [PATCH 44/89] progress --- .../server/dataloader/customLoaderMakers.ts | 110 +----------------- .../server/dataloader/customRedisQueries.ts | 28 +++-- .../dataloader/foreignKeyLoaderMakers.ts | 8 +- .../dataloader/primaryKeyLoaderMakers.ts | 5 +- .../rethinkForeignKeyLoaderMakers.ts | 27 ----- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../graphql/mutations/createReflection.ts | 7 -- .../graphql/mutations/dragDiscussionTopic.ts | 7 -- .../graphql/mutations/dragEstimatingTask.ts | 7 -- .../server/graphql/mutations/endCheckIn.ts | 36 +----- 10 files changed, 24 insertions(+), 212 deletions(-) diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index a809dae9d0a..4b91bcfd2f0 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -1,5 +1,4 @@ import DataLoader from 'dataloader' -import tracer from 'dd-trace' import {Selectable, SqlBool, sql} from 'kysely' import {PARABOL_AI_USER_ID} from '../../client/utils/constants' import getRethink from '../database/rethinkDriver' @@ -34,7 +33,6 @@ import isUserVerified from '../utils/isUserVerified' import NullableDataLoader from './NullableDataLoader' import RootDataLoader, {RegisterDependsOn} from './RootDataLoader' import normalizeArrayResults from './normalizeArrayResults' -import normalizeResults from './normalizeResults' export interface MeetingSettingsKey { teamId: string @@ -478,39 +476,8 @@ type MeetingStat = { meetingType: MeetingTypeEnum createdAt: Date } -export const meetingStatsByOrgId = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { - dependsOn('newMeetings') - return new DataLoader( - async (orgIds) => { - const r = await getRethink() - const meetingStatsByOrgId = await Promise.all( - orgIds.map(async (orgId) => { - // note: does not include archived teams! - const teams = await parent.get('teamsByOrgIds').load(orgId) - const teamIds = teams.map(({id}) => id) - const stats = (await r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .pluck('createdAt', 'meetingType') - // DO NOT CALL orderBy, it makes the query 10x more expensive! - // .orderBy('createdAt') - .run()) as {createdAt: Date; meetingType: MeetingTypeEnum}[] - return stats.map((stat) => ({ - createdAt: stat.createdAt, - meetingType: stat.meetingType, - id: `ms${stat.createdAt.getTime()}` - })) - }) - ) - return meetingStatsByOrgId - }, - { - ...parent.dataLoaderOptions - } - ) -} -export const _pgmeetingStatsByOrgId = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { +export const meetingStatsByOrgId = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { dependsOn('newMeetings') return new DataLoader( async (orgIds) => { @@ -606,28 +573,6 @@ export const meetingHighlightedTaskId = (parent: RootDataLoader) => { export const activeMeetingsByMeetingSeriesId = ( parent: RootDataLoader, dependsOn: RegisterDependsOn -) => { - dependsOn('newMeetings') - return new DataLoader( - async (keys) => { - const r = await getRethink() - const res = await r - .table('NewMeeting') - .getAll(r.args(keys), {index: 'meetingSeriesId'}) - .filter({endedAt: null}, {default: true}) - .orderBy(r.asc('createdAt')) - .run() - return normalizeArrayResults(keys, res, 'meetingSeriesId') - }, - { - ...parent.dataLoaderOptions - } - ) -} - -export const _pgactiveMeetingsByMeetingSeriesId = ( - parent: RootDataLoader, - dependsOn: RegisterDependsOn ) => { dependsOn('newMeetings') return new DataLoader( @@ -649,34 +594,6 @@ export const _pgactiveMeetingsByMeetingSeriesId = ( export const lastMeetingByMeetingSeriesId = ( parent: RootDataLoader, dependsOn: RegisterDependsOn -) => { - dependsOn('newMeetings') - return new DataLoader( - async (keys) => - tracer.trace('lastMeetingByMeetingSeriesId', async () => { - const r = await getRethink() - const res = await ( - r - .table('NewMeeting') - .getAll(r.args(keys), {index: 'meetingSeriesId'}) - .group('meetingSeriesId') as RDatum - ) - .orderBy(r.desc('createdAt')) - .nth(0) - .default(null) - .ungroup()('reduction') - .run() - return normalizeResults(keys, res as AnyMeeting[], 'meetingSeriesId') - }), - { - ...parent.dataLoaderOptions - } - ) -} - -export const _pglastMeetingByMeetingSeriesId = ( - parent: RootDataLoader, - dependsOn: RegisterDependsOn ) => { dependsOn('newMeetings') return new DataLoader( @@ -895,31 +812,6 @@ export const fileStoreAsset = (parent: RootDataLoader) => { } export const meetingCount = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { - dependsOn('newMeetings') - return new DataLoader<{teamId: string; meetingType: MeetingTypeEnum}, number, string>( - async (keys) => { - const r = await getRethink() - const res = await Promise.all( - keys.map(async ({teamId, meetingType}) => { - return r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType: meetingType as any}) - .count() - .default(0) - .run() - }) - ) - return res - }, - { - ...parent.dataLoaderOptions, - cacheKeyFn: (key) => `${key.teamId}:${key.meetingType}` - } - ) -} - -export const _pgmeetingCount = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { dependsOn('newMeetings') return new DataLoader<{teamId: string; meetingType: MeetingTypeEnum}, number, string>( async (keys) => { diff --git a/packages/server/dataloader/customRedisQueries.ts b/packages/server/dataloader/customRedisQueries.ts index 8ce3cd85ac7..f14fbcc86c7 100644 --- a/packages/server/dataloader/customRedisQueries.ts +++ b/packages/server/dataloader/customRedisQueries.ts @@ -3,28 +3,26 @@ import {sql, SqlBool} from 'kysely' import ms from 'ms' -import getRethink from '../database/rethinkDriver' -import {RDatum} from '../database/stricterR' import getKysely from '../postgres/getKysely' // All results must be mapped to their ids! const customRedisQueries = { endTimesByTemplateId: async (templateIds: string[]) => { - const r = await getRethink() + const pg = getKysely() const aQuarterAgo = new Date(Date.now() - ms('90d')) - const meetings = (await ( - r - .table('NewMeeting') - .getAll(r.args(templateIds), {index: 'templateId'}) - .pluck('templateId', 'endedAt') - .filter((row: RDatum) => row('endedAt').ge(aQuarterAgo)) - .group('templateId' as any) as any - ) - .limit(1000)('endedAt') - .run()) as {group: string; reduction: Date[]}[] + const meetings = await pg + .selectFrom('NewMeeting') + .select('templateId') + .select(({fn}) => [fn.agg('array_agg', ['endedAt']).as('endedAts')]) + .where('templateId', 'in', templateIds) + .where('endedAt', '>=', aQuarterAgo) + .groupBy('templateId') + .limit(1000) + .execute() + return templateIds.map((id) => { - const group = meetings.find((meeting) => meeting.group === id) - return group ? group.reduction.map((date) => date.getTime()) : [] + const group = meetings.find((meeting) => meeting.templateId === id) + return group ? group.endedAts.map((date) => date.getTime()) : [] }) }, publicTemplates: async (meetingTypes: string[]) => { diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 5b0e9d2eb2e..2f14218cd5c 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -229,8 +229,8 @@ export const reflectPromptsByTemplateId = foreignKeyLoaderMaker( } ) -export const _pgactiveMeetingsByTeamId = foreignKeyLoaderMaker( - '_pgnewMeetings', +export const activeMeetingsByTeamId = foreignKeyLoaderMaker( + 'newMeetings', 'teamId', async (teamIds) => { return selectNewMeetings() @@ -240,8 +240,8 @@ export const _pgactiveMeetingsByTeamId = foreignKeyLoaderMaker( .execute() } ) -export const _pgcompletedMeetingsByTeamId = foreignKeyLoaderMaker( - '_pgnewMeetings', +export const completedMeetingsByTeamId = foreignKeyLoaderMaker( + 'newMeetings', 'teamId', async (teamIds) => { return selectNewMeetings() diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 842397eb089..edb43a0935b 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -23,6 +23,7 @@ import { selectTemplateScaleRef, selectTimelineEvent } from '../postgres/select' +import {AnyMeeting} from '../postgres/types/Meeting' import {primaryKeyLoaderMaker} from './primaryKeyLoaderMaker' export const users = primaryKeyLoaderMaker(getUsersByIds) @@ -117,6 +118,6 @@ export const reflectPrompts = primaryKeyLoaderMaker((ids: readonly string[]) => return selectReflectPrompts().where('id', 'in', ids).execute() }) -export const _pgnewMeetings = primaryKeyLoaderMaker((ids: readonly string[]) => { - return selectNewMeetings().where('id', 'in', ids).execute() +export const newMeetings = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectNewMeetings().where('id', 'in', ids).$narrowType().execute() }) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index 15317dc9d3c..06741f45f69 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -2,33 +2,6 @@ import getRethink from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' import RethinkForeignKeyLoaderMaker from './RethinkForeignKeyLoaderMaker' -export const activeMeetingsByTeamId = new RethinkForeignKeyLoaderMaker( - 'newMeetings', - 'teamId', - async (teamIds) => { - const r = await getRethink() - return r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter({endedAt: null}, {default: true}) - .orderBy(r.desc('createdAt')) - .run() - } -) -export const completedMeetingsByTeamId = new RethinkForeignKeyLoaderMaker( - 'newMeetings', - 'teamId', - async (teamIds) => { - const r = await getRethink() - return r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RDatum) => row('endedAt').default(null).ne(null)) - .orderBy(r.desc('endedAt')) - .run() - } -) - export const massInvitationsByTeamMemberId = new RethinkForeignKeyLoaderMaker( 'massInvitations', 'teamMemberId', diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index 2a7baaebc59..0bef3795af3 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -5,7 +5,6 @@ import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' */ export const massInvitations = new RethinkPrimaryKeyLoaderMaker('MassInvitation') export const meetingMembers = new RethinkPrimaryKeyLoaderMaker('MeetingMember') -export const newMeetings = new RethinkPrimaryKeyLoaderMaker('NewMeeting') export const newFeatures = new RethinkPrimaryKeyLoaderMaker('NewFeature') export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') export const tasks = new RethinkPrimaryKeyLoaderMaker('Task') diff --git a/packages/server/graphql/mutations/createReflection.ts b/packages/server/graphql/mutations/createReflection.ts index 7a20208c72d..7020a7de025 100644 --- a/packages/server/graphql/mutations/createReflection.ts +++ b/packages/server/graphql/mutations/createReflection.ts @@ -112,13 +112,6 @@ export default { let unlockedStageIds if (!groupStage?.isNavigableByFacilitator) { unlockedStageIds = unlockAllStagesForPhase(phases, 'group', true) - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases - }) - .run() await pg .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) diff --git a/packages/server/graphql/mutations/dragDiscussionTopic.ts b/packages/server/graphql/mutations/dragDiscussionTopic.ts index 472c314197c..931194d10f1 100644 --- a/packages/server/graphql/mutations/dragDiscussionTopic.ts +++ b/packages/server/graphql/mutations/dragDiscussionTopic.ts @@ -58,13 +58,6 @@ export default { stages.sort((a, b) => { return a.sortOrder > b.sortOrder ? 1 : -1 }) - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases - }) - .run() await pg .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) diff --git a/packages/server/graphql/mutations/dragEstimatingTask.ts b/packages/server/graphql/mutations/dragEstimatingTask.ts index 5539ab1667b..3d956975879 100644 --- a/packages/server/graphql/mutations/dragEstimatingTask.ts +++ b/packages/server/graphql/mutations/dragEstimatingTask.ts @@ -87,13 +87,6 @@ export default { stages.sort((a, b) => { return a.sortOrder > b.sortOrder ? 1 : -1 }) - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases - }) - .run() await pg .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index c28b73a9fad..edaff8dce30 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -151,19 +151,7 @@ const summarizeCheckInMeeting = async (meeting: CheckInMeeting, dataLoader: Data taskCount: tasks.length }) .where('id', '=', meetingId) - .execute(), - r - .table('NewMeeting') - .get(meetingId) - .update( - { - agendaItemCount: activeAgendaItems.length, - commentCount, - taskCount: tasks.length - }, - {nonAtomic: true} - ) - .run() + .execute() ]) dataLoader.clearAll('newMeetings') return {updatedTaskIds: [...tasks, ...doneTasks].map(({id}) => id)} @@ -211,19 +199,6 @@ export default { const phase = getMeetingPhase(phases) const insights = await gatherInsights(meeting, dataLoader) - const completedCheckIn = await r - .table('NewMeeting') - .get(meetingId) - .update( - { - endedAt: now, - phases, - ...insights - }, - {returnChanges: true} - )('changes')(0)('new_val') - .default(null) - .run() await pg .updateTable('NewMeeting') .set({ @@ -235,13 +210,8 @@ export default { .where('id', '=', meetingId) .execute() dataLoader.clearAll('newMeetings') - if (!completedCheckIn) { - return standardError(new Error('Completed check-in meeting does not exist'), { - userId: viewerId - }) - } - - if (completedCheckIn.meetingType === 'action') { + const completedCheckIn = await dataLoader.get('newMeetings').loadNonNull(meetingId) + if (completedCheckIn.meetingType !== 'action') { return standardError(new Error('Completed check-in meeting is not an action'), { userId: viewerId }) From f2b7b79e253e6d36fff0a38baf3e37af42f3aaff Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 24 Sep 2024 17:29:44 -0700 Subject: [PATCH 45/89] fix equality check --- packages/server/graphql/mutations/endCheckIn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index ca676bbaeee..c3ea3419772 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -220,7 +220,7 @@ export default { }) } - if (completedCheckIn.meetingType === 'action') { + if (completedCheckIn.meetingType !== 'action') { return standardError(new Error('Completed check-in meeting is not an action'), { userId: viewerId }) From d208f730e0b2fff573e417ccd54d38bd519c1ff2 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 26 Sep 2024 12:50:05 -0700 Subject: [PATCH 46/89] refactor to PG Signed-off-by: Matt Krick --- packages/server/database/rethinkDriver.ts | 11 +- .../dataloader/foreignKeyLoaderMakers.ts | 3 + .../email/newMeetingSummaryEmailCreator.tsx | 2 +- .../graphql/mutations/createReflection.ts | 2 - .../graphql/mutations/dragDiscussionTopic.ts | 2 - .../graphql/mutations/dragEstimatingTask.ts | 2 - .../server/graphql/mutations/endCheckIn.ts | 1 - .../graphql/mutations/endSprintPoker.ts | 25 +--- .../graphql/mutations/flagReadyToAdvance.ts | 4 - .../addAgendaItemToActiveActionMeeting.ts | 51 +++----- .../graphql/mutations/helpers/addRecallBot.ts | 3 - .../helpers/createNewMeetingPhases.ts | 52 ++++---- .../endMeeting/sendNewMeetingSummary.ts | 4 - .../mutations/helpers/generateGroups.ts | 13 +- .../mutations/helpers/handleCompletedStage.ts | 8 -- .../mutations/helpers/hideConversionModal.ts | 9 -- .../helpers/notifications/SlackNotifier.ts | 18 +-- .../helpers/removeStagesFromMeetings.ts | 11 -- .../mutations/helpers/removeTeamMember.ts | 23 ++-- .../helpers/removeUserFromMeetingStages.ts | 11 -- .../mutations/helpers/safeEndRetrospective.ts | 37 +----- .../mutations/helpers/safeEndTeamPrompt.ts | 39 +----- .../helpers/updateQualAIMeetingsCount.ts | 2 +- .../mutations/helpers/updateTeamInsights.ts | 46 +++---- .../server/graphql/mutations/joinMeeting.ts | 27 ---- .../graphql/mutations/navigateMeeting.ts | 24 +--- packages/server/graphql/mutations/payLater.ts | 37 +++--- .../graphql/mutations/pokerResetDimension.ts | 5 +- .../mutations/promoteNewMeetingFacilitator.ts | 11 -- .../graphql/mutations/removeReflection.ts | 13 +- .../server/graphql/mutations/renameMeeting.ts | 9 -- .../resetRetroMeetingToGroupStage.ts | 5 +- .../server/graphql/mutations/setPhaseFocus.ts | 3 - .../server/graphql/mutations/setStageTimer.ts | 12 +- .../graphql/mutations/startSprintPoker.ts | 16 +-- .../mutations/updateNewCheckInQuestion.ts | 11 -- .../graphql/mutations/updatePokerScope.ts | 11 -- .../mutations/updateReflectionContent.ts | 2 +- .../mutations/updateReflectionGroupTitle.ts | 2 +- .../graphql/mutations/updateRetroMaxVotes.ts | 37 ++---- .../graphql/mutations/updateTemplateScope.ts | 24 ++-- .../mutations/voteForReflectionGroup.ts | 2 +- .../mutations/generateMeetingSummary.ts | 41 +++---- .../private/mutations/hardDeleteUser.ts | 29 ++--- .../private/mutations/processRecurrence.ts | 20 ++- .../private/mutations/runOrgActivityReport.ts | 90 ++++++-------- .../private/mutations/runScheduledJobs.ts | 2 +- .../graphql/public/mutations/addComment.ts | 2 +- .../public/mutations/addReactjiToReactable.ts | 2 +- .../graphql/public/mutations/autogroup.ts | 3 - .../graphql/public/mutations/endTeamPrompt.ts | 5 +- .../public/mutations/generateInsight.ts | 2 +- .../public/mutations/helpers/getSummaries.ts | 41 ++++--- .../public/mutations/helpers/getTopics.ts | 39 +++--- .../public/mutations/resetReflectionGroups.ts | 7 -- .../graphql/public/mutations/shareTopic.ts | 2 +- .../graphql/public/mutations/startCheckIn.ts | 20 +-- .../public/mutations/startRetrospective.ts | 16 +-- .../public/mutations/startTeamPrompt.ts | 9 +- .../public/mutations/updateAgendaItem.ts | 9 -- .../public/mutations/updateMeetingPrompt.ts | 9 -- .../public/mutations/updateMeetingTemplate.ts | 3 - .../mutations/updateRecurrenceSettings.ts | 47 ++----- .../graphql/public/types/ActionMeeting.ts | 2 +- .../public/types/ActionMeetingMember.ts | 2 +- .../public/types/AddAgendaItemPayload.ts | 2 +- .../types/AddTranscriptionBotSuccess.ts | 2 +- .../graphql/public/types/AutogroupSuccess.ts | 2 +- .../server/graphql/public/types/Company.ts | 115 ++++++++---------- .../server/graphql/public/types/Discussion.ts | 2 +- .../public/types/EndTeamPromptSuccess.ts | 2 +- .../graphql/public/types/EstimateStage.ts | 6 +- .../public/types/GenerateGroupsSuccess.ts | 2 +- .../graphql/public/types/MeetingSeries.ts | 17 ++- .../graphql/public/types/NewMeetingStage.ts | 7 +- .../NotificationMeetingStageTimeLimitEnd.ts | 2 +- .../public/types/NotifyDiscussionMentioned.ts | 2 +- .../public/types/NotifyResponseMentioned.ts | 2 +- .../public/types/NotifyResponseReplied.ts | 2 +- .../graphql/public/types/ReflectPhase.ts | 4 +- .../public/types/RemoveAgendaItemPayload.ts | 2 +- .../types/ResetReflectionGroupsSuccess.ts | 2 +- .../graphql/public/types/RetroDiscussStage.ts | 4 +- .../graphql/public/types/RetroReflection.ts | 8 +- .../public/types/RetroReflectionGroup.ts | 4 +- .../public/types/RetrospectiveMeeting.ts | 4 +- .../types/RetrospectiveMeetingMember.ts | 2 +- .../graphql/public/types/ShareTopicSuccess.ts | 2 +- .../public/types/StartCheckInSuccess.ts | 2 +- .../public/types/StartRetrospectiveSuccess.ts | 2 +- .../public/types/StartTeamPromptSuccess.ts | 2 +- .../graphql/public/types/TeamPromptMeeting.ts | 45 +++---- .../types/TimelineEventTeamPromptComplete.ts | 2 +- .../public/types/UpdateAgendaItemPayload.ts | 2 +- .../types/UpdateDimensionFieldSuccess.ts | 2 +- .../UpdateGitLabDimensionFieldSuccess.ts | 2 +- .../types/UpdateMeetingPromptSuccess.ts | 2 +- .../types/UpdateMeetingTemplateSuccess.ts | 2 +- .../types/UpdateRecurrenceSettingsSuccess.ts | 2 +- .../types/UpsertTeamPromptResponseSuccess.ts | 2 +- .../helpers/getActiveTeamCountByTeamIds.ts | 86 +++++++------ packages/server/graphql/resolvers.ts | 2 +- .../server/graphql/resolvers/resolveStage.ts | 2 +- .../types/DragEstimatingTaskPayload.ts | 2 +- .../types/FlagReadyToAdvancePayload.ts | 2 +- .../graphql/types/NavigateMeetingPayload.ts | 4 +- .../PromoteNewMeetingFacilitatorPayload.ts | 2 +- .../graphql/types/SetStageTimerPayload.ts | 2 +- .../graphql/types/UpdatePokerScopePayload.ts | 4 +- .../1726174453131_NewMeeting-phase1.ts | 2 +- .../incrementUserPayLaterClickCountQuery.sql | 7 -- 111 files changed, 477 insertions(+), 934 deletions(-) delete mode 100644 packages/server/postgres/queries/src/incrementUserPayLaterClickCountQuery.sql diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index 0776e9efdad..e3db1bfb74e 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -1,6 +1,6 @@ import {MasterPool, r} from 'rethinkdb-ts' import TeamInvitation from '../database/types/TeamInvitation' -import {AnyMeeting, AnyMeetingTeamMember} from '../postgres/types/Meeting' +import {AnyMeetingTeamMember} from '../postgres/types/Meeting' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' import MassInvitation from './types/MassInvitation' @@ -25,15 +25,6 @@ export type RethinkSchema = { type: AnyMeetingTeamMember index: 'meetingId' | 'teamId' | 'userId' } - NewMeeting: { - type: AnyMeeting - index: - | 'facilitatorUserId' - | 'teamId' - | 'templateId' - | 'meetingSeriesId' - | 'hasEndedScheduledEndTime' - } NewFeature: { type: any index: '' diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 2f14218cd5c..030f62bf990 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -15,6 +15,7 @@ import { selectTemplateScale, selectTimelineEvent } from '../postgres/select' +import {AnyMeeting} from '../postgres/types/Meeting' import {foreignKeyLoaderMaker} from './foreignKeyLoaderMaker' export const teamsByOrgIds = foreignKeyLoaderMaker('teams', 'orgId', (orgIds) => @@ -237,6 +238,7 @@ export const activeMeetingsByTeamId = foreignKeyLoaderMaker( .where('teamId', 'in', teamIds) .where('endedAt', 'is', null) .orderBy('createdAt desc') + .$narrowType() .execute() } ) @@ -248,6 +250,7 @@ export const completedMeetingsByTeamId = foreignKeyLoaderMaker( .where('teamId', 'in', teamIds) .where('endedAt', 'is not', null) .orderBy('endedAt desc') + .$narrowType() .execute() } ) diff --git a/packages/server/email/newMeetingSummaryEmailCreator.tsx b/packages/server/email/newMeetingSummaryEmailCreator.tsx index f0e065a2f33..811e20e6687 100644 --- a/packages/server/email/newMeetingSummaryEmailCreator.tsx +++ b/packages/server/email/newMeetingSummaryEmailCreator.tsx @@ -18,7 +18,7 @@ const newMeetingSummaryEmailCreator = async (props: Props) => { const {dataLoader} = context const dataLoaderId = dataLoader.share() - const newMeeting = await dataLoader.get('newMeetings').load(meetingId) + const newMeeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const facilitator = await dataLoader.get('users').loadNonNull(newMeeting.facilitatorUserId!) const {tms} = facilitator const authToken = new AuthToken({sub: newMeeting.facilitatorUserId!, tms, rol: 'impersonate'}) diff --git a/packages/server/graphql/mutations/createReflection.ts b/packages/server/graphql/mutations/createReflection.ts index 7020a7de025..ca41613f6bb 100644 --- a/packages/server/graphql/mutations/createReflection.ts +++ b/packages/server/graphql/mutations/createReflection.ts @@ -5,7 +5,6 @@ import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTitle' import unlockAllStagesForPhase from 'parabol-client/utils/unlockAllStagesForPhase' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' -import getRethink from '../../database/rethinkDriver' import ReflectionGroup from '../../database/types/ReflectionGroup' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' @@ -34,7 +33,6 @@ export default { {input}: {input: CreateReflectionInputType}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { - const r = await getRethink() const pg = getKysely() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} diff --git a/packages/server/graphql/mutations/dragDiscussionTopic.ts b/packages/server/graphql/mutations/dragDiscussionTopic.ts index 931194d10f1..304944856d9 100644 --- a/packages/server/graphql/mutations/dragDiscussionTopic.ts +++ b/packages/server/graphql/mutations/dragDiscussionTopic.ts @@ -1,6 +1,5 @@ import {GraphQLFloat, 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 getPhase from '../../utils/getPhase' @@ -29,7 +28,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) diff --git a/packages/server/graphql/mutations/dragEstimatingTask.ts b/packages/server/graphql/mutations/dragEstimatingTask.ts index 3d956975879..7b639e3c48e 100644 --- a/packages/server/graphql/mutations/dragEstimatingTask.ts +++ b/packages/server/graphql/mutations/dragEstimatingTask.ts @@ -1,7 +1,6 @@ import {GraphQLID, GraphQLInt, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {ESTIMATE_TASK_SORT_ORDER} from '../../../client/utils/constants' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -36,7 +35,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index edaff8dce30..e97dcc3aee1 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -169,7 +169,6 @@ export default { async resolve(_source: unknown, {meetingId}: {meetingId: string}, context: GQLContext) { const {authToken, socketId: mutatorId, dataLoader} = context const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const now = new Date() diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index f057e37a57c..4f827428b08 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -4,7 +4,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' 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 TimelineEventPokerComplete from '../../database/types/TimelineEventPokerComplete' import getKysely from '../../postgres/getKysely' import {Logger} from '../../utils/Logger' @@ -34,7 +33,6 @@ export default { }, async resolve(_source: unknown, {meetingId}: {meetingId: string}, context: GQLContext) { const {authToken, socketId: mutatorId, dataLoader} = context - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const now = new Date() @@ -80,21 +78,6 @@ export default { 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) - .update( - { - endedAt: now, - phases, - commentCount, - storyCount, - ...insights - }, - {returnChanges: true, nonAtomic: true} - )('changes')(0)('new_val') - .default(null) - .run() await getKysely() .updateTable('NewMeeting') .set({ @@ -106,13 +89,9 @@ export default { engagement: insights.engagement }) .where('id', '=', meetingId) - .executeTakeFirst() + .execute() dataLoader.clearAll('newMeetings') - if (!completedMeeting) { - return standardError(new Error('Completed poker meeting does not exist'), { - userId: viewerId - }) - } + const completedMeeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (completedMeeting.meetingType !== 'poker') { return standardError(new Error('Meeting is not a poker meeting'), {userId: viewerId}) } diff --git a/packages/server/graphql/mutations/flagReadyToAdvance.ts b/packages/server/graphql/mutations/flagReadyToAdvance.ts index 24a07512bf5..289b2ddaa74 100644 --- a/packages/server/graphql/mutations/flagReadyToAdvance.ts +++ b/packages/server/graphql/mutations/flagReadyToAdvance.ts @@ -2,7 +2,6 @@ import {GraphQLBoolean, GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import findStageById from 'parabol-client/utils/meetings/findStageById' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' @@ -31,9 +30,7 @@ const flagReadyToAdvance = { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { const pg = getKysely() - const r = await getRethink() const viewerId = getUserId(authToken) - const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -82,7 +79,6 @@ const flagReadyToAdvance = { // RESOLUTION // TODO there's enough evidence showing that we should probably worry about atomicity - await r.table('NewMeeting').get(meetingId).update({phases, updatedAt: now}).run() await pg .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) diff --git a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index b7a34bc7b6f..6370fb1fbe3 100644 --- a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts +++ b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts @@ -1,4 +1,3 @@ -import getRethink from '../../../database/rethinkDriver' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' import getKysely from '../../../postgres/getKysely' import getPhase from '../../../utils/getPhase' @@ -12,8 +11,6 @@ const addAgendaItemToActiveActionMeeting = async ( teamId: string, dataLoader: DataLoaderWorker ) => { - const now = new Date() - const r = await getRethink() const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const actionMeeting = activeMeetings.find( (activeMeeting) => activeMeeting.meetingType === 'action' @@ -36,36 +33,26 @@ const addAgendaItemToActiveActionMeeting = async ( const {discussionId} = newStage stages.push(newStage) - await Promise.all([ - r - .table('NewMeeting') - .get(meetingId) - .update({ - phases, - updatedAt: now + await getKysely() + .with('UpdatePhases', (qb) => + qb + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + ) + .with('InsertDiscussion', (qb) => + qb.insertInto('Discussion').values({ + id: discussionId, + teamId, + meetingId, + discussionTopicType: 'agendaItem', + discussionTopicId: agendaItemId }) - .run(), - getKysely() - .with('UpdatePhases', (qb) => - qb - .updateTable('NewMeeting') - .set({phases: JSON.stringify(phases)}) - .where('id', '=', meetingId) - ) - .with('InsertDiscussion', (qb) => - qb.insertInto('Discussion').values({ - id: discussionId, - teamId, - meetingId, - discussionTopicType: 'agendaItem', - discussionTopicId: agendaItemId - }) - ) - .updateTable('AgendaItem') - .set({meetingId}) - .where('id', '=', agendaItemId) - .execute() - ]) + ) + .updateTable('AgendaItem') + .set({meetingId}) + .where('id', '=', agendaItemId) + .execute() return meetingId } diff --git a/packages/server/graphql/mutations/helpers/addRecallBot.ts b/packages/server/graphql/mutations/helpers/addRecallBot.ts index 3a16b97c0d2..5a466eee7df 100644 --- a/packages/server/graphql/mutations/helpers/addRecallBot.ts +++ b/packages/server/graphql/mutations/helpers/addRecallBot.ts @@ -1,4 +1,3 @@ -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import RecallAIServerManager from '../../../utils/RecallAIServerManager' @@ -9,9 +8,7 @@ const getBotId = async (videoMeetingURL: string) => { } const addRecallBot = async (meetingId: string, videoMeetingURL: string) => { - const r = await getRethink() const recallBotId = (await getBotId(videoMeetingURL)) ?? undefined - await r.table('NewMeeting').get(meetingId).update({recallBotId, videoMeetingURL}).run() await getKysely() .updateTable('NewMeeting') .set({recallBotId, videoMeetingURL}) diff --git a/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts b/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts index 44846ed791f..1f8b8afd09c 100644 --- a/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts +++ b/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts @@ -10,8 +10,6 @@ import { VOTE } from 'parabol-client/utils/constants' import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import AgendaItemsPhase from '../../../database/types/AgendaItemsPhase' import CheckInPhase from '../../../database/types/CheckInPhase' import CheckInStage from '../../../database/types/CheckInStage' @@ -23,9 +21,10 @@ import TeamHealthPhase from '../../../database/types/TeamHealthPhase' import TeamHealthStage from '../../../database/types/TeamHealthStage' import UpdatesPhase from '../../../database/types/UpdatesPhase' import UpdatesStage from '../../../database/types/UpdatesStage' +import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' -import {NewMeetingPhase} from '../../../postgres/types/NewMeetingPhase' +import {NewMeetingPhase, NewMeetingStages} from '../../../postgres/types/NewMeetingPhase' import isPhaseAvailable from '../../../utils/isPhaseAvailable' import {DataLoaderWorker} from '../../graphql' import {getFeatureTier} from '../../types/helpers/getFeatureTier' @@ -44,29 +43,28 @@ export const primePhases = (phases: GenericMeetingPhase[], startIndex = 0) => { } } -const getPastStageDurations = async (teamId: string) => { - const r = await getRethink() - return ( - r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter({isLegacy: false}, {default: true}) - // .orderBy(r.desc('endedAt')) - .concatMap((row: RValue) => row('phases')) - .concatMap((row: RValue) => row('stages')) - .filter((row: RValue) => row.hasFields('startAt', 'endAt')) - // convert seconds to ms - .merge((row: RValue) => ({ - duration: r.sub(row('endAt'), row('startAt')).mul(1000).floor() - })) - // remove stages that took under 1 minute - .filter((row: RValue) => row('duration').ge(60000)) - .orderBy(r.desc('startAt')) - .group('phaseType') - .ungroup() - .map((row) => [row('group'), row('reduction')('duration')]) - .coerceTo('object') - .run() as unknown as {[key: string]: number[]} +const getPastStageDurations = async (teamId: string, dataLoader: DataLoaderInstance) => { + const completedMeetings = await dataLoader.get('completedMeetingsByTeamId').load(teamId) + const phases = completedMeetings.flatMap((meeting) => meeting.phases as NewMeetingPhase[]) + const stages = phases + .flatMap((phase) => phase.stages as NewMeetingStages[]) + .map((stage) => ({ + phaseType: stage.phaseType, + duration: + stage.startAt && stage.endAt + ? new Date(stage.endAt).getTime() - new Date(stage.startAt).getTime() + : 0 + })) + .filter((stage) => stage.duration >= 60_000) + return stages.reduce( + (acc, stage) => { + if (!acc[stage.phaseType]) { + acc[stage.phaseType] = [] + } + acc[stage.phaseType]!.push(stage.duration) + return acc + }, + {} as Record ) } @@ -81,7 +79,7 @@ const createNewMeetingPhases = async { const pg = getKysely() const now = new Date() - const r = await getRethink() const [activeMeetings, completedMeetings] = await Promise.all([ dataLoader.get('activeMeetingsByTeamId').load(teamId), dataLoader.get('completedMeetingsByTeamId').load(teamId) @@ -52,15 +50,6 @@ const removeStagesFromMeetings = async ( .set({facilitatorStageId: meeting.facilitatorStageId, phases: JSON.stringify(phases)}) .where('id', '=', meetingId) .execute() - return r - .table('NewMeeting') - .get(meetingId) - .update({ - facilitatorStageId: meeting.facilitatorStageId, - phases, - updatedAt: now - }) - .run() }) ) return meetings.map((meeting) => meeting.id) diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 9a9452ba27e..815ba184870 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -27,7 +27,6 @@ const removeTeamMember = async ( const {evictorUserId} = options const r = await getRethink() const pg = getKysely() - const now = new Date() const {userId, teamId} = fromTeamMemberId(teamMemberId) // see if they were a leader, make a new guy leader so later we can reassign tasks const activeTeamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) @@ -141,13 +140,13 @@ const removeTeamMember = async ( .run() // Reassign facilitator for meetings this user is facilitating. - const facilitatingMeetings = await r - .table('NewMeeting') - .getAll(r.args(meetingIds), {index: 'id'}) - .filter({ - facilitatorUserId: userId - }) - .run() + + const facilitatingMeetings = await pg + .selectFrom('NewMeeting') + .select('id') + .where('id', 'in', meetingIds) + .where('facilitatorUserId', '=', userId) + .execute() const newMeetingFacilitators = ( await dataLoader @@ -171,14 +170,6 @@ const removeTeamMember = async ( .set({facilitatorUserId: newFacilitator.userId}) .where('id', '=', newFacilitator.meetingId) .execute() - await r - .table('NewMeeting') - .get(newFacilitator.meetingId) - .update({ - facilitatorUserId: newFacilitator.userId, - updatedAt: now - }) - .run() }) ) diff --git a/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts b/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts index dff4349cf30..470a20c7943 100644 --- a/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts +++ b/packages/server/graphql/mutations/helpers/removeUserFromMeetingStages.ts @@ -1,4 +1,3 @@ -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {DataLoaderWorker} from '../../graphql' import {isEstimateStage} from '../../meetingTypePredicates' @@ -14,8 +13,6 @@ const removeUserFromMeetingStages = async ( dataLoader: DataLoaderWorker ) => { const pg = getKysely() - const now = new Date() - const r = await getRethink() const [activeMeetings, completedMeetings] = await Promise.all([ dataLoader.get('activeMeetingsByTeamId').load(teamId), dataLoader.get('completedMeetingsByTeamId').load(teamId) @@ -52,14 +49,6 @@ const removeUserFromMeetingStages = async ( .set({phases: JSON.stringify(phases)}) .where('id', '=', meetingId) .execute() - return r - .table('NewMeeting') - .get(meetingId) - .update({ - phases, - updatedAt: now - }) - .run() }) ) return meetings.map((meeting) => meeting.id) diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index 6a0b1e21689..c486815af94 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -76,23 +76,6 @@ const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: Int }) .where('id', '=', meetingId) .execute() - await r - .table('NewMeeting') - .get(meetingId) - .update( - { - commentCount, - taskCount, - topicCount: reflectionGroupIds.length, - reflectionCount: reflections.length, - sentimentScore, - summary, - transcription - }, - {nonAtomic: true} - ) - .run() - dataLoader.clearAll('newMeetings') // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount sendNewMeetingSummary(meeting, context).catch(Logger.log) @@ -116,7 +99,6 @@ const safeEndRetrospective = async ({ }) => { const {authToken, socketId: mutatorId, dataLoader} = context const {id: meetingId, phases, facilitatorStageId, teamId} = meeting - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -132,19 +114,6 @@ const safeEndRetrospective = async ({ const phase = getMeetingPhase(phases) const insights = await gatherInsights(meeting, dataLoader) - const completedRetrospective = await r - .table('NewMeeting') - .get(meetingId) - .update( - { - endedAt: now, - phases, - ...insights - }, - {returnChanges: true} - )('changes')(0)('new_val') - .default(null) - .run() await getKysely() .updateTable('NewMeeting') .set({ @@ -156,11 +125,7 @@ const safeEndRetrospective = async ({ .where('id', '=', meetingId) .executeTakeFirst() dataLoader.clearAll('newMeetings') - if (!completedRetrospective) { - return standardError(new Error('Completed retrospective meeting does not exist'), { - userId: viewerId - }) - } + const completedRetrospective = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (completedRetrospective.meetingType !== 'retrospective') { return standardError(new Error('Meeting type is not retrospective'), { userId: viewerId diff --git a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts index 33364edf2fb..b080f1d6a34 100644 --- a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts @@ -1,7 +1,6 @@ import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {checkTeamsLimit} from '../../../billing/helpers/teamLimitsCheck' -import getRethink, {ParabolR} from '../../../database/rethinkDriver' import TimelineEventTeamPromptComplete from '../../../database/types/TimelineEventTeamPromptComplete' import getKysely from '../../../postgres/getKysely' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' @@ -21,17 +20,9 @@ import updateTeamInsights from './updateTeamInsights' const summarizeTeamPrompt = async (meeting: TeamPromptMeeting, context: InternalContext) => { const {dataLoader} = context const pg = getKysely() - const r = await getRethink() const summary = await generateStandupMeetingSummary(meeting, dataLoader) await pg.updateTable('NewMeeting').set({summary}).where('id', '=', meeting.id).execute() - await r - .table('NewMeeting') - .get(meeting.id) - .update({ - summary - }) - .run() dataLoader.clearAll('newMeetings') // wait for whole meeting summary to be generated before sending summary email and updating qualAIMeetingCount @@ -47,16 +38,12 @@ const summarizeTeamPrompt = async (meeting: TeamPromptMeeting, context: Internal const safeEndTeamPrompt = async ({ meeting, - now, viewerId, - r, context, subOptions }: { meeting: TeamPromptMeeting - now: Date viewerId?: string - r: ParabolR context: InternalContext subOptions: SubOptions }) => { @@ -77,30 +64,10 @@ const safeEndTeamPrompt = async ({ engagement: insights.engagement }) .execute() - const completedTeamPrompt = await r - .table('NewMeeting') - .get(meetingId) - .update( - { - endedAt: now, - ...insights - }, - {returnChanges: true} - )('changes')(0)('new_val') - .default(null) - .run() - - if (!completedTeamPrompt) { - return standardError(new Error('Completed team prompt meeting does not exist'), { - userId: viewerId - }) - } - - if (completedTeamPrompt.meetingType !== 'teamPrompt') { - return standardError(new Error('Meeting is not a team prompt'), {userId: viewerId}) - } + dataLoader.clearAll('newMeetings') - const [meetingMembers, team, teamMembers, responses] = await Promise.all([ + const [completedTeamPrompt, meetingMembers, team, teamMembers, responses] = await Promise.all([ + dataLoader.get('newMeetings').loadNonNull(meetingId), dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('teamMembersByTeamId').load(teamId), diff --git a/packages/server/graphql/mutations/helpers/updateQualAIMeetingsCount.ts b/packages/server/graphql/mutations/helpers/updateQualAIMeetingsCount.ts index b6d11940cef..1c2cbb7d7de 100644 --- a/packages/server/graphql/mutations/helpers/updateQualAIMeetingsCount.ts +++ b/packages/server/graphql/mutations/helpers/updateQualAIMeetingsCount.ts @@ -10,7 +10,7 @@ const updateQualAIMeetingsCount = async ( dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').load(teamId), dataLoader.get('retroReflectionsByMeetingId').load(meetingId), - dataLoader.get('newMeetings').load(meetingId) + dataLoader.get('newMeetings').loadNonNull(meetingId) ]) if (meetingMembers.length < 3 || !team || !meeting.summary || reflections.length < 5) return const {qualAIMeetingsCount} = team diff --git a/packages/server/graphql/mutations/helpers/updateTeamInsights.ts b/packages/server/graphql/mutations/helpers/updateTeamInsights.ts index ba9bc32cc5f..15f8e2fe571 100644 --- a/packages/server/graphql/mutations/helpers/updateTeamInsights.ts +++ b/packages/server/graphql/mutations/helpers/updateTeamInsights.ts @@ -1,6 +1,5 @@ +import {NotNull} from 'kysely' import ms from 'ms' -import {RValue} from 'rethinkdb-ts' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {DataLoaderWorker} from '../../graphql' @@ -21,30 +20,27 @@ const updateTeamInsights = async (teamId: string, dataLoader: DataLoaderWorker) if (organization?.featureFlags?.includes('noTeamInsights')) return // actual update - const r = await getRethink() const pg = getKysely() const now = new Date() const insightsPeriod = new Date(now.getTime() - TEAM_INSIGHTS_PERIOD) const topRetroTemplatesPeriod = new Date(now.getTime() - TOP_RETRO_TEMPLATES_PERIOD) const [meetingInsights, retroTemplates] = await Promise.all([ - r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter((row: RValue) => row('createdAt').gt(insightsPeriod)) - .pluck('endedAt', 'usedReactjis', 'meetingType', 'templateId', 'engagement') - .run(), - ( - r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter((row: RValue) => - row('meetingType').eq('retrospective').and(row('createdAt').gt(topRetroTemplatesPeriod)) - ) as any - ) - .group('templateId') - .count() - .run() as Promise<{group: string; reduction: number}[]> + pg + .selectFrom('NewMeeting') + .select(['endedAt', 'usedReactjis', 'meetingType', 'templateId', 'engagement']) + .where('teamId', '=', teamId) + .where('createdAt', '>', insightsPeriod) + .execute(), + pg + .selectFrom('NewMeeting') + .select(({fn}) => ['templateId as group', fn.count('id').as('reduction')]) + .where('teamId', '=', teamId) + .where('meetingType', '==', 'retrospective') + .where('createdAt', '>', topRetroTemplatesPeriod) + .groupBy('templateId') + .$narrowType<{group: NotNull}>() + .execute() ]) // emojis @@ -88,12 +84,10 @@ const updateTeamInsights = async (teamId: string, dataLoader: DataLoaderWorker) ) // top retro template - const topRetroTemplates = retroTemplates.map( - ({group, reduction}: {group: string; reduction: number}) => ({ - reflectTemplateId: group, - count: reduction - }) - ) + const topRetroTemplates = retroTemplates.map(({group, reduction}) => ({ + reflectTemplateId: group, + count: Number(reduction) + })) topRetroTemplates.sort((a, b) => b.count - a.count) const update = { diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index e830e298608..7a209aafbae 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -1,7 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' -import rMapIf from '../../database/rMapIf' import getRethink from '../../database/rethinkDriver' import ActionMeetingMember from '../../database/types/ActionMeetingMember' import CheckInStage from '../../database/types/CheckInStage' @@ -89,8 +88,6 @@ const joinMeeting = { return {error: {message: 'Already joined meeting'}} } - const mapIf = rMapIf(r) - const addStageToPhase = async ( stage: CheckInStage | UpdatesStage | TeamPromptResponseStage, phaseType: NewMeetingPhase['phaseType'] @@ -122,30 +119,6 @@ const joinMeeting = { .where('id', '=', meetingId) .execute() }) - return r - .table('NewMeeting') - .get(meetingId) - .update((meeting: any) => ({ - phases: mapIf( - meeting('phases'), - (phase: any) => phase('phaseType').eq(phaseType), - (phase: any) => - phase.merge({ - stages: phase('stages').append({ - ...stage, - // this is a departure from before. Let folks move ahead while the check-in is going on! - isNavigable: true, - isNavigableByFacilitator: true, - // the stage is complete if all other stages are complete & there's at least 1 - isComplete: r.and( - phase('stages')('isComplete').contains(false).not(), - phase('stages').count().ge(1) - ) - }) - }) - ) - })) - .run() } const appendToCheckin = async () => { diff --git a/packages/server/graphql/mutations/navigateMeeting.ts b/packages/server/graphql/mutations/navigateMeeting.ts index 8563af871b1..20172d7cec9 100644 --- a/packages/server/graphql/mutations/navigateMeeting.ts +++ b/packages/server/graphql/mutations/navigateMeeting.ts @@ -3,7 +3,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import findStageById from 'parabol-client/utils/meetings/findStageById' import startStage_ from 'parabol-client/utils/startStage_' import unlockNextStages from 'parabol-client/utils/unlockNextStages' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {Logger} from '../../utils/Logger' import {getUserId} from '../../utils/authorization' @@ -40,7 +39,6 @@ export default { }: {completedStageId: string | null; facilitatorStageId: string | null; meetingId: string}, {authToken, socketId: mutatorId, dataLoader}: GQLContext ) { - const r = await getRethink() const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -100,7 +98,9 @@ export default { if (!facilitatorStage.isNavigableByFacilitator) { return standardError(new Error('Stage has not started'), {userId: viewerId}) } - + if (meeting.facilitatorStageId === facilitatorStageId) { + return standardError(new Error('Already at this stage'), {userId: viewerId}) + } // mutative // NOTE: it is possible to start a stage then move backwards & complete another phase, which would make it seem like this phase took a long time // the cleanest way to fix this is to store start/stop on each stage visit, since i could visit B, then visit A, then move B before A, then visit B @@ -111,19 +111,6 @@ export default { } // RESOLUTION - const oldFacilitatorStageId = await r - .table('NewMeeting') - .get(meetingId) - .update( - { - facilitatorStageId: facilitatorStageId ?? undefined, - phases, - updatedAt: now - }, - {returnChanges: true} - )('changes')(0)('old_val')('facilitatorStageId') - .default(null) - .run() await getKysely() .updateTable('NewMeeting') .set({ @@ -133,13 +120,10 @@ export default { .where('id', '=', meetingId) .execute() dataLoader.clearAll('newMeetings') - if (!oldFacilitatorStageId) { - return {error: {message: 'Stage already advanced'}} - } const data = { meetingId, - oldFacilitatorStageId, + oldFacilitatorStageId: meeting.facilitatorStageId, facilitatorStageId, unlockedStageIds, ...phaseCompleteData diff --git a/packages/server/graphql/mutations/payLater.ts b/packages/server/graphql/mutations/payLater.ts index fa4c0189a89..af614ca0fc0 100644 --- a/packages/server/graphql/mutations/payLater.ts +++ b/packages/server/graphql/mutations/payLater.ts @@ -1,9 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' -import getPg from '../../postgres/getPg' -import {incrementUserPayLaterClickCountQuery} from '../../postgres/queries/generated/incrementUserPayLaterClickCountQuery' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -25,7 +22,6 @@ export default { {meetingId}: {meetingId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -50,26 +46,27 @@ export default { const team = await dataLoader.get('teams').loadNonNull(teamId) const {orgId} = team await getKysely() - .updateTable('Organization') - .set((eb) => ({ - payLaterClickCount: eb('payLaterClickCount', '+', 1) - })) - .where('id', '=', orgId) - .execute() - await r - .table('NewMeeting') - .get(meetingId) - .update({ - showConversionModal: false - }) - .run() - await getKysely() + .with('UpdateOrg', (qc) => + qc + .updateTable('Organization') + .set((eb) => ({ + payLaterClickCount: eb('payLaterClickCount', '+', 1) + })) + .where('id', '=', orgId) + ) + .with('UpdateUser', (qc) => + qc + .updateTable('User') + .set((eb) => ({ + payLaterClickCount: eb('payLaterClickCount', '+', 1) + })) + .where('id', '=', viewerId) + ) .updateTable('NewMeeting') .set({showConversionModal: false}) .where('id', '=', meetingId) .execute() - dataLoader.clearAll('newMeetings') - await incrementUserPayLaterClickCountQuery.run({id: viewerId}, getPg()) + dataLoader.clearAll(['newMeetings', 'organizations', 'users']) analytics.conversionModalPayLaterClicked(viewer) const data = {orgId, meetingId} diff --git a/packages/server/graphql/mutations/pokerResetDimension.ts b/packages/server/graphql/mutations/pokerResetDimension.ts index 24b118742c5..af168b87ea5 100644 --- a/packages/server/graphql/mutations/pokerResetDimension.ts +++ b/packages/server/graphql/mutations/pokerResetDimension.ts @@ -40,16 +40,13 @@ const pokerResetDimension = { if (meeting.meetingType !== 'poker') { return {error: {message: 'Not a poker meeting'}} } - const {endedAt, phases, meetingType, teamId, createdBy, facilitatorUserId} = meeting + const {endedAt, phases, teamId, createdBy, facilitatorUserId} = meeting if (!isTeamMember(authToken, teamId)) { return {error: {message: 'Not on the team'}} } if (endedAt) { return {error: {message: 'Meeting has ended'}} } - if (meetingType !== 'poker') { - return {error: {message: 'Not a poker meeting'}} - } if (isPhaseComplete('ESTIMATE', phases)) { return {error: {message: 'Estimate phase is already complete'}} } diff --git a/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts b/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts index ce3a30827f9..c1de141c948 100644 --- a/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts +++ b/packages/server/graphql/mutations/promoteNewMeetingFacilitator.ts @@ -1,6 +1,5 @@ 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' @@ -25,10 +24,8 @@ export default { {facilitatorUserId, meetingId}: {facilitatorUserId: string; meetingId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} - const now = new Date() const viewerId = getUserId(authToken) // AUTH @@ -52,14 +49,6 @@ export default { } // RESOLUTION - await r - .table('NewMeeting') - .get(meetingId) - .update({ - facilitatorUserId, - updatedAt: now - }) - .run() await getKysely() .updateTable('NewMeeting') .set({facilitatorUserId}) diff --git a/packages/server/graphql/mutations/removeReflection.ts b/packages/server/graphql/mutations/removeReflection.ts index b864cfff51a..01f1712a789 100644 --- a/packages/server/graphql/mutations/removeReflection.ts +++ b/packages/server/graphql/mutations/removeReflection.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' import unlockAllStagesForPhase from 'parabol-client/utils/unlockAllStagesForPhase' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -24,7 +23,6 @@ export default { {reflectionId}: {reflectionId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { - const r = await getRethink() const pg = getKysely() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -40,7 +38,7 @@ export default { if (creatorId !== viewerId) { return standardError(new Error('Reflection'), {userId: viewerId}) } - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {endedAt, phases, teamId} = meeting if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) @@ -61,14 +59,7 @@ export default { let unlockedStageIds if (reflections.length === 0) { unlockedStageIds = unlockAllStagesForPhase(phases, 'group', true, false) - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases - }) - .run() - await getKysely() + await pg .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) .where('id', '=', meetingId) diff --git a/packages/server/graphql/mutations/renameMeeting.ts b/packages/server/graphql/mutations/renameMeeting.ts index a4d2263145b..99f6c0a53d3 100644 --- a/packages/server/graphql/mutations/renameMeeting.ts +++ b/packages/server/graphql/mutations/renameMeeting.ts @@ -1,7 +1,6 @@ import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import linkify from 'parabol-client/utils/linkify' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' @@ -28,7 +27,6 @@ const renameMeeting = { {name, meetingId}: {name: string; meetingId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -55,13 +53,6 @@ const renameMeeting = { // RESOLUTION meeting.name = name - await r - .table('NewMeeting') - .get(meetingId) - .update({ - name - }) - .run() await getKysely().updateTable('NewMeeting').set({name}).where('id', '=', meetingId).execute() const data = {meetingId} IntegrationNotifier.updateMeeting?.(dataLoader, meetingId, teamId) diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index 6985db0c274..ec97b6837c7 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -119,8 +119,9 @@ const resetRetroMeetingToGroupStage = { .where('id', '=', meetingId) .execute(), r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run(), - r.table('NewMeeting').get(meetingId).update({phases: newPhases}).run(), - (r.table('MeetingMember').getAll(meetingId, {index: 'meetingId'}) as any) + r + .table('MeetingMember') + .getAll(meetingId, {index: 'meetingId'}) .update({votesRemaining: meeting.totalVotes}) .run() ]) diff --git a/packages/server/graphql/mutations/setPhaseFocus.ts b/packages/server/graphql/mutations/setPhaseFocus.ts index 301502c2aa6..e48f2550ad2 100644 --- a/packages/server/graphql/mutations/setPhaseFocus.ts +++ b/packages/server/graphql/mutations/setPhaseFocus.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {GROUP} from 'parabol-client/utils/constants' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -28,7 +27,6 @@ const setPhaseFocus = { {meetingId, focusedPromptId}: {meetingId: string; focusedPromptId?: string | null}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -52,7 +50,6 @@ const setPhaseFocus = { // RESOLUTION // mutative reflectPhase.focusedPromptId = focusedPromptId ?? undefined - await r.table('NewMeeting').get(meetingId).update(meeting).run() await getKysely() .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) diff --git a/packages/server/graphql/mutations/setStageTimer.ts b/packages/server/graphql/mutations/setStageTimer.ts index 6e93cdd39ff..b580df1ca1a 100644 --- a/packages/server/graphql/mutations/setStageTimer.ts +++ b/packages/server/graphql/mutations/setStageTimer.ts @@ -1,7 +1,6 @@ import {GraphQLFloat, GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import findStageById from 'parabol-client/utils/meetings/findStageById' -import getRethink from '../../database/rethinkDriver' import ScheduledJobMeetingStageTimeLimit from '../../database/types/ScheduledJobMetingStageTimeLimit' import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' @@ -46,7 +45,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -54,7 +52,7 @@ export default { // AUTH const [meeting, viewer] = await Promise.all([ - dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('newMeetings').loadNonNull(meetingId), dataLoader.get('users').loadNonNull(viewerId) ]) const {endedAt, facilitatorStageId, facilitatorUserId, phases, teamId} = meeting @@ -108,14 +106,6 @@ export default { } // RESOLUTION - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases, - updatedAt: now - }) - .run() await pg .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) diff --git a/packages/server/graphql/mutations/startSprintPoker.ts b/packages/server/graphql/mutations/startSprintPoker.ts index abb727757e7..e1836d9b83f 100644 --- a/packages/server/graphql/mutations/startSprintPoker.ts +++ b/packages/server/graphql/mutations/startSprintPoker.ts @@ -102,7 +102,6 @@ export default { const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) - const DUPLICATE_THRESHOLD = 3000 // AUTH if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Not on team'), {userId: viewerId}) @@ -148,26 +147,17 @@ export default { }) as PokerMeeting const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) - await Promise.allSettled([ + const [newMeetingRes] = await Promise.allSettled([ pg .insertInto('NewMeeting') .values({...meeting, phases: JSON.stringify(phases)}) .execute(), - r.table('NewMeeting').insert(meeting).run(), updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) ]) - dataLoader.clearAll('newMeetings') - // Disallow accidental starts (2 meetings within 2 seconds) - const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) - const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { - const {createdAt, id} = activeMeeting - if (id === meetingId || activeMeeting.meetingType !== 'poker') return false - return createdAt.getTime() > Date.now() - DUPLICATE_THRESHOLD - }) - if (otherActiveMeeting) { - await r.table('NewMeeting').get(meetingId).delete().run() + if (newMeetingRes.status === 'rejected') { return {error: {message: 'Meeting already started'}} } + dataLoader.clearAll('newMeetings') const teamMemberId = toTeamMemberId(teamId, viewerId) const teamMember = await dataLoader.get('teamMembers').loadNonNull(teamMemberId) diff --git a/packages/server/graphql/mutations/updateNewCheckInQuestion.ts b/packages/server/graphql/mutations/updateNewCheckInQuestion.ts index 0446229d527..e00258dcb32 100644 --- a/packages/server/graphql/mutations/updateNewCheckInQuestion.ts +++ b/packages/server/graphql/mutations/updateNewCheckInQuestion.ts @@ -3,7 +3,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import convertToTaskContent from 'parabol-client/utils/draftjs/convertToTaskContent' import {makeCheckinQuestion} from 'parabol-client/utils/makeCheckinGreeting' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -31,10 +30,8 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} - const now = new Date() const viewerId = getUserId(authToken) // AUTH @@ -58,14 +55,6 @@ export default { // mutative checkInPhase.checkInQuestion = normalizedCheckInQuestion - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases, - updatedAt: now - }) - .run() await pg .updateTable('NewMeeting') .set({phases: JSON.stringify(phases)}) diff --git a/packages/server/graphql/mutations/updatePokerScope.ts b/packages/server/graphql/mutations/updatePokerScope.ts index f104dc07282..f6d7af19549 100644 --- a/packages/server/graphql/mutations/updatePokerScope.ts +++ b/packages/server/graphql/mutations/updatePokerScope.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLList, GraphQLNonNull} from 'graphql' import {Insertable} from 'kysely' import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' import {ESTIMATE_TASK_SORT_ORDER} from '../../../client/utils/constants' -import getRethink from '../../database/rethinkDriver' import EstimateStage from '../../database/types/EstimateStage' import {TaskServiceEnum} from '../../database/types/Task' import getKysely from '../../postgres/getKysely' @@ -43,7 +42,6 @@ const updatePokerScope = { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { const pg = getKysely() - const r = await getRethink() const redis = getRedis() const viewerId = getUserId(authToken) const operationId = dataLoader.share() @@ -166,15 +164,6 @@ const updatePokerScope = { }) .where('id', '=', meetingId) .execute() - await r - .table('NewMeeting') - .get(meetingId) - .update({ - facilitatorStageId: meeting.facilitatorStageId, - phases, - updatedAt: now - }) - .run() if (newDiscussions.length > 0) { await getKysely().insertInto('Discussion').values(newDiscussions).execute() } diff --git a/packages/server/graphql/mutations/updateReflectionContent.ts b/packages/server/graphql/mutations/updateReflectionContent.ts index 36b79bfb958..180a101d0a0 100644 --- a/packages/server/graphql/mutations/updateReflectionContent.ts +++ b/packages/server/graphql/mutations/updateReflectionContent.ts @@ -51,7 +51,7 @@ export default { return standardError(new Error('Category not found'), {userId: viewerId}) } const {question} = reflectPrompt - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {endedAt, phases, teamId} = meeting if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) diff --git a/packages/server/graphql/mutations/updateReflectionGroupTitle.ts b/packages/server/graphql/mutations/updateReflectionGroupTitle.ts index 41a3ca4978c..7470bddbbf1 100644 --- a/packages/server/graphql/mutations/updateReflectionGroupTitle.ts +++ b/packages/server/graphql/mutations/updateReflectionGroupTitle.ts @@ -47,7 +47,7 @@ export default { return {error: {message: 'Group already renamed'}} } const [meeting, viewer] = await Promise.all([ - dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('newMeetings').loadNonNull(meetingId), dataLoader.get('users').loadNonNull(viewerId) ]) const {endedAt, phases, teamId} = meeting diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index 329ce6646fd..47fcf5c673a 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -138,31 +138,18 @@ const updateRetroMaxVotes = { } // RESOLUTION - await Promise.all([ - getKysely() - .with('MeetingUpdates', (qb) => - qb - .updateTable('NewMeeting') - .set({totalVotes, maxVotesPerGroup}) - .where('id', '=', meetingId) - ) - .updateTable('MeetingSettings') - .set({ - totalVotes, - maxVotesPerGroup - }) - .where('teamId', '=', teamId) - .where('meetingType', '=', 'retrospective') - .execute(), - r - .table('NewMeeting') - .get(meetingId) - .update({ - totalVotes, - maxVotesPerGroup - }) - .run() - ]) + await getKysely() + .with('MeetingUpdates', (qb) => + qb.updateTable('NewMeeting').set({totalVotes, maxVotesPerGroup}).where('id', '=', meetingId) + ) + .updateTable('MeetingSettings') + .set({ + totalVotes, + maxVotesPerGroup + }) + .where('teamId', '=', teamId) + .where('meetingType', '=', 'retrospective') + .execute() dataLoader.get('newMeetings').clear(meetingId) const data = {meetingId} publish(SubscriptionChannel.MEETING, meetingId, 'UpdateRetroMaxVotesSuccess', data, subOptions) diff --git a/packages/server/graphql/mutations/updateTemplateScope.ts b/packages/server/graphql/mutations/updateTemplateScope.ts index 41a0c48b590..41acfed50ab 100644 --- a/packages/server/graphql/mutations/updateTemplateScope.ts +++ b/packages/server/graphql/mutations/updateTemplateScope.ts @@ -1,7 +1,5 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -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' @@ -32,7 +30,7 @@ const updateTemplateScope = { {templateId, scope: newScope}: {templateId: string; scope: SharingScopeEnumType}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { - const r = await getRethink() + const pg = getKysely() const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -59,16 +57,16 @@ const updateTemplateScope = { template.scope = newScope // mutate the cached record const SCOPES: ESharingScope[] = ['TEAM', 'ORGANIZATION', 'PUBLIC'] const isDownscope = SCOPES.indexOf(newScope) < SCOPES.indexOf(scope) - const shouldClone = isDownscope - ? await r - .table('NewMeeting') - .getAll(templateId, {index: 'templateId'}) - .filter((meeting: RDatum) => meeting('teamId').ne(teamId)) - .nth(0) - .default(null) - .ne(null) - .run() - : false + const usedMeeting = isDownscope + ? await pg + .selectFrom('NewMeeting') + .select('id') + .where('templateId', '=', templateId) + .where('teamId', '!=', teamId) + .limit(1) + .executeTakeFirst() + : null + const shouldClone = !!usedMeeting let clonedTemplateId: string | undefined const cloneReflectTemplate = async () => { diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index 6afe994dee6..304051a39e8 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -42,7 +42,7 @@ export default { }) } const {meetingId} = reflectionGroup - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'retrospective') { return {error: {message: 'Meeting type is not retrospective'}} } diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 0d5d21ede25..20f4a843204 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -1,6 +1,7 @@ import yaml from 'js-yaml' -import getRethink from '../../../database/rethinkDriver' +import {sql} from 'kysely' import getKysely from '../../../postgres/getKysely' +import {selectNewMeetings} from '../../../postgres/select' import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import getPhase from '../../../utils/getPhase' @@ -11,7 +12,6 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn {teamIds, prompt}, {dataLoader} ) => { - const r = await getRethink() const pg = getKysely() const MIN_MILLISECONDS = 60 * 1000 // 1 minute const MIN_REFLECTION_COUNT = 3 @@ -20,19 +20,24 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const twoYearsAgo = new Date() twoYearsAgo.setFullYear(endDate.getFullYear() - 2) - const rawMeetings = await r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: any) => - row('meetingType') - .eq('retrospective') - .and(row('createdAt').ge(twoYearsAgo)) - .and(row('createdAt').le(endDate)) - .and(row('reflectionCount').gt(MIN_REFLECTION_COUNT)) - .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) - .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) - ) - .run() + const rawMeetingsWithAnyMembers = await selectNewMeetings() + .where('teamId', 'in', teamIds) + .where('meetingType', '=', 'retrospective') + .where('createdAt', '>=', twoYearsAgo) + .where('createdAt', '<=', endDate) + .where('reflectionCount', '>', MIN_REFLECTION_COUNT) + .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_MILLISECONDS}`) + .$narrowType() + .execute() + + const allMeetingMembers = await dataLoader + .get('meetingMembersByMeetingId') + .loadMany(rawMeetingsWithAnyMembers.map(({id}) => id)) + + const rawMeetings = rawMeetingsWithAnyMembers.filter((_, idx) => { + const meetingMembers = allMeetingMembers[idx] + return Array.isArray(meetingMembers) && meetingMembers.length > 1 + }) const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] @@ -168,17 +173,11 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const newSummary = await manager.generateSummary(yamlData, prompt) if (!newSummary) return null - const now = new Date() await getKysely() .updateTable('NewMeeting') .set({summary: newSummary}) .where('id', '=', meeting.id) .execute() - await r - .table('NewMeeting') - .get(meeting.id) - .update({summary: newSummary, updatedAt: now}) - .run() meeting.summary = newSummary return meeting.id }) diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 27995849eea..24a4796137d 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -16,12 +16,13 @@ const setFacilitatedUserIdOrDelete = async ( dataLoader: DataLoaderInstance ) => { const pg = getKysely() - const r = await getRethink() - const facilitatedMeetings = await r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RValue) => row('createdBy').eq(userIdToDelete)) - .run() + const facilitatedMeetings = await pg + .selectFrom('NewMeeting') + .select('id') + .where('teamId', 'in', teamIds) + .where('createdBy', '=', userIdToDelete) + .execute() + facilitatedMeetings.map(async (meeting) => { const {id: meetingId} = meeting const meetingMembers = await dataLoader.get('meetingMembersByMeetingId').load(meetingId) @@ -32,17 +33,9 @@ const setFacilitatedUserIdOrDelete = async ( .set({facilitatorUserId: otherMember.userId}) .where('id', '=', meetingId) .execute() - await r - .table('NewMeeting') - .get(meetingId) - .update({ - facilitatorUserId: otherMember.userId - }) - .run() } else { - await pg.deleteFrom('NewMeeting').where('id', '=', meetingId).execute() // single-person meeting must be deleted because facilitatorUserId must be non-null - await r.table('NewMeeting').get(meetingId).delete().run() + await pg.deleteFrom('NewMeeting').where('id', '=', meetingId).execute() } }) } @@ -95,12 +88,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .where('createdBy', '=', userIdToDelete) .execute() await r({ - nullifyCreatedBy: r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RValue) => row('createdBy').eq(userIdToDelete)) - .update({createdBy: null}) - .run(), meetingMember: r.table('MeetingMember').getAll(userIdToDelete, {index: 'userId'}).delete(), notification: r.table('Notification').getAll(userIdToDelete, {index: 'userId'}).delete(), createdTasks: r diff --git a/packages/server/graphql/private/mutations/processRecurrence.ts b/packages/server/graphql/private/mutations/processRecurrence.ts index 6d0b2099e3c..5d37cec09f0 100644 --- a/packages/server/graphql/private/mutations/processRecurrence.ts +++ b/packages/server/graphql/private/mutations/processRecurrence.ts @@ -1,14 +1,15 @@ import dayjs from 'dayjs' import tracer from 'dd-trace' +import {sql} from 'kysely' import ms from 'ms' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DateTime, RRuleSet} from 'rrule-rust' import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId' import {fromDateTime, toDateTime} from '../../../../client/shared/rruleUtil' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {getActiveMeetingSeries} from '../../../postgres/queries/getActiveMeetingSeries' -import {RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting' +import {selectNewMeetings} from '../../../postgres/select' +import {AnyMeeting, RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting' import {MeetingSeries} from '../../../postgres/types/MeetingSeries' import {analytics} from '../../../utils/analytics/analytics' import {getNextRRuleDate} from '../../../utils/getNextRRuleDate' @@ -32,7 +33,6 @@ const startRecurringMeeting = async ( subOptions: SubOptions ) => { const pg = getKysely() - const r = await getRethink() const {id: meetingSeriesId, teamId, facilitatorId, meetingType} = meetingSeries // AUTH @@ -63,7 +63,6 @@ const startRecurringMeeting = async ( .insertInto('NewMeeting') .values({...meeting, phases: JSON.stringify(meeting.phases)}) .execute() - await r.table('NewMeeting').insert(meeting).run() const data = {teamId, meetingId: meeting.id} publish(SubscriptionChannel.TEAM, teamId, 'StartTeamPromptSuccess', data, subOptions) return meeting @@ -88,7 +87,6 @@ const startRecurringMeeting = async ( }, dataLoader ) - await r.table('NewMeeting').insert(meeting).run() await pg .insertInto('NewMeeting') .values({...meeting, phases: JSON.stringify(meeting.phases)}) @@ -117,23 +115,23 @@ const processRecurrence: MutationResolvers['processRecurrence'] = async ( context ) => { const {dataLoader, socketId: mutatorId} = context - const r = await getRethink() const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} // RESOLUTION // Find any meetings with a scheduledEndTime before now, and close them - const meetingsToEnd = await r - .table('NewMeeting') - .between([false, r.minval], [false, now], {index: 'hasEndedScheduledEndTime'}) - .run() + const meetingsToEnd = await selectNewMeetings() + .where('scheduledEndTime', '<', sql`CURRENT_TIMESTAMP`) + .where('endedAt', 'is', null) + .$narrowType() + .execute() const res = await tracer.trace('processRecurrence.endMeetings', async () => Promise.all( meetingsToEnd.map((meeting) => { if (meeting.meetingType === 'teamPrompt') { - return safeEndTeamPrompt({meeting, now, context, r, subOptions}) + return safeEndTeamPrompt({meeting, context, subOptions}) } else if (meeting.meetingType === 'retrospective') { return safeEndRetrospective({meeting, now, context}) } else { diff --git a/packages/server/graphql/private/mutations/runOrgActivityReport.ts b/packages/server/graphql/private/mutations/runOrgActivityReport.ts index 17c27f8d8c0..11c8b03bec0 100644 --- a/packages/server/graphql/private/mutations/runOrgActivityReport.ts +++ b/packages/server/graphql/private/mutations/runOrgActivityReport.ts @@ -1,6 +1,6 @@ import {sql} from 'kysely' +import {RValue} from 'rethinkdb-ts' import getRethink from '../../../database/rethinkDriver' -import {RDatum, RValue} from '../../../database/stricterR' import getKysely from '../../../postgres/getKysely' import {MutationResolvers} from '../resolverTypes' @@ -26,8 +26,8 @@ const runOrgActivityReport: MutationResolvers['runOrgActivityReport'] = async ( const userSignups = pg .selectFrom('User') .select([ - sql`date_trunc('month', "createdAt")`.as('month'), - sql`COUNT(DISTINCT "id")`.as('signup_count') + sql`date_trunc('month', "createdAt")`.as('month'), + sql`COUNT(DISTINCT "id")`.as('signup_count') ]) .where('createdAt', '>=', queryStartDate) .where('createdAt', '<', queryEndDate) @@ -39,67 +39,55 @@ const runOrgActivityReport: MutationResolvers['runOrgActivityReport'] = async ( join.onRef(sql`m."monthStart"`, '=', sql`us.month::timestamp`) ) .select([ - sql`m."monthStart"`.as('monthStart'), - sql`COALESCE(us.signup_count, 0)`.as('signupCount') + sql`m."monthStart"`.as('monthStart'), + sql`COALESCE(us.signup_count, 0)`.as('signupCount') ]) .orderBy('monthStart') const r = await getRethink() try { - const [pgResults, rethinkResults] = await Promise.all([ + const [signupCounts, rawMeetingCounts] = await Promise.all([ query.execute(), - r - .table('NewMeeting') - .between( - r.epochTime(queryStartDate.getTime() / 1000), - r.epochTime(queryEndDate.getTime() / 1000), - {index: 'createdAt'} - ) - .merge((row: RValue) => ({ - yearMonth: { - year: row('createdAt').year(), - month: row('createdAt').month() - } - })) - .group((row) => row('yearMonth')) - .ungroup() - .map((group: RDatum) => ({ - yearMonth: group('group'), - meetingCount: group('reduction').count(), - participantIds: group('reduction') - .concatMap((row: RDatum) => - r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'})('userId') - ) - .distinct() - })) - .map((row: RDatum) => - row.merge({ - participantCount: row('participantIds').count() - }) - ) - .without('participantIds') - .run() + pg + .selectFrom('NewMeeting') + .select(({fn, ref, val}) => [ + fn('date_trunc', [val('month'), ref('createdAt')]).as('monthStart'), + fn('array_agg', ['id']).as('meetingIds') + ]) + .where('createdAt', '>=', queryStartDate) + .where('createdAt', '<', queryEndDate) + .groupBy('monthStart') + .execute() ]) + const meetingIds = rawMeetingCounts.flatMap((row) => row.meetingIds) + const participantCounts = (await ( + r + .table('MeetingMember') + .getAll(r.args(meetingIds), {index: 'meetingId'}) + .group('meetingId') as any + ) + .count() + .ungroup() + .map((group: RValue) => ({ + meetingId: group('group'), + participantCount: group('reduction') + })) + .run()) as {meetingId: string; participantCount: number}[] // Combine PostgreSQL and RethinkDB results - const combinedResults = pgResults.map((pgRow: any) => { - const monthStart = new Date(pgRow.monthStart) - const rethinkParticipants = rethinkResults.find( - (r: any) => - r.yearMonth.month === monthStart.getUTCMonth() + 1 && - r.yearMonth.year === monthStart.getUTCFullYear() + const combinedResults = signupCounts.map((pgRow) => { + const epochMonthStart = pgRow.monthStart.getTime() + const meetingCount = rawMeetingCounts.find( + (rmc) => rmc.monthStart.getTime() === epochMonthStart ) - const rethinkMeetings = rethinkResults.find( - (r: any) => - r.yearMonth.month === monthStart.getUTCMonth() + 1 && - r.yearMonth.year === monthStart.getUTCFullYear() + const participantCount = participantCounts.find((pc) => + meetingCount?.meetingIds.includes(pc.meetingId) ) - return { monthStart: pgRow.monthStart, - signupCount: pgRow.signupCount ? pgRow.signupCount : 0, - participantCount: rethinkParticipants ? rethinkParticipants.participantCount : 0, - meetingCount: rethinkMeetings ? rethinkMeetings.meetingCount : 0 + signupCount: pgRow.signupCount ? Number(pgRow.signupCount) : 0, + participantCount: participantCount?.participantCount ?? 0, + meetingCount: meetingCount?.meetingIds.length ?? 0 } }) return {rows: combinedResults} diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts index bc93f4f84f8..49651b3cef9 100644 --- a/packages/server/graphql/private/mutations/runScheduledJobs.ts +++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts @@ -25,7 +25,7 @@ const processMeetingStageTimeLimits = async ( // if mattermost, send mattermost // if no integrated notification services, send an in-app notification const {meetingId} = job - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId, facilitatorUserId} = meeting IntegrationNotifier.endTimeLimit(dataLoader, meetingId, teamId) diff --git a/packages/server/graphql/public/mutations/addComment.ts b/packages/server/graphql/public/mutations/addComment.ts index e20405c6184..616b0118b2e 100644 --- a/packages/server/graphql/public/mutations/addComment.ts +++ b/packages/server/graphql/public/mutations/addComment.ts @@ -90,7 +90,7 @@ const addComment: MutationResolvers['addComment'] = async ( } const meetingMemberId = MeetingMemberId.join(meetingId, viewerId) const [meeting, viewerMeetingMember, viewer] = await Promise.all([ - dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('newMeetings').loadNonNull(meetingId), dataLoader.get('meetingMembers').load(meetingMemberId), dataLoader.get('users').loadNonNull(viewerId) ]) diff --git a/packages/server/graphql/public/mutations/addReactjiToReactable.ts b/packages/server/graphql/public/mutations/addReactjiToReactable.ts index 2debd57a3ac..1cb2e78a67f 100644 --- a/packages/server/graphql/public/mutations/addReactjiToReactable.ts +++ b/packages/server/graphql/public/mutations/addReactjiToReactable.ts @@ -105,7 +105,7 @@ const addReactjiToReactable: MutationResolvers['addReactjiToReactable'] = async } const [meeting] = await Promise.all([ - dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('newMeetings').loadNonNull(meetingId), updatePG(tableName) ]) dataLoader.clearAll(['comments', 'teamPromptResponses', 'retroReflections']) diff --git a/packages/server/graphql/public/mutations/autogroup.ts b/packages/server/graphql/public/mutations/autogroup.ts index 21db1e150a7..a711b33f6fc 100644 --- a/packages/server/graphql/public/mutations/autogroup.ts +++ b/packages/server/graphql/public/mutations/autogroup.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -15,7 +14,6 @@ const autogroup: MutationResolvers['autogroup'] = async ( context: GQLContext ) => { const pg = getKysely() - const r = await getRethink() const {authToken, dataLoader, socketId: mutatorId} = context const viewerId = getUserId(authToken) const operationId = dataLoader.share() @@ -72,7 +70,6 @@ const autogroup: MutationResolvers['autogroup'] = async ( ) ) }), - r.table('NewMeeting').get(meetingId).update({resetReflectionGroups}).run(), pg .updateTable('NewMeeting') .set({resetReflectionGroups: JSON.stringify(resetReflectionGroups)}) diff --git a/packages/server/graphql/public/mutations/endTeamPrompt.ts b/packages/server/graphql/public/mutations/endTeamPrompt.ts index b98a70f1261..b255bc4a4d6 100644 --- a/packages/server/graphql/public/mutations/endTeamPrompt.ts +++ b/packages/server/graphql/public/mutations/endTeamPrompt.ts @@ -1,4 +1,3 @@ -import getRethink from '../../../database/rethinkDriver' import {getUserId, isTeamMember} from '../../../utils/authorization' import standardError from '../../../utils/standardError' import safeEndTeamPrompt from '../../mutations/helpers/safeEndTeamPrompt' @@ -6,9 +5,7 @@ import {MutationResolvers} from '../resolverTypes' const endTeamPrompt: MutationResolvers['endTeamPrompt'] = async (_source, {meetingId}, context) => { const {authToken, dataLoader, socketId: mutatorId} = context - const r = await getRethink() const viewerId = getUserId(authToken) - const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -24,7 +21,7 @@ const endTeamPrompt: MutationResolvers['endTeamPrompt'] = async (_source, {meeti if (!isTeamMember(authToken, teamId) && authToken.rol !== 'su') { return standardError(new Error('Team not found'), {userId: viewerId}) } - return safeEndTeamPrompt({meeting, now, r, context, subOptions, viewerId}) + return safeEndTeamPrompt({meeting, context, subOptions, viewerId}) } export default endTeamPrompt diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 903383135f0..c7ec6cbb459 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -20,7 +20,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } const response = useSummaries - ? await getSummaries(teamId, startDate, endDate, prompt) + ? await getSummaries(teamId, startDate, endDate, dataLoader, prompt) : await getTopics(teamId, startDate, endDate, dataLoader, prompt) if ('error' in response) { diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 30ff97c9f9d..42bd5e99b79 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -1,5 +1,8 @@ import yaml from 'js-yaml' -import getRethink from '../../../../database/rethinkDriver' +import {sql} from 'kysely' +import {DataLoaderInstance} from '../../../../dataloader/RootDataLoader' +import getKysely from '../../../../postgres/getKysely' +import {RetrospectiveMeeting} from '../../../../postgres/types/Meeting' import OpenAIServerManager from '../../../../utils/OpenAIServerManager' import standardError from '../../../../utils/standardError' @@ -7,26 +10,32 @@ export const getSummaries = async ( teamId: string, startDate: Date, endDate: Date, + dataLoader: DataLoaderInstance, prompt?: string | null ) => { - const r = await getRethink() + const pg = getKysely() const MIN_MILLISECONDS = 60 * 1000 // 1 minute const MIN_REFLECTION_COUNT = 3 + const rawMeetingsWithAnyMembers = await pg + .selectFrom('NewMeeting') + .select(['id', 'name', 'createdAt', 'summary']) + .where('teamId', '=', teamId) + .where('summary', 'is not', null) + .where('meetingType', '=', 'retrospective') + .where('createdAt', '>=', startDate) + .where('createdAt', '<=', endDate) + .where('reflectionCount', '>=', MIN_REFLECTION_COUNT) + .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_MILLISECONDS}`) + .$narrowType() + .execute() + const allMeetingMembers = await dataLoader + .get('meetingMembersByMeetingId') + .loadMany(rawMeetingsWithAnyMembers.map(({id}) => id)) - const rawMeetings = await r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter((row: any) => - row('meetingType') - .eq('retrospective') - .and(row('createdAt').ge(startDate)) - .and(row('createdAt').le(endDate)) - .and(row('reflectionCount').gt(MIN_REFLECTION_COUNT)) - .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) - .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) - .and(row.hasFields('summary')) - ) - .run() + const rawMeetings = rawMeetingsWithAnyMembers.filter((_, idx) => { + const meetingMembers = allMeetingMembers[idx] + return Array.isArray(meetingMembers) && meetingMembers.length > 1 + }) if (!rawMeetings.length) { return standardError(new Error('No meetings found')) diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index 47782e177f9..dd17904ae52 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -1,10 +1,11 @@ import yaml from 'js-yaml' -import getRethink from '../../../../database/rethinkDriver' +import {sql} from 'kysely' import getKysely from '../../../../postgres/getKysely' import OpenAIServerManager from '../../../../utils/OpenAIServerManager' import sendToSentry from '../../../../utils/sendToSentry' import standardError from '../../../../utils/standardError' import {DataLoaderWorker} from '../../../graphql' +import {RetrospectiveMeeting} from '../../resolverTypes' const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWorker) => { const pg = getKysely() @@ -109,23 +110,29 @@ export const getTopics = async ( dataLoader: DataLoaderWorker, prompt?: string | null ) => { - const r = await getRethink() + const pg = getKysely() const MIN_REFLECTION_COUNT = 3 const MIN_MILLISECONDS = 60 * 1000 // 1 minute - const rawAnyMeetings = await r - .table('NewMeeting') - .getAll(teamId, {index: 'teamId'}) - .filter((row: any) => - row('meetingType') - .eq('retrospective') - .and(row('createdAt').ge(startDate)) - .and(row('createdAt').le(endDate)) - .and(row('reflectionCount').gt(MIN_REFLECTION_COUNT)) - .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) - .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) - ) - .run() - const rawMeetings = rawAnyMeetings.filter((m) => m.meetingType === 'retrospective') + const rawMeetingsWithAnyMembers = await pg + .selectFrom('NewMeeting') + .select(['id', 'name', 'createdAt', 'disableAnonymity']) + .where('teamId', '=', teamId) + .where('meetingType', '=', 'retrospective') + .where('createdAt', '>=', startDate) + .where('createdAt', '<=', endDate) + .where('reflectionCount', '>=', MIN_REFLECTION_COUNT) + .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_MILLISECONDS}`) + .$narrowType() + .execute() + const allMeetingMembers = await dataLoader + .get('meetingMembersByMeetingId') + .loadMany(rawMeetingsWithAnyMembers.map(({id}) => id)) + + const rawMeetings = rawMeetingsWithAnyMembers.filter((_, idx) => { + const meetingMembers = allMeetingMembers[idx] + return Array.isArray(meetingMembers) && meetingMembers.length > 1 + }) + const meetings = await Promise.all( rawMeetings.map(async (meeting) => { const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting diff --git a/packages/server/graphql/public/mutations/resetReflectionGroups.ts b/packages/server/graphql/public/mutations/resetReflectionGroups.ts index 991eea4ddda..27d74bc6107 100644 --- a/packages/server/graphql/public/mutations/resetReflectionGroups.ts +++ b/packages/server/graphql/public/mutations/resetReflectionGroups.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -27,7 +26,6 @@ const resetReflectionGroups: MutationResolvers['resetReflectionGroups'] = async const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) - const r = await getRethink() const [meeting, viewer] = await Promise.all([ dataLoader.get('newMeetings').load(meetingId), dataLoader.get('users').loadNonNull(viewerId) @@ -72,11 +70,6 @@ const resetReflectionGroups: MutationResolvers['resetReflectionGroups'] = async .flat() ) - await r - .table('NewMeeting') - .get(meetingId) - .replace(r.row.without('resetReflectionGroups') as any) - .run() await pg .updateTable('NewMeeting') .set({resetReflectionGroups: null}) diff --git a/packages/server/graphql/public/mutations/shareTopic.ts b/packages/server/graphql/public/mutations/shareTopic.ts index f8ab1e3f4c5..916dcf5df7a 100644 --- a/packages/server/graphql/public/mutations/shareTopic.ts +++ b/packages/server/graphql/public/mutations/shareTopic.ts @@ -11,7 +11,7 @@ const shareTopic: MutationResolvers['shareTopic'] = async ( {authToken, dataLoader} ) => { const viewerId = getUserId(authToken) - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting if (!isTeamMember(authToken, teamId)) { diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index 3714e6a2042..94c19a319fc 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -61,20 +61,12 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( phases, facilitatorUserId: viewerId }) as CheckInMeeting - await r.table('NewMeeting').insert(meeting).run() - await pg - .insertInto('NewMeeting') - .values({...meeting, phases: JSON.stringify(phases)}) - .execute() - // Disallow 2 active check-in meetings - const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) - const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { - const {id} = activeMeeting - if (id === meetingId || activeMeeting.meetingType !== meetingType) return false - return true - }) - if (otherActiveMeeting) { - await r.table('NewMeeting').get(meetingId).delete().run() + try { + await pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(phases)}) + .execute() + } catch (e) { return {error: {message: 'Meeting already started'}} } dataLoader.clearAll('newMeetings') diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index a27d4cb4b0d..6f82f89d7ef 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -26,7 +26,6 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} - const DUPLICATE_THRESHOLD = 3000 // AUTH const viewerId = getUserId(authToken) if (!isTeamMember(authToken, teamId)) { @@ -74,25 +73,14 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( const meetingId = meeting.id const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) - await Promise.allSettled([ - r.table('NewMeeting').insert(meeting).run(), + const [newMeetingRes] = await Promise.allSettled([ pg .insertInto('NewMeeting') .values({...meeting, phases: JSON.stringify(meeting.phases)}) .execute(), updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) ]) - - // Disallow accidental starts (2 meetings within 2 seconds) - const newActiveMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) - const otherActiveMeeting = newActiveMeetings.find((activeMeeting) => { - const {createdAt, id} = activeMeeting - if (id === meetingId || activeMeeting.meetingType !== meetingType) return false - return createdAt.getTime() > Date.now() - DUPLICATE_THRESHOLD - }) - if (otherActiveMeeting) { - // trigger exists in PG to prevent this - await r.table('NewMeeting').get(meetingId).delete().run() + if (newMeetingRes.status === 'rejected') { return {error: {message: 'Meeting already started'}} } diff --git a/packages/server/graphql/public/mutations/startTeamPrompt.ts b/packages/server/graphql/public/mutations/startTeamPrompt.ts index 0f22ea40ab6..bf0b8b32612 100644 --- a/packages/server/graphql/public/mutations/startTeamPrompt.ts +++ b/packages/server/graphql/public/mutations/startTeamPrompt.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import RedisLockQueue from '../../../utils/RedisLockQueue' import {analytics} from '../../../utils/analytics/analytics' @@ -22,7 +21,6 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async ( {authToken, dataLoader, socketId: mutatorId} ) => { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -51,8 +49,7 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async ( const eventName = rrule ? name || 'Standup' : meetingName const meeting = await safeCreateTeamPrompt(meetingName, teamId, viewerId, dataLoader) - await Promise.all([ - r.table('NewMeeting').insert(meeting).run(), + const [newMeetingRes] = await Promise.allSettled([ pg .with('NewMeetingInsert', (qb) => qb.insertInto('NewMeeting').values({...meeting, phases: JSON.stringify(meeting.phases)}) @@ -62,7 +59,9 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async ( .where('id', '=', teamId) .execute() ]) - + if (newMeetingRes.status === 'rejected') { + return {error: {message: 'Meeting already started'}} + } const {id: meetingId} = meeting const meetingSeries = rrule && (await startNewMeetingSeries(meeting, rrule, name)) if (meetingSeries) { diff --git a/packages/server/graphql/public/mutations/updateAgendaItem.ts b/packages/server/graphql/public/mutations/updateAgendaItem.ts index 6acc7e5efd2..bae71ce0eba 100644 --- a/packages/server/graphql/public/mutations/updateAgendaItem.ts +++ b/packages/server/graphql/public/mutations/updateAgendaItem.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' import getKysely from '../../../postgres/getKysely' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -13,7 +12,6 @@ const updateAgendaItem: MutationResolvers['updateAgendaItem'] = async ( {updatedAgendaItem}, {authToken, dataLoader, socketId: mutatorId} ) => { - const r = await getRethink() const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -62,13 +60,6 @@ const updateAgendaItem: MutationResolvers['updateAgendaItem'] = async ( .set({phases: JSON.stringify(phases)}) .where('id', '=', meetingId) .execute() - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases - }) - .run() } const data = {agendaItemId, meetingId} publish(SubscriptionChannel.TEAM, teamId, 'UpdateAgendaItemPayload', data, subOptions) diff --git a/packages/server/graphql/public/mutations/updateMeetingPrompt.ts b/packages/server/graphql/public/mutations/updateMeetingPrompt.ts index edc59a97d9c..104c4b41024 100644 --- a/packages/server/graphql/public/mutations/updateMeetingPrompt.ts +++ b/packages/server/graphql/public/mutations/updateMeetingPrompt.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -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 updateMeetingPrompt: MutationResolvers['updateMeetingPrompt'] = async ( {authToken, dataLoader, socketId: mutatorId} ) => { const pg = getKysely() - const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -43,13 +41,6 @@ const updateMeetingPrompt: MutationResolvers['updateMeetingPrompt'] = async ( .set({meetingPrompt: newPrompt}) .where('id', '=', meetingId) .execute() - await r - .table('NewMeeting') - .get(meetingId) - .update({ - meetingPrompt: newPrompt - }) - .run() dataLoader.get('newMeetings').clear(meetingId) // RESOLUTION diff --git a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts index de34566210a..ad0afa1e53f 100644 --- a/packages/server/graphql/public/mutations/updateMeetingTemplate.ts +++ b/packages/server/graphql/public/mutations/updateMeetingTemplate.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' @@ -14,7 +13,6 @@ const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async ) => { const pg = getKysely() const viewerId = getUserId(authToken) - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const meeting = await dataLoader.get('newMeetings').load(meetingId) @@ -40,7 +38,6 @@ const updateMeetingTemplate: MutationResolvers['updateMeetingTemplate'] = async ) } await pg.updateTable('NewMeeting').set({templateId}).where('id', '=', meetingId).execute() - await r.table('NewMeeting').get(meetingId).update({templateId}).run() meeting.templateId = templateId const data = {meetingId, templateId} diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 2692015636f..44c18dc709a 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -3,7 +3,7 @@ import {sql} from 'kysely' import {toDateTime} from 'parabol-client/shared/rruleUtil' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DateTime, RRuleSet} from 'rrule-rust' -import getRethink from '../../../database/rethinkDriver' +import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../../../postgres/queries/insertMeetingSeries' import restartMeetingSeries from '../../../postgres/queries/restartMeetingSeries' @@ -36,7 +36,6 @@ export const startNewMeetingSeries = async ( facilitatorUserId: facilitatorId } = meeting const pg = getKysely() - const r = await getRethink() if (!facilitatorId) { throw new Error('No facilitatorId') } @@ -52,14 +51,6 @@ export const startNewMeetingSeries = async ( const newMeetingSeriesId = await insertMeetingSeriesQuery(newMeetingSeriesParams) const nextMeetingStartDate = getNextRRuleDate(recurrenceRule) - await r - .table('NewMeeting') - .get(meetingId) - .update({ - meetingSeriesId: newMeetingSeriesId, - scheduledEndTime: nextMeetingStartDate - }) - .run() await pg .updateTable('NewMeeting') .set({meetingSeriesId: newMeetingSeriesId, scheduledEndTime: nextMeetingStartDate}) @@ -71,20 +62,21 @@ export const startNewMeetingSeries = async ( } } -const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRule: RRuleSet) => { +const updateMeetingSeries = async ( + meetingSeries: MeetingSeries, + newRecurrenceRule: RRuleSet, + dataLoader: DataLoaderInstance +) => { const pg = getKysely() - const r = await getRethink() const {id: meetingSeriesId} = meetingSeries await restartMeetingSeries(meetingSeriesId, {recurrenceRule: newRecurrenceRule.toString()}) // lets close all active meetings at the time when // a new meeting will be created (tomorrow at 9 AM, same as date start of new recurrence rule) - const activeMeetings = await r - .table('NewMeeting') - .getAll(meetingSeriesId, {index: 'meetingSeriesId'}) - .filter({endedAt: null}, {default: true}) - .run() + const activeMeetings = await dataLoader + .get('activeMeetingsByMeetingSeriesId') + .load(meetingSeriesId) if (activeMeetings.length > 0) { const meetingIds = activeMeetings.map(({id}) => id) const scheduledEndTime = getNextRRuleDate(newRecurrenceRule) @@ -93,22 +85,11 @@ const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRu .set({scheduledEndTime}) .where('id', 'in', meetingIds) .execute() - const updates = activeMeetings.map((meeting) => - r - .table('NewMeeting') - .get(meeting.id) - .update({ - scheduledEndTime - }) - .run() - ) - await Promise.all(updates) } } export const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { const pg = getKysely() - const r = await getRethink() await pg .with('NewMeetingUpdateEnd', (qb) => qb @@ -121,14 +102,6 @@ export const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { .set({cancelledAt: sql`CURRENT_TIMESTAMP`}) .where('id', '=', meetingSeries.id) .execute() - await r - .table('NewMeeting') - .getAll(meetingSeries.id, {index: 'meetingSeriesId'}) - .filter({endedAt: null}, {default: true}) - .update({ - scheduledEndTime: null - }) - .run() } const updateGCalRecurrenceRule = (oldRule: RRuleSet, newRule: RRuleSet | null | undefined) => { @@ -175,7 +148,7 @@ const updateRecurrenceSettings: MutationResolvers['updateRecurrenceSettings'] = await stopMeetingSeries(meetingSeries) analytics.recurrenceStopped(viewer, meetingSeries) } else { - await updateMeetingSeries(meetingSeries, rrule) + await updateMeetingSeries(meetingSeries, rrule, dataLoader) analytics.recurrenceStarted(viewer, meetingSeries) } if (gcalSeriesId) { diff --git a/packages/server/graphql/public/types/ActionMeeting.ts b/packages/server/graphql/public/types/ActionMeeting.ts index f6a7e04939f..4e2928042e7 100644 --- a/packages/server/graphql/public/types/ActionMeeting.ts +++ b/packages/server/graphql/public/types/ActionMeeting.ts @@ -29,7 +29,7 @@ const ActionMeeting: ActionMeetingResolvers = { }, tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) return filterTasksByMeeting(teamTasks, meetingId, viewerId) diff --git a/packages/server/graphql/public/types/ActionMeetingMember.ts b/packages/server/graphql/public/types/ActionMeetingMember.ts index 625df304179..036dbf7b2b9 100644 --- a/packages/server/graphql/public/types/ActionMeetingMember.ts +++ b/packages/server/graphql/public/types/ActionMeetingMember.ts @@ -16,7 +16,7 @@ const ActionMeetingMember: ActionMeetingMemberResolvers = { }, tasks: async ({meetingId, userId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) return teamTasks.filter((task) => { diff --git a/packages/server/graphql/public/types/AddAgendaItemPayload.ts b/packages/server/graphql/public/types/AddAgendaItemPayload.ts index f82f7966890..335b81d13bc 100644 --- a/packages/server/graphql/public/types/AddAgendaItemPayload.ts +++ b/packages/server/graphql/public/types/AddAgendaItemPayload.ts @@ -16,7 +16,7 @@ const AddAgendaItemPayload: AddAgendaItemPayloadResolvers = { meeting: (source, _args, {dataLoader}) => { return 'meetingId' in source && source.meetingId - ? dataLoader.get('newMeetings').load(source.meetingId) + ? dataLoader.get('newMeetings').loadNonNull(source.meetingId) : null } } diff --git a/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts index 44839c75df1..5316798aae3 100644 --- a/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts +++ b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts @@ -6,7 +6,7 @@ export type AddTranscriptionBotSuccessSource = { const AddTranscriptionBotSuccess: AddTranscriptionBotSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'retrospective') throw new Error('Meeting type is not retrospective') return meeting diff --git a/packages/server/graphql/public/types/AutogroupSuccess.ts b/packages/server/graphql/public/types/AutogroupSuccess.ts index 1c86eb2efce..194b64c0bf4 100644 --- a/packages/server/graphql/public/types/AutogroupSuccess.ts +++ b/packages/server/graphql/public/types/AutogroupSuccess.ts @@ -6,7 +6,7 @@ export type AutogroupSuccessSource = { const AutogroupSuccess: AutogroupSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') return meeting } diff --git a/packages/server/graphql/public/types/Company.ts b/packages/server/graphql/public/types/Company.ts index d04004afed0..2ef5d342342 100644 --- a/packages/server/graphql/public/types/Company.ts +++ b/packages/server/graphql/public/types/Company.ts @@ -1,6 +1,5 @@ -import getRethink from '../../../database/rethinkDriver' -import {RDatum, RValue} from '../../../database/stricterR' import AuthToken from '../../../database/types/AuthToken' +import getKysely from '../../../postgres/getKysely' import {TeamMember} from '../../../postgres/types' import {getUserId} from '../../../utils/authorization' import errorFilter from '../../errorFilter' @@ -120,89 +119,77 @@ const Company: CompanyResolvers = { return orgsIdsWithSufficientTeamMembers.length }, lastMetAt: async ({id: domain}, _args, {authToken, dataLoader}) => { - const r = await getRethink() + const pg = getKysely() const organizations = await getSuggestedTierOrganizations(domain, authToken, dataLoader) const orgIds = organizations.map(({id}) => id) const teams = (await dataLoader.get('teamsByOrgIds').loadMany(orgIds)).filter(isValid).flat() const teamIds = teams.map(({id}) => id) - if (teamIds.length === 0) return 0 - const lastMetAt = await r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .max('createdAt' as any)('createdAt') - .default(null) - .run() - return lastMetAt + if (teamIds.length === 0) return null + const lastMetAt = await pg + .selectFrom('NewMeeting') + .select('createdAt') + .where('teamId', 'in', teamIds) + .orderBy('createdAt desc') + .limit(1) + .executeTakeFirst() + return lastMetAt?.createdAt ?? null }, meetingCount: async ({id: domain}, {after}, {authToken, dataLoader}) => { // number of meetings created by teams on organizations assigned to the domain - const r = await getRethink() + const pg = getKysely() const organizations = await getSuggestedTierOrganizations(domain, authToken, dataLoader) const orgIds = organizations.map(({id}) => id) const teams = (await dataLoader.get('teamsByOrgIds').loadMany(orgIds)).filter(isValid).flat() const teamIds = teams.map(({id}) => id) if (teamIds.length === 0) return 0 - const filterFn = after ? (meeting: any) => meeting('createdAt').ge(after) : () => true - return r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter(filterFn) - .count() - .default(0) - .run() + const res = await pg + .selectFrom('NewMeeting') + .select(({fn}) => fn.count('id').as('count')) + .where('teamId', 'in', teamIds) + .$if(!!after, (qb) => qb.where('createdAt', '>=', after!)) + .executeTakeFirstOrThrow() + return res.count ? Number(res.count) : 0 }, monthlyTeamStreakMax: async ({id: domain}, _args, {authToken, dataLoader}) => { - const r = await getRethink() const organizations = await getSuggestedTierOrganizations(domain, authToken, dataLoader) const orgIds = organizations.map(({id}) => id) const teams = (await dataLoader.get('teamsByOrgIds').loadMany(orgIds)).filter(isValid).flat() const teamIds = teams.map(({id}) => id) if (teamIds.length === 0) return 0 - return ( - r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RDatum) => row('endedAt').default(null).ne(null)) - // number of months since unix epoch - .merge((row: RValue) => ({ - epochMonth: row('endedAt').month().add(row('endedAt').year().mul(12)) - })) - .group((row) => [row('teamId'), row('epochMonth')]) - .count() - .ungroup() - .map((row) => ({ - teamId: row('group')(0), - epochMonth: row('group')(1) - })) - .group('teamId')('epochMonth') - .ungroup() - .map((row) => ({ - teamId: row('group'), - epochMonth: row('reduction'), - // epochMonth shifted 1 index position - shift: row('reduction') - .deleteAt(0) - .map((z) => z.add(-1)) - })) - .merge((row: RValue) => ({ - // 1 if there are 2 consecutive epochMonths next to each other, else 0 - teamStreak: r - .map(row('shift'), row('epochMonth'), (shift, epochMonth) => - r.branch(shift.eq(epochMonth), '1', '0') - ) - .reduce((left, right) => left.add(right).default('')) - .default('') - // get an array of all the groupings of 1 - .split('0') - .map((val) => val.count()) - .max() - .add(1) - })) - .max('teamStreak')('teamStreak') - .run() - ) + const completedMeetingsRes = await dataLoader.get('completedMeetingsByTeamId').loadMany(teamIds) + const endTimes = completedMeetingsRes + .filter(isValid) + .flat() + .map(({endedAt}) => endedAt!) + .sort((a, b) => (a.getTime() < b.getTime() ? -1 : 1)) + + let longestStreak = 1 + let currentStreak = 1 + + // Step 2: Traverse the sorted array and count streaks of consecutive months + for (let i = 1; i < endTimes.length; i++) { + const prevDate = endTimes[i - 1]! + const currDate = endTimes[i]! + + // Calculate year and month differences + const yearDiff = currDate.getFullYear() - prevDate.getFullYear() + const monthDiff = currDate.getMonth() - prevDate.getMonth() + + // Step 3: Check if dates are consecutive months + if ((yearDiff === 0 && monthDiff === 1) || (yearDiff === 1 && monthDiff === -11)) { + currentStreak++ + } else { + // Reset streak if not consecutive + longestStreak = Math.max(longestStreak, currentStreak) + currentStreak = 1 + } + } + + // Step 4: Ensure the last streak is accounted for + longestStreak = Math.max(longestStreak, currentStreak) + return longestStreak }, organizations: async ({id: domain}, _args, {authToken, dataLoader}) => { diff --git a/packages/server/graphql/public/types/Discussion.ts b/packages/server/graphql/public/types/Discussion.ts index bf3f5908851..029cc285455 100644 --- a/packages/server/graphql/public/types/Discussion.ts +++ b/packages/server/graphql/public/types/Discussion.ts @@ -17,7 +17,7 @@ const Discussion: DiscussionResolvers = { }, stage: async ({discussionTopicId, discussionTopicType, meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {phases, teamId} = meeting switch (discussionTopicType) { case 'agendaItem': { diff --git a/packages/server/graphql/public/types/EndTeamPromptSuccess.ts b/packages/server/graphql/public/types/EndTeamPromptSuccess.ts index 2452403289d..afc3b6905f3 100644 --- a/packages/server/graphql/public/types/EndTeamPromptSuccess.ts +++ b/packages/server/graphql/public/types/EndTeamPromptSuccess.ts @@ -8,7 +8,7 @@ export type EndTeamPromptSuccessSource = { const EndTeamPromptSuccess: EndTeamPromptSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') throw new Error('Meeting is not a team prompt') return meeting }, diff --git a/packages/server/graphql/public/types/EstimateStage.ts b/packages/server/graphql/public/types/EstimateStage.ts index 9e949ac0de7..dbb12ab652d 100644 --- a/packages/server/graphql/public/types/EstimateStage.ts +++ b/packages/server/graphql/public/types/EstimateStage.ts @@ -21,7 +21,7 @@ const EstimateStage: EstimateStageResolvers = { if (!integration) return NULL_FIELD const {service} = integration const getDimensionName = async (meetingId: string) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'poker') throw new Error('Meeting is not a poker meeting') const {templateRefId} = meeting const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) @@ -175,7 +175,7 @@ const EstimateStage: EstimateStageResolvers = { }, dimensionRef: async ({meetingId, dimensionRefIdx}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'poker') return null const {templateRefId} = meeting const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) @@ -191,7 +191,7 @@ const EstimateStage: EstimateStageResolvers = { finalScore: async ({taskId, meetingId, dimensionRefIdx}, _args, {dataLoader}) => { const [meeting, estimates] = await Promise.all([ - dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('newMeetings').loadNonNull(meetingId), dataLoader.get('meetingTaskEstimates').load({taskId, meetingId}) ]) if (meeting.meetingType !== 'poker') return null diff --git a/packages/server/graphql/public/types/GenerateGroupsSuccess.ts b/packages/server/graphql/public/types/GenerateGroupsSuccess.ts index a15db891039..20e5d9ef97e 100644 --- a/packages/server/graphql/public/types/GenerateGroupsSuccess.ts +++ b/packages/server/graphql/public/types/GenerateGroupsSuccess.ts @@ -6,7 +6,7 @@ export type GenerateGroupsSuccessSource = { const GenerateGroupsSuccess: GenerateGroupsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'retrospective') throw new Error('Meeting type is not retrospective') return meeting diff --git a/packages/server/graphql/public/types/MeetingSeries.ts b/packages/server/graphql/public/types/MeetingSeries.ts index 5812ed6e566..73f410ed4fd 100644 --- a/packages/server/graphql/public/types/MeetingSeries.ts +++ b/packages/server/graphql/public/types/MeetingSeries.ts @@ -1,5 +1,6 @@ import MeetingSeriesId from 'parabol-client/shared/gqlIds/MeetingSeriesId' -import getRethink from '../../../database/rethinkDriver' +import {selectNewMeetings} from '../../../postgres/select' +import {AnyMeeting} from '../../../postgres/types/Meeting' import {MeetingSeriesResolvers} from '../resolverTypes' const MeetingSeries: MeetingSeriesResolvers = { @@ -10,15 +11,13 @@ const MeetingSeries: MeetingSeriesResolvers = { return dataLoader.get('activeMeetingsByMeetingSeriesId').load(meetingSeries.id) }, mostRecentMeeting: async ({id: meetingSeriesId}, _args, _context) => { - const r = await getRethink() - const meetings = await r - .table('NewMeeting') - .getAll(meetingSeriesId, {index: 'meetingSeriesId'}) - // Sort order: active meetings first, then sorted by when created. - .orderBy((doc) => (doc('endedAt') ? 0 : 1), r.desc('createdAt')) + const meeting = await selectNewMeetings() + .where('meetingSeriesId', '=', meetingSeriesId) + .orderBy(['endedAt desc', 'createdAt desc']) .limit(1) - .run() - return meetings[0]! + .$narrowType() + .executeTakeFirstOrThrow() + return meeting } } diff --git a/packages/server/graphql/public/types/NewMeetingStage.ts b/packages/server/graphql/public/types/NewMeetingStage.ts index f94256304a7..9a32aba3862 100644 --- a/packages/server/graphql/public/types/NewMeetingStage.ts +++ b/packages/server/graphql/public/types/NewMeetingStage.ts @@ -10,10 +10,11 @@ export interface NewMeetingStageSource extends GenericMeetingStage { } const NewMeetingStage: NewMeetingStageResolvers = { - meeting: ({meetingId}, _args, {dataLoader}) => dataLoader.get('newMeetings').load(meetingId), + meeting: ({meetingId}, _args, {dataLoader}) => + dataLoader.get('newMeetings').loadNonNull(meetingId), phase: async ({meetingId, phaseType, teamId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {phases} = meeting const phase = phases.find((phase) => phase.phaseType === phaseType)! return {...phase, meetingId, teamId} @@ -27,7 +28,7 @@ const NewMeetingStage: NewMeetingStageResolvers = { readyCount: async ({meetingId, readyToAdvance}, _args, {dataLoader}, ref) => { if (!readyToAdvance) return 0 if (!meetingId) Logger.log('no meetingid', ref) - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {facilitatorUserId} = meeting return readyToAdvance.filter((userId: string) => userId !== facilitatorUserId).length }, diff --git a/packages/server/graphql/public/types/NotificationMeetingStageTimeLimitEnd.ts b/packages/server/graphql/public/types/NotificationMeetingStageTimeLimitEnd.ts index cb903179e48..4a5b5f46b07 100644 --- a/packages/server/graphql/public/types/NotificationMeetingStageTimeLimitEnd.ts +++ b/packages/server/graphql/public/types/NotificationMeetingStageTimeLimitEnd.ts @@ -3,7 +3,7 @@ import {NotificationMeetingStageTimeLimitEndResolvers} from '../resolverTypes' const NotificationMeetingStageTimeLimitEnd: NotificationMeetingStageTimeLimitEndResolvers = { __isTypeOf: ({type}) => type === 'MEETING_STAGE_TIME_LIMIT_END', meeting: async ({meetingId}, _args, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) + return dataLoader.get('newMeetings').loadNonNull(meetingId) } } diff --git a/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts b/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts index d87b5bfefda..376f0a1482d 100644 --- a/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts +++ b/packages/server/graphql/public/types/NotifyDiscussionMentioned.ts @@ -3,7 +3,7 @@ import {NotifyDiscussionMentionedResolvers} from '../resolverTypes' const NotifyDiscussionMentioned: NotifyDiscussionMentionedResolvers = { __isTypeOf: ({type}) => type === 'DISCUSSION_MENTIONED', meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) return meeting }, author: async ({authorId, commentId}, _args: unknown, {dataLoader}) => { diff --git a/packages/server/graphql/public/types/NotifyResponseMentioned.ts b/packages/server/graphql/public/types/NotifyResponseMentioned.ts index a9bc5d31501..06a3a6cd992 100644 --- a/packages/server/graphql/public/types/NotifyResponseMentioned.ts +++ b/packages/server/graphql/public/types/NotifyResponseMentioned.ts @@ -4,7 +4,7 @@ import {NotifyResponseMentionedResolvers} from '../resolverTypes' const NotifyResponseMentioned: NotifyResponseMentionedResolvers = { __isTypeOf: ({type}) => type === 'RESPONSE_MENTIONED', meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') throw new Error('Meeting is not a team prompt') return meeting }, diff --git a/packages/server/graphql/public/types/NotifyResponseReplied.ts b/packages/server/graphql/public/types/NotifyResponseReplied.ts index 87ff6af7198..4a09395067f 100644 --- a/packages/server/graphql/public/types/NotifyResponseReplied.ts +++ b/packages/server/graphql/public/types/NotifyResponseReplied.ts @@ -4,7 +4,7 @@ import {NotifyResponseRepliedResolvers} from '../resolverTypes' const NotifyResponseReplied: NotifyResponseRepliedResolvers = { __isTypeOf: ({type}) => type === 'RESPONSE_REPLIED', meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') throw new Error('Meeting is not a team prompt') return meeting }, diff --git a/packages/server/graphql/public/types/ReflectPhase.ts b/packages/server/graphql/public/types/ReflectPhase.ts index e11232d3cc6..7f0e93a7f96 100644 --- a/packages/server/graphql/public/types/ReflectPhase.ts +++ b/packages/server/graphql/public/types/ReflectPhase.ts @@ -8,8 +8,8 @@ const ReflectPhase: ReflectPhaseResolvers = { }, reflectPrompts: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) - if (!('templateId' in meeting)) return [] + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) + if (meeting.meetingType !== 'retrospective') return [] const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(meeting.templateId) // only show prompts that were created before the meeting and // either have not been removed or they were removed after the meeting was created diff --git a/packages/server/graphql/public/types/RemoveAgendaItemPayload.ts b/packages/server/graphql/public/types/RemoveAgendaItemPayload.ts index 3527f938e25..6807092f4f3 100644 --- a/packages/server/graphql/public/types/RemoveAgendaItemPayload.ts +++ b/packages/server/graphql/public/types/RemoveAgendaItemPayload.ts @@ -16,7 +16,7 @@ const RemoveAgendaItemPayload: RemoveAgendaItemPayloadResolvers = { meeting: (source, _args, {dataLoader}) => { return 'meetingId' in source && source.meetingId - ? dataLoader.get('newMeetings').load(source.meetingId) + ? dataLoader.get('newMeetings').loadNonNull(source.meetingId) : null } } diff --git a/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts b/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts index be7d98e9b8c..8429501acf6 100644 --- a/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts +++ b/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts @@ -6,7 +6,7 @@ export type ResetReflectionGroupsSuccessSource = { const ResetReflectionGroupsSuccess: ResetReflectionGroupsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') return meeting } diff --git a/packages/server/graphql/public/types/RetroDiscussStage.ts b/packages/server/graphql/public/types/RetroDiscussStage.ts index 3d4aa611c20..f0cbf747b4e 100644 --- a/packages/server/graphql/public/types/RetroDiscussStage.ts +++ b/packages/server/graphql/public/types/RetroDiscussStage.ts @@ -26,9 +26,9 @@ const RetroDiscussStage: RetroDiscussStageResolvers = { reflectionGroup: async ({reflectionGroupId, meetingId}, _args, {dataLoader}) => { if (!reflectionGroupId) { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (!('templateId' in meeting)) throw new Error('Meeting has no template') - const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(meeting.templateId) + const prompts = await dataLoader.get('reflectPromptsByTemplateId').load(meeting.templateId!) return new ReflectionGroup({ id: `${meetingId}:dummyGroup`, meetingId, diff --git a/packages/server/graphql/public/types/RetroReflection.ts b/packages/server/graphql/public/types/RetroReflection.ts index 1443aba8d35..a1697b86259 100644 --- a/packages/server/graphql/public/types/RetroReflection.ts +++ b/packages/server/graphql/public/types/RetroReflection.ts @@ -4,7 +4,7 @@ import {RetroReflectionResolvers} from '../resolverTypes' const RetroReflection: RetroReflectionResolvers = { creatorId: async ({creatorId, meetingId}, _args, {authToken, dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {meetingType} = meeting if (!isSuperUser(authToken) && (meetingType !== 'retrospective' || !meeting.disableAnonymity)) { return null @@ -13,7 +13,7 @@ const RetroReflection: RetroReflectionResolvers = { }, creator: async ({creatorId, meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {meetingType} = meeting // let's not allow super users to grap this in case the UI does not check `disableAnonymity` in which case @@ -33,7 +33,7 @@ const RetroReflection: RetroReflectionResolvers = { }, meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') return meeting }, @@ -52,7 +52,7 @@ const RetroReflection: RetroReflectionResolvers = { }, team: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) return dataLoader.get('teams').loadNonNull(meeting.teamId) } } diff --git a/packages/server/graphql/public/types/RetroReflectionGroup.ts b/packages/server/graphql/public/types/RetroReflectionGroup.ts index c5435b3a00e..8a262b491d5 100644 --- a/packages/server/graphql/public/types/RetroReflectionGroup.ts +++ b/packages/server/graphql/public/types/RetroReflectionGroup.ts @@ -7,7 +7,7 @@ export interface RetroReflectionGroupSource extends Selectable { - const retroMeeting = await dataLoader.get('newMeetings').load(meetingId) + const retroMeeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (retroMeeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') return retroMeeting }, @@ -24,7 +24,7 @@ const RetroReflectionGroup: RetroReflectionGroupResolvers = { return filteredReflections }, team: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) return dataLoader.get('teams').loadNonNull(meeting.teamId) }, titleIsUserDefined: ({title, smartTitle}) => { diff --git a/packages/server/graphql/public/types/RetrospectiveMeeting.ts b/packages/server/graphql/public/types/RetrospectiveMeeting.ts index 1501e3805e7..6b1fd9e1fd5 100644 --- a/packages/server/graphql/public/types/RetrospectiveMeeting.ts +++ b/packages/server/graphql/public/types/RetrospectiveMeeting.ts @@ -30,7 +30,7 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { reflectionGroups.sort((a, b) => (a.voterIds.length < b.voterIds.length ? 1 : -1)) return reflectionGroups } else if (sortBy === 'stageOrder') { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {phases} = meeting const discussPhase = getPhase(phases, 'discuss') if (!discussPhase) return reflectionGroups @@ -52,7 +52,7 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { taskCount: ({taskCount}) => taskCount || 0, tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) return filterTasksByMeeting(teamTasks, meetingId, viewerId) diff --git a/packages/server/graphql/public/types/RetrospectiveMeetingMember.ts b/packages/server/graphql/public/types/RetrospectiveMeetingMember.ts index 06f8acb57c0..0d3c0b433df 100644 --- a/packages/server/graphql/public/types/RetrospectiveMeetingMember.ts +++ b/packages/server/graphql/public/types/RetrospectiveMeetingMember.ts @@ -4,7 +4,7 @@ import {RetrospectiveMeetingMemberResolvers} from '../resolverTypes' const RetrospectiveMeetingMember: RetrospectiveMeetingMemberResolvers = { __isTypeOf: ({meetingType}) => meetingType === 'retrospective', tasks: async ({meetingId, userId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) return teamTasks.filter((task) => { diff --git a/packages/server/graphql/public/types/ShareTopicSuccess.ts b/packages/server/graphql/public/types/ShareTopicSuccess.ts index 65d59fb5d12..2824308938e 100644 --- a/packages/server/graphql/public/types/ShareTopicSuccess.ts +++ b/packages/server/graphql/public/types/ShareTopicSuccess.ts @@ -6,7 +6,7 @@ export type ShareTopicSuccessSource = { const ShareTopicSuccess: ShareTopicSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) + return dataLoader.get('newMeetings').loadNonNull(meetingId) } } diff --git a/packages/server/graphql/public/types/StartCheckInSuccess.ts b/packages/server/graphql/public/types/StartCheckInSuccess.ts index 7b44a4004ce..fe82e136fcc 100644 --- a/packages/server/graphql/public/types/StartCheckInSuccess.ts +++ b/packages/server/graphql/public/types/StartCheckInSuccess.ts @@ -7,7 +7,7 @@ export type StartCheckInSuccessSource = { const StartCheckInSuccess: StartCheckInSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'action') throw new Error('Not a check-in meeting') return meeting }, diff --git a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts index 261edb42fec..bce6cc25d44 100644 --- a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts +++ b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts @@ -8,7 +8,7 @@ export type StartRetrospectiveSuccessSource = { const StartRetrospectiveSuccess: StartRetrospectiveSuccessResolvers = { meeting: async ({meetingId}, _args: unknown, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') return meeting }, diff --git a/packages/server/graphql/public/types/StartTeamPromptSuccess.ts b/packages/server/graphql/public/types/StartTeamPromptSuccess.ts index 017c7b63180..dcc2b532758 100644 --- a/packages/server/graphql/public/types/StartTeamPromptSuccess.ts +++ b/packages/server/graphql/public/types/StartTeamPromptSuccess.ts @@ -7,7 +7,7 @@ export type StartTeamPromptSuccessSource = { const StartTeamPromptSuccess: StartTeamPromptSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') throw new Error('Not a team prompt meeting') return meeting }, diff --git a/packages/server/graphql/public/types/TeamPromptMeeting.ts b/packages/server/graphql/public/types/TeamPromptMeeting.ts index 870dd09843b..d4c013869a8 100644 --- a/packages/server/graphql/public/types/TeamPromptMeeting.ts +++ b/packages/server/graphql/public/types/TeamPromptMeeting.ts @@ -1,6 +1,6 @@ import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' +import {selectNewMeetings} from '../../../postgres/select' import {TeamPromptMeeting as TeamPromptMeetingSource} from '../../../postgres/types/Meeting' import {getUserId} from '../../../utils/authorization' import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' @@ -18,17 +18,15 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { return null } - const r = await getRethink() - const meetings = await r - .table('NewMeeting') - .getAll(meetingSeriesId, {index: 'meetingSeriesId'}) - .filter({meetingType: 'teamPrompt'}) - .filter((row: RValue) => row('createdAt').lt(createdAt)) - .orderBy(r.desc('createdAt')) + const meeting = await selectNewMeetings() + .where('meetingSeriesId', '=', meetingSeriesId) + .where('meetingType', '=', 'teamPrompt') + .where('createdAt', '<', createdAt) + .orderBy('createdAt desc') .limit(1) - .run() - - return meetings[0] as TeamPromptMeetingSource + .$narrowType() + .executeTakeFirst() + return meeting || null }, nextMeeting: async ({meetingSeriesId, createdAt}, _args, {dataLoader}) => { if (!meetingSeriesId) return null @@ -37,22 +35,19 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { if (!series || series.cancelledAt) { return null } - - const r = await getRethink() - const meetings = await r - .table('NewMeeting') - .getAll(meetingSeriesId, {index: 'meetingSeriesId'}) - .filter({meetingType: 'teamPrompt'}) - .filter((doc: RValue) => doc('createdAt').gt(createdAt)) - .orderBy(r.asc('createdAt')) + const meeting = await selectNewMeetings() + .where('meetingSeriesId', '=', meetingSeriesId) + .where('meetingType', '=', 'teamPrompt') + .where('createdAt', '>', createdAt) + .orderBy('createdAt asc') .limit(1) - .run() - - return meetings[0] as TeamPromptMeetingSource + .$narrowType() + .executeTakeFirst() + return meeting || null }, tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting const teamTasks = await dataLoader.get('tasksByTeamId').load(teamId) return filterTasksByMeeting(teamTasks, meetingId, viewerId) @@ -73,7 +68,7 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { }, taskCount: async ({id: meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') { return 0 } @@ -91,7 +86,7 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { }, commentCount: async ({id: meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') { return 0 } diff --git a/packages/server/graphql/public/types/TimelineEventTeamPromptComplete.ts b/packages/server/graphql/public/types/TimelineEventTeamPromptComplete.ts index 75505524099..bea3273bc71 100644 --- a/packages/server/graphql/public/types/TimelineEventTeamPromptComplete.ts +++ b/packages/server/graphql/public/types/TimelineEventTeamPromptComplete.ts @@ -11,7 +11,7 @@ const TimelineEventTeamPromptComplete: TimelineEventTeamPromptCompleteResolvers ...timelineEventInterfaceResolvers(), __isTypeOf: ({type}) => type === 'TEAM_PROMPT_COMPLETE', meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') throw new Error('Invalid meetingId') return meeting }, diff --git a/packages/server/graphql/public/types/UpdateAgendaItemPayload.ts b/packages/server/graphql/public/types/UpdateAgendaItemPayload.ts index f267c8be980..129eb1b4b28 100644 --- a/packages/server/graphql/public/types/UpdateAgendaItemPayload.ts +++ b/packages/server/graphql/public/types/UpdateAgendaItemPayload.ts @@ -16,7 +16,7 @@ const UpdateAgendaItemPayload: UpdateAgendaItemPayloadResolvers = { meeting: (source, _args, {dataLoader}) => { return 'meetingId' in source && source.meetingId - ? dataLoader.get('newMeetings').load(source.meetingId) + ? dataLoader.get('newMeetings').loadNonNull(source.meetingId) : null } } diff --git a/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts b/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts index a6e28d189a1..dc6ac18e1b9 100644 --- a/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts +++ b/packages/server/graphql/public/types/UpdateDimensionFieldSuccess.ts @@ -8,7 +8,7 @@ export type UpdateDimensionFieldSuccessSource = { const UpdateDimensionFieldSuccess: UpdateDimensionFieldSuccessResolvers = { team: ({teamId}, _args, {dataLoader}) => dataLoader.get('teams').loadNonNull(teamId), meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'poker') throw new Error('Not a poker meeting') return meeting } diff --git a/packages/server/graphql/public/types/UpdateGitLabDimensionFieldSuccess.ts b/packages/server/graphql/public/types/UpdateGitLabDimensionFieldSuccess.ts index 38b2dd60b61..b2fd41603da 100644 --- a/packages/server/graphql/public/types/UpdateGitLabDimensionFieldSuccess.ts +++ b/packages/server/graphql/public/types/UpdateGitLabDimensionFieldSuccess.ts @@ -10,7 +10,7 @@ const UpdateGitLabDimensionFieldSuccess: UpdateGitLabDimensionFieldSuccessResolv return await dataLoader.get('teams').loadNonNull(teamId) }, meeting: async ({meetingId}, _args, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) + return dataLoader.get('newMeetings').loadNonNull(meetingId) } } diff --git a/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts b/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts index 0873d892c7d..7fd58d5c1c7 100644 --- a/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts +++ b/packages/server/graphql/public/types/UpdateMeetingPromptSuccess.ts @@ -7,7 +7,7 @@ export type UpdateMeetingPromptSuccessSource = { const UpdateMeetingPromptSuccess: UpdateMeetingPromptSuccessResolvers = { meeting: async (source, _args, {dataLoader}) => { const {meetingId} = source - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') throw new Error('Not a team prompt meeting') return meeting } diff --git a/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts b/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts index 33a52658893..6105f1fc975 100644 --- a/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts +++ b/packages/server/graphql/public/types/UpdateMeetingTemplateSuccess.ts @@ -6,7 +6,7 @@ export type UpdateMeetingTemplateSuccessSource = { const UpdateMeetingTemplateSuccess: UpdateMeetingTemplateSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) return meeting } } diff --git a/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts b/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts index d9e4f566501..bc17c47df51 100644 --- a/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts +++ b/packages/server/graphql/public/types/UpdateRecurrenceSettingsSuccess.ts @@ -6,7 +6,7 @@ export type UpdateRecurrenceSettingsSuccessSource = { const UpdateRecurrenceSettingsSuccess: UpdateRecurrenceSettingsSuccessResolvers = { meeting: async ({meetingId}, _args, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') throw new Error('Not a team prompt') return meeting } diff --git a/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts b/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts index d4a3042d4ee..7169e237801 100644 --- a/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts +++ b/packages/server/graphql/public/types/UpsertTeamPromptResponseSuccess.ts @@ -12,7 +12,7 @@ const UpsertTeamPromptResponseSuccess: UpsertTeamPromptResponseSuccessResolvers }, meeting: async (source, _args, {dataLoader}) => { const {meetingId} = source - return dataLoader.get('newMeetings').load(meetingId) + return dataLoader.get('newMeetings').loadNonNull(meetingId) } } diff --git a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts index 0108e303221..8f5c4e4f405 100644 --- a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts +++ b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts @@ -1,6 +1,6 @@ import {Threshold} from '~/types/constEnums' import getRethink from '../../../../database/rethinkDriver' -import {RDatum, RValue} from '../../../../database/stricterR' +import getKysely from '../../../../postgres/getKysely' // Uncomment for easier testing //import { ThresholdTest as Threshold } from "~/types/constEnums"; @@ -10,50 +10,48 @@ import {RDatum, RValue} from '../../../../database/stricterR' // TODO: store all calculations in the database, e.g. meeting.attendeeCount (see #7975) const getActiveTeamCountByTeamIds = async (teamIds: string[]) => { const r = await getRethink() - - return r - .table('NewMeeting') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RDatum) => row('endedAt').default(null).ne(null))('id') - .coerceTo('array') - .distinct() - .do((endedMeetingIds: RValue) => { - return ( - r - .table('MeetingMember') - .getAll(r.args(endedMeetingIds), {index: 'meetingId'}) - .group('teamId', 'meetingId') as RDatum - ) - .count() - .ungroup() - .map((row: RDatum) => ({ - teamId: row('group')(0), - meetingId: row('group')(1), - meetingMembers: row('reduction') - })) - .filter((row: RDatum) => - row('meetingMembers').ge(Threshold.MIN_STICKY_TEAM_MEETING_ATTENDEES) - ) - .group('teamId') - .ungroup() - .filter((row: RDatum) => row('reduction').count().ge(Threshold.MIN_STICKY_TEAM_MEETINGS)) - .filter((row: RValue) => { - const meetingIds = row('reduction')('meetingId') - return r - .table('NewMeeting') - .getAll(r.args(meetingIds)) - .filter((meeting: RValue) => { - return meeting('endedAt').during( - r.now().sub(Threshold.STICKY_TEAM_LAST_MEETING_TIMEFRAME), - r.now() - ) - }) - .count() - .gt(0) - }) - .count() - }) + const pg = getKysely() + const meetingIdsByTeamId = await pg + .selectFrom('NewMeeting') + .select(({fn}) => ['teamId', fn('array_agg', ['id']).as('meetingIds')]) + .where('teamId', 'in', teamIds) + .groupBy('teamId') + .execute() + const meetingIds = meetingIdsByTeamId.flatMap((row) => row.meetingIds) + const meetingMembers = await r + .table('MeetingMember') + .getAll(r.args(meetingIds), {index: 'meetingId'}) .run() + const teamsIdsWithMinMeetingsAndMembers = meetingIdsByTeamId + .map(({teamId, meetingIds}) => ({ + teamId, + meetingIds: meetingIds.filter((meetingId) => { + const memberCount = meetingMembers.filter( + (meetingMember) => meetingMember.meetingId === meetingId + ).length + return memberCount >= Threshold.MIN_STICKY_TEAM_MEETING_ATTENDEES + }) + })) + .filter((row) => row.meetingIds.length > Threshold.MIN_STICKY_TEAM_MEETINGS) + .map((row) => row.teamId) + const recentMeetings = await pg + .selectFrom('NewMeeting') + .distinctOn('teamId') + .select(['teamId', 'createdAt']) + .where('teamId', 'in', teamsIdsWithMinMeetingsAndMembers) + .orderBy(['teamId', 'createdAt desc']) + .execute() + + return teamsIdsWithMinMeetingsAndMembers.filter((teamId) => { + const recentMeetingsForTeam = recentMeetings.find( + (recentMeeting) => recentMeeting.teamId === teamId + ) + if (!recentMeetingsForTeam) return false + return ( + recentMeetingsForTeam.createdAt.getTime() > + Date.now() - Threshold.STICKY_TEAM_LAST_MEETING_TIMEFRAME + ) + }).length } export default getActiveTeamCountByTeamIds diff --git a/packages/server/graphql/resolvers.ts b/packages/server/graphql/resolvers.ts index 09c762ab07a..a038a5f7c95 100644 --- a/packages/server/graphql/resolvers.ts +++ b/packages/server/graphql/resolvers.ts @@ -158,7 +158,7 @@ export const resolveUnlockedStages = async ( {dataLoader}: GQLContext ) => { if (!unlockedStageIds || unlockedStageIds.length === 0 || !meetingId) return undefined - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) return unlockedStageIds.map((stageId) => resolveGQLStageFromId(stageId, meeting)) } diff --git a/packages/server/graphql/resolvers/resolveStage.ts b/packages/server/graphql/resolvers/resolveStage.ts index 51c6b39a2a2..87f75e05be5 100644 --- a/packages/server/graphql/resolvers/resolveStage.ts +++ b/packages/server/graphql/resolvers/resolveStage.ts @@ -11,7 +11,7 @@ const resolveStage = _args: unknown, {dataLoader}: GQLContext ) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {phases, teamId} = meeting const phase = phases.find((phase: GenericMeetingPhase) => phase.phaseType === phaseType)! const {stages} = phase diff --git a/packages/server/graphql/types/DragEstimatingTaskPayload.ts b/packages/server/graphql/types/DragEstimatingTaskPayload.ts index c09556af20a..4f021386647 100644 --- a/packages/server/graphql/types/DragEstimatingTaskPayload.ts +++ b/packages/server/graphql/types/DragEstimatingTaskPayload.ts @@ -25,7 +25,7 @@ export const DragEstimatingTaskSuccess = new GraphQLObjectType( stages: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(EstimateStage))), resolve: async ({meetingId, stageIds}, _args: unknown, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {phases, teamId} = meeting const phase = getPhase(phases, 'ESTIMATE') const {stages} = phase diff --git a/packages/server/graphql/types/FlagReadyToAdvancePayload.ts b/packages/server/graphql/types/FlagReadyToAdvancePayload.ts index fbb129308e6..91fb22256f6 100644 --- a/packages/server/graphql/types/FlagReadyToAdvancePayload.ts +++ b/packages/server/graphql/types/FlagReadyToAdvancePayload.ts @@ -19,7 +19,7 @@ export const FlagReadyToAdvanceSuccess = new GraphQLObjectType( type: new GraphQLNonNull(NewMeetingStage), description: 'the stage with the updated readyCount', resolve: async ({meetingId, stageId}, _args: unknown, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) return resolveGQLStageFromId(stageId, meeting) } } diff --git a/packages/server/graphql/types/NavigateMeetingPayload.ts b/packages/server/graphql/types/NavigateMeetingPayload.ts index 3c0f06f8c46..1ee2d44c6bb 100644 --- a/packages/server/graphql/types/NavigateMeetingPayload.ts +++ b/packages/server/graphql/types/NavigateMeetingPayload.ts @@ -22,7 +22,7 @@ const NavigateMeetingPayload = new GraphQLObjectType({ description: 'The stage that the facilitator is now on', resolve: async ({meetingId, facilitatorStageId}, _args: unknown, {dataLoader}) => { if (!meetingId) return null - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const stageRes = findStageById(meeting.phases, facilitatorStageId) return stageRes && stageRes.stage } @@ -32,7 +32,7 @@ const NavigateMeetingPayload = new GraphQLObjectType({ description: 'The stage that the facilitator left', resolve: async ({meetingId, oldFacilitatorStageId}, _args: unknown, {dataLoader}) => { if (!meetingId) return null - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const stageRes = findStageById(meeting.phases, oldFacilitatorStageId) return stageRes && stageRes.stage } diff --git a/packages/server/graphql/types/PromoteNewMeetingFacilitatorPayload.ts b/packages/server/graphql/types/PromoteNewMeetingFacilitatorPayload.ts index 2b14fa129d7..c416dc9de0a 100644 --- a/packages/server/graphql/types/PromoteNewMeetingFacilitatorPayload.ts +++ b/packages/server/graphql/types/PromoteNewMeetingFacilitatorPayload.ts @@ -20,7 +20,7 @@ const PromoteNewMeetingFacilitatorPayload = new GraphQLObjectType { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {facilitatorStageId} = meeting return resolveGQLStageFromId(facilitatorStageId, meeting) } diff --git a/packages/server/graphql/types/SetStageTimerPayload.ts b/packages/server/graphql/types/SetStageTimerPayload.ts index f5d2e386d5a..f144391ac21 100644 --- a/packages/server/graphql/types/SetStageTimerPayload.ts +++ b/packages/server/graphql/types/SetStageTimerPayload.ts @@ -15,7 +15,7 @@ const SetStageTimerPayload = new GraphQLObjectType({ description: 'The updated stage', resolve: async ({meetingId, stageId}, _args: unknown, {dataLoader}) => { if (!meetingId || !stageId) return null - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const stageRes = findStageById(meeting.phases, stageId) return stageRes!.stage } diff --git a/packages/server/graphql/types/UpdatePokerScopePayload.ts b/packages/server/graphql/types/UpdatePokerScopePayload.ts index 9c438469d94..03d74b841a5 100644 --- a/packages/server/graphql/types/UpdatePokerScopePayload.ts +++ b/packages/server/graphql/types/UpdatePokerScopePayload.ts @@ -13,14 +13,14 @@ export const UpdatePokerScopeSuccess = new GraphQLObjectType({ type: new GraphQLNonNull(PokerMeeting), description: 'The meeting with the updated estimate phases', resolve: ({meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('newMeetings').load(meetingId) + return dataLoader.get('newMeetings').loadNonNull(meetingId) } }, newStages: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(EstimateStage))), description: 'The estimate stages added to the meeting', resolve: async ({meetingId, newStageIds}, _args: unknown, {dataLoader}) => { - const meeting = await dataLoader.get('newMeetings').load(meetingId) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {phases, teamId} = meeting const phase = getPhase(phases, 'ESTIMATE') const {stages} = phase diff --git a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts index ecaa2a94cf3..87318cabb7a 100644 --- a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts +++ b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts @@ -77,7 +77,7 @@ export async function up() { ); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_createdAt" ON "NewMeeting"("createdAt"); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_facilitatorUserId" ON "NewMeeting"("facilitatorUserId"); - CREATE INDEX IF NOT EXISTS "idx_NewMeeting_scheduledEndTime" ON "NewMeeting"("scheduledEndTime") WHERE "scheduledEndTime" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_scheduledEndTime" ON "NewMeeting"("scheduledEndTime") WHERE "scheduledEndTime" IS NOT NULL AND "endedAt" IS NULL; CREATE INDEX IF NOT EXISTS "idx_NewMeeting_meetingSeriesId" ON "NewMeeting"("meetingSeriesId") WHERE "meetingSeriesId" IS NOT NULL; CREATE INDEX IF NOT EXISTS "idx_NewMeeting_teamId" ON "NewMeeting"("teamId"); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_templateId" ON "NewMeeting"("templateId") WHERE "templateId" IS NOT NULL; diff --git a/packages/server/postgres/queries/src/incrementUserPayLaterClickCountQuery.sql b/packages/server/postgres/queries/src/incrementUserPayLaterClickCountQuery.sql deleted file mode 100644 index e4f89358676..00000000000 --- a/packages/server/postgres/queries/src/incrementUserPayLaterClickCountQuery.sql +++ /dev/null @@ -1,7 +0,0 @@ -/* - @name incrementUserPayLaterClickCountQuery -*/ - -UPDATE "User" SET - "payLaterClickCount" = "payLaterClickCount" + 1 -WHERE id = :id; From 703b109145632343d5d47ab6a22404c994ce20f8 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 27 Sep 2024 10:23:26 -0700 Subject: [PATCH 47/89] fix endTime index Signed-off-by: Matt Krick --- .../postgres/migrations/1726174453131_NewMeeting-phase1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts index ecaa2a94cf3..87318cabb7a 100644 --- a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts +++ b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts @@ -77,7 +77,7 @@ export async function up() { ); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_createdAt" ON "NewMeeting"("createdAt"); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_facilitatorUserId" ON "NewMeeting"("facilitatorUserId"); - CREATE INDEX IF NOT EXISTS "idx_NewMeeting_scheduledEndTime" ON "NewMeeting"("scheduledEndTime") WHERE "scheduledEndTime" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_NewMeeting_scheduledEndTime" ON "NewMeeting"("scheduledEndTime") WHERE "scheduledEndTime" IS NOT NULL AND "endedAt" IS NULL; CREATE INDEX IF NOT EXISTS "idx_NewMeeting_meetingSeriesId" ON "NewMeeting"("meetingSeriesId") WHERE "meetingSeriesId" IS NOT NULL; CREATE INDEX IF NOT EXISTS "idx_NewMeeting_teamId" ON "NewMeeting"("teamId"); CREATE INDEX IF NOT EXISTS "idx_NewMeeting_templateId" ON "NewMeeting"("templateId") WHERE "templateId" IS NOT NULL; From 8c12687db95dd80482d38b9dfdb669da1a383bfd Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 27 Sep 2024 10:35:35 -0700 Subject: [PATCH 48/89] fix tsc Signed-off-by: Matt Krick --- .../server/graphql/public/mutations/updateRecurrenceSettings.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts index 2c65125dede..2692015636f 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -108,6 +108,7 @@ const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRu export const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { const pg = getKysely() + const r = await getRethink() await pg .with('NewMeetingUpdateEnd', (qb) => qb From a2c582e2a9b16906ae601636ffb41b2b7e6ca2c0 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 27 Sep 2024 12:01:41 -0700 Subject: [PATCH 49/89] fix nullability Signed-off-by: Matt Krick --- packages/embedder/indexing/retrospectiveDiscussionTopic.ts | 2 +- packages/embedder/workflows/relatedDiscussionsStart.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts index c962d3bcbdf..b695d77c049 100644 --- a/packages/embedder/indexing/retrospectiveDiscussionTopic.ts +++ b/packages/embedder/indexing/retrospectiveDiscussionTopic.ts @@ -68,7 +68,7 @@ export const createTextFromRetrospectiveDiscussionTopic = async ( if (!discussion) throw new Error(`Discussion not found: ${discussionId}`) const {discussionTopicId: reflectionGroupId, meetingId, summary: discussionSummary} = discussion const [newMeeting, reflectionGroup, reflections] = await Promise.all([ - dataLoader.get('newMeetings').load(meetingId), + dataLoader.get('newMeetings').loadNonNull(meetingId), dataLoader.get('retroReflectionGroups').load(reflectionGroupId), dataLoader.get('retroReflectionsByGroupId').load(reflectionGroupId) ]) diff --git a/packages/embedder/workflows/relatedDiscussionsStart.ts b/packages/embedder/workflows/relatedDiscussionsStart.ts index a9881f2999c..aa280a01326 100644 --- a/packages/embedder/workflows/relatedDiscussionsStart.ts +++ b/packages/embedder/workflows/relatedDiscussionsStart.ts @@ -15,7 +15,7 @@ export const relatedDiscussionsStart: JobQueueStepRun< const {meetingId} = data const [discussions, meeting] = await Promise.all([ dataLoader.get('discussionsByMeetingId').load(meetingId), - dataLoader.get('newMeetings').load(meetingId) + dataLoader.get('newMeetings').loadNonNull(meetingId) ]) const {phases} = meeting const discussPhase = getPhase(phases, 'discuss') From f2d5f151f1449dca29ab5129dc1eeba5cfe6b742 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 27 Sep 2024 12:39:48 -0700 Subject: [PATCH 50/89] disable check_meeting_overlap trigger for tests Signed-off-by: Matt Krick --- packages/server/__tests__/globalSetup.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/server/__tests__/globalSetup.ts b/packages/server/__tests__/globalSetup.ts index 555249a35c2..46ee00b3191 100644 --- a/packages/server/__tests__/globalSetup.ts +++ b/packages/server/__tests__/globalSetup.ts @@ -5,7 +5,11 @@ import getKysely from '../postgres/getKysely' async function setup() { // The IP address is always localhost // so the safety checks will eventually fail if run too much - await sql`TRUNCATE TABLE "PasswordResetRequest"`.execute(getKysely()) + const pg = getKysely() + await sql` + TRUNCATE TABLE "PasswordResetRequest"; + ALTER TABLE "NewMeeting" DISABLE TRIGGER "check_meeting_overlap"; + `.execute(pg) } export default setup From 882cb0bc240edd17eb09e5bbed218c64eb559aba Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 27 Sep 2024 14:59:00 -0700 Subject: [PATCH 51/89] handle empty arrays Signed-off-by: Matt Krick --- .../server/dataloader/customRedisQueries.ts | 1 + .../mutations/helpers/hideConversionModal.ts | 12 ++-- .../mutations/helpers/removeTeamMember.ts | 62 +++++++++---------- .../private/mutations/hardDeleteUser.ts | 8 +-- .../helpers/getActiveTeamCountByTeamIds.ts | 1 + 5 files changed, 43 insertions(+), 41 deletions(-) diff --git a/packages/server/dataloader/customRedisQueries.ts b/packages/server/dataloader/customRedisQueries.ts index f14fbcc86c7..ba6123abfef 100644 --- a/packages/server/dataloader/customRedisQueries.ts +++ b/packages/server/dataloader/customRedisQueries.ts @@ -8,6 +8,7 @@ import getKysely from '../postgres/getKysely' // All results must be mapped to their ids! const customRedisQueries = { endTimesByTemplateId: async (templateIds: string[]) => { + if (!templateIds.length) return [] const pg = getKysely() const aQuarterAgo = new Date(Date.now() - ms('90d')) const meetings = await pg diff --git a/packages/server/graphql/mutations/helpers/hideConversionModal.ts b/packages/server/graphql/mutations/helpers/hideConversionModal.ts index 9cf061adb8b..fd01e6ab4cc 100644 --- a/packages/server/graphql/mutations/helpers/hideConversionModal.ts +++ b/packages/server/graphql/mutations/helpers/hideConversionModal.ts @@ -24,11 +24,13 @@ const hideConversionModal = async (orgId: string, dataLoader: DataLoaderWorker) meeting.showConversionModal = false }) const meetingIds = activeMeetings.map(({id}) => id) - await pg - .updateTable('NewMeeting') - .set({showConversionModal: false}) - .where('id', 'in', meetingIds) - .execute() + if (meetingIds.length > 0) { + await pg + .updateTable('NewMeeting') + .set({showConversionModal: false}) + .where('id', 'in', meetingIds) + .execute() + } return activeMeetings } } diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 815ba184870..82d6bd0bc8f 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -140,39 +140,39 @@ const removeTeamMember = async ( .run() // Reassign facilitator for meetings this user is facilitating. + if (meetingIds) { + const facilitatingMeetings = await pg + .selectFrom('NewMeeting') + .select('id') + .where('id', 'in', meetingIds) + .where('facilitatorUserId', '=', userId) + .execute() - const facilitatingMeetings = await pg - .selectFrom('NewMeeting') - .select('id') - .where('id', 'in', meetingIds) - .where('facilitatorUserId', '=', userId) - .execute() - - const newMeetingFacilitators = ( - await dataLoader - .get('meetingMembersByMeetingId') - .loadMany(facilitatingMeetings.map((meeting) => meeting.id)) - ) - .filter(errorFilter) - .map((members) => members[0]) - .filter((member) => !!member) - - Promise.allSettled( - newMeetingFacilitators.map(async (newFacilitator) => { - if (!newFacilitator) { - // This user is the only meeting member, so do nothing. - // :TODO: (jmtaber129): Consider closing meetings where this user is the only meeting - // member. - return - } - await pg - .updateTable('NewMeeting') - .set({facilitatorUserId: newFacilitator.userId}) - .where('id', '=', newFacilitator.meetingId) - .execute() - }) - ) + const newMeetingFacilitators = ( + await dataLoader + .get('meetingMembersByMeetingId') + .loadMany(facilitatingMeetings.map((meeting) => meeting.id)) + ) + .filter(errorFilter) + .map((members) => members[0]) + .filter((member) => !!member) + await Promise.allSettled( + newMeetingFacilitators.map(async (newFacilitator) => { + if (!newFacilitator) { + // This user is the only meeting member, so do nothing. + // :TODO: (jmtaber129): Consider closing meetings where this user is the only meeting + // member. + return + } + await pg + .updateTable('NewMeeting') + .set({facilitatorUserId: newFacilitator.userId}) + .where('id', '=', newFacilitator.meetingId) + .execute() + }) + ) + } return { user, notificationId, diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 24a4796137d..b4c4903891e 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -69,11 +69,9 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( const teamIds = teamMembers.map(({teamId}) => teamId) const meetingIds = meetingMembers.map(({meetingId}) => meetingId) - const discussions = await pg - .selectFrom('Discussion') - .select('id') - .where('id', 'in', teamIds) - .execute() + const discussions = teamIds.length + ? await pg.selectFrom('Discussion').select('id').where('id', 'in', teamIds).execute() + : [] const teamDiscussionIds = discussions.map(({id}) => id) // soft delete first for side effects diff --git a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts index 8f5c4e4f405..57c828deb6a 100644 --- a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts +++ b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts @@ -9,6 +9,7 @@ import getKysely from '../../../../postgres/getKysely' // Warning: the query is very expensive // TODO: store all calculations in the database, e.g. meeting.attendeeCount (see #7975) const getActiveTeamCountByTeamIds = async (teamIds: string[]) => { + if (!teamIds.length) return 0 const r = await getRethink() const pg = getKysely() const meetingIdsByTeamId = await pg From baec1cea92f226bc4794532ab43d143b040d19a5 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 27 Sep 2024 16:36:03 -0700 Subject: [PATCH 52/89] fix typo Signed-off-by: Matt Krick --- packages/server/graphql/mutations/helpers/removeTeamMember.ts | 2 +- packages/server/graphql/mutations/helpers/updateTeamInsights.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 82d6bd0bc8f..73a9f934dfd 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -140,7 +140,7 @@ const removeTeamMember = async ( .run() // Reassign facilitator for meetings this user is facilitating. - if (meetingIds) { + if (meetingIds.length > 0) { const facilitatingMeetings = await pg .selectFrom('NewMeeting') .select('id') diff --git a/packages/server/graphql/mutations/helpers/updateTeamInsights.ts b/packages/server/graphql/mutations/helpers/updateTeamInsights.ts index 15f8e2fe571..4f39249cf20 100644 --- a/packages/server/graphql/mutations/helpers/updateTeamInsights.ts +++ b/packages/server/graphql/mutations/helpers/updateTeamInsights.ts @@ -36,7 +36,7 @@ const updateTeamInsights = async (teamId: string, dataLoader: DataLoaderWorker) .selectFrom('NewMeeting') .select(({fn}) => ['templateId as group', fn.count('id').as('reduction')]) .where('teamId', '=', teamId) - .where('meetingType', '==', 'retrospective') + .where('meetingType', '=', 'retrospective') .where('createdAt', '>', topRetroTemplatesPeriod) .groupBy('templateId') .$narrowType<{group: NotNull}>() From e803ce7b42cfd5785f1c1f6fbd38cc8a9a20ee9c Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 27 Sep 2024 17:06:54 -0700 Subject: [PATCH 53/89] fix processRecurrence.test Signed-off-by: Matt Krick --- packages/server/__tests__/processRecurrence.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/__tests__/processRecurrence.test.ts b/packages/server/__tests__/processRecurrence.test.ts index adfe4632193..50b6476a534 100644 --- a/packages/server/__tests__/processRecurrence.test.ts +++ b/packages/server/__tests__/processRecurrence.test.ts @@ -358,7 +358,7 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` const lastMeeting = await pg .selectFrom('NewMeeting') .selectAll() - .where('meetingType', '=', 'teamPrompt') + .where('meetingType', '=', 'retrospective') .orderBy('createdAt desc') .limit(1) .executeTakeFirst() From c1668ae920a46e7410db99ba173bd321ffb3a788 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 1 Oct 2024 18:02:53 -0700 Subject: [PATCH 54/89] fix: bugs in self review Signed-off-by: Matt Krick --- .../mutations/generateMeetingSummary.ts | 4 +-- .../private/mutations/runOrgActivityReport.ts | 10 +++---- .../public/mutations/helpers/getSummaries.ts | 4 +-- .../public/mutations/helpers/getTopics.ts | 4 +-- .../helpers/getActiveTeamCountByTeamIds.ts | 1 + .../helpers/getPublicScoredTemplates.ts | 29 ------------------- 6 files changed, 12 insertions(+), 40 deletions(-) delete mode 100644 packages/server/graphql/queries/helpers/getPublicScoredTemplates.ts diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 20f4a843204..04ca683f4c6 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -13,7 +13,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn {dataLoader} ) => { const pg = getKysely() - const MIN_MILLISECONDS = 60 * 1000 // 1 minute + const MIN_SECONDS = 60 const MIN_REFLECTION_COUNT = 3 const endDate = new Date() @@ -26,7 +26,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn .where('createdAt', '>=', twoYearsAgo) .where('createdAt', '<=', endDate) .where('reflectionCount', '>', MIN_REFLECTION_COUNT) - .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_MILLISECONDS}`) + .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_SECONDS}`) .$narrowType() .execute() diff --git a/packages/server/graphql/private/mutations/runOrgActivityReport.ts b/packages/server/graphql/private/mutations/runOrgActivityReport.ts index 11c8b03bec0..9ca6636b446 100644 --- a/packages/server/graphql/private/mutations/runOrgActivityReport.ts +++ b/packages/server/graphql/private/mutations/runOrgActivityReport.ts @@ -73,20 +73,20 @@ const runOrgActivityReport: MutationResolvers['runOrgActivityReport'] = async ( participantCount: group('reduction') })) .run()) as {meetingId: string; participantCount: number}[] - // Combine PostgreSQL and RethinkDB results const combinedResults = signupCounts.map((pgRow) => { const epochMonthStart = pgRow.monthStart.getTime() const meetingCount = rawMeetingCounts.find( (rmc) => rmc.monthStart.getTime() === epochMonthStart ) - const participantCount = participantCounts.find((pc) => - meetingCount?.meetingIds.includes(pc.meetingId) - ) + const participantCount = participantCounts + .filter((pc) => meetingCount?.meetingIds.includes(pc.meetingId)) + .map((pc) => pc.participantCount) + .reduce((a, b) => a + b, 0) return { monthStart: pgRow.monthStart, signupCount: pgRow.signupCount ? Number(pgRow.signupCount) : 0, - participantCount: participantCount?.participantCount ?? 0, + participantCount, meetingCount: meetingCount?.meetingIds.length ?? 0 } }) diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 42bd5e99b79..c27e9f7a905 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -14,7 +14,7 @@ export const getSummaries = async ( prompt?: string | null ) => { const pg = getKysely() - const MIN_MILLISECONDS = 60 * 1000 // 1 minute + const MIN_SECONDS = 60 const MIN_REFLECTION_COUNT = 3 const rawMeetingsWithAnyMembers = await pg .selectFrom('NewMeeting') @@ -25,7 +25,7 @@ export const getSummaries = async ( .where('createdAt', '>=', startDate) .where('createdAt', '<=', endDate) .where('reflectionCount', '>=', MIN_REFLECTION_COUNT) - .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_MILLISECONDS}`) + .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_SECONDS}`) .$narrowType() .execute() const allMeetingMembers = await dataLoader diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index dd17904ae52..06bd515c2b4 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -112,7 +112,7 @@ export const getTopics = async ( ) => { const pg = getKysely() const MIN_REFLECTION_COUNT = 3 - const MIN_MILLISECONDS = 60 * 1000 // 1 minute + const MIN_SECONDS = 60 const rawMeetingsWithAnyMembers = await pg .selectFrom('NewMeeting') .select(['id', 'name', 'createdAt', 'disableAnonymity']) @@ -121,7 +121,7 @@ export const getTopics = async ( .where('createdAt', '>=', startDate) .where('createdAt', '<=', endDate) .where('reflectionCount', '>=', MIN_REFLECTION_COUNT) - .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_MILLISECONDS}`) + .where(sql`EXTRACT(EPOCH FROM ("endedAt" - "createdAt")) > ${MIN_SECONDS}`) .$narrowType() .execute() const allMeetingMembers = await dataLoader diff --git a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts index 57c828deb6a..72a04e06aa0 100644 --- a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts +++ b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts @@ -35,6 +35,7 @@ const getActiveTeamCountByTeamIds = async (teamIds: string[]) => { })) .filter((row) => row.meetingIds.length > Threshold.MIN_STICKY_TEAM_MEETINGS) .map((row) => row.teamId) + if (teamsIdsWithMinMeetingsAndMembers.length === 0) return 0 const recentMeetings = await pg .selectFrom('NewMeeting') .distinctOn('teamId') diff --git a/packages/server/graphql/queries/helpers/getPublicScoredTemplates.ts b/packages/server/graphql/queries/helpers/getPublicScoredTemplates.ts deleted file mode 100644 index 384fb023a48..00000000000 --- a/packages/server/graphql/queries/helpers/getPublicScoredTemplates.ts +++ /dev/null @@ -1,29 +0,0 @@ -import db from '../../../db' -import getTemplateScore from '../../../utils/getTemplateScore' - -const getPublicScoredTemplates = async ( - templates: {createdAt: Date; id: string; isStarter?: boolean; isFree?: boolean}[] -) => { - const sharedTemplateIds = templates.map(({id}) => id) - const sharedTemplateEndTimes = await db.readMany('endTimesByTemplateId', sharedTemplateIds) - const scoreByTemplateId = {} as {[templateId: string]: number} - templates.forEach((template, idx) => { - const {id: templateId, createdAt, isStarter, isFree} = template - const endTimes = sharedTemplateEndTimes[idx]! - const isFreeBonus = isFree ? 1000 : 0 - const starterBonus = isStarter ? 100 : 0 - const bonuses = isFreeBonus + starterBonus - const minUsagePenalty = sharedTemplateEndTimes.length < 10 && !bonuses - scoreByTemplateId[templateId] = minUsagePenalty - ? -1 - : getTemplateScore(createdAt, endTimes, 0.2) + bonuses - }) - // mutative, but doesn't matter if we change the sort oder - return templates - .filter((template) => scoreByTemplateId[template.id]! > 0) - .sort((a, b) => { - return scoreByTemplateId[a.id]! > scoreByTemplateId[b.id]! ? -1 : 1 - }) -} - -export default getPublicScoredTemplates From 2b7f4906d132c505d00eceb5be4ae85bbfd0a90f Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 2 Oct 2024 15:58:38 -0700 Subject: [PATCH 55/89] chore: write to PG Signed-off-by: Matt Krick --- .../server/database/types/MeetingMember.ts | 1 + .../server/dataloader/customLoaderMakers.ts | 2 - .../dataloader/foreignKeyLoaderMakers.ts | 20 +++- .../dataloader/primaryKeyLoaderMakers.ts | 8 +- .../mutations/helpers/removeTeamMember.ts | 18 ++-- .../mutations/helpers/safelyCastVote.ts | 67 +++++++++---- .../mutations/helpers/safelyWithdrawVote.ts | 5 + .../server/graphql/mutations/joinMeeting.ts | 95 ++++++++----------- .../resetRetroMeetingToGroupStage.ts | 6 ++ .../graphql/mutations/setPokerSpectate.ts | 6 ++ .../graphql/mutations/startSprintPoker.ts | 27 ++---- .../graphql/mutations/updateRetroMaxVotes.ts | 14 +++ .../mutations/voteForReflectionGroup.ts | 12 +-- .../private/mutations/processRecurrence.ts | 3 +- .../graphql/public/mutations/startCheckIn.ts | 31 +++--- .../public/mutations/startRetrospective.ts | 24 ++--- .../graphql/public/types/MeetingSeries.ts | 2 - .../1727893031268_MeetingMember-phase1.ts | 49 ++++++++++ packages/server/postgres/select.ts | 22 ++--- packages/server/postgres/types/Meeting.d.ts | 54 +++++------ 20 files changed, 282 insertions(+), 184 deletions(-) create mode 100644 packages/server/postgres/migrations/1727893031268_MeetingMember-phase1.ts diff --git a/packages/server/database/types/MeetingMember.ts b/packages/server/database/types/MeetingMember.ts index c7cc6e8c4a7..19b91972321 100644 --- a/packages/server/database/types/MeetingMember.ts +++ b/packages/server/database/types/MeetingMember.ts @@ -17,6 +17,7 @@ export default abstract class MeetingMember { teamId: string updatedAt = new Date() userId: string + constructor(input: MeetingMemberInput) { const {teamId, meetingType, id, updatedAt, meetingId, userId} = input this.id = id ?? toTeamMemberId(meetingId, userId) diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 4b91bcfd2f0..f3245725f0d 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -581,7 +581,6 @@ export const activeMeetingsByMeetingSeriesId = ( .where('meetingSeriesId', 'in', keys) .where('endedAt', 'is', null) .orderBy('createdAt') - .$narrowType() .execute() return normalizeArrayResults(keys, res, 'meetingSeriesId') }, @@ -604,7 +603,6 @@ export const lastMeetingByMeetingSeriesId = ( .where('meetingSeriesId', '=', key) .orderBy('createdAt desc') .limit(1) - .$narrowType() .executeTakeFirst() return latestMeeting || null }) diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 030f62bf990..be87e4620ea 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -3,6 +3,7 @@ import {getTeamPromptResponsesByMeetingIds} from '../postgres/queries/getTeamPro import { selectAgendaItems, selectComments, + selectMeetingMembers, selectNewMeetings, selectOrganizations, selectReflectPrompts, @@ -15,7 +16,6 @@ import { selectTemplateScale, selectTimelineEvent } from '../postgres/select' -import {AnyMeeting} from '../postgres/types/Meeting' import {foreignKeyLoaderMaker} from './foreignKeyLoaderMaker' export const teamsByOrgIds = foreignKeyLoaderMaker('teams', 'orgId', (orgIds) => @@ -238,7 +238,6 @@ export const activeMeetingsByTeamId = foreignKeyLoaderMaker( .where('teamId', 'in', teamIds) .where('endedAt', 'is', null) .orderBy('createdAt desc') - .$narrowType() .execute() } ) @@ -250,7 +249,22 @@ export const completedMeetingsByTeamId = foreignKeyLoaderMaker( .where('teamId', 'in', teamIds) .where('endedAt', 'is not', null) .orderBy('endedAt desc') - .$narrowType() .execute() } ) + +export const _pgmeetingMembersByMeetingId = foreignKeyLoaderMaker( + '_pgmeetingMembers', + 'meetingId', + async (meetingIds) => { + return selectMeetingMembers().where('meetingId', 'in', meetingIds).execute() + } +) + +export const _pgmeetingMembersByUserId = foreignKeyLoaderMaker( + '_pgmeetingMembers', + 'userId', + async (userIds) => { + return selectMeetingMembers().where('userId', 'in', userIds).execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index edb43a0935b..0d8eb405769 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -8,6 +8,7 @@ import {getUsersByIds} from '../postgres/queries/getUsersByIds' import { selectAgendaItems, selectComments, + selectMeetingMembers, selectMeetingSettings, selectNewMeetings, selectOrganizations, @@ -23,7 +24,6 @@ import { selectTemplateScaleRef, selectTimelineEvent } from '../postgres/select' -import {AnyMeeting} from '../postgres/types/Meeting' import {primaryKeyLoaderMaker} from './primaryKeyLoaderMaker' export const users = primaryKeyLoaderMaker(getUsersByIds) @@ -119,5 +119,9 @@ export const reflectPrompts = primaryKeyLoaderMaker((ids: readonly string[]) => }) export const newMeetings = primaryKeyLoaderMaker((ids: readonly string[]) => { - return selectNewMeetings().where('id', 'in', ids).$narrowType().execute() + return selectNewMeetings().where('id', 'in', ids).execute() +}) + +export const _pgmeetingMembers = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectMeetingMembers().where('id', 'in', ids).execute() }) diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 73a9f934dfd..49fd21ce16e 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -132,16 +132,22 @@ const removeTeamMember = async ( // TODO should probably just inactivate the meeting member const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const meetingIds = activeMeetings.map(({id}) => id) - await r - .table('MeetingMember') - .getAll(r.args(meetingIds), {index: 'meetingId'}) - .filter({userId}) - .delete() - .run() // Reassign facilitator for meetings this user is facilitating. if (meetingIds.length > 0) { + await r + .table('MeetingMember') + .getAll(r.args(meetingIds), {index: 'meetingId'}) + .filter({userId}) + .delete() + .run() const facilitatingMeetings = await pg + .with('DeleteMeetingMembers', (qb) => + qb + .deleteFrom('MeetingMember') + .where('userId', '=', userId) + .where('meetingId', 'in', meetingIds) + ) .selectFrom('NewMeeting') .select('id') .where('id', 'in', meetingIds) diff --git a/packages/server/graphql/mutations/helpers/safelyCastVote.ts b/packages/server/graphql/mutations/helpers/safelyCastVote.ts index d0e8e030ee9..edac81955fb 100644 --- a/packages/server/graphql/mutations/helpers/safelyCastVote.ts +++ b/packages/server/graphql/mutations/helpers/safelyCastVote.ts @@ -39,28 +39,53 @@ const safelyCastVote = async ( return standardError(new Error('No votes remaining'), {userId: viewerId}) } - const voteAddedResult = await pg - .updateTable('RetroReflectionGroup') - .set({voterIds: sql`ARRAY_APPEND("voterIds",${userId})`}) - .where('id', '=', reflectionGroupId) - .where( - sql`COALESCE(array_length(array_positions("voterIds", ${userId}),1),0)`, - '<', - maxVotesPerGroup - ) - .executeTakeFirst() + // in a transaction add to the reflection group and the meeting member + try { + await pg.transaction().execute(async (trx) => { + const res = await trx + .with('MeetingMemberUpdate', (qb) => + qb + .updateTable('MeetingMember') + .set((eb) => ({votesRemaining: eb('votesRemaining', '-', 1)})) + .where('id', '=', meetingMemberId) + .returning('id') + ) + .updateTable('RetroReflectionGroup') + .set({voterIds: sql`ARRAY_APPEND("voterIds",${userId})`}) + .where('id', '=', reflectionGroupId) + .where( + sql`COALESCE(array_length(array_positions("voterIds", ${userId}),1),0)`, + '<', + maxVotesPerGroup + ) + .returning((eb) => [ + 'id', + eb.selectFrom('MeetingMemberUpdate').select('id').as('meetingMemberUpdate') + ]) + .executeTakeFirst() - const isVoteAddedToGroup = voteAddedResult.numUpdatedRows === BigInt(1) - - if (!isVoteAddedToGroup) { - await r - .table('MeetingMember') - .get(meetingMemberId) - .update((member: RValue) => ({ - votesRemaining: member('votesRemaining').add(1) - })) - .run() - return standardError(new Error('Max votes per group exceeded'), {userId: viewerId}) + if (!res) { + await r + .table('MeetingMember') + .get(meetingMemberId) + .update((member: RValue) => ({ + votesRemaining: member('votesRemaining').add(1) + })) + .run() + throw new Error('Max votes per group exceeded') + } + if (!res.meetingMemberUpdate) { + // just for phase 2, make sure the row exists in the DB + const hasMember = await trx + .selectFrom('MeetingMember') + .select('id') + .where('id', '=', meetingMemberId) + .executeTakeFirst() + if (hasMember) throw new Error('No votes remaining') + } + }) + } catch (e) { + return {error: {message: (e as Error).message}} } return undefined } diff --git a/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts b/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts index b78559ee901..b91452779b3 100644 --- a/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts +++ b/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts @@ -41,6 +41,11 @@ const safelyWithdrawVote = async ( votesRemaining: member('votesRemaining').add(1) })) .run() + await pg + .updateTable('MeetingMember') + .set((eb) => ({votesRemaining: eb('votesRemaining', '+', 1)})) + .where('id', '=', meetingMemberId) + .execute() return undefined } diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index 7a209aafbae..ea25db42b14 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -1,12 +1,9 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' import getRethink from '../../database/rethinkDriver' -import ActionMeetingMember from '../../database/types/ActionMeetingMember' import CheckInStage from '../../database/types/CheckInStage' -import PokerMeetingMember from '../../database/types/PokerMeetingMember' -import RetroMeetingMember from '../../database/types/RetroMeetingMember' -import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember' import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStage' import UpdatesStage from '../../database/types/UpdatesStage' import getKysely from '../../postgres/getKysely' @@ -20,30 +17,21 @@ import publish from '../../utils/publish' import {GQLContext} from '../graphql' import JoinMeetingPayload from '../types/JoinMeetingPayload' -const createMeetingMember = (meeting: AnyMeeting, teamMember: TeamMember) => { +export const createMeetingMember = ( + meeting: AnyMeeting, + teamMember: Pick +) => { const {userId, teamId, isSpectatingPoker} = teamMember - switch (meeting.meetingType) { - case 'action': - return new ActionMeetingMember({teamId, userId, meetingId: meeting.id}) - case 'retrospective': - const {id: meetingId, totalVotes} = meeting - return new RetroMeetingMember({ - teamId, - userId, - meetingId, - votesRemaining: totalVotes - }) - case 'poker': - return new PokerMeetingMember({ - teamId, - userId, - meetingId: meeting.id, - isSpectating: isSpectatingPoker - }) - case 'teamPrompt': - return new TeamPromptMeetingMember({teamId, userId, meetingId: meeting.id}) - default: - throw new Error('Invalid meeting type') + const {id: meetingId, meetingType} = meeting + return { + id: MeetingMemberId.join(meetingId, userId), + updatedAt: new Date(), // can remove this in phase 3 + teamId, + userId, + meetingId, + meetingType, + isSpectating: meetingType === 'poker' ? isSpectatingPoker : null, + totalVotes: meetingType === 'retrospective' ? meeting.totalVotes : null } } @@ -60,6 +48,7 @@ const joinMeeting = { {meetingId}: {meetingId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { + const pg = getKysely() const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() @@ -87,38 +76,36 @@ const joinMeeting = { if (errors > 0) { return {error: {message: 'Already joined meeting'}} } - + await pg.insertInto('MeetingMember').values(meetingMember).execute() const addStageToPhase = async ( stage: CheckInStage | UpdatesStage | TeamPromptResponseStage, phaseType: NewMeetingPhase['phaseType'] ) => { - await getKysely() - .transaction() - .execute(async (trx) => { - const meeting = await trx - .selectFrom('NewMeeting') - .select(({fn}) => fn('to_json', ['phases']).as('phases')) - .where('id', '=', meetingId) - .forUpdate() - // NewMeeting: add OrThrow in phase 3 - .executeTakeFirst() - if (!meeting) return - const {phases} = meeting - const phase = getPhase(phases, phaseType) - const stages = phase.stages as NewMeetingStages[] - stages.push({ - ...stage, - isNavigable: true, - isNavigableByFacilitator: true, - // the stage is complete if all other stages are complete & there's at least 1 - isComplete: stages.length >= 1 && stages.every((stage) => stage.isComplete) - }) - await trx - .updateTable('NewMeeting') - .set({phases: JSON.stringify(phases)}) - .where('id', '=', meetingId) - .execute() + await pg.transaction().execute(async (trx) => { + const meeting = await trx + .selectFrom('NewMeeting') + .select(({fn}) => fn('to_json', ['phases']).as('phases')) + .where('id', '=', meetingId) + .forUpdate() + // NewMeeting: add OrThrow in phase 3 + .executeTakeFirst() + if (!meeting) return + const {phases} = meeting + const phase = getPhase(phases, phaseType) + const stages = phase.stages as NewMeetingStages[] + stages.push({ + ...stage, + isNavigable: true, + isNavigableByFacilitator: true, + // the stage is complete if all other stages are complete & there's at least 1 + isComplete: stages.length >= 1 && stages.every((stage) => stage.isComplete) }) + await trx + .updateTable('NewMeeting') + .set({phases: JSON.stringify(phases)}) + .where('id', '=', meetingId) + .execute() + }) } const appendToCheckin = async () => { diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index ec97b6837c7..fd89e91f137 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -114,6 +114,12 @@ const resetRetroMeetingToGroupStage = { .set({voterIds: [], discussionPromptQuestion: null}) .where('id', 'in', reflectionGroupIds) ) + .with('ResetMeetingMember', (qb) => + qb + .updateTable('MeetingMember') + .set({votesRemaining: meeting.totalVotes}) + .where('meetingId', '=', meetingId) + ) .updateTable('NewMeeting') .set({phases: JSON.stringify(newPhases)}) .where('id', '=', meetingId) diff --git a/packages/server/graphql/mutations/setPokerSpectate.ts b/packages/server/graphql/mutations/setPokerSpectate.ts index 61b7fbf9868..2d9afc9c348 100644 --- a/packages/server/graphql/mutations/setPokerSpectate.ts +++ b/packages/server/graphql/mutations/setPokerSpectate.ts @@ -62,6 +62,12 @@ const setPokerSpectate = { // RESOLUTION const teamMemberId = toTeamMemberId(teamId, viewerId) await pg + .with('MeetingMemberUpdate', (qb) => + qb + .updateTable('MeetingMember') + .set({isSpectating: isSpectating}) + .where('id', '=', meetingMemberId) + ) .updateTable('TeamMember') .set({isSpectatingPoker: isSpectating}) .where('id', '=', teamMemberId) diff --git a/packages/server/graphql/mutations/startSprintPoker.ts b/packages/server/graphql/mutations/startSprintPoker.ts index e1836d9b83f..bef930b4c60 100644 --- a/packages/server/graphql/mutations/startSprintPoker.ts +++ b/packages/server/graphql/mutations/startSprintPoker.ts @@ -3,11 +3,9 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' import getRethink from '../../database/rethinkDriver' import MeetingPoker from '../../database/types/MeetingPoker' -import PokerMeetingMember from '../../database/types/PokerMeetingMember' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import updateMeetingTemplateLastUsedAt from '../../postgres/queries/updateMeetingTemplateLastUsedAt' -import updateTeamByTeamId from '../../postgres/queries/updateTeamByTeamId' import {MeetingTypeEnum, PokerMeeting} from '../../postgres/types/Meeting' import {PokerMeetingPhase} from '../../postgres/types/NewMeetingPhase' import {analytics} from '../../utils/analytics/analytics' @@ -23,6 +21,7 @@ import createGcalEvent from './helpers/createGcalEvent' import createNewMeetingPhases from './helpers/createNewMeetingPhases' import isStartMeetingLocked from './helpers/isStartMeetingLocked' import {IntegrationNotifier} from './helpers/notifications/IntegrationNotifier' +import {createMeetingMember} from './joinMeeting' const freezeTemplateAsRef = async (templateId: string, dataLoader: DataLoaderWorker) => { const pg = getKysely() @@ -161,23 +160,15 @@ export default { const teamMemberId = toTeamMemberId(teamId, viewerId) const teamMember = await dataLoader.get('teamMembers').loadNonNull(teamMemberId) - const {isSpectatingPoker} = teamMember - const updates = { - lastMeetingType: meetingType - } + const meetingMember = createMeetingMember(meeting, teamMember) await Promise.all([ - r - .table('MeetingMember') - .insert( - new PokerMeetingMember({ - meetingId, - userId: viewerId, - teamId, - isSpectating: isSpectatingPoker - }) - ) - .run(), - updateTeamByTeamId(updates, teamId) + pg + .with('MeetingMemberInsert', (qb) => qb.insertInto('MeetingMember').values(meetingMember)) + .updateTable('Team') + .set({lastMeetingType: meetingType}) + .where('id', '=', teamId) + .execute(), + r.table('MeetingMember').insert(meetingMember).run() ]) IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting, template) diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index 47fcf5c673a..3b26dc8b010 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -37,6 +37,7 @@ const updateRetroMaxVotes = { }: {totalVotes: number; maxVotesPerGroup: number; meetingId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { + const pg = getKysely() const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() @@ -120,6 +121,19 @@ const updateRetroMaxVotes = { }) .run() + await pg + .with('HasNegativeVotes', (qb) => + qb + .selectFrom('MeetingMember') + .select(({fn}) => fn.count('id').as('count')) + .where('meetingId', '=', meetingId) + .where(({eb}) => eb(eb('votesRemaining', '+', delta), '<', 0)) + ) + .updateTable('MeetingMember') + .where('meetingId', '=', meetingId) + .where((eb) => eb(eb.selectFrom('HasNegativeVotes').select('count'), '=', 0)) + .execute() + if (hasError) { return {error: {message: 'Your team has already spent their votes'}} } diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index 304051a39e8..86eec620058 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -2,7 +2,7 @@ import {GraphQLBoolean, GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {VOTE} from 'parabol-client/utils/constants' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' -import getRethink from '../../database/rethinkDriver' +import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -28,7 +28,6 @@ export default { {isUnvote = false, reflectionGroupId}: {isUnvote: boolean; reflectionGroupId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -56,13 +55,8 @@ export default { } // VALIDATION - const meetingMember = await r - .table('MeetingMember') - .getAll(meetingId, {index: 'meetingId'}) - .filter({userId: viewerId}) - .nth(0) - .default(null) - .run() + const meetingMemberId = MeetingMemberId.join(meetingId, viewerId) + const meetingMember = await dataLoader.get('meetingMembers').load(meetingMemberId) if (!meetingMember) { return standardError(new Error('Meeting member not found'), {userId: viewerId}) } diff --git a/packages/server/graphql/private/mutations/processRecurrence.ts b/packages/server/graphql/private/mutations/processRecurrence.ts index 5d37cec09f0..737ab494653 100644 --- a/packages/server/graphql/private/mutations/processRecurrence.ts +++ b/packages/server/graphql/private/mutations/processRecurrence.ts @@ -9,7 +9,7 @@ import {fromDateTime, toDateTime} from '../../../../client/shared/rruleUtil' import getKysely from '../../../postgres/getKysely' import {getActiveMeetingSeries} from '../../../postgres/queries/getActiveMeetingSeries' import {selectNewMeetings} from '../../../postgres/select' -import {AnyMeeting, RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting' +import {RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting' import {MeetingSeries} from '../../../postgres/types/MeetingSeries' import {analytics} from '../../../utils/analytics/analytics' import {getNextRRuleDate} from '../../../utils/getNextRRuleDate' @@ -124,7 +124,6 @@ const processRecurrence: MutationResolvers['processRecurrence'] = async ( const meetingsToEnd = await selectNewMeetings() .where('scheduledEndTime', '<', sql`CURRENT_TIMESTAMP`) .where('endedAt', 'is', null) - .$narrowType() .execute() const res = await tracer.trace('processRecurrence.endMeetings', async () => diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index 94c19a319fc..ffe6ab4c87d 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -1,10 +1,8 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import ActionMeetingMember from '../../../database/types/ActionMeetingMember' import MeetingAction from '../../../database/types/MeetingAction' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' -import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' import {CheckInMeeting, MeetingTypeEnum} from '../../../postgres/types/Meeting' import {CheckInPhase} from '../../../postgres/types/NewMeetingPhase' import {analytics} from '../../../utils/analytics/analytics' @@ -15,6 +13,7 @@ import createGcalEvent from '../../mutations/helpers/createGcalEvent' import createNewMeetingPhases from '../../mutations/helpers/createNewMeetingPhases' import isStartMeetingLocked from '../../mutations/helpers/isStartMeetingLocked' import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier' +import {createMeetingMember} from '../../mutations/joinMeeting' import {MutationResolvers} from '../resolverTypes' const startCheckIn: MutationResolvers['startCheckIn'] = async ( @@ -72,22 +71,22 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( dataLoader.clearAll('newMeetings') const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) const agendaItemIds = agendaItems.map(({id}) => id) - - const updates = { - lastMeetingType: meetingType - } + const meetingMember = createMeetingMember(meeting, { + userId: viewerId, + teamId, + isSpectatingPoker: false + }) await Promise.all([ - r - .table('MeetingMember') - .insert(new ActionMeetingMember({meetingId, userId: viewerId, teamId})) - .run(), - updateTeamByTeamId(updates, teamId), + pg + .with('TeamUpdates', (qb) => + qb.updateTable('Team').set({lastMeetingType: meetingType}).where('id', '=', teamId) + ) + .insertInto('MeetingMember') + .values(meetingMember) + .execute(), + r.table('MeetingMember').insert(meetingMember).run(), agendaItemIds.length && - getKysely() - .updateTable('AgendaItem') - .set({meetingId}) - .where('id', 'in', agendaItemIds) - .execute() + pg.updateTable('AgendaItem').set({meetingId}).where('id', 'in', agendaItemIds).execute() ]) IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting) diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index 6f82f89d7ef..8ab00de43e2 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -1,9 +1,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import RetroMeetingMember from '../../../database/types/RetroMeetingMember' import getKysely from '../../../postgres/getKysely' import updateMeetingTemplateLastUsedAt from '../../../postgres/queries/updateMeetingTemplateLastUsedAt' -import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../../utils/authorization' @@ -14,6 +12,7 @@ import {createMeetingSeriesTitle} from '../../mutations/helpers/createMeetingSer import isStartMeetingLocked from '../../mutations/helpers/isStartMeetingLocked' import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier' import safeCreateRetrospective from '../../mutations/helpers/safeCreateRetrospective' +import {createMeetingMember} from '../../mutations/joinMeeting' import {MutationResolvers} from '../resolverTypes' import {startNewMeetingSeries} from './updateRecurrenceSettings' @@ -84,18 +83,21 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( return {error: {message: 'Meeting already started'}} } - const updates = { - lastMeetingType: meetingType - } + const meetingMember = createMeetingMember(meeting, { + userId: viewerId, + teamId, + isSpectatingPoker: false + }) const [meetingSeries] = await Promise.all([ rrule && startNewMeetingSeries(meeting, rrule, meetingSeriesName), - r - .table('MeetingMember') - .insert( - new RetroMeetingMember({meetingId, userId: viewerId, teamId, votesRemaining: totalVotes}) + pg + .with('TeamUpdates', (qb) => + qb.updateTable('Team').set({lastMeetingType: meetingType}).where('id', '=', teamId) ) - .run(), - updateTeamByTeamId(updates, teamId), + .insertInto('MeetingMember') + .values(meetingMember) + .execute(), + r.table('MeetingMember').insert(meetingMember).run(), videoMeetingURL && pg .updateTable('MeetingSettings') diff --git a/packages/server/graphql/public/types/MeetingSeries.ts b/packages/server/graphql/public/types/MeetingSeries.ts index 73f410ed4fd..b9a714b6c14 100644 --- a/packages/server/graphql/public/types/MeetingSeries.ts +++ b/packages/server/graphql/public/types/MeetingSeries.ts @@ -1,6 +1,5 @@ import MeetingSeriesId from 'parabol-client/shared/gqlIds/MeetingSeriesId' import {selectNewMeetings} from '../../../postgres/select' -import {AnyMeeting} from '../../../postgres/types/Meeting' import {MeetingSeriesResolvers} from '../resolverTypes' const MeetingSeries: MeetingSeriesResolvers = { @@ -15,7 +14,6 @@ const MeetingSeries: MeetingSeriesResolvers = { .where('meetingSeriesId', '=', meetingSeriesId) .orderBy(['endedAt desc', 'createdAt desc']) .limit(1) - .$narrowType() .executeTakeFirstOrThrow() return meeting } diff --git a/packages/server/postgres/migrations/1727893031268_MeetingMember-phase1.ts b/packages/server/postgres/migrations/1727893031268_MeetingMember-phase1.ts new file mode 100644 index 00000000000..d0c0eabfabb --- /dev/null +++ b/packages/server/postgres/migrations/1727893031268_MeetingMember-phase1.ts @@ -0,0 +1,49 @@ +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 "MeetingMember" ( + "id" VARCHAR(100) PRIMARY KEY, + "meetingType" "MeetingTypeEnum" NOT NULL, + "meetingId" VARCHAR(100) NOT NULL, + "teamId" VARCHAR(100) NOT NULL, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "userId" VARCHAR(100) NOT NULL, + "isSpectating" BOOLEAN, + "votesRemaining" SMALLINT, + CONSTRAINT "fk_meetingId" + FOREIGN KEY("meetingId") + REFERENCES "NewMeeting"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_userId" + FOREIGN KEY("userId") + REFERENCES "User"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_MeetingMember_meetingId" ON "MeetingMember"("meetingId"); + CREATE INDEX IF NOT EXISTS "idx_MeetingMember_teamId" ON "MeetingMember"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_MeetingMember_userId" ON "MeetingMember"("userId"); + DROP TRIGGER IF EXISTS "update_MeetingMember_updatedAt" ON "MeetingMember"; + CREATE TRIGGER "update_MeetingMember_updatedAt" BEFORE UPDATE ON "MeetingMember" FOR EACH ROW EXECUTE PROCEDURE "set_updatedAt"(); + END $$; +`) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "MeetingMember"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index d0eacbffb2e..11f3d9c9369 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -2,8 +2,8 @@ import type {JSONContent} from '@tiptap/core' import {NotNull, sql} from 'kysely' import {NewMeetingPhaseTypeEnum} from '../graphql/public/resolverTypes' import getKysely from './getKysely' -import {AutogroupReflectionGroupType, ReactjiDB, TranscriptBlock, UsedReactjis} from './types' -import type {NewMeetingPhase} from './types/NewMeetingPhase' +import {ReactjiDB} from './types' +import {AnyMeeting, AnyMeetingTeamMember} from './types/Meeting' export const selectTimelineEvent = () => { return getKysely().selectFrom('TimelineEvent').selectAll().$narrowType< | { @@ -270,13 +270,13 @@ export const selectNewMeetings = () => 'videoMeetingURL', 'templateRefId', 'meetingPrompt', - fn('to_json', ['phases']).as('phases'), - fn('to_json', ['usedReactjis']).as('usedReactjis'), - fn('to_json', ['transcription']).as('transcription'), - fn('to_json', ['autogroupReflectionGroups']).as( - 'autogroupReflectionGroups' - ), - fn('to_json', ['resetReflectionGroups']).as( - 'resetReflectionGroups' - ) + fn('to_json', ['phases']).as('phases'), + fn('to_json', ['usedReactjis']).as('usedReactjis'), + fn('to_json', ['transcription']).as('transcription'), + fn('to_json', ['autogroupReflectionGroups']).as('autogroupReflectionGroups'), + fn('to_json', ['resetReflectionGroups']).as('resetReflectionGroups') ]) + .$narrowType() + +export const selectMeetingMembers = () => + getKysely().selectFrom('MeetingMember').selectAll().$narrowType() diff --git a/packages/server/postgres/types/Meeting.d.ts b/packages/server/postgres/types/Meeting.d.ts index b4d38c02e0e..1900574b64d 100644 --- a/packages/server/postgres/types/Meeting.d.ts +++ b/packages/server/postgres/types/Meeting.d.ts @@ -1,12 +1,7 @@ +import {Selectable} from 'kysely' import {NonNullableProps} from '../../../client/types/generics' -import ActionMeetingMember from '../../database/types/ActionMeetingMember' -import PokerMeetingMember from '../../database/types/PokerMeetingMember' -import RetroMeetingMember from '../../database/types/RetroMeetingMember' -import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember' -import {NewMeeting as NewMeetingDB} from '../pg' -import {NewMeeting} from './index.d' - -import {Insertable} from 'kysely' +import {NewMeeting as NewMeetingPG} from '../pg.d' +import {AutogroupReflectionGroupType, UsedReactjis} from './index.d' import { CheckInMeetingPhase, NewMeetingPhase, @@ -15,6 +10,7 @@ import { TeamPromptPhase } from './NewMeetingPhase' +type NewMeeting = Selectable export type MeetingTypeEnum = NewMeeting['meetingType'] type BaseNewMeeting = Pick< @@ -38,19 +34,9 @@ type BaseNewMeeting = Pick< | 'scheduledEndTime' | 'summary' | 'sentimentScore' - | 'usedReactjis' | 'slackTs' | 'engagement' -> & {phases: NewMeetingPhase[]} - -type InsertableRetrospectiveMeeting = Insertable & { - meetingType: 'retrospective' - phases: RetroMeetingPhase[] - totalVotes: number - maxVotesPerGroup: number - disableAnonymity: boolean - templateId: string -} +> & {phases: NewMeetingPhase[]; usedReactjis: UsedReactjis | null} export type RetrospectiveMeeting = BaseNewMeeting & NonNullableProps< @@ -62,14 +48,14 @@ export type RetrospectiveMeeting = BaseNewMeeting & | 'taskCount' | 'topicCount' | 'reflectionCount' - | 'transcription' | 'recallBotId' | 'videoMeetingURL' - | 'autogroupReflectionGroups' - | 'resetReflectionGroups' > & { meetingType: 'retrospective' phases: RetroMeetingPhase[] + autogroupReflectionGroups: AutogroupReflectionGroupType[] | null + resetReflectionGroups: AutogroupReflectionGroupType[] | null + transcription: TranscriptBlock[] | null } export type PokerMeeting = BaseNewMeeting & @@ -93,8 +79,22 @@ export type TeamPromptMeeting = BaseNewMeeting & export type AnyMeeting = RetrospectiveMeeting | PokerMeeting | CheckInMeeting | TeamPromptMeeting -export type AnyMeetingTeamMember = - | PokerMeetingMember - | RetroMeetingMember - | ActionMeetingMember - | TeamPromptMeetingMember +export interface MeetingMember { + id: string + meetingType: MeetingTypeEnum + meetingId: string + teamId: string + updatedAt: Date + userId: string +} + +export interface RetroMeetingMember extends MeetingMember { + meetingType: 'retrospective' + votesRemaining: number +} + +export interface PokerMeetingMember extends MeetingMember { + meetingType: 'poker' + isSpectating: boolean +} +export type AnyMeetingTeamMember = PokerMeetingMember | RetroMeetingMember | MeetingMember From bc524d55ee1ed99d3e5d75529aae5afa44370c66 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 2 Oct 2024 16:41:52 -0700 Subject: [PATCH 56/89] remove MeetingMember db type Signed-off-by: Matt Krick --- codegen.json | 8 ++--- packages/server/database/rethinkDriver.ts | 4 +-- .../database/types/ActionMeetingMember.ts | 15 ---------- .../server/database/types/MeetingMember.ts | 30 ------------------- .../database/types/PokerMeetingMember.ts | 19 ------------ .../database/types/RetroMeetingMember.ts | 19 ------------ .../database/types/TeamPromptMeetingMember.ts | 17 ----------- .../server/dataloader/NullableDataLoader.ts | 4 +++ .../helpers/sendPokerMeetingRevoteEvent.ts | 5 ++-- .../graphql/mutations/pokerRevealVotes.ts | 4 +-- .../graphql/mutations/setPokerSpectate.ts | 11 ++++--- .../mutations/voteForReflectionGroup.ts | 1 + .../graphql/public/types/ActionMeeting.ts | 9 ++++-- .../public/types/RetrospectiveMeeting.ts | 20 +++++-------- .../server/graphql/public/types/TeamMember.ts | 2 +- packages/server/graphql/public/types/User.ts | 12 +++----- packages/server/postgres/select.ts | 4 +-- packages/server/postgres/types/Meeting.d.ts | 15 ++++++++-- packages/server/utils/analytics/analytics.ts | 18 ++++++----- packages/server/utils/analytics/helpers.ts | 5 ++-- 20 files changed, 66 insertions(+), 156 deletions(-) delete mode 100644 packages/server/database/types/ActionMeetingMember.ts delete mode 100644 packages/server/database/types/MeetingMember.ts delete mode 100644 packages/server/database/types/PokerMeetingMember.ts delete mode 100644 packages/server/database/types/RetroMeetingMember.ts delete mode 100644 packages/server/database/types/TeamPromptMeetingMember.ts diff --git a/codegen.json b/codegen.json index 14b74d7dc35..3efd65664eb 100644 --- a/codegen.json +++ b/codegen.json @@ -74,7 +74,7 @@ "AcceptRequestToJoinDomainSuccess": "./types/AcceptRequestToJoinDomainSuccess#AcceptRequestToJoinDomainSuccessSource", "AcceptTeamInvitationPayload": "./types/AcceptTeamInvitationPayload#AcceptTeamInvitationPayloadSource", "ActionMeeting": "../../postgres/types/Meeting#CheckInMeeting", - "ActionMeetingMember": "../../database/types/ActionMeetingMember#default as ActionMeetingMemberDB", + "ActionMeetingMember": "../../postgres/types/Meeting.d#ActionMeetingMember as ActionMeetingMemberDB", "AddApprovedOrganizationDomainsSuccess": "./types/AddApprovedOrganizationDomainsSuccess#AddApprovedOrganizationDomainsSuccessSource", "AddPokerTemplateSuccess": "./types/AddPokerTemplateSuccess#AddPokerTemplateSuccessSource", "AddReactjiToReactableSuccess": "./types/AddReactjiToReactableSuccess#AddReactjiToReactableSuccessSource", @@ -144,7 +144,7 @@ "OrgIntegrationProviders": "./types/OrgIntegrationProviders#OrgIntegrationProvidersSource", "OrganizationUser": "../../postgres/types/index#OrganizationUser as OrganizationUserDB", "PokerMeeting": "../../postgres/types/Meeting#PokerMeeting", - "PokerMeetingMember": "../../database/types/MeetingPokerMeetingMember#default as PokerMeetingMemberDB", + "PokerMeetingMember": "../../postgres/types/Meeting.d#PokerMeetingMember as PokerMeetingMemberDB", "PokerTemplate": "../../database/types/PokerTemplate#default as PokerTemplateDB", "RRule": "rrule-rust#RRuleSet", "Reactable": "../../database/types/Reactable#Reactable", @@ -160,7 +160,7 @@ "RetroReflection": "../../postgres/types/index#RetroReflection as RetroReflectionDB", "RetroReflectionGroup": "./types/RetroReflectionGroup#RetroReflectionGroupSource", "RetrospectiveMeeting": "../../postgres/types/Meeting#RetrospectiveMeeting", - "RetrospectiveMeetingMember": "../../database/types/RetroMeetingMember#default", + "RetrospectiveMeetingMember": "../../postgres/types/Meeting.d#RetroMeetingMember as RetroMeetingMemberDB", "SAML": "./types/SAML#SAMLSource", "SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource", "SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource", @@ -183,7 +183,7 @@ "TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrations": "./types/TeamMemberIntegrations#TeamMemberIntegrationsSource", "TeamPromptMeeting": "../../postgres/types/Meeting#TeamPromptMeeting", - "TeamPromptMeetingMember": "../../database/types/TeamPromptMeetingMember#default as TeamPromptMeetingMemberDB", + "TeamPromptMeetingMember": "../../postgres/types/Meeting.d#TeamPromptMeetingMember as TeamPromptMeetingMemberDB", "TeamPromptResponse": "../../postgres/types/index#TeamPromptResponse as TeamPromptResponseDB", "TemplateDimension": "../../postgres/types/index#TemplateDimension as TemplateDimensionDB", "TimelineEventTeamPromptComplete": "./types/TimelineEventTeamPromptComplete#TimelineEventTeamPromptCompleteSource", diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index e3db1bfb74e..f1049f80727 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -1,6 +1,6 @@ import {MasterPool, r} from 'rethinkdb-ts' import TeamInvitation from '../database/types/TeamInvitation' -import {AnyMeetingTeamMember} from '../postgres/types/Meeting' +import {AnyMeetingMember} from '../postgres/types/Meeting' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' import MassInvitation from './types/MassInvitation' @@ -22,7 +22,7 @@ export type RethinkSchema = { index: 'teamMemberId' } MeetingMember: { - type: AnyMeetingTeamMember + type: AnyMeetingMember index: 'meetingId' | 'teamId' | 'userId' } NewFeature: { diff --git a/packages/server/database/types/ActionMeetingMember.ts b/packages/server/database/types/ActionMeetingMember.ts deleted file mode 100644 index 5c6e80b1270..00000000000 --- a/packages/server/database/types/ActionMeetingMember.ts +++ /dev/null @@ -1,15 +0,0 @@ -import MeetingMember from './MeetingMember' - -interface Input { - id?: string - updatedAt?: Date - teamId: string - userId: string - meetingId: string -} - -export default class ActionMeetingMember extends MeetingMember { - constructor(input: Input) { - super({...input, meetingType: 'action'}) - } -} diff --git a/packages/server/database/types/MeetingMember.ts b/packages/server/database/types/MeetingMember.ts deleted file mode 100644 index 19b91972321..00000000000 --- a/packages/server/database/types/MeetingMember.ts +++ /dev/null @@ -1,30 +0,0 @@ -import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import {MeetingTypeEnum} from '../../graphql/public/resolverTypes' - -interface MeetingMemberInput { - id?: string - updatedAt?: Date - teamId: string - userId: string - meetingType: MeetingTypeEnum - meetingId: string -} - -export default abstract class MeetingMember { - id: string - meetingType: MeetingTypeEnum - meetingId: string - teamId: string - updatedAt = new Date() - userId: string - - constructor(input: MeetingMemberInput) { - const {teamId, meetingType, id, updatedAt, meetingId, userId} = input - this.id = id ?? toTeamMemberId(meetingId, userId) - this.meetingType = meetingType - this.meetingId = meetingId - this.teamId = teamId - this.updatedAt = updatedAt ?? new Date() - this.userId = userId - } -} diff --git a/packages/server/database/types/PokerMeetingMember.ts b/packages/server/database/types/PokerMeetingMember.ts deleted file mode 100644 index 02051a9131c..00000000000 --- a/packages/server/database/types/PokerMeetingMember.ts +++ /dev/null @@ -1,19 +0,0 @@ -import MeetingMember from './MeetingMember' - -interface Input { - id?: string - updatedAt?: Date - teamId: string - userId: string - meetingId: string - isSpectating: boolean -} - -export default class PokerMeetingMember extends MeetingMember { - isSpectating: boolean - constructor(input: Input) { - const {isSpectating} = input - super({...input, meetingType: 'poker'}) - this.isSpectating = isSpectating - } -} diff --git a/packages/server/database/types/RetroMeetingMember.ts b/packages/server/database/types/RetroMeetingMember.ts deleted file mode 100644 index f53cf488346..00000000000 --- a/packages/server/database/types/RetroMeetingMember.ts +++ /dev/null @@ -1,19 +0,0 @@ -import MeetingMember from './MeetingMember' - -interface Input { - id?: string - updatedAt?: Date - teamId: string - userId: string - meetingId: string - votesRemaining: number -} - -export default class RetroMeetingMember extends MeetingMember { - votesRemaining: number - constructor(input: Input) { - const {votesRemaining, ...superInput} = input - super({...superInput, meetingType: 'retrospective'}) - this.votesRemaining = votesRemaining - } -} diff --git a/packages/server/database/types/TeamPromptMeetingMember.ts b/packages/server/database/types/TeamPromptMeetingMember.ts deleted file mode 100644 index 13a7cb11273..00000000000 --- a/packages/server/database/types/TeamPromptMeetingMember.ts +++ /dev/null @@ -1,17 +0,0 @@ -import MeetingMember from './MeetingMember' - -interface Input { - id?: string - updatedAt?: Date - teamId: string - userId: string - meetingId: string -} - -export default class TeamPromptMeetingMember extends MeetingMember { - meetingType!: 'teamPrompt' - - constructor(input: Input) { - super({...input, meetingType: 'teamPrompt'}) - } -} diff --git a/packages/server/dataloader/NullableDataLoader.ts b/packages/server/dataloader/NullableDataLoader.ts index baba3c414f0..0cfb4393e1a 100644 --- a/packages/server/dataloader/NullableDataLoader.ts +++ b/packages/server/dataloader/NullableDataLoader.ts @@ -17,6 +17,10 @@ class NullableDataLoader extends UpdatableCacheDataL super(batchLoadFn, options) } + load(key: Key) { + return super.load(key) as Promise<(Value & NarrowType) | undefined> + } + async loadNonNull(key: Key): Promise { const value = await this.load(key) if (value === undefined) { diff --git a/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts b/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts index 88c74280788..579cc033036 100644 --- a/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts +++ b/packages/server/graphql/mutations/helpers/sendPokerMeetingRevoteEvent.ts @@ -1,13 +1,12 @@ -import MeetingMember from '../../../database/types/MeetingMember' import {TeamMember} from '../../../postgres/types' -import {AnyMeeting} from '../../../postgres/types/Meeting' +import {AnyMeeting, AnyMeetingMember} from '../../../postgres/types/Meeting' import {analytics} from '../../../utils/analytics/analytics' import {DataLoaderWorker} from '../../graphql' const sendPokerMeetingRevoteEvent = async ( meeting: AnyMeeting, teamMembers: TeamMember[], - meetingMembers: MeetingMember[], + meetingMembers: AnyMeetingMember[], dataLoader: DataLoaderWorker ) => { const {facilitatorUserId, meetingNumber, phases, teamId} = meeting diff --git a/packages/server/graphql/mutations/pokerRevealVotes.ts b/packages/server/graphql/mutations/pokerRevealVotes.ts index 63d23dccfbb..ba1cbf2a87f 100644 --- a/packages/server/graphql/mutations/pokerRevealVotes.ts +++ b/packages/server/graphql/mutations/pokerRevealVotes.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {sql} from 'kysely' import {PokerCards, SubscriptionChannel} from 'parabol-client/types/constEnums' import EstimateUserScore from '../../database/types/EstimateUserScore' -import PokerMeetingMember from '../../database/types/PokerMeetingMember' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -80,7 +79,8 @@ const pokerRevealVotes = { // add a pass card for everyone who was present but did not vote const {scores} = stage meetingMembers.forEach((meetingMember) => { - const {userId, isSpectating} = meetingMember as PokerMeetingMember + if (meetingMember.meetingType !== 'poker') return + const {userId, isSpectating} = meetingMember if (isSpectating) return const userScore = scores.find((score) => score.userId === userId) if (!userScore) { diff --git a/packages/server/graphql/mutations/setPokerSpectate.ts b/packages/server/graphql/mutations/setPokerSpectate.ts index 2d9afc9c348..173b63720a2 100644 --- a/packages/server/graphql/mutations/setPokerSpectate.ts +++ b/packages/server/graphql/mutations/setPokerSpectate.ts @@ -3,7 +3,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' import getRethink from '../../database/rethinkDriver' import EstimateStage from '../../database/types/EstimateStage' -import PokerMeetingMember from '../../database/types/PokerMeetingMember' import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -38,7 +37,7 @@ const setPokerSpectate = { //AUTH const meetingMemberId = toTeamMemberId(meetingId, viewerId) const [meetingMember, meeting] = await Promise.all([ - dataLoader.get('meetingMembers').load(meetingMemberId) as Promise, + dataLoader.get('meetingMembers').load(meetingMemberId), dataLoader.get('newMeetings').load(meetingId) ]) if (!meeting) { @@ -51,6 +50,9 @@ const setPokerSpectate = { if (meetingType !== 'poker') { return {error: {message: 'Not a poker meeting'}} } + if (meetingMember.meetingType !== 'poker') { + return {error: {message: 'Not a poker meeting'}} + } if (!meetingMember) { return {error: {message: 'Not in meeting'}} } @@ -63,10 +65,7 @@ const setPokerSpectate = { const teamMemberId = toTeamMemberId(teamId, viewerId) await pg .with('MeetingMemberUpdate', (qb) => - qb - .updateTable('MeetingMember') - .set({isSpectating: isSpectating}) - .where('id', '=', meetingMemberId) + qb.updateTable('MeetingMember').set({isSpectating}).where('id', '=', meetingMemberId) ) .updateTable('TeamMember') .set({isSpectatingPoker: isSpectating}) diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index 86eec620058..d848cebed3b 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -81,6 +81,7 @@ export default { ) if (votingError) return votingError } + dataLoader.clearAll('meetingMembers') const data = { meetingId, diff --git a/packages/server/graphql/public/types/ActionMeeting.ts b/packages/server/graphql/public/types/ActionMeeting.ts index 4e2928042e7..5a4e5ef7644 100644 --- a/packages/server/graphql/public/types/ActionMeeting.ts +++ b/packages/server/graphql/public/types/ActionMeeting.ts @@ -1,4 +1,5 @@ import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' +import {ActionMeetingMember} from '../../../postgres/types/Meeting' import {getUserId} from '../../../utils/authorization' import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' import {ActionMeetingResolvers} from '../resolverTypes' @@ -20,8 +21,10 @@ const ActionMeeting: ActionMeetingResolvers = { // only populated after the meeting has been completed (not killed) return commentCount || 0 }, - meetingMembers: ({id: meetingId}, _args, {dataLoader}) => { - return dataLoader.get('meetingMembersByMeetingId').load(meetingId) + meetingMembers: async ({id: meetingId}, _args, {dataLoader}) => { + return (await dataLoader + .get('meetingMembersByMeetingId') + .load(meetingId)) as ActionMeetingMember[] }, taskCount: async ({taskCount}) => { // only populated after the meeting has been completed (not killed) @@ -38,7 +41,7 @@ const ActionMeeting: ActionMeetingResolvers = { const viewerId = getUserId(authToken) const meetingMemberId = toTeamMemberId(meetingId, viewerId) const meetingMember = await dataLoader.get('meetingMembers').load(meetingMemberId) - return meetingMember || null + return (meetingMember as ActionMeetingMember) || null } } diff --git a/packages/server/graphql/public/types/RetrospectiveMeeting.ts b/packages/server/graphql/public/types/RetrospectiveMeeting.ts index 6b1fd9e1fd5..8d77b876562 100644 --- a/packages/server/graphql/public/types/RetrospectiveMeeting.ts +++ b/packages/server/graphql/public/types/RetrospectiveMeeting.ts @@ -1,9 +1,8 @@ import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' -import RetroMeetingMember from '../../../database/types/RetroMeetingMember' +import {RetroMeetingMember} from '../../../postgres/types/Meeting' import {getUserId} from '../../../utils/authorization' import filterTasksByMeeting from '../../../utils/filterTasksByMeeting' import getPhase from '../../../utils/getPhase' -import {GQLContext} from '../../graphql' import {resolveForSU} from '../../resolvers' import {RetrospectiveMeetingResolvers} from '../resolverTypes' @@ -11,10 +10,9 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { autoGroupThreshold: resolveForSU('autoGroupThreshold'), commentCount: ({commentCount}) => commentCount || 0, disableAnonymity: ({disableAnonymity}) => disableAnonymity ?? false, - meetingMembers: ({id: meetingId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('meetingMembersByMeetingId').load(meetingId) as Promise< - RetroMeetingMember[] - > + meetingMembers: async ({id: meetingId}, _args, {dataLoader}) => { + const res = await dataLoader.get('meetingMembersByMeetingId').load(meetingId) + return res as RetroMeetingMember[] }, reflectionCount: ({reflectionCount}) => reflectionCount || 0, reflectionGroup: async ({id: meetingId}, {reflectionGroupId}, {dataLoader}) => { @@ -50,7 +48,7 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { return reflectionGroups }, taskCount: ({taskCount}) => taskCount || 0, - tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { + tasks: async ({id: meetingId}, _args, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting @@ -58,17 +56,13 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { return filterTasksByMeeting(teamTasks, meetingId, viewerId) }, topicCount: ({topicCount}) => topicCount || 0, - votesRemaining: async ({id: meetingId}, _args: unknown, {dataLoader}) => { + votesRemaining: async ({id: meetingId}, _args, {dataLoader}) => { const meetingMembers = (await dataLoader .get('meetingMembersByMeetingId') .load(meetingId)) as RetroMeetingMember[] return meetingMembers.reduce((sum, member) => sum + member.votesRemaining, 0) }, - viewerMeetingMember: async ( - {id: meetingId}, - _args: unknown, - {authToken, dataLoader}: GQLContext - ) => { + viewerMeetingMember: async ({id: meetingId}, _args, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) const meetingMemberId = toTeamMemberId(meetingId, viewerId) const meetingMember = (await dataLoader diff --git a/packages/server/graphql/public/types/TeamMember.ts b/packages/server/graphql/public/types/TeamMember.ts index 477914e7a21..9d86d955e04 100644 --- a/packages/server/graphql/public/types/TeamMember.ts +++ b/packages/server/graphql/public/types/TeamMember.ts @@ -36,7 +36,7 @@ const TeamMember: TeamMemberResolvers = { meetingMember: async ({userId}, {meetingId}, {dataLoader}) => { const meetingMemberId = MeetingMemberId.join(meetingId, userId) - return meetingId ? dataLoader.get('meetingMembers').load(meetingMemberId) : undefined + return meetingId ? dataLoader.get('meetingMembers').load(meetingMemberId) : null }, prevUsedRepoIntegrations: async ({teamId, userId}, {first}, context) => { diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index d1f63613754..ce9e3e3463c 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -13,7 +13,6 @@ import { import groupReflections from '../../../../client/utils/smartGroup/groupReflections' import getRethink from '../../../database/rethinkDriver' import {RDatum, RValue} from '../../../database/stricterR' -import MeetingMemberType from '../../../database/types/MeetingMember' import MeetingTemplate from '../../../database/types/MeetingTemplate' import Task from '../../../database/types/Task' import getKysely from '../../../postgres/getKysely' @@ -302,10 +301,7 @@ const User: ReqResolvers<'User'> = { lastMetAt: async ({id: userId}, _args, {dataLoader}) => { const meetingMembers = await dataLoader.get('meetingMembersByUserId').load(userId) - const lastMetAt = Math.max( - 0, - ...meetingMembers.map(({updatedAt}: MeetingMemberType) => updatedAt.getTime()) - ) + const lastMetAt = Math.max(0, ...meetingMembers.map(({updatedAt}) => updatedAt.getTime())) return lastMetAt ? new Date(lastMetAt) : null }, @@ -317,7 +313,7 @@ const User: ReqResolvers<'User'> = { monthlyStreakMax: async ({id: userId}, _args, {dataLoader}) => { const meetingMembers = await dataLoader.get('meetingMembersByUserId').load(userId) const meetingDates = meetingMembers - .map(({updatedAt}: MeetingMemberType) => updatedAt.getTime()) + .map(({updatedAt}) => updatedAt.getTime()) .sort((a, b) => (a < b ? 1 : -1)) return getMonthlyStreak(meetingDates) @@ -326,7 +322,7 @@ const User: ReqResolvers<'User'> = { monthlyStreakCurrent: async ({id: userId}, _args, {dataLoader}) => { const meetingMembers = await dataLoader.get('meetingMembersByUserId').load(userId) const meetingDates = meetingMembers - .map(({updatedAt}: MeetingMemberType) => updatedAt.getTime()) + .map(({updatedAt}) => updatedAt.getTime()) .sort((a, b) => (a < b ? 1 : -1)) return getMonthlyStreak(meetingDates, true) }, @@ -427,7 +423,7 @@ const User: ReqResolvers<'User'> = { meetingMember: async ({id: userId}, {meetingId}, {dataLoader}) => { const meetingMemberId = toTeamMemberId(meetingId, userId) - return meetingId ? dataLoader.get('meetingMembers').load(meetingMemberId) : undefined + return meetingId ? dataLoader.get('meetingMembers').load(meetingMemberId) : null }, organizationUser: async ({id: userId}, {orgId}, {authToken, dataLoader}) => { diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 11f3d9c9369..c23c3138416 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -3,7 +3,7 @@ import {NotNull, sql} from 'kysely' import {NewMeetingPhaseTypeEnum} from '../graphql/public/resolverTypes' import getKysely from './getKysely' import {ReactjiDB} from './types' -import {AnyMeeting, AnyMeetingTeamMember} from './types/Meeting' +import {AnyMeeting, AnyMeetingMember} from './types/Meeting' export const selectTimelineEvent = () => { return getKysely().selectFrom('TimelineEvent').selectAll().$narrowType< | { @@ -279,4 +279,4 @@ export const selectNewMeetings = () => .$narrowType() export const selectMeetingMembers = () => - getKysely().selectFrom('MeetingMember').selectAll().$narrowType() + getKysely().selectFrom('MeetingMember').selectAll().$narrowType() diff --git a/packages/server/postgres/types/Meeting.d.ts b/packages/server/postgres/types/Meeting.d.ts index 1900574b64d..4dfa0d3e4f5 100644 --- a/packages/server/postgres/types/Meeting.d.ts +++ b/packages/server/postgres/types/Meeting.d.ts @@ -88,13 +88,24 @@ export interface MeetingMember { userId: string } +export interface ActionMeetingMember extends MeetingMember { + meetingType: 'action' +} + +export interface TeamPromptMeetingMember extends MeetingMember { + meetingType: 'teamPrompt' +} export interface RetroMeetingMember extends MeetingMember { meetingType: 'retrospective' votesRemaining: number } -export interface PokerMeetingMember extends MeetingMember { +export type PokerMeetingMember = MeetingMember & { meetingType: 'poker' isSpectating: boolean } -export type AnyMeetingTeamMember = PokerMeetingMember | RetroMeetingMember | MeetingMember +export type AnyMeetingMember = + | PokerMeetingMember + | RetroMeetingMember + | ActionMeetingMember + | TeamPromptMeetingMember diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 568cda427e8..5a3ead7e35e 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -3,14 +3,18 @@ import type {UpgradeCTALocationEnumType} from '../../../client/shared/UpgradeCTA import TeamPromptResponseId from '../../../client/shared/gqlIds/TeamPromptResponseId' import {PARABOL_AI_USER_ID} from '../../../client/utils/constants' import {TeamLimitsEmailType} from '../../billing/helpers/sendTeamsLimitEmail' -import MeetingMember from '../../database/types/MeetingMember' import MeetingTemplate from '../../database/types/MeetingTemplate' import {TaskServiceEnum} from '../../database/types/Task' import {DataLoaderWorker} from '../../graphql/graphql' import {ModifyType, ReactableEnum} from '../../graphql/public/resolverTypes' import {IntegrationProviderServiceEnumType} from '../../graphql/types/IntegrationProviderServiceEnum' import {SlackNotification, TeamPromptResponse, TemplateScale} from '../../postgres/types' -import {AnyMeeting, MeetingTypeEnum, RetrospectiveMeeting} from '../../postgres/types/Meeting' +import { + AnyMeeting, + AnyMeetingMember, + MeetingTypeEnum, + RetrospectiveMeeting +} from '../../postgres/types/Meeting' import {MeetingSeries} from '../../postgres/types/MeetingSeries' import {AmplitudeAnalytics} from './amplitude/AmplitudeAnalytics' import {createMeetingProperties} from './helpers' @@ -192,7 +196,7 @@ class Analytics { // meeting teamPromptEnd = async ( completedMeeting: AnyMeeting, - meetingMembers: MeetingMember[], + meetingMembers: AnyMeetingMember[], responses: TeamPromptResponse[], dataLoader: DataLoaderWorker ) => { @@ -219,7 +223,7 @@ class Analytics { checkInEnd = async ( completedMeeting: AnyMeeting, - meetingMembers: MeetingMember[], + meetingMembers: AnyMeetingMember[], dataLoader: DataLoaderWorker ) => Promise.all( @@ -237,7 +241,7 @@ class Analytics { retrospectiveEnd = async ( completedMeeting: RetrospectiveMeeting, - meetingMembers: MeetingMember[], + meetingMembers: AnyMeetingMember[], template: MeetingTemplate, dataLoader: DataLoaderWorker ) => { @@ -260,7 +264,7 @@ class Analytics { sprintPokerEnd = ( completedMeeting: AnyMeeting, - meetingMembers: MeetingMember[], + meetingMembers: AnyMeetingMember[], template: MeetingTemplate, dataLoader: DataLoaderWorker ) => { @@ -281,7 +285,7 @@ class Analytics { dataloader: DataLoaderWorker, userId: string, completedMeeting: AnyMeeting, - meetingMembers: MeetingMember[], + meetingMembers: AnyMeetingMember[], template?: MeetingTemplate, meetingSpecificProperties?: any ) => { diff --git a/packages/server/utils/analytics/helpers.ts b/packages/server/utils/analytics/helpers.ts index 1e9f965b50c..38313c235a7 100644 --- a/packages/server/utils/analytics/helpers.ts +++ b/packages/server/utils/analytics/helpers.ts @@ -1,11 +1,10 @@ import {CHECKIN} from '../../../client/utils/constants' -import MeetingMember from '../../database/types/MeetingMember' import MeetingTemplate from '../../database/types/MeetingTemplate' -import {AnyMeeting} from '../../postgres/types/Meeting' +import {AnyMeeting, AnyMeetingMember} from '../../postgres/types/Meeting' export const createMeetingProperties = ( meeting: AnyMeeting, - meetingMembers?: MeetingMember[], + meetingMembers?: AnyMeetingMember[], template?: MeetingTemplate ) => { const {id: meetingId, teamId, facilitatorUserId, meetingType, phases} = meeting From ca192880da14dc6926166294f346d1985016bd4f Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 3 Oct 2024 14:29:15 -0700 Subject: [PATCH 57/89] fix: updateRetroMaxVotes Signed-off-by: Matt Krick --- .../server/graphql/mutations/joinMeeting.ts | 6 +- .../graphql/mutations/pokerRevealVotes.ts | 4 +- .../graphql/mutations/updateRetroMaxVotes.ts | 99 ++++++++++++++----- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index ea25db42b14..2d7cbb7002d 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -1,4 +1,5 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' +import {Insertable} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' @@ -7,6 +8,7 @@ import CheckInStage from '../../database/types/CheckInStage' import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStage' import UpdatesStage from '../../database/types/UpdatesStage' import getKysely from '../../postgres/getKysely' +import {MeetingMember} from '../../postgres/pg' import {TeamMember} from '../../postgres/types' import {AnyMeeting} from '../../postgres/types/Meeting' import {NewMeetingPhase, NewMeetingStages} from '../../postgres/types/NewMeetingPhase' @@ -20,7 +22,7 @@ import JoinMeetingPayload from '../types/JoinMeetingPayload' export const createMeetingMember = ( meeting: AnyMeeting, teamMember: Pick -) => { +): Insertable => { const {userId, teamId, isSpectatingPoker} = teamMember const {id: meetingId, meetingType} = meeting return { @@ -31,7 +33,7 @@ export const createMeetingMember = ( meetingId, meetingType, isSpectating: meetingType === 'poker' ? isSpectatingPoker : null, - totalVotes: meetingType === 'retrospective' ? meeting.totalVotes : null + votesRemaining: meetingType === 'retrospective' ? meeting.totalVotes : null } } diff --git a/packages/server/graphql/mutations/pokerRevealVotes.ts b/packages/server/graphql/mutations/pokerRevealVotes.ts index ba1cbf2a87f..56033b5085e 100644 --- a/packages/server/graphql/mutations/pokerRevealVotes.ts +++ b/packages/server/graphql/mutations/pokerRevealVotes.ts @@ -3,6 +3,7 @@ import {sql} from 'kysely' import {PokerCards, SubscriptionChannel} from 'parabol-client/types/constEnums' import EstimateUserScore from '../../database/types/EstimateUserScore' import getKysely from '../../postgres/getKysely' +import {PokerMeetingMember} from '../../postgres/types/Meeting' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -79,8 +80,7 @@ const pokerRevealVotes = { // add a pass card for everyone who was present but did not vote const {scores} = stage meetingMembers.forEach((meetingMember) => { - if (meetingMember.meetingType !== 'poker') return - const {userId, isSpectating} = meetingMember + const {userId, isSpectating} = meetingMember as PokerMeetingMember if (isSpectating) return const userScore = scores.find((score) => score.userId === userId) if (!userScore) { diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index 3b26dc8b010..847a480ba37 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -50,9 +50,6 @@ const updateRetroMaxVotes = { return {error: {message: 'Meeting not found'}} } - if (meeting.meetingType !== 'retrospective') { - return {error: {message: `Meeting not retrospective`}} - } const { endedAt, meetingType, @@ -121,19 +118,6 @@ const updateRetroMaxVotes = { }) .run() - await pg - .with('HasNegativeVotes', (qb) => - qb - .selectFrom('MeetingMember') - .select(({fn}) => fn.count('id').as('count')) - .where('meetingId', '=', meetingId) - .where(({eb}) => eb(eb('votesRemaining', '+', delta), '<', 0)) - ) - .updateTable('MeetingMember') - .where('meetingId', '=', meetingId) - .where((eb) => eb(eb.selectFrom('HasNegativeVotes').select('count'), '=', 0)) - .execute() - if (hasError) { return {error: {message: 'Your team has already spent their votes'}} } @@ -152,18 +136,79 @@ const updateRetroMaxVotes = { } // RESOLUTION - await getKysely() - .with('MeetingUpdates', (qb) => - qb.updateTable('NewMeeting').set({totalVotes, maxVotesPerGroup}).where('id', '=', meetingId) - ) - .updateTable('MeetingSettings') - .set({ - totalVotes, - maxVotesPerGroup + try { + await pg.transaction().execute(async (trx) => { + const canChangeMaxVotesPerGroup = + maxVotesPerGroup >= oldMaxVotesPerGroup + ? true + : await trx + .with('GroupVotes', (qb) => + qb + .selectFrom('RetroReflectionGroup') + .where('meetingId', '=', meetingId) + .where('isActive', '=', true) + .select(({fn}) => ['id', fn('unnest', ['voterIds']).as('userIds')]) + ) + .with('GroupVoteCount', (qb) => + qb + .selectFrom('GroupVotes') + .select(({fn}) => ['id', fn.count('userIds').as('mode')]) + .groupBy(['id', 'userIds']) + ) + .selectFrom('GroupVoteCount') + .select(({eb, fn}) => eb(fn.max('mode'), '<', maxVotesPerGroup).as('isValid')) + .executeTakeFirst() + if (!canChangeMaxVotesPerGroup) { + throw new Error('A topic already has too many votes') + } + const res = await trx + .with('MeetingMemberUpdates', (qb) => + qb + .updateTable('MeetingMember') + .set((eb) => ({votesRemaining: eb('votesRemaining', '+', delta)})) + .where('meetingId', '=', meetingId) + // TURN THIS ON IN PHASE 2 + // .$if(delta < 0, (qb) => + // qb.where(({selectFrom, eb}) => + // eb( + // selectFrom('MeetingMember') + // .select((eb) => eb.fn('min', ['votesRemaining']).as('min')) + // .where('meetingId', '=', meetingId), + // '>', + // -delta + // ) + // ) + // ) + .returning('id') + ) + .with( + 'NewMeetingUpdates', + (qb) => + qb + .updateTable('NewMeeting') + .set({totalVotes, maxVotesPerGroup}) + .where('id', '=', meetingId) + // TURN THIS ON IN PHASE 2 + // .where(({exists, selectFrom}) => exists(selectFrom('MeetingMemberUpdates').select('id'))) + ) + .updateTable('MeetingSettings') + .set({ + totalVotes, + maxVotesPerGroup + }) + .where('teamId', '=', teamId) + .where('meetingType', '=', 'retrospective') + // TURN THIS ON IN PHASE 2 + // .where(({exists, selectFrom}) => exists(selectFrom('MeetingMemberUpdates').select('id'))) + .executeTakeFirstOrThrow() + + if (res.numUpdatedRows === BigInt(0)) { + throw new Error('Your team has already spent their votes') + } }) - .where('teamId', '=', teamId) - .where('meetingType', '=', 'retrospective') - .execute() + } catch (e) { + return {error: {message: (e as Error).message}} + } dataLoader.get('newMeetings').clear(meetingId) const data = {meetingId} publish(SubscriptionChannel.MEETING, meetingId, 'UpdateRetroMaxVotesSuccess', data, subOptions) From a588b0d2293979d1e8b1293b1d5d0360ffac4d2f Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 3 Oct 2024 15:21:33 -0700 Subject: [PATCH 58/89] fix: re-add MeetingMember Signed-off-by: Matt Krick --- .../server/database/types/MeetingMember.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 packages/server/database/types/MeetingMember.ts diff --git a/packages/server/database/types/MeetingMember.ts b/packages/server/database/types/MeetingMember.ts new file mode 100644 index 00000000000..82e8f856c89 --- /dev/null +++ b/packages/server/database/types/MeetingMember.ts @@ -0,0 +1,30 @@ +// Can remove this after getting rid of the old RethinkDB migrations +import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' +import {MeetingTypeEnum} from '../../graphql/public/resolverTypes' + +interface MeetingMemberInput { + id?: string + updatedAt?: Date + teamId: string + userId: string + meetingType: MeetingTypeEnum + meetingId: string +} + +export default abstract class MeetingMember { + id: string + meetingType: MeetingTypeEnum + meetingId: string + teamId: string + updatedAt = new Date() + userId: string + constructor(input: MeetingMemberInput) { + const {teamId, meetingType, id, updatedAt, meetingId, userId} = input + this.id = id ?? toTeamMemberId(meetingId, userId) + this.meetingType = meetingType + this.meetingId = meetingId + this.teamId = teamId + this.updatedAt = updatedAt ?? new Date() + this.userId = userId + } +} From 011e64610aec7eb439cfde1ebde25d41713a08a6 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 3 Oct 2024 16:43:35 -0700 Subject: [PATCH 59/89] chore: migrate existing rows Signed-off-by: Matt Krick --- .../1727995266026_MeetingMember-phase2.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 packages/server/postgres/migrations/1727995266026_MeetingMember-phase2.ts diff --git a/packages/server/postgres/migrations/1727995266026_MeetingMember-phase2.ts b/packages/server/postgres/migrations/1727995266026_MeetingMember-phase2.ts new file mode 100644 index 00000000000..650075cde3c --- /dev/null +++ b/packages/server/postgres/migrations/1727995266026_MeetingMember-phase2.ts @@ -0,0 +1,117 @@ +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('MeetingMember') + .indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')]) + .run() + await r.table('MeetingMember').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'meetingType', + 'meetingId', + 'teamId', + 'updatedAt', + 'userId', + 'isSpectating', + 'votesRemaining' + ] as const + type MeetingMember = { + [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 + + const insertRow = async (row) => { + if (!row.teamId) { + console.log('MeetingMember has no teamId, skipping insert', row.id) + return + } + try { + await pg + .insertInto('MeetingMember') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_teamId') { + console.log('MeetingMember has no team, skipping insert', row.id) + return + } + if (e.constraint === 'fk_meetingId') { + console.log('MeetingMember has no meeting, skipping insert', row.id) + return + } + if (e.constraint === 'fk_userId') { + console.log('MeetingMember has no user, skipping insert', row.id) + return + } + throw e + } + } + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('MeetingMember') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as MeetingMember[] + + const rowsToInsert = rawRowsToInsert.map((row) => { + const {id, meetingType, meetingId, teamId, updatedAt, userId, isSpectating, votesRemaining} = + row as any + return { + id, + meetingType, + meetingId, + teamId, + updatedAt, + userId, + isSpectating, + votesRemaining + } + }) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.updatedAt + curId = lastRow.id + await Promise.all(rowsToInsert.map(async (row) => insertRow(row))) + } +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "MeetingMember" CASCADE`.execute(pg) +} From 7c844ceaa5f80a64cf3c64ed4a3992e20a61383a Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 4 Oct 2024 10:38:10 -0700 Subject: [PATCH 60/89] first pass removing R Signed-off-by: Matt Krick --- packages/server/database/rethinkDriver.ts | 5 - .../dataloader/foreignKeyLoaderMakers.ts | 8 +- .../dataloader/primaryKeyLoaderMakers.ts | 2 +- .../rethinkForeignKeyLoaderMakers.ts | 17 ---- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../mutations/helpers/removeTeamMember.ts | 6 -- .../mutations/helpers/safelyCastVote.ts | 49 +--------- .../mutations/helpers/safelyWithdrawVote.ts | 12 --- .../server/graphql/mutations/joinMeeting.ts | 7 -- .../graphql/mutations/newMeetingCheckIn.ts | 25 ----- .../resetRetroMeetingToGroupStage.ts | 7 +- .../graphql/mutations/setPokerSpectate.ts | 7 +- .../graphql/mutations/startSprintPoker.ts | 17 ++-- .../graphql/mutations/updateRetroMaxVotes.ts | 92 ++++--------------- .../mutations/voteForReflectionGroup.ts | 1 - .../private/mutations/hardDeleteUser.ts | 1 - .../private/mutations/runOrgActivityReport.ts | 26 ++---- .../graphql/public/mutations/startCheckIn.ts | 3 - .../public/mutations/startRetrospective.ts | 3 - .../subscriptions/meetingSubscription.ts | 9 +- .../graphql/public/typeDefs/Mutation.graphql | 21 ----- .../public/types/NewMeetingTeamMemberStage.ts | 2 +- .../public/types/RetrospectiveMeeting.ts | 4 +- .../server/graphql/public/types/TeamMember.ts | 2 +- packages/server/graphql/public/types/User.ts | 2 +- .../helpers/getActiveTeamCountByTeamIds.ts | 9 +- packages/server/graphql/rootMutation.ts | 2 - .../server/graphql/types/MeetingMember.ts | 8 -- .../graphql/types/NewMeetingCheckInPayload.ts | 25 ----- packages/server/postgres/types/Meeting.d.ts | 2 + 30 files changed, 59 insertions(+), 316 deletions(-) delete mode 100644 packages/server/graphql/mutations/newMeetingCheckIn.ts delete mode 100644 packages/server/graphql/types/MeetingMember.ts delete mode 100644 packages/server/graphql/types/NewMeetingCheckInPayload.ts diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index f1049f80727..b76040d74d6 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -1,6 +1,5 @@ import {MasterPool, r} from 'rethinkdb-ts' import TeamInvitation from '../database/types/TeamInvitation' -import {AnyMeetingMember} from '../postgres/types/Meeting' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' import MassInvitation from './types/MassInvitation' @@ -21,10 +20,6 @@ export type RethinkSchema = { type: MassInvitation index: 'teamMemberId' } - MeetingMember: { - type: AnyMeetingMember - index: 'meetingId' | 'teamId' | 'userId' - } NewFeature: { type: any index: '' diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index be87e4620ea..d1e8d1060f2 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -253,16 +253,16 @@ export const completedMeetingsByTeamId = foreignKeyLoaderMaker( } ) -export const _pgmeetingMembersByMeetingId = foreignKeyLoaderMaker( - '_pgmeetingMembers', +export const meetingMembersByMeetingId = foreignKeyLoaderMaker( + 'meetingMembers', 'meetingId', async (meetingIds) => { return selectMeetingMembers().where('meetingId', 'in', meetingIds).execute() } ) -export const _pgmeetingMembersByUserId = foreignKeyLoaderMaker( - '_pgmeetingMembers', +export const meetingMembersByUserId = foreignKeyLoaderMaker( + 'meetingMembers', 'userId', async (userIds) => { return selectMeetingMembers().where('userId', 'in', userIds).execute() diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 0d8eb405769..a32cf7e905e 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -122,6 +122,6 @@ export const newMeetings = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectNewMeetings().where('id', 'in', ids).execute() }) -export const _pgmeetingMembers = primaryKeyLoaderMaker((ids: readonly string[]) => { +export const meetingMembers = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectMeetingMembers().where('id', 'in', ids).execute() }) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index 06741f45f69..32bf2c76d6c 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -14,23 +14,6 @@ export const massInvitationsByTeamMemberId = new RethinkForeignKeyLoaderMaker( .run() } ) -export const meetingMembersByMeetingId = new RethinkForeignKeyLoaderMaker( - 'meetingMembers', - 'meetingId', - async (meetingIds) => { - const r = await getRethink() - return r.table('MeetingMember').getAll(r.args(meetingIds), {index: 'meetingId'}).run() - } -) - -export const meetingMembersByUserId = new RethinkForeignKeyLoaderMaker( - 'meetingMembers', - 'userId', - async (userIds) => { - const r = await getRethink() - return r.table('MeetingMember').getAll(r.args(userIds), {index: 'userId'}).run() - } -) export const tasksByDiscussionId = new RethinkForeignKeyLoaderMaker( 'tasks', diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index 0bef3795af3..a0b698abd6e 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -4,7 +4,6 @@ import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' * all rethink dataloader types which also must exist in {@link rethinkDriver/RethinkSchema} */ export const massInvitations = new RethinkPrimaryKeyLoaderMaker('MassInvitation') -export const meetingMembers = new RethinkPrimaryKeyLoaderMaker('MeetingMember') export const newFeatures = new RethinkPrimaryKeyLoaderMaker('NewFeature') export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') export const tasks = new RethinkPrimaryKeyLoaderMaker('Task') diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 49fd21ce16e..48e7551fa50 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -135,12 +135,6 @@ const removeTeamMember = async ( // Reassign facilitator for meetings this user is facilitating. if (meetingIds.length > 0) { - await r - .table('MeetingMember') - .getAll(r.args(meetingIds), {index: 'meetingId'}) - .filter({userId}) - .delete() - .run() const facilitatingMeetings = await pg .with('DeleteMeetingMembers', (qb) => qb diff --git a/packages/server/graphql/mutations/helpers/safelyCastVote.ts b/packages/server/graphql/mutations/helpers/safelyCastVote.ts index edac81955fb..ddea293ebfe 100644 --- a/packages/server/graphql/mutations/helpers/safelyCastVote.ts +++ b/packages/server/graphql/mutations/helpers/safelyCastVote.ts @@ -1,43 +1,15 @@ import {sql} from 'kysely' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' -import AuthToken from '../../../database/types/AuthToken' import getKysely from '../../../postgres/getKysely' -import {getUserId} from '../../../utils/authorization' -import standardError from '../../../utils/standardError' const safelyCastVote = async ( - authToken: AuthToken, meetingId: string, userId: string, reflectionGroupId: string, maxVotesPerGroup: number ) => { const meetingMemberId = toTeamMemberId(meetingId, userId) - const r = await getRethink() const pg = getKysely() - const now = new Date() - const viewerId = getUserId(authToken) - const isVoteRemovedFromUser = await r - .table('MeetingMember') - .get(meetingMemberId) - .update((member: RValue) => { - // go atomic. no cheating allowed - return r.branch( - member('votesRemaining').ge(1), - { - updatedAt: now, - votesRemaining: member('votesRemaining').sub(1) - }, - {} - ) - })('replaced') - .eq(1) - .run() - if (!isVoteRemovedFromUser) { - return standardError(new Error('No votes remaining'), {userId: viewerId}) - } // in a transaction add to the reflection group and the meeting member try { @@ -64,25 +36,8 @@ const safelyCastVote = async ( ]) .executeTakeFirst() - if (!res) { - await r - .table('MeetingMember') - .get(meetingMemberId) - .update((member: RValue) => ({ - votesRemaining: member('votesRemaining').add(1) - })) - .run() - throw new Error('Max votes per group exceeded') - } - if (!res.meetingMemberUpdate) { - // just for phase 2, make sure the row exists in the DB - const hasMember = await trx - .selectFrom('MeetingMember') - .select('id') - .where('id', '=', meetingMemberId) - .executeTakeFirst() - if (hasMember) throw new Error('No votes remaining') - } + if (!res) throw new Error('Max votes per group exceeded') + if (!res.meetingMemberUpdate) throw new Error('No votes remaining') }) } catch (e) { return {error: {message: (e as Error).message}} diff --git a/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts b/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts index b91452779b3..4ce03c429d5 100644 --- a/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts +++ b/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts @@ -1,7 +1,5 @@ import {sql} from 'kysely' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import AuthToken from '../../../database/types/AuthToken' import getKysely from '../../../postgres/getKysely' import {getUserId} from '../../../utils/authorization' @@ -14,9 +12,7 @@ const safelyWithdrawVote = async ( reflectionGroupId: string ) => { const meetingMemberId = toTeamMemberId(meetingId, userId) - const r = await getRethink() const pg = getKysely() - const now = new Date() const viewerId = getUserId(authToken) const voteRemovedResult = await pg .updateTable('RetroReflectionGroup') @@ -33,14 +29,6 @@ const safelyWithdrawVote = async ( if (!isVoteRemovedFromGroup) { return standardError(new Error('Already removed vote'), {userId: viewerId}) } - await r - .table('MeetingMember') - .get(meetingMemberId) - .update((member: RValue) => ({ - updatedAt: now, - votesRemaining: member('votesRemaining').add(1) - })) - .run() await pg .updateTable('MeetingMember') .set((eb) => ({votesRemaining: eb('votesRemaining', '+', 1)})) diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index 2d7cbb7002d..9ae3eec2766 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -3,7 +3,6 @@ import {Insertable} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' -import getRethink from '../../database/rethinkDriver' import CheckInStage from '../../database/types/CheckInStage' import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStage' import UpdatesStage from '../../database/types/UpdatesStage' @@ -51,7 +50,6 @@ const joinMeeting = { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { const pg = getKysely() - const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -73,11 +71,6 @@ const joinMeeting = { const teamMemberId = toTeamMemberId(teamId, viewerId) const teamMember = await dataLoader.get('teamMembers').loadNonNull(teamMemberId) const meetingMember = createMeetingMember(meeting, teamMember) - const {errors} = await r.table('MeetingMember').insert(meetingMember).run() - // if this is called concurrently, only 1 will be error free - if (errors > 0) { - return {error: {message: 'Already joined meeting'}} - } await pg.insertInto('MeetingMember').values(meetingMember).execute() const addStageToPhase = async ( stage: CheckInStage | UpdatesStage | TeamPromptResponseStage, diff --git a/packages/server/graphql/mutations/newMeetingCheckIn.ts b/packages/server/graphql/mutations/newMeetingCheckIn.ts deleted file mode 100644 index e8ee23122f1..00000000000 --- a/packages/server/graphql/mutations/newMeetingCheckIn.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {GraphQLBoolean, GraphQLID, GraphQLNonNull} from 'graphql' -import NewMeetingCheckInPayload from '../types/NewMeetingCheckInPayload' - -export default { - type: NewMeetingCheckInPayload, - description: 'Check a member in as present or absent', - deprecationReason: 'Members now join lazily and joining means they are present', - args: { - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The id of the user being marked present or absent' - }, - meetingId: { - type: new GraphQLNonNull(GraphQLID), - description: 'the meeting currently in progress' - }, - isCheckedIn: { - type: GraphQLBoolean, - description: 'true if the member is present, false if absent, null if undecided' - } - }, - async resolve(_source: unknown, {userId, meetingId}: {userId: string; meetingId: string}) { - return {meetingId, userId} - } -} diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index fd89e91f137..6bd36cb6eb1 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -124,12 +124,7 @@ const resetRetroMeetingToGroupStage = { .set({phases: JSON.stringify(newPhases)}) .where('id', '=', meetingId) .execute(), - r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run(), - r - .table('MeetingMember') - .getAll(meetingId, {index: 'meetingId'}) - .update({votesRemaining: meeting.totalVotes}) - .run() + r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run() ]) dataLoader.clearAll([ 'newMeetings', diff --git a/packages/server/graphql/mutations/setPokerSpectate.ts b/packages/server/graphql/mutations/setPokerSpectate.ts index 173b63720a2..9dfe7099fec 100644 --- a/packages/server/graphql/mutations/setPokerSpectate.ts +++ b/packages/server/graphql/mutations/setPokerSpectate.ts @@ -1,7 +1,6 @@ import {GraphQLBoolean, GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' -import getRethink from '../../database/rethinkDriver' import EstimateStage from '../../database/types/EstimateStage' import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' @@ -28,7 +27,6 @@ const setPokerSpectate = { {meetingId, isSpectating}: {meetingId: string; isSpectating: boolean}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { - const r = await getRethink() const pg = getKysely() const viewerId = getUserId(authToken) const operationId = dataLoader.share() @@ -37,7 +35,7 @@ const setPokerSpectate = { //AUTH const meetingMemberId = toTeamMemberId(meetingId, viewerId) const [meetingMember, meeting] = await Promise.all([ - dataLoader.get('meetingMembers').load(meetingMemberId), + dataLoader.get('meetingMembers').loadNonNull(meetingMemberId), dataLoader.get('newMeetings').load(meetingId) ]) if (!meeting) { @@ -71,9 +69,6 @@ const setPokerSpectate = { .set({isSpectatingPoker: isSpectating}) .where('id', '=', teamMemberId) .execute() - await r({ - meetingMember: r.table('MeetingMember').get(meetingMemberId).update({isSpectating}) - }).run() dataLoader.clearAll('teamMembers') // mutate the dataLoader cache meetingMember.isSpectating = isSpectating diff --git a/packages/server/graphql/mutations/startSprintPoker.ts b/packages/server/graphql/mutations/startSprintPoker.ts index bef930b4c60..3396c6921bc 100644 --- a/packages/server/graphql/mutations/startSprintPoker.ts +++ b/packages/server/graphql/mutations/startSprintPoker.ts @@ -1,7 +1,6 @@ import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' -import getRethink from '../../database/rethinkDriver' import MeetingPoker from '../../database/types/MeetingPoker' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' @@ -97,7 +96,6 @@ export default { {authToken, socketId: mutatorId, dataLoader}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -161,15 +159,12 @@ export default { const teamMemberId = toTeamMemberId(teamId, viewerId) const teamMember = await dataLoader.get('teamMembers').loadNonNull(teamMemberId) const meetingMember = createMeetingMember(meeting, teamMember) - await Promise.all([ - pg - .with('MeetingMemberInsert', (qb) => qb.insertInto('MeetingMember').values(meetingMember)) - .updateTable('Team') - .set({lastMeetingType: meetingType}) - .where('id', '=', teamId) - .execute(), - r.table('MeetingMember').insert(meetingMember).run() - ]) + await pg + .with('MeetingMemberInsert', (qb) => qb.insertInto('MeetingMember').values(meetingMember)) + .updateTable('Team') + .set({lastMeetingType: meetingType}) + .where('id', '=', teamId) + .execute() IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting, template) const {error} = await createGcalEvent({ diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index 847a480ba37..f89f218ad41 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -1,9 +1,6 @@ import {GraphQLID, GraphQLInt, GraphQLNonNull} from 'graphql' import {MeetingSettingsThreshold, SubscriptionChannel} from 'parabol-client/types/constEnums' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' -import mode from 'parabol-client/utils/mode' -import getRethink from '../../database/rethinkDriver' -import {RValue} from '../../database/stricterR' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -38,7 +35,6 @@ const updateRetroMaxVotes = { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) => { const pg = getKysely() - const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -89,51 +85,6 @@ const updateRetroMaxVotes = { } const delta = totalVotes - oldTotalVotes - // this isn't 100% atomic, but it's done in a single call, so it's pretty close - // eventual consistancy is OK, it's just possible for a client to get a bad data in between the 2 updates - // if votesRemaining goes negative for any user, we know we can't decrease any more - const hasError = await r - .table('MeetingMember') - .getAll(meetingId, {index: 'meetingId'}) - .update( - (member: RValue) => ({ - votesRemaining: member('votesRemaining').add(delta) - }), - {returnChanges: true} - )('changes')('new_val')('votesRemaining') - .min() - .lt(0) - .default(false) - .do((undo: RValue) => { - return r.branch( - undo, - r - .table('MeetingMember') - .getAll(meetingId, {index: 'meetingId'}) - .update((member: RValue) => ({ - votesRemaining: member('votesRemaining').add(-delta) - })), - null - ) - }) - .run() - - if (hasError) { - return {error: {message: 'Your team has already spent their votes'}} - } - - if (maxVotesPerGroup < oldMaxVotesPerGroup) { - const reflectionGroups = await dataLoader - .get('retroReflectionGroupsByMeetingId') - .load(meetingId) - - const maxVotesByASingleUser = Math.max( - ...reflectionGroups.map(({voterIds}) => mode(voterIds)[0]) - ) - if (maxVotesByASingleUser > maxVotesPerGroup) { - return {error: {message: 'Your team has already spent their votes'}} - } - } // RESOLUTION try { @@ -167,29 +118,27 @@ const updateRetroMaxVotes = { .updateTable('MeetingMember') .set((eb) => ({votesRemaining: eb('votesRemaining', '+', delta)})) .where('meetingId', '=', meetingId) - // TURN THIS ON IN PHASE 2 - // .$if(delta < 0, (qb) => - // qb.where(({selectFrom, eb}) => - // eb( - // selectFrom('MeetingMember') - // .select((eb) => eb.fn('min', ['votesRemaining']).as('min')) - // .where('meetingId', '=', meetingId), - // '>', - // -delta - // ) - // ) - // ) + .$if(delta < 0, (qb) => + qb.where(({selectFrom, eb}) => + eb( + selectFrom('MeetingMember') + .select((eb) => eb.fn('min', ['votesRemaining']).as('min')) + .where('meetingId', '=', meetingId), + '>', + -delta + ) + ) + ) .returning('id') ) - .with( - 'NewMeetingUpdates', - (qb) => - qb - .updateTable('NewMeeting') - .set({totalVotes, maxVotesPerGroup}) - .where('id', '=', meetingId) - // TURN THIS ON IN PHASE 2 - // .where(({exists, selectFrom}) => exists(selectFrom('MeetingMemberUpdates').select('id'))) + .with('NewMeetingUpdates', (qb) => + qb + .updateTable('NewMeeting') + .set({totalVotes, maxVotesPerGroup}) + .where('id', '=', meetingId) + .where(({exists, selectFrom}) => + exists(selectFrom('MeetingMemberUpdates').select('id')) + ) ) .updateTable('MeetingSettings') .set({ @@ -198,8 +147,7 @@ const updateRetroMaxVotes = { }) .where('teamId', '=', teamId) .where('meetingType', '=', 'retrospective') - // TURN THIS ON IN PHASE 2 - // .where(({exists, selectFrom}) => exists(selectFrom('MeetingMemberUpdates').select('id'))) + .where(({exists, selectFrom}) => exists(selectFrom('MeetingMemberUpdates').select('id'))) .executeTakeFirstOrThrow() if (res.numUpdatedRows === BigInt(0)) { diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index d848cebed3b..589bb93ed6d 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -73,7 +73,6 @@ export default { if (votingError) return votingError } else { const votingError = await safelyCastVote( - authToken, meetingId, viewerId, reflectionGroupId, diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index b4c4903891e..8effc9a13ad 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -86,7 +86,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .where('createdBy', '=', userIdToDelete) .execute() await r({ - meetingMember: r.table('MeetingMember').getAll(userIdToDelete, {index: 'userId'}).delete(), notification: r.table('Notification').getAll(userIdToDelete, {index: 'userId'}).delete(), createdTasks: r .table('Task') diff --git a/packages/server/graphql/private/mutations/runOrgActivityReport.ts b/packages/server/graphql/private/mutations/runOrgActivityReport.ts index 9ca6636b446..35f7205b61f 100644 --- a/packages/server/graphql/private/mutations/runOrgActivityReport.ts +++ b/packages/server/graphql/private/mutations/runOrgActivityReport.ts @@ -1,6 +1,4 @@ import {sql} from 'kysely' -import {RValue} from 'rethinkdb-ts' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import {MutationResolvers} from '../resolverTypes' @@ -44,7 +42,6 @@ const runOrgActivityReport: MutationResolvers['runOrgActivityReport'] = async ( ]) .orderBy('monthStart') - const r = await getRethink() try { const [signupCounts, rawMeetingCounts] = await Promise.all([ query.execute(), @@ -60,20 +57,13 @@ const runOrgActivityReport: MutationResolvers['runOrgActivityReport'] = async ( .execute() ]) const meetingIds = rawMeetingCounts.flatMap((row) => row.meetingIds) - const participantCounts = (await ( - r - .table('MeetingMember') - .getAll(r.args(meetingIds), {index: 'meetingId'}) - .group('meetingId') as any - ) - .count() - .ungroup() - .map((group: RValue) => ({ - meetingId: group('group'), - participantCount: group('reduction') - })) - .run()) as {meetingId: string; participantCount: number}[] - // Combine PostgreSQL and RethinkDB results + const participantCounts = await pg + .selectFrom('MeetingMember') + .select(({fn}) => ['meetingId', fn.count('id').as('participantCount')]) + .where('meetingId', 'in', meetingIds) + .groupBy('meetingId') + .execute() + // Combine results const combinedResults = signupCounts.map((pgRow) => { const epochMonthStart = pgRow.monthStart.getTime() const meetingCount = rawMeetingCounts.find( @@ -81,7 +71,7 @@ const runOrgActivityReport: MutationResolvers['runOrgActivityReport'] = async ( ) const participantCount = participantCounts .filter((pc) => meetingCount?.meetingIds.includes(pc.meetingId)) - .map((pc) => pc.participantCount) + .map((pc) => Number(pc.participantCount)) .reduce((a, b) => a + b, 0) return { monthStart: pgRow.monthStart, diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index ffe6ab4c87d..eab8efe5fb5 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import MeetingAction from '../../../database/types/MeetingAction' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' @@ -22,7 +21,6 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( context ) => { const pg = getKysely() - const r = await getRethink() const {authToken, socketId: mutatorId, dataLoader} = context const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -84,7 +82,6 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( .insertInto('MeetingMember') .values(meetingMember) .execute(), - r.table('MeetingMember').insert(meetingMember).run(), agendaItemIds.length && pg.updateTable('AgendaItem').set({meetingId}).where('id', 'in', agendaItemIds).execute() ]) diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index 8ab00de43e2..f45d27aae82 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' import updateMeetingTemplateLastUsedAt from '../../../postgres/queries/updateMeetingTemplateLastUsedAt' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' @@ -21,7 +20,6 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( {teamId, name, rrule, gcalInput}, {authToken, socketId: mutatorId, dataLoader} ) => { - const r = await getRethink() const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -97,7 +95,6 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( .insertInto('MeetingMember') .values(meetingMember) .execute(), - r.table('MeetingMember').insert(meetingMember).run(), videoMeetingURL && pg .updateTable('MeetingSettings') diff --git a/packages/server/graphql/public/subscriptions/meetingSubscription.ts b/packages/server/graphql/public/subscriptions/meetingSubscription.ts index b7c5e354770..3a2dba1855c 100644 --- a/packages/server/graphql/public/subscriptions/meetingSubscription.ts +++ b/packages/server/graphql/public/subscriptions/meetingSubscription.ts @@ -1,6 +1,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {getUserId} from '../../../utils/authorization' import getPubSub from '../../../utils/getPubSub' import {SubscriptionResolvers} from '../resolverTypes' @@ -8,10 +8,13 @@ import {SubscriptionResolvers} from '../resolverTypes' const meetingSubscription: SubscriptionResolvers['meetingSubscription'] = { subscribe: async (_source, {meetingId}, {authToken}) => { // AUTH - const r = await getRethink() const viewerId = getUserId(authToken) const meetingMemberId = toTeamMemberId(meetingId, viewerId) - const meetingMember = await r.table('MeetingMember').get(meetingMemberId).run() + const meetingMember = await getKysely() + .selectFrom('MeetingMember') + .select('id') + .where('id', '=', meetingMemberId) + .executeTakeFirst() if (!meetingMember) { throw new Error('Not invited to the meeting. Cannot subscribe') } diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql index 7b59ea7a5a5..41f4b156a44 100644 --- a/packages/server/graphql/public/typeDefs/Mutation.graphql +++ b/packages/server/graphql/public/typeDefs/Mutation.graphql @@ -470,27 +470,6 @@ type Mutation { meetingId: ID! ): NavigateMeetingPayload! - """ - Check a member in as present or absent - """ - newMeetingCheckIn( - """ - The id of the user being marked present or absent - """ - userId: ID! - - """ - the meeting currently in progress - """ - meetingId: ID! - - """ - true if the member is present, false if absent, null if undecided - """ - isCheckedIn: Boolean - ): NewMeetingCheckInPayload - @deprecated(reason: "Members now join lazily and joining means they are present") - """ Increment the count of times the org has clicked pay later """ diff --git a/packages/server/graphql/public/types/NewMeetingTeamMemberStage.ts b/packages/server/graphql/public/types/NewMeetingTeamMemberStage.ts index 05d63550f50..f2c754657e4 100644 --- a/packages/server/graphql/public/types/NewMeetingTeamMemberStage.ts +++ b/packages/server/graphql/public/types/NewMeetingTeamMemberStage.ts @@ -6,7 +6,7 @@ const NewMeetingTeamMemberStage: NewMeetingTeamMemberStageResolvers = { meetingMember: async ({meetingId, teamMemberId}, _args, {dataLoader}) => { const {userId} = fromTeamMemberId(teamMemberId) const meetingMemberId = toTeamMemberId(meetingId, userId) - return dataLoader.get('meetingMembers').load(meetingMemberId) + return dataLoader.get('meetingMembers').loadNonNull(meetingMemberId) }, teamMember: async ({teamMemberId}, _args, {dataLoader}) => { return dataLoader.get('teamMembers').loadNonNull(teamMemberId) diff --git a/packages/server/graphql/public/types/RetrospectiveMeeting.ts b/packages/server/graphql/public/types/RetrospectiveMeeting.ts index 8d77b876562..5595eb11133 100644 --- a/packages/server/graphql/public/types/RetrospectiveMeeting.ts +++ b/packages/server/graphql/public/types/RetrospectiveMeeting.ts @@ -48,7 +48,7 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { return reflectionGroups }, taskCount: ({taskCount}) => taskCount || 0, - tasks: async ({id: meetingId}, _args, {authToken, dataLoader}) => { + tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting @@ -56,7 +56,7 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { return filterTasksByMeeting(teamTasks, meetingId, viewerId) }, topicCount: ({topicCount}) => topicCount || 0, - votesRemaining: async ({id: meetingId}, _args, {dataLoader}) => { + votesRemaining: async ({id: meetingId}, _args: unknown, {dataLoader}) => { const meetingMembers = (await dataLoader .get('meetingMembersByMeetingId') .load(meetingId)) as RetroMeetingMember[] diff --git a/packages/server/graphql/public/types/TeamMember.ts b/packages/server/graphql/public/types/TeamMember.ts index 9d86d955e04..cb414ea2558 100644 --- a/packages/server/graphql/public/types/TeamMember.ts +++ b/packages/server/graphql/public/types/TeamMember.ts @@ -36,7 +36,7 @@ const TeamMember: TeamMemberResolvers = { meetingMember: async ({userId}, {meetingId}, {dataLoader}) => { const meetingMemberId = MeetingMemberId.join(meetingId, userId) - return meetingId ? dataLoader.get('meetingMembers').load(meetingMemberId) : null + return meetingId ? dataLoader.get('meetingMembers').loadNonNull(meetingMemberId) : null }, prevUsedRepoIntegrations: async ({teamId, userId}, {first}, context) => { diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index ce9e3e3463c..9b1e6cb2977 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -423,7 +423,7 @@ const User: ReqResolvers<'User'> = { meetingMember: async ({id: userId}, {meetingId}, {dataLoader}) => { const meetingMemberId = toTeamMemberId(meetingId, userId) - return meetingId ? dataLoader.get('meetingMembers').load(meetingMemberId) : null + return meetingId ? dataLoader.get('meetingMembers').loadNonNull(meetingMemberId) : null }, organizationUser: async ({id: userId}, {orgId}, {authToken, dataLoader}) => { diff --git a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts index 72a04e06aa0..c4acbd51737 100644 --- a/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts +++ b/packages/server/graphql/public/types/helpers/getActiveTeamCountByTeamIds.ts @@ -1,6 +1,6 @@ import {Threshold} from '~/types/constEnums' -import getRethink from '../../../../database/rethinkDriver' import getKysely from '../../../../postgres/getKysely' +import {selectMeetingMembers} from '../../../../postgres/select' // Uncomment for easier testing //import { ThresholdTest as Threshold } from "~/types/constEnums"; @@ -10,7 +10,6 @@ import getKysely from '../../../../postgres/getKysely' // TODO: store all calculations in the database, e.g. meeting.attendeeCount (see #7975) const getActiveTeamCountByTeamIds = async (teamIds: string[]) => { if (!teamIds.length) return 0 - const r = await getRethink() const pg = getKysely() const meetingIdsByTeamId = await pg .selectFrom('NewMeeting') @@ -19,10 +18,8 @@ const getActiveTeamCountByTeamIds = async (teamIds: string[]) => { .groupBy('teamId') .execute() const meetingIds = meetingIdsByTeamId.flatMap((row) => row.meetingIds) - const meetingMembers = await r - .table('MeetingMember') - .getAll(r.args(meetingIds), {index: 'meetingId'}) - .run() + if (!meetingIds.length) return 0 + const meetingMembers = await selectMeetingMembers().where('id', 'in', meetingIds).execute() const teamsIdsWithMinMeetingsAndMembers = meetingIdsByTeamId .map(({teamId, meetingIds}) => ({ teamId, diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 918463c1dae..c86f83691fc 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -42,7 +42,6 @@ import movePokerTemplateDimension from './mutations/movePokerTemplateDimension' import movePokerTemplateScaleValue from './mutations/movePokerTemplateScaleValue' import moveTeamToOrg from './mutations/moveTeamToOrg' import navigateMeeting from './mutations/navigateMeeting' -import newMeetingCheckIn from './mutations/newMeetingCheckIn' import oldUpdateCreditCard from './mutations/oldUpdateCreditCard' import oldUpgradeToTeamTier from './mutations/oldUpgradeToTeamTier' import payLater from './mutations/payLater' @@ -143,7 +142,6 @@ export default new GraphQLObjectType({ movePokerTemplateDimension, moveTeamToOrg, navigateMeeting, - newMeetingCheckIn, payLater, persistJiraSearchQuery, pushInvitation, diff --git a/packages/server/graphql/types/MeetingMember.ts b/packages/server/graphql/types/MeetingMember.ts deleted file mode 100644 index 496875361ad..00000000000 --- a/packages/server/graphql/types/MeetingMember.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {GraphQLInterfaceType} from 'graphql' - -const MeetingMember: GraphQLInterfaceType = new GraphQLInterfaceType({ - name: 'MeetingMember', - fields: {} -}) - -export default MeetingMember diff --git a/packages/server/graphql/types/NewMeetingCheckInPayload.ts b/packages/server/graphql/types/NewMeetingCheckInPayload.ts deleted file mode 100644 index b5010e1079c..00000000000 --- a/packages/server/graphql/types/NewMeetingCheckInPayload.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import {resolveMeetingMember, resolveNewMeeting} from '../resolvers' -import MeetingMember from './MeetingMember' -import NewMeeting from './NewMeeting' -import StandardMutationError from './StandardMutationError' - -const NewMeetingCheckInPayload = new GraphQLObjectType({ - name: 'NewMeetingCheckInPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - meetingMember: { - type: MeetingMember, - resolve: resolveMeetingMember - }, - meeting: { - type: NewMeeting, - resolve: resolveNewMeeting - } - }) -}) - -export default NewMeetingCheckInPayload diff --git a/packages/server/postgres/types/Meeting.d.ts b/packages/server/postgres/types/Meeting.d.ts index 4dfa0d3e4f5..17368e4b68d 100644 --- a/packages/server/postgres/types/Meeting.d.ts +++ b/packages/server/postgres/types/Meeting.d.ts @@ -109,3 +109,5 @@ export type AnyMeetingMember = | RetroMeetingMember | ActionMeetingMember | TeamPromptMeetingMember + +type Z = RetroMeetingMember & AnyMeetingMember From 120cf15d001f7552ab78c966c72f624d5a861944 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 7 Oct 2024 16:50:39 -0700 Subject: [PATCH 61/89] fix: voting transaction logic Signed-off-by: Matt Krick --- .../mutations/helpers/safelyCastVote.ts | 50 ++++++++------ .../mutations/helpers/safelyWithdrawVote.ts | 69 ++++++++++++------- .../graphql/mutations/updateRetroMaxVotes.ts | 68 +++++++----------- .../mutations/voteForReflectionGroup.ts | 16 +---- 4 files changed, 100 insertions(+), 103 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/safelyCastVote.ts b/packages/server/graphql/mutations/helpers/safelyCastVote.ts index ddea293ebfe..d8393a14671 100644 --- a/packages/server/graphql/mutations/helpers/safelyCastVote.ts +++ b/packages/server/graphql/mutations/helpers/safelyCastVote.ts @@ -2,42 +2,48 @@ import {sql} from 'kysely' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import getKysely from '../../../postgres/getKysely' -const safelyCastVote = async ( - meetingId: string, - userId: string, - reflectionGroupId: string, - maxVotesPerGroup: number -) => { +const safelyCastVote = async (meetingId: string, userId: string, reflectionGroupId: string) => { const meetingMemberId = toTeamMemberId(meetingId, userId) const pg = getKysely() - - // in a transaction add to the reflection group and the meeting member try { await pg.transaction().execute(async (trx) => { - const res = await trx + // Lock the rows here in case updateRetroMaxVotes gets called, which could use stale values + const [meetingMember, reflectionGroup] = await Promise.all([ + trx + .selectFrom('MeetingMember') + .select('id') + .where('id', '=', meetingMemberId) + .where('votesRemaining', '>', 0) + .forUpdate() + .executeTakeFirst(), + trx + .selectFrom('RetroReflectionGroup') + .select('id') + .where('id', '=', reflectionGroupId) + .where('isActive', '=', true) + .where(({eb, selectFrom}) => + eb( + sql`COALESCE(array_length(array_positions("voterIds", ${userId}),1),0)`, + '<', + selectFrom('NewMeeting').select('maxVotesPerGroup').where('id', '=', meetingId) + ) + ) + .forUpdate() + .executeTakeFirst() + ]) + if (!meetingMember) throw new Error('No votes remaining') + if (!reflectionGroup) throw new Error('Max votes per group reached') + await trx .with('MeetingMemberUpdate', (qb) => qb .updateTable('MeetingMember') .set((eb) => ({votesRemaining: eb('votesRemaining', '-', 1)})) .where('id', '=', meetingMemberId) - .returning('id') ) .updateTable('RetroReflectionGroup') .set({voterIds: sql`ARRAY_APPEND("voterIds",${userId})`}) .where('id', '=', reflectionGroupId) - .where( - sql`COALESCE(array_length(array_positions("voterIds", ${userId}),1),0)`, - '<', - maxVotesPerGroup - ) - .returning((eb) => [ - 'id', - eb.selectFrom('MeetingMemberUpdate').select('id').as('meetingMemberUpdate') - ]) .executeTakeFirst() - - if (!res) throw new Error('Max votes per group exceeded') - if (!res.meetingMemberUpdate) throw new Error('No votes remaining') }) } catch (e) { return {error: {message: (e as Error).message}} diff --git a/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts b/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts index 4ce03c429d5..9a3a49e485b 100644 --- a/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts +++ b/packages/server/graphql/mutations/helpers/safelyWithdrawVote.ts @@ -1,39 +1,58 @@ import {sql} from 'kysely' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import AuthToken from '../../../database/types/AuthToken' import getKysely from '../../../postgres/getKysely' -import {getUserId} from '../../../utils/authorization' -import standardError from '../../../utils/standardError' -const safelyWithdrawVote = async ( - authToken: AuthToken, - meetingId: string, - userId: string, - reflectionGroupId: string -) => { +const safelyWithdrawVote = async (meetingId: string, userId: string, reflectionGroupId: string) => { const meetingMemberId = toTeamMemberId(meetingId, userId) const pg = getKysely() - const viewerId = getUserId(authToken) - const voteRemovedResult = await pg - .updateTable('RetroReflectionGroup') - .set({ - voterIds: sql`array_cat( + try { + await pg.transaction().execute(async (trx) => { + // Lock the rows here in case updateRetroMaxVotes gets called, which could use stale values + const [meetingMember, reflectionGroup] = await Promise.all([ + trx + .selectFrom('MeetingMember') + .select('id') + .where('id', '=', meetingMemberId) + .where(({eb, selectFrom}) => + eb( + 'votesRemaining', + '<', + selectFrom('NewMeeting').select('totalVotes').where('id', '=', meetingId) + ) + ) + .forUpdate() + .executeTakeFirst(), + trx + .selectFrom('RetroReflectionGroup') + .select('id') + .where('id', '=', reflectionGroupId) + .where('isActive', '=', true) + .where(sql`${userId}`, '=', sql`ANY("voterIds")`) + .forUpdate() + .executeTakeFirst() + ]) + if (!meetingMember) throw new Error('Vote already withdrawn') + if (!reflectionGroup) throw new Error('Group vote already withdrawn') + await trx + .with('MeetingMemberUpdate', (qb) => + qb + .updateTable('MeetingMember') + .set((eb) => ({votesRemaining: eb('votesRemaining', '+', 1)})) + .where('id', '=', meetingMemberId) + ) + .updateTable('RetroReflectionGroup') + .set({ + voterIds: sql`array_cat( "voterIds"[1:array_position("voterIds",${userId})-1], "voterIds"[array_position("voterIds",${userId})+1:] )` + }) + .where('id', '=', reflectionGroupId) + .execute() }) - .where('id', '=', reflectionGroupId) - .where(sql`${userId}`, '=', sql`ANY("voterIds")`) - .executeTakeFirst() - const isVoteRemovedFromGroup = voteRemovedResult.numUpdatedRows === BigInt(1) - if (!isVoteRemovedFromGroup) { - return standardError(new Error('Already removed vote'), {userId: viewerId}) + } catch (e) { + return {error: {message: (e as Error).message}} } - await pg - .updateTable('MeetingMember') - .set((eb) => ({votesRemaining: eb('votesRemaining', '+', 1)})) - .where('id', '=', meetingMemberId) - .execute() return undefined } diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index f89f218ad41..4e4bc0c91ea 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -1,6 +1,7 @@ import {GraphQLID, GraphQLInt, GraphQLNonNull} from 'graphql' import {MeetingSettingsThreshold, SubscriptionChannel} from 'parabol-client/types/constEnums' import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete' +import mode from '../../../client/utils/mode' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -89,56 +90,42 @@ const updateRetroMaxVotes = { // RESOLUTION try { await pg.transaction().execute(async (trx) => { - const canChangeMaxVotesPerGroup = - maxVotesPerGroup >= oldMaxVotesPerGroup - ? true - : await trx - .with('GroupVotes', (qb) => - qb - .selectFrom('RetroReflectionGroup') - .where('meetingId', '=', meetingId) - .where('isActive', '=', true) - .select(({fn}) => ['id', fn('unnest', ['voterIds']).as('userIds')]) - ) - .with('GroupVoteCount', (qb) => - qb - .selectFrom('GroupVotes') - .select(({fn}) => ['id', fn.count('userIds').as('mode')]) - .groupBy(['id', 'userIds']) - ) - .selectFrom('GroupVoteCount') - .select(({eb, fn}) => eb(fn.max('mode'), '<', maxVotesPerGroup).as('isValid')) - .executeTakeFirst() - if (!canChangeMaxVotesPerGroup) { - throw new Error('A topic already has too many votes') + if (maxVotesPerGroup < oldMaxVotesPerGroup) { + const reflectionGroups = await trx + .selectFrom('RetroReflectionGroup') + .select('voterIds') + .where('meetingId', '=', meetingId) + .where('isActive', '=', true) + .forUpdate() + .execute() + const maxVotesPerGroupSpent = Math.max( + ...reflectionGroups.map(({voterIds}) => mode(voterIds)[0]) + ) + if (maxVotesPerGroupSpent > maxVotesPerGroup) + throw new Error('A topic already has too many votes') + } + if (delta < 0) { + const res = await trx + .selectFrom('MeetingMember') + .select('votesRemaining') + .where('meetingId', '=', meetingId) + .forUpdate() + .execute() + const min = Math.min(...res.map((m) => Number(m.votesRemaining!))) + if (min < -delta) throw new Error('Your team has already spent their votes') } - const res = await trx + await trx .with('MeetingMemberUpdates', (qb) => qb .updateTable('MeetingMember') .set((eb) => ({votesRemaining: eb('votesRemaining', '+', delta)})) .where('meetingId', '=', meetingId) - .$if(delta < 0, (qb) => - qb.where(({selectFrom, eb}) => - eb( - selectFrom('MeetingMember') - .select((eb) => eb.fn('min', ['votesRemaining']).as('min')) - .where('meetingId', '=', meetingId), - '>', - -delta - ) - ) - ) - .returning('id') ) .with('NewMeetingUpdates', (qb) => qb .updateTable('NewMeeting') .set({totalVotes, maxVotesPerGroup}) .where('id', '=', meetingId) - .where(({exists, selectFrom}) => - exists(selectFrom('MeetingMemberUpdates').select('id')) - ) ) .updateTable('MeetingSettings') .set({ @@ -147,12 +134,7 @@ const updateRetroMaxVotes = { }) .where('teamId', '=', teamId) .where('meetingType', '=', 'retrospective') - .where(({exists, selectFrom}) => exists(selectFrom('MeetingMemberUpdates').select('id'))) .executeTakeFirstOrThrow() - - if (res.numUpdatedRows === BigInt(0)) { - throw new Error('Your team has already spent their votes') - } }) } catch (e) { return {error: {message: (e as Error).message}} diff --git a/packages/server/graphql/mutations/voteForReflectionGroup.ts b/packages/server/graphql/mutations/voteForReflectionGroup.ts index 589bb93ed6d..c0fb162bb65 100644 --- a/packages/server/graphql/mutations/voteForReflectionGroup.ts +++ b/packages/server/graphql/mutations/voteForReflectionGroup.ts @@ -45,7 +45,7 @@ export default { if (meeting.meetingType !== 'retrospective') { return {error: {message: 'Meeting type is not retrospective'}} } - const {endedAt, phases, maxVotesPerGroup, teamId} = meeting + const {endedAt, phases, teamId} = meeting if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) } @@ -64,20 +64,10 @@ export default { // RESOLUTION dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) if (isUnvote) { - const votingError = await safelyWithdrawVote( - authToken, - meetingId, - viewerId, - reflectionGroupId - ) + const votingError = await safelyWithdrawVote(meetingId, viewerId, reflectionGroupId) if (votingError) return votingError } else { - const votingError = await safelyCastVote( - meetingId, - viewerId, - reflectionGroupId, - maxVotesPerGroup - ) + const votingError = await safelyCastVote(meetingId, viewerId, reflectionGroupId) if (votingError) return votingError } dataLoader.clearAll('meetingMembers') From 3d43dfc7e45269ac640f580360bc28b6bd726df5 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 8 Oct 2024 12:25:41 -0700 Subject: [PATCH 62/89] chore: migrate MassInvitation Signed-off-by: Matt Krick --- packages/server/database/rethinkDriver.ts | 5 -- .../dataloader/foreignKeyLoaderMakers.ts | 12 ++++ .../dataloader/primaryKeyLoaderMakers.ts | 5 ++ .../rethinkForeignKeyLoaderMakers.ts | 13 ---- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../graphql/mutations/createMassInvitation.ts | 21 ++++-- packages/server/graphql/types/Team.ts | 22 +++--- .../1728411506375_MassInvitation-1shot.ts | 71 +++++++++++++++++++ packages/server/postgres/select.ts | 2 + 9 files changed, 116 insertions(+), 36 deletions(-) create mode 100644 packages/server/postgres/migrations/1728411506375_MassInvitation-1shot.ts diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index b76040d74d6..ffe5a5ca287 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -2,7 +2,6 @@ import {MasterPool, r} from 'rethinkdb-ts' import TeamInvitation from '../database/types/TeamInvitation' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' -import MassInvitation from './types/MassInvitation' import NotificationKickedOut from './types/NotificationKickedOut' import NotificationMeetingStageTimeLimitEnd from './types/NotificationMeetingStageTimeLimitEnd' import NotificationMentioned from './types/NotificationMentioned' @@ -16,10 +15,6 @@ import NotificationTeamInvitation from './types/NotificationTeamInvitation' import Task from './types/Task' export type RethinkSchema = { - MassInvitation: { - type: MassInvitation - index: 'teamMemberId' - } NewFeature: { type: any index: '' diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index d1e8d1060f2..1d2ace54f0b 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -3,6 +3,7 @@ import {getTeamPromptResponsesByMeetingIds} from '../postgres/queries/getTeamPro import { selectAgendaItems, selectComments, + selectMassInvitations, selectMeetingMembers, selectNewMeetings, selectOrganizations, @@ -268,3 +269,14 @@ export const meetingMembersByUserId = foreignKeyLoaderMaker( return selectMeetingMembers().where('userId', 'in', userIds).execute() } ) + +export const massInvitationsByTeamMemberId = foreignKeyLoaderMaker( + 'massInvitations', + 'teamMemberId', + async (teamMemberIds) => { + return selectMassInvitations() + .where('teamMemberId', 'in', teamMemberIds) + .orderBy('expiration desc') + .execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index b7d005d1f6f..6905e1ffa18 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -8,6 +8,7 @@ import {getUsersByIds} from '../postgres/queries/getUsersByIds' import { selectAgendaItems, selectComments, + selectMassInvitations, selectMeetingMembers, selectMeetingSettings, selectNewMeetings, @@ -129,3 +130,7 @@ export const newMeetings = primaryKeyLoaderMaker((ids: readonly string[]) => { export const meetingMembers = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectMeetingMembers().where('id', 'in', ids).execute() }) + +export const massInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectMassInvitations().where('id', 'in', ids).execute() +}) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index 32bf2c76d6c..d558c0f190c 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -2,19 +2,6 @@ import getRethink from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' import RethinkForeignKeyLoaderMaker from './RethinkForeignKeyLoaderMaker' -export const massInvitationsByTeamMemberId = new RethinkForeignKeyLoaderMaker( - 'massInvitations', - 'teamMemberId', - async (teamMemberIds) => { - const r = await getRethink() - return r - .table('MassInvitation') - .getAll(r.args(teamMemberIds), {index: 'teamMemberId'}) - .orderBy(r.desc('expiration')) - .run() - } -) - export const tasksByDiscussionId = new RethinkForeignKeyLoaderMaker( 'tasks', 'discussionId', diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index a0b698abd6e..695470a8201 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 massInvitations = new RethinkPrimaryKeyLoaderMaker('MassInvitation') export const newFeatures = new RethinkPrimaryKeyLoaderMaker('NewFeature') export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') export const tasks = new RethinkPrimaryKeyLoaderMaker('Task') diff --git a/packages/server/graphql/mutations/createMassInvitation.ts b/packages/server/graphql/mutations/createMassInvitation.ts index 32fd444f548..2a2b41dfb2c 100644 --- a/packages/server/graphql/mutations/createMassInvitation.ts +++ b/packages/server/graphql/mutations/createMassInvitation.ts @@ -1,7 +1,8 @@ import {GraphQLBoolean, GraphQLID, GraphQLNonNull} from 'graphql' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import getRethink from '../../database/rethinkDriver' -import MassInvitation from '../../database/types/MassInvitation' +import {Security, Threshold} from '../../../client/types/constEnums' +import generateRandomString from '../../generateRandomString' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import {GQLContext} from '../graphql' import CreateMassInvitationPayload from '../types/CreateMassInvitationPayload' @@ -32,7 +33,7 @@ const createMassInvitation = { }: {meetingId?: string | null; teamId: string; voidOld?: boolean | null}, {authToken}: GQLContext ) => { - const r = await getRethink() + const pg = getKysely() const viewerId = getUserId(authToken) //AUTH @@ -43,11 +44,17 @@ const createMassInvitation = { // RESOLUTION const teamMemberId = toTeamMemberId(teamId, viewerId) if (voidOld) { - await r.table('MassInvitation').getAll(teamMemberId, {index: 'teamMemberId'}).delete().run() + await pg.deleteFrom('MassInvitation').where('teamMemberId', '=', teamMemberId).execute() } - const massInvitation = new MassInvitation({meetingId: meetingId ?? undefined, teamMemberId}) - - await r.table('MassInvitation').insert(massInvitation, {conflict: 'replace'}).run() + await pg + .insertInto('MassInvitation') + .values({ + id: generateRandomString(Security.MASS_INVITATION_TOKEN_LENGTH), + meetingId, + teamMemberId, + expiration: new Date(Date.now() + Threshold.MASS_INVITATION_TOKEN_LIFESPAN) + }) + .execute() return {teamId} } } diff --git a/packages/server/graphql/types/Team.ts b/packages/server/graphql/types/Team.ts index 385be345be2..7a92925c44a 100644 --- a/packages/server/graphql/types/Team.ts +++ b/packages/server/graphql/types/Team.ts @@ -9,10 +9,11 @@ import { } from 'graphql' import isTaskPrivate from 'parabol-client/utils/isTaskPrivate' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' -import getRethink from '../../database/rethinkDriver' -import MassInvitationDB from '../../database/types/MassInvitation' +import {Security, Threshold} from '../../../client/types/constEnums' import Task from '../../database/types/Task' import ITeam from '../../database/types/Team' +import generateRandomString from '../../generateRandomString' +import getKysely from '../../postgres/getKysely' import {getUserId, isSuperUser, isTeamMember, isUserBillingLeader} from '../../utils/authorization' import standardError from '../../utils/standardError' import isValid from '../isValid' @@ -75,7 +76,7 @@ const Team: GraphQLObjectType = new GraphQLObjectType({ {authToken, dataLoader}: GQLContext ) => { if (!isTeamMember(authToken, teamId)) return null - const r = await getRethink() + const pg = getKysely() const viewerId = getUserId(authToken) const teamMemberId = toTeamMemberId(teamId, viewerId) const invitationTokens = await dataLoader @@ -87,14 +88,15 @@ const Team: GraphQLObjectType = new GraphQLObjectType({ return newestInvitationToken // if the token is not valid, delete it to keep the table clean of expired things if (newestInvitationToken) { - await r - .table('MassInvitation') - .getAll(teamMemberId, {index: 'teamMemberId'}) - .delete() - .run() + await pg.deleteFrom('MassInvitation').where('teamMemberId', '=', teamMemberId).execute() } - const massInvitation = new MassInvitationDB({meetingId, teamMemberId}) - await r.table('MassInvitation').insert(massInvitation, {conflict: 'replace'}).run() + const massInvitation = { + id: generateRandomString(Security.MASS_INVITATION_TOKEN_LENGTH), + meetingId, + teamMemberId, + expiration: new Date(Date.now() + Threshold.MASS_INVITATION_TOKEN_LIFESPAN) + } + await pg.insertInto('MassInvitation').values(massInvitation).execute() invitationTokens.length = 1 invitationTokens[0] = massInvitation return massInvitation diff --git a/packages/server/postgres/migrations/1728411506375_MassInvitation-1shot.ts b/packages/server/postgres/migrations/1728411506375_MassInvitation-1shot.ts new file mode 100644 index 00000000000..09a93e8853a --- /dev/null +++ b/packages/server/postgres/migrations/1728411506375_MassInvitation-1shot.ts @@ -0,0 +1,71 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "MassInvitation" ( + "id" CHAR(12) NOT NULL PRIMARY KEY, + "expiration" TIMESTAMP WITH TIME ZONE NOT NULL, + "meetingId" VARCHAR(100), + "teamMemberId" VARCHAR(100) NOT NULL, + CONSTRAINT "fk_meetingId" + FOREIGN KEY("meetingId") + REFERENCES "NewMeeting"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_teamMemberId" + FOREIGN KEY("teamMemberId") + REFERENCES "TeamMember"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_MassInvitation_meetingId" ON "MassInvitation"("meetingId") WHERE "meetingId" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_MassInvitation_teamMemberId" ON "MassInvitation"("teamMemberId"); + END $$; +`.execute(pg) + + const rRequests = await r + .table('MassInvitation') + .filter((row) => row('expiration').ge(r.now())) + .coerceTo('array') + .run() + + const insertRow = async (row) => { + try { + await pg + .insertInto('MassInvitation') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_meetingId') { + return insertRow({...row, meetingId: null}) + } + if (e.constraint === 'fk_teamMemberId') { + console.log('MassInvitation has no teamMember, skipping insert', row.id) + return + } + throw e + } + } + await Promise.all(rRequests.map(async (row) => insertRow(row))) +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "MassInvitation"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index b0c9a168e85..45e15ee777c 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -279,3 +279,5 @@ export const selectNewMeetings = () => export const selectMeetingMembers = () => getKysely().selectFrom('MeetingMember').selectAll().$narrowType() + +export const selectMassInvitations = () => getKysely().selectFrom('MassInvitation').selectAll() From 4c4e791e537378a4f6125f4e40ee0a16f81b2996 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 8 Oct 2024 15:00:12 -0700 Subject: [PATCH 63/89] chore: move NewFeature to PG Signed-off-by: Matt Krick --- codegen.json | 2 + packages/server/database/types/User.ts | 2 +- .../dataloader/primaryKeyLoaderMakers.ts | 5 ++ .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../private/mutations/addNewFeature.ts | 23 ++++----- .../public/types/NewFeatureBroadcast.ts | 7 +++ packages/server/graphql/public/types/User.ts | 2 +- .../graphql/types/NewFeatureBroadcast.ts | 26 ---------- .../1728418948136_NewFeature-oneshot.ts | 48 +++++++++++++++++++ packages/server/postgres/select.ts | 2 + packages/server/postgres/types/index.d.ts | 2 + 11 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 packages/server/graphql/public/types/NewFeatureBroadcast.ts delete mode 100644 packages/server/graphql/types/NewFeatureBroadcast.ts create mode 100644 packages/server/postgres/migrations/1728418948136_NewFeature-oneshot.ts diff --git a/codegen.json b/codegen.json index 08c29e714e5..ceaf9293682 100644 --- a/codegen.json +++ b/codegen.json @@ -25,6 +25,7 @@ "GenerateMeetingSummarySuccess": "./types/GenerateMeetingSummarySuccess#GenerateMeetingSummarySuccessSource", "LoginsPayload": "./types/LoginsPayload#LoginsPayloadSource", "MeetingTemplate": "../../database/types/MeetingTemplate#default as IMeetingTemplate", + "NewFeatureBroadcast": "../../postgres/types/index#NewFeature", "NewMeeting": "../../postgres/types/Meeting#AnyMeeting", "Organization": "../../postgres/types/index#Organization as OrganizationDB", "PingableServices": "./types/PingableServices#PingableServicesSource", @@ -50,6 +51,7 @@ "contextType": "../graphql#GQLContext", "mappers": { "ReflectTemplatePromptUpdateDescriptionPayload": "./types/ReflectTemplatePromptUpdateDescriptionPayload#ReflectTemplatePromptUpdateDescriptionPayloadSource", + "NewFeatureBroadcast": "../../postgres/types/index#NewFeature", "ReflectTemplatePromptUpdateGroupColorPayload": "./types/ReflectTemplatePromptUpdateGroupColorPayload#ReflectTemplatePromptUpdateGroupColorPayloadSource", "RemoveReflectTemplatePromptPayload": "./types/RemoveReflectTemplatePromptPayload#RemoveReflectTemplatePromptPayloadSource", "RenameReflectTemplatePromptPayload": "./types/RenameReflectTemplatePromptPayload#RenameReflectTemplatePromptPayloadSource", diff --git a/packages/server/database/types/User.ts b/packages/server/database/types/User.ts index 3c278e3816a..84e3ffea909 100644 --- a/packages/server/database/types/User.ts +++ b/packages/server/database/types/User.ts @@ -32,7 +32,7 @@ export default class User { lastSeenAt: Date lastSeenAtURLs: string[] | null updatedAt: Date - newFeatureId?: string | null + newFeatureId?: number | null overLimitCopy?: string | null picture: string inactive: boolean diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 6905e1ffa18..813fb985c8f 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -11,6 +11,7 @@ import { selectMassInvitations, selectMeetingMembers, selectMeetingSettings, + selectNewFeatures, selectNewMeetings, selectOrganizations, selectReflectPrompts, @@ -134,3 +135,7 @@ export const meetingMembers = primaryKeyLoaderMaker((ids: readonly string[]) => export const massInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectMassInvitations().where('id', 'in', ids).execute() }) + +export const newFeatures = primaryKeyLoaderMaker((ids: readonly number[]) => { + return selectNewFeatures().where('id', 'in', ids).execute() +}) diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index 695470a8201..d0f67f23712 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 newFeatures = new RethinkPrimaryKeyLoaderMaker('NewFeature') export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') export const tasks = new RethinkPrimaryKeyLoaderMaker('Task') export const teamInvitations = new RethinkPrimaryKeyLoaderMaker('TeamInvitation') diff --git a/packages/server/graphql/private/mutations/addNewFeature.ts b/packages/server/graphql/private/mutations/addNewFeature.ts index b612061ee9d..ca084664f0c 100644 --- a/packages/server/graphql/private/mutations/addNewFeature.ts +++ b/packages/server/graphql/private/mutations/addNewFeature.ts @@ -1,6 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' -import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import getRedis from '../../../utils/getRedis' import publish from '../../../utils/publish' @@ -12,7 +10,6 @@ const addNewFeature: MutationResolvers['addNewFeature'] = async ( {actionButtonCopy, snackbarMessage, url}, {dataLoader} ) => { - const r = await getRethink() const redis = getRedis() const pg = getKysely() @@ -21,18 +18,16 @@ const addNewFeature: MutationResolvers['addNewFeature'] = async ( const subOptions = {operationId} // RESOLUTION - const newFeatureId = generateUID() - const newFeature = { - id: newFeatureId, - actionButtonCopy, - snackbarMessage, - url - } - await Promise.all([ - r.table('NewFeature').insert(newFeature).run(), - pg.updateTable('User').set({newFeatureId}).execute() - ]) + const newFeatureRes = await pg + .with('NewFeatureInsert', (qb) => + qb.insertInto('NewFeature').values({actionButtonCopy, snackbarMessage, url}).returning('id') + ) + .updateTable('User') + .set((eb) => ({newFeatureId: eb.selectFrom('NewFeatureInsert').select('NewFeatureInsert.id')})) + .returning((eb) => [eb.selectFrom('NewFeatureInsert').select('NewFeatureInsert.id').as('id')]) + .executeTakeFirstOrThrow() + const newFeature = {actionButtonCopy, snackbarMessage, url, id: newFeatureRes.id!} const onlineUserIds = new Set() const stream = redis.scanStream({match: 'presence:*'}) stream.on('data', (keys) => { diff --git a/packages/server/graphql/public/types/NewFeatureBroadcast.ts b/packages/server/graphql/public/types/NewFeatureBroadcast.ts new file mode 100644 index 00000000000..5ca7f15ec80 --- /dev/null +++ b/packages/server/graphql/public/types/NewFeatureBroadcast.ts @@ -0,0 +1,7 @@ +import {NewFeatureBroadcastResolvers} from '../resolverTypes' + +const NewFeatureBroadcast: NewFeatureBroadcastResolvers = { + id: ({id}) => `NewFeature:${id}` +} + +export default NewFeatureBroadcast diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index cb65785f3e7..d736cb65a26 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -411,7 +411,7 @@ const User: ReqResolvers<'User'> = { }, newFeature: ({newFeatureId}, _args, {dataLoader}) => { - return newFeatureId ? dataLoader.get('newFeatures').load(newFeatureId) : null + return newFeatureId ? dataLoader.get('newFeatures').loadNonNull(newFeatureId) : null }, lastSeenAtURLs: async ({id: userId}) => { diff --git a/packages/server/graphql/types/NewFeatureBroadcast.ts b/packages/server/graphql/types/NewFeatureBroadcast.ts deleted file mode 100644 index 069525b2694..00000000000 --- a/packages/server/graphql/types/NewFeatureBroadcast.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' - -const NewFeatureBroadcast = new GraphQLObjectType({ - name: 'NewFeatureBroadcast', - description: 'The latest feature released by Parabol', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID) - }, - actionButtonCopy: { - type: new GraphQLNonNull(GraphQLString), - description: 'The text of the action button in the snackbar' - }, - snackbarMessage: { - type: new GraphQLNonNull(GraphQLString), - description: 'The description of the new feature' - }, - url: { - type: new GraphQLNonNull(GraphQLString), - description: 'The permalink to the blog post describing the new feature' - } - }) -}) - -export default NewFeatureBroadcast diff --git a/packages/server/postgres/migrations/1728418948136_NewFeature-oneshot.ts b/packages/server/postgres/migrations/1728418948136_NewFeature-oneshot.ts new file mode 100644 index 00000000000..2b6e137980f --- /dev/null +++ b/packages/server/postgres/migrations/1728418948136_NewFeature-oneshot.ts @@ -0,0 +1,48 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "NewFeature" ( + "id" INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + "actionButtonCopy" VARCHAR(50) NOT NULL, + "snackbarMessage" VARCHAR(255) NOT NULL, + "url" VARCHAR(2056) NOT NULL + ); + END $$; +`.execute(pg) + + // empty out old new features, do not migrate them over + await pg.updateTable('User').set({newFeatureId: null}).execute() + await pg.schema + .alterTable('User') + .alterColumn('newFeatureId', (builder) => + builder.setDataType(sql`INTEGER USING "newFeatureId"::integer`) + ) + .execute() + await pg.schema + .alterTable('User') + .addForeignKeyConstraint('fk_newFeatureId', ['newFeatureId'], 'NewFeature', ['id']) + .onDelete('set null') + .execute() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "NewFeature"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 45e15ee777c..1f2461e231a 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -281,3 +281,5 @@ export const selectMeetingMembers = () => getKysely().selectFrom('MeetingMember').selectAll().$narrowType() export const selectMassInvitations = () => getKysely().selectFrom('MassInvitation').selectAll() + +export const selectNewFeatures = () => getKysely().selectFrom('NewFeature').selectAll() diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 2186bdc5ee3..3db0a425a1b 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -8,6 +8,7 @@ import { selectAgendaItems, selectComments, selectMeetingSettings, + selectNewFeatures, selectNewMeetings, selectOrganizations, selectReflectPrompts, @@ -72,3 +73,4 @@ export type Comment = ExtractTypeFromQueryBuilderSelect export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect export type NewMeeting = ExtractTypeFromQueryBuilderSelect +export type NewFeature = ExtractTypeFromQueryBuilderSelect From 1c14d5d6aa893404cdc9c7fa137aaf1e892a2235 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 8 Oct 2024 15:46:52 -0700 Subject: [PATCH 64/89] fixup lint --- packages/server/graphql/public/types/RetrospectiveMeeting.ts | 4 ++-- packages/server/postgres/types/Meeting.d.ts | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/server/graphql/public/types/RetrospectiveMeeting.ts b/packages/server/graphql/public/types/RetrospectiveMeeting.ts index 5595eb11133..8d77b876562 100644 --- a/packages/server/graphql/public/types/RetrospectiveMeeting.ts +++ b/packages/server/graphql/public/types/RetrospectiveMeeting.ts @@ -48,7 +48,7 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { return reflectionGroups }, taskCount: ({taskCount}) => taskCount || 0, - tasks: async ({id: meetingId}, _args: unknown, {authToken, dataLoader}) => { + tasks: async ({id: meetingId}, _args, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) const {teamId} = meeting @@ -56,7 +56,7 @@ const RetrospectiveMeeting: RetrospectiveMeetingResolvers = { return filterTasksByMeeting(teamTasks, meetingId, viewerId) }, topicCount: ({topicCount}) => topicCount || 0, - votesRemaining: async ({id: meetingId}, _args: unknown, {dataLoader}) => { + votesRemaining: async ({id: meetingId}, _args, {dataLoader}) => { const meetingMembers = (await dataLoader .get('meetingMembersByMeetingId') .load(meetingId)) as RetroMeetingMember[] diff --git a/packages/server/postgres/types/Meeting.d.ts b/packages/server/postgres/types/Meeting.d.ts index 17368e4b68d..4dfa0d3e4f5 100644 --- a/packages/server/postgres/types/Meeting.d.ts +++ b/packages/server/postgres/types/Meeting.d.ts @@ -109,5 +109,3 @@ export type AnyMeetingMember = | RetroMeetingMember | ActionMeetingMember | TeamPromptMeetingMember - -type Z = RetroMeetingMember & AnyMeetingMember From 74607a81e0bf4b2bdf09e571b2933b4c100928cc Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 9 Oct 2024 10:51:58 -0700 Subject: [PATCH 65/89] chore: add writes to PG Signed-off-by: Matt Krick --- codegen.json | 2 +- packages/server/database/rethinkDriver.ts | 2 +- .../dataloader/foreignKeyLoaderMakers.ts | 14 +++++ .../dataloader/primaryKeyLoaderMakers.ts | 5 ++ .../helpers/handleMassInviteToken.ts | 18 ++++-- .../helpers/handleTeamInviteToken.ts | 2 +- .../mutations/helpers/inviteToTeamHelper.ts | 28 +++++---- .../types/AcceptTeamInvitationPayload.ts | 2 +- .../1728425112300_TeamInvitation-phase1.ts | 60 +++++++++++++++++++ packages/server/postgres/select.ts | 2 + packages/server/postgres/types/index.d.ts | 2 + .../safeMutations/acceptTeamInvitation.ts | 9 ++- .../server/safeMutations/safeArchiveTeam.ts | 8 +++ .../server/utils/getBestInvitationMeeting.ts | 2 +- 14 files changed, 133 insertions(+), 23 deletions(-) create mode 100644 packages/server/postgres/migrations/1728425112300_TeamInvitation-phase1.ts diff --git a/codegen.json b/codegen.json index ceaf9293682..77d538e22cc 100644 --- a/codegen.json +++ b/codegen.json @@ -180,7 +180,7 @@ "Team": "../../postgres/types/index#Team as TeamDB", "TeamHealthPhase": "./types/TeamHealthPhase#TeamHealthPhaseSource", "TeamHealthStage": "./types/TeamHealthStage#TeamHealthStageSource", - "TeamInvitation": "../../database/types/TeamInvitation#default", + "TeamInvitation": "../../postgres/types/index/#TeamInvitation", "TeamMember": "../../postgres/types/index#TeamMember as TeamMember", "TeamMemberIntegrationAuthOAuth1": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index ffe5a5ca287..dd498997ecf 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -1,5 +1,5 @@ import {MasterPool, r} from 'rethinkdb-ts' -import TeamInvitation from '../database/types/TeamInvitation' +import {TeamInvitation} from '../postgres/types/index' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' import NotificationKickedOut from './types/NotificationKickedOut' diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 1d2ace54f0b..522dbcd1ba2 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -1,3 +1,4 @@ +import {sql} from 'kysely' import getKysely from '../postgres/getKysely' import {getTeamPromptResponsesByMeetingIds} from '../postgres/queries/getTeamPromptResponsesByMeetingIds' import { @@ -12,6 +13,7 @@ import { selectSlackAuths, selectSlackNotifications, selectSuggestedAction, + selectTeamInvitations, selectTeams, selectTemplateDimension, selectTemplateScale, @@ -280,3 +282,15 @@ export const massInvitationsByTeamMemberId = foreignKeyLoaderMaker( .execute() } ) + +export const _pgteamInvitationsByTeamId = foreignKeyLoaderMaker( + '_pgteamInvitations', + 'teamId', + async (teamIds) => { + return selectTeamInvitations() + .where('teamId', 'in', teamIds) + .where('acceptedAt', 'is', null) + .where('expiresAt', '>=', sql`CURRENT_TIMESTAMP`) + .execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 813fb985c8f..811069fb210 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -19,6 +19,7 @@ import { selectSlackAuths, selectSlackNotifications, selectSuggestedAction, + selectTeamInvitations, selectTeamPromptResponses, selectTeams, selectTemplateDimension, @@ -139,3 +140,7 @@ export const massInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => export const newFeatures = primaryKeyLoaderMaker((ids: readonly number[]) => { return selectNewFeatures().where('id', 'in', ids).execute() }) + +export const _pgteamInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectTeamInvitations().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts b/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts index c8ac2e69502..8c6d0200bae 100644 --- a/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts +++ b/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts @@ -1,6 +1,7 @@ import {InvitationTokenError} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import TeamInvitation from '../../../database/types/TeamInvitation' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import {verifyMassInviteToken} from '../../../utils/massInviteToken' import {DataLoaderWorker} from '../../graphql' @@ -17,15 +18,22 @@ const handleMassInviteToken = async ( if (tms?.includes(teamId)) { return {error: InvitationTokenError.ALREADY_ACCEPTED, teamId, meetingId} } - const invitation = new TeamInvitation({ + const invitation = { + id: generateUID(), token: invitationToken, invitedBy, meetingId, teamId, expiresAt, - email - }) - await r.table('TeamInvitation').insert(invitation).run() + email, + isMassInvite: true, + acceptedAt: null + } + await getKysely().insertInto('TeamInvitation').values(invitation).execute() + await r + .table('TeamInvitation') + .insert({...invitation, createdAt: new Date()}) + .run() return {invitation} } diff --git a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts index bfed33eb19c..544dc5b1a83 100644 --- a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts +++ b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts @@ -1,6 +1,6 @@ import {InvitationTokenError} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import TeamInvitation from '../../../database/types/TeamInvitation' +import {TeamInvitation} from '../../../postgres/types' const handleTeamInviteToken = async ( invitationToken: string, diff --git a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts index 916faae658a..561febbd547 100644 --- a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts +++ b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts @@ -7,9 +7,10 @@ import {isNotNull} from '../../../../client/utils/predicates' import appOrigin from '../../../appOrigin' import getRethink from '../../../database/rethinkDriver' import NotificationTeamInvitation from '../../../database/types/NotificationTeamInvitation' -import TeamInvitation from '../../../database/types/TeamInvitation' import getMailManager from '../../../email/getMailManager' import teamInviteEmailCreator from '../../../email/teamInviteEmailCreator' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import {getUsersByEmails} from '../../../postgres/queries/getUsersByEmails' import removeSuggestedAction from '../../../safeMutations/removeSuggestedAction' import {analytics} from '../../../utils/analytics/analytics' @@ -34,6 +35,7 @@ const inviteToTeamHelper = async ( const {authToken, dataLoader, socketId: mutatorId} = context const viewerId = getUserId(authToken) const r = await getRethink() + const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -112,18 +114,20 @@ const inviteToTeamHelper = async ( ) const expiresAt = new Date(Date.now() + Threshold.TEAM_INVITATION_LIFESPAN) // insert invitation records - const teamInvitationsToInsert = newAllowedInvitees.map((email, idx) => { - return new TeamInvitation({ - expiresAt, - email, - invitedBy: viewerId, - meetingId: meetingId ?? undefined, - teamId, - token: tokens[idx]! - }) - }) + const teamInvitationsToInsert = newAllowedInvitees.map((email, idx) => ({ + id: generateUID(), + expiresAt, + email, + invitedBy: viewerId, + meetingId: meetingId ?? undefined, + teamId, + token: tokens[idx]!, + isMassInvite: false, + createdAt: new Date(), + acceptedAt: null + })) await r.table('TeamInvitation').insert(teamInvitationsToInsert).run() - + await pg.insertInto('TeamInvitation').values(teamInvitationsToInsert).execute() // remove suggested action, if any let removedSuggestedActionId if (isOnboardTeam) { diff --git a/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts b/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts index 1590a4b281b..ff21995cf78 100644 --- a/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts +++ b/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts @@ -6,7 +6,7 @@ import isValid from '../../isValid' import {AcceptTeamInvitationPayloadResolvers} from '../resolverTypes' export type AcceptTeamInvitationPayloadSource = { - meetingId?: string + meetingId?: string | null teamId?: string teamMemberId?: string invitationNotificationIds?: string[] diff --git a/packages/server/postgres/migrations/1728425112300_TeamInvitation-phase1.ts b/packages/server/postgres/migrations/1728425112300_TeamInvitation-phase1.ts new file mode 100644 index 00000000000..875feca6b68 --- /dev/null +++ b/packages/server/postgres/migrations/1728425112300_TeamInvitation-phase1.ts @@ -0,0 +1,60 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "TeamInvitation" ( + "id" VARCHAR(100) NOT NULL PRIMARY KEY, + "acceptedAt" TIMESTAMP WITH TIME ZONE, + "acceptedBy" VARCHAR(100), + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "expiresAt" TIMESTAMP WITH TIME ZONE NOT NULL, + "email" "citext" NOT NULL, + "invitedBy" VARCHAR(100) NOT NULL, + "isMassInvite" BOOLEAN NOT NULL DEFAULT FALSE, + "meetingId" VARCHAR(100), + "teamId" VARCHAR(100) NOT NULL, + "token" VARCHAR(200) NOT NULL, + CONSTRAINT "fk_meetingId" + FOREIGN KEY("meetingId") + REFERENCES "NewMeeting"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_invitedBy" + FOREIGN KEY("invitedBy") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_acceptedBy" + FOREIGN KEY("acceptedBy") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_TeamInvitation_email" ON "TeamInvitation"("email"); + CREATE INDEX IF NOT EXISTS "idx_TeamInvitation_teamId" ON "TeamInvitation"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_TeamInvitation_token" ON "TeamInvitation"("token"); + END $$; +`.execute(pg) +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "TeamInvitation"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 1f2461e231a..2b3c4421d72 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -283,3 +283,5 @@ export const selectMeetingMembers = () => export const selectMassInvitations = () => getKysely().selectFrom('MassInvitation').selectAll() export const selectNewFeatures = () => getKysely().selectFrom('NewFeature').selectAll() + +export const selectTeamInvitations = () => getKysely().selectFrom('TeamInvitation').selectAll() diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 3db0a425a1b..587e2372564 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -16,6 +16,7 @@ import { selectSlackAuths, selectSlackNotifications, selectSuggestedAction, + selectTeamInvitations, selectTeamPromptResponses, selectTeams, selectTemplateScale, @@ -74,3 +75,4 @@ export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect export type NewFeature = ExtractTypeFromQueryBuilderSelect +export type TeamInvitation = ExtractTypeFromQueryBuilderSelect diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index 63171c108eb..8c7f3a425c0 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -93,6 +93,14 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data .set({tms: sql`arr_append_uniq("tms", ${teamId})`}) .where('id', '=', userId) ) + .with('TeamInvitationUpdate', (qb) => + // redeem all invitations, otherwise if they have 2 someone could join after they've been kicked out + qb + .updateTable('TeamInvitation') + .set({acceptedAt: sql`CURRENT_TIMESTAMP`, acceptedBy: userId}) + .where('email', '=', email) + .where('teamId', '=', teamId) + ) .insertInto('TeamMember') .values({ id: TeamMemberId.join(teamId, userId), @@ -108,7 +116,6 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data r .table('TeamInvitation') .getAll(teamId, {index: 'teamId'}) - // redeem all invitations, otherwise if they have 2 someone could join after they've been kicked out .filter({email}) .update({ acceptedAt: now, diff --git a/packages/server/safeMutations/safeArchiveTeam.ts b/packages/server/safeMutations/safeArchiveTeam.ts index ea18bb807da..8194b4c2577 100644 --- a/packages/server/safeMutations/safeArchiveTeam.ts +++ b/packages/server/safeMutations/safeArchiveTeam.ts @@ -1,3 +1,4 @@ +import {sql} from 'kysely' import getRethink from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' import {DataLoaderWorker} from '../graphql/graphql' @@ -32,6 +33,13 @@ const safeArchiveTeam = async (teamId: string, dataLoader: DataLoaderWorker) => .returningAll() .executeTakeFirst(), pg + .with('TeamInvitationUpdate', (qb) => + qb + .updateTable('TeamInvitation') + .set({expiresAt: sql`CURRENT_TIMESTAMP`}) + .where('teamId', '=', teamId) + .where('acceptedAt', 'is', null) + ) .updateTable('User') .set(({fn, ref, val}) => ({tms: fn('ARRAY_REMOVE', [ref('tms'), val(teamId)])})) .where('id', 'in', userIds) diff --git a/packages/server/utils/getBestInvitationMeeting.ts b/packages/server/utils/getBestInvitationMeeting.ts index 3b36f940ebf..8138915f558 100644 --- a/packages/server/utils/getBestInvitationMeeting.ts +++ b/packages/server/utils/getBestInvitationMeeting.ts @@ -2,7 +2,7 @@ import {DataLoaderWorker} from '../graphql/graphql' const getBestInvitationMeeting = async ( teamId: string, - maybeMeetingId: string | undefined, + maybeMeetingId: string | undefined | null, dataLoader: DataLoaderWorker ) => { const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) From 7407c2c7968185fca93db1123429aec28c311b05 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 9 Oct 2024 12:24:38 -0700 Subject: [PATCH 66/89] chore: migrate rows to PG Signed-off-by: Matt Krick --- .../1728496970486_TeamInvitation-phase2.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 packages/server/postgres/migrations/1728496970486_TeamInvitation-phase2.ts diff --git a/packages/server/postgres/migrations/1728496970486_TeamInvitation-phase2.ts b/packages/server/postgres/migrations/1728496970486_TeamInvitation-phase2.ts new file mode 100644 index 00000000000..54271a6721e --- /dev/null +++ b/packages/server/postgres/migrations/1728496970486_TeamInvitation-phase2.ts @@ -0,0 +1,134 @@ +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('TeamInvitation') + .indexCreate('updatedAtId', (row: any) => [row('expiresAt'), row('id')]) + .run() + await r.table('TeamInvitation').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'acceptedAt', + 'acceptedBy', + 'createdAt', + 'expiresAt', + 'email', + 'invitedBy', + 'isMassInvite', + 'meetingId', + 'teamId', + 'token' + ] as const + type TeamInvitation = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + let curUpdatedAt = new Date() + let curId = r.minval + + const insertRow = async (row) => { + try { + await pg + .insertInto('TeamInvitation') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_teamId') { + console.log('TeamInvitation has no team, skipping insert', row.id) + return + } + if (e.constraint === 'fk_meetingId') { + console.log('TeamInvitation has no meeting, skipping insert', row.id) + return + } + if (e.constraint === 'fk_acceptedBy') { + console.log('TeamInvitation has no acceptedBy user, skipping insert', row.id) + return + } + if (e.constraint === 'fk_invitedBy') { + console.log('TeamInvitation has no invitedBy user, skipping insert', row.id) + return + } + throw e + } + } + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('TeamInvitation') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as TeamInvitation[] + + const rowsToInsert = rawRowsToInsert.map((row) => { + const { + id, + acceptedAt, + acceptedBy, + createdAt, + expiresAt, + email, + invitedBy, + isMassInvite, + meetingId, + teamId, + token + } = row as any + return { + id, + acceptedAt, + acceptedBy, + createdAt, + expiresAt, + email, + invitedBy, + isMassInvite, + meetingId, + teamId, + token + } + }) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.expiresAt + curId = lastRow.id + await Promise.all(rowsToInsert.map(async (row) => insertRow(row))) + } +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "TeamInvitation" CASCADE`.execute(pg) +} From b53fbd8cef9d3222c9625a47d1b3c95fe28833ff Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 9 Oct 2024 12:42:47 -0700 Subject: [PATCH 67/89] chore: move reads to PG Signed-off-by: Matt Krick --- .../dataloader/foreignKeyLoaderMakers.ts | 4 ++-- .../dataloader/primaryKeyLoaderMakers.ts | 2 +- .../rethinkForeignKeyLoaderMakers.ts | 15 ------------ .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../helpers/handleMassInviteToken.ts | 9 +------- .../helpers/handleTeamInviteToken.ts | 15 ++++++------ .../mutations/helpers/inviteToTeamHelper.ts | 23 +++++++++++-------- .../private/mutations/hardDeleteUser.ts | 12 +--------- .../graphql/queries/verifiedInvitation.ts | 17 +++++++------- .../safeMutations/acceptTeamInvitation.ts | 12 +--------- .../server/safeMutations/safeArchiveTeam.ts | 15 +----------- 11 files changed, 38 insertions(+), 87 deletions(-) diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 522dbcd1ba2..5f9131e0708 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -283,8 +283,8 @@ export const massInvitationsByTeamMemberId = foreignKeyLoaderMaker( } ) -export const _pgteamInvitationsByTeamId = foreignKeyLoaderMaker( - '_pgteamInvitations', +export const teamInvitationsByTeamId = foreignKeyLoaderMaker( + 'teamInvitations', 'teamId', async (teamIds) => { return selectTeamInvitations() diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 811069fb210..fe94fd0ea23 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -141,6 +141,6 @@ export const newFeatures = primaryKeyLoaderMaker((ids: readonly number[]) => { return selectNewFeatures().where('id', 'in', ids).execute() }) -export const _pgteamInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => { +export const teamInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectTeamInvitations().where('id', 'in', ids).execute() }) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index d558c0f190c..e7d6a9342b7 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -30,18 +30,3 @@ export const tasksByTeamId = new RethinkForeignKeyLoaderMaker( .run() } ) - -export const teamInvitationsByTeamId = new RethinkForeignKeyLoaderMaker( - 'teamInvitations', - 'teamId', - async (teamIds) => { - const r = await getRethink() - const now = new Date() - return r - .table('TeamInvitation') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter({acceptedAt: null}) - .filter((row: RDatum) => row('expiresAt').ge(now)) - .run() - } -) diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index d0f67f23712..973c2c959d0 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -5,4 +5,3 @@ import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' */ export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') export const tasks = new RethinkPrimaryKeyLoaderMaker('Task') -export const teamInvitations = new RethinkPrimaryKeyLoaderMaker('TeamInvitation') diff --git a/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts b/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts index 8c6d0200bae..4a4fa2d588a 100644 --- a/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts +++ b/packages/server/graphql/mutations/helpers/handleMassInviteToken.ts @@ -1,5 +1,4 @@ import {InvitationTokenError} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {verifyMassInviteToken} from '../../../utils/massInviteToken' @@ -13,7 +12,6 @@ const handleMassInviteToken = async ( ) => { const validToken = await verifyMassInviteToken(invitationToken, dataLoader) if ('error' in validToken) return {error: validToken.error} - const r = await getRethink() const {teamId, userId: invitedBy, exp: expiresAt, meetingId} = validToken if (tms?.includes(teamId)) { return {error: InvitationTokenError.ALREADY_ACCEPTED, teamId, meetingId} @@ -26,14 +24,9 @@ const handleMassInviteToken = async ( teamId, expiresAt, email, - isMassInvite: true, - acceptedAt: null + isMassInvite: true } await getKysely().insertInto('TeamInvitation').values(invitation).execute() - await r - .table('TeamInvitation') - .insert({...invitation, createdAt: new Date()}) - .run() return {invitation} } diff --git a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts index 544dc5b1a83..8205be8c520 100644 --- a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts +++ b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts @@ -1,6 +1,6 @@ import {InvitationTokenError} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import {TeamInvitation} from '../../../postgres/types' +import getKysely from '../../../postgres/getKysely' const handleTeamInviteToken = async ( invitationToken: string, @@ -9,12 +9,13 @@ const handleTeamInviteToken = async ( notificationId?: string ) => { const r = await getRethink() - const invitation = (await r - .table('TeamInvitation') - .getAll(invitationToken, {index: 'token'}) - .nth(0) - .default(null) - .run()) as TeamInvitation + const pg = getKysely() + const invitation = await pg + .selectFrom('TeamInvitation') + .selectAll() + .where('token', '=', invitationToken) + .limit(1) + .executeTakeFirst() if (!invitation) return {error: InvitationTokenError.NOT_FOUND} const {expiresAt} = invitation if (expiresAt.getTime() < Date.now()) { diff --git a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts index 561febbd547..81688c94fbc 100644 --- a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts +++ b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts @@ -39,15 +39,21 @@ const inviteToTeamHelper = async ( const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} - const [total, pending] = await Promise.all([ - r.table('TeamInvitation').getAll(teamId, {index: 'teamId'}).count().run(), - r - .table('TeamInvitation') - .getAll(teamId, {index: 'teamId'}) - .filter({acceptedAt: null}) - .count() - .run() + const [totalRes, pendingRes] = await Promise.all([ + pg + .selectFrom('TeamInvitation') + .select(({fn}) => fn.count('id').as('count')) + .where('teamId', '=', teamId) + .executeTakeFirstOrThrow(), + pg + .selectFrom('TeamInvitation') + .select(({fn}) => fn.count('id').as('count')) + .where('teamId', '=', teamId) + .where('acceptedAt', 'is', null) + .executeTakeFirstOrThrow() ]) + const total = Number(totalRes.count) + const pending = Number(pendingRes.count) const accepted = total - pending // if no one has accepted one of their 100+ invites, don't trust them if (accepted === 0 && total + invitees.length >= 100) { @@ -126,7 +132,6 @@ const inviteToTeamHelper = async ( createdAt: new Date(), acceptedAt: null })) - await r.table('TeamInvitation').insert(teamInvitationsToInsert).run() await pg.insertInto('TeamInvitation').values(teamInvitationsToInsert).execute() // remove suggested action, if any let removedSuggestedActionId diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 8effc9a13ad..63f9571af59 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -91,17 +91,7 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .table('Task') .getAll(r.args(teamIds), {index: 'teamId'}) .filter((row: RValue) => row('createdBy').eq(userIdToDelete)) - .delete(), - invitedByTeamInvitation: r - .table('TeamInvitation') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RValue) => row('invitedBy').eq(userIdToDelete)) - .delete(), - createdByTeamInvitations: r - .table('TeamInvitation') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RValue) => row('acceptedBy').eq(userIdToDelete)) - .update({acceptedBy: ''}) + .delete() }).run() // now postgres, after FKs are added then triggers should take care of children diff --git a/packages/server/graphql/queries/verifiedInvitation.ts b/packages/server/graphql/queries/verifiedInvitation.ts index c4f959b4bc4..1eae17749ef 100644 --- a/packages/server/graphql/queries/verifiedInvitation.ts +++ b/packages/server/graphql/queries/verifiedInvitation.ts @@ -3,7 +3,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {InvitationTokenError} from 'parabol-client/types/constEnums' import util from 'util' import {AuthIdentityTypeEnum} from '../../../client/types/constEnums' -import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {getUserByEmail} from '../../postgres/queries/getUsersByEmails' import IUser from '../../postgres/types/IUser' import getBestInvitationMeeting from '../../utils/getBestInvitationMeeting' @@ -41,14 +41,15 @@ export default { }, resolve: rateLimit({perMinute: 60, perHour: 1800})( async (_source: unknown, {token}, {dataLoader}: GQLContext) => { - const r = await getRethink() + const pg = getKysely() const now = new Date() - const teamInvitation = await r - .table('TeamInvitation') - .getAll(token, {index: 'token'}) - .nth(0) - .default(null) - .run() + const teamInvitation = await pg + .selectFrom('TeamInvitation') + .selectAll() + .where('token', '=', token) + .limit(1) + .executeTakeFirst() + if (!teamInvitation) return {errorType: InvitationTokenError.NOT_FOUND} const { email, diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index 8c7f3a425c0..c2cd852458d 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -63,7 +63,6 @@ const handleFirstAcceptedInvitation = async ( const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: DataLoaderWorker) => { const r = await getRethink() const pg = getKysely() - const now = new Date() const {id: teamId, orgId} = team const [user, organizationUser] = await Promise.all([ dataLoader.get('users').loadNonNull(userId), @@ -112,16 +111,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data openDrawer: 'manageTeam' }) .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false})) - .execute(), - r - .table('TeamInvitation') - .getAll(teamId, {index: 'teamId'}) - .filter({email}) - .update({ - acceptedAt: now, - acceptedBy: userId - }) - .run() + .execute() ]) dataLoader.clearAll(['teamMembers', 'users']) if (!organizationUser) { diff --git a/packages/server/safeMutations/safeArchiveTeam.ts b/packages/server/safeMutations/safeArchiveTeam.ts index 8194b4c2577..ea2a4ef5b78 100644 --- a/packages/server/safeMutations/safeArchiveTeam.ts +++ b/packages/server/safeMutations/safeArchiveTeam.ts @@ -1,25 +1,13 @@ import {sql} from 'kysely' -import getRethink from '../database/rethinkDriver' -import {RDatum} from '../database/stricterR' import {DataLoaderWorker} from '../graphql/graphql' import getKysely from '../postgres/getKysely' const safeArchiveTeam = async (teamId: string, dataLoader: DataLoaderWorker) => { - const r = await getRethink() const pg = getKysely() const now = new Date() const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) const userIds = teamMembers.map((tm) => tm.userId) - const [rethinkResult, removedSuggestedActions, team] = await Promise.all([ - r({ - invitations: r - .table('TeamInvitation') - .getAll(teamId, {index: 'teamId'}) - .filter({acceptedAt: null}) - .update((invitation: RDatum) => ({ - expiresAt: r.min([invitation('expiresAt'), now]) - })) as unknown as null - }).run(), + const [removedSuggestedActions, team] = await Promise.all([ pg .updateTable('SuggestedAction') .set({removedAt: now}) @@ -48,7 +36,6 @@ const safeArchiveTeam = async (teamId: string, dataLoader: DataLoaderWorker) => dataLoader.clearAll(['teamMembers', 'users', 'teams']) const users = await Promise.all(userIds.map((userId) => dataLoader.get('users').load(userId))) return { - invitations: rethinkResult.invitations, removedSuggestedActionIds: removedSuggestedActions.map(({id}) => id), team: team ?? null, users From 466c1758a6ec55624e2473428ad5276152216ea0 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 9 Oct 2024 13:04:50 -0700 Subject: [PATCH 68/89] remove unused references Signed-off-by: Matt Krick --- packages/server/database/rethinkDriver.ts | 11 +---- .../server/database/types/TeamInvitation.ts | 42 ------------------- packages/server/dataloader/RethinkDBCache.ts | 2 +- 3 files changed, 2 insertions(+), 53 deletions(-) delete mode 100644 packages/server/database/types/TeamInvitation.ts diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index dd498997ecf..649fd167cc7 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -1,5 +1,4 @@ import {MasterPool, r} from 'rethinkdb-ts' -import {TeamInvitation} from '../postgres/types/index' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' import NotificationKickedOut from './types/NotificationKickedOut' @@ -15,10 +14,6 @@ import NotificationTeamInvitation from './types/NotificationTeamInvitation' import Task from './types/Task' export type RethinkSchema = { - NewFeature: { - type: any - index: '' - } Notification: { type: | NotificationTaskInvolves @@ -44,14 +39,10 @@ export type RethinkSchema = { | 'userId' | 'integrationHash' } - TeamInvitation: { - type: TeamInvitation - index: 'email' | 'teamId' | 'token' - } } export type DBType = { - [P in keyof RethinkSchema]: RethinkSchema[P]['type'] + [P in keyof RethinkSchema]: any } export type ParabolR = R diff --git a/packages/server/database/types/TeamInvitation.ts b/packages/server/database/types/TeamInvitation.ts deleted file mode 100644 index ebfc1a41765..00000000000 --- a/packages/server/database/types/TeamInvitation.ts +++ /dev/null @@ -1,42 +0,0 @@ -import generateUID from '../../generateUID' -import getIsMassInviteToken from '../../graphql/mutations/helpers/getIsMassInviteToken' - -interface Input { - id?: string - acceptedAt?: Date - acceptedBy?: string - expiresAt: Date - email: string - invitedBy: string - meetingId?: string - teamId: string - token: string -} -export default class TeamInvitation { - id: string - acceptedAt: Date | null - acceptedBy?: string - createdAt: Date - expiresAt: Date - email: string - invitedBy: string - isMassInvite: boolean - meetingId?: string - teamId: string - token: string - constructor(input: Input) { - const {teamId, acceptedAt, acceptedBy, email, expiresAt, id, invitedBy, meetingId, token} = - input - this.id = id || generateUID() - this.acceptedAt = acceptedAt || null - this.acceptedBy = acceptedBy - this.createdAt = new Date() - this.expiresAt = expiresAt - this.email = email - this.invitedBy = invitedBy - this.isMassInvite = getIsMassInviteToken(token) - this.meetingId = meetingId - this.teamId = teamId - this.token = token - } -} diff --git a/packages/server/dataloader/RethinkDBCache.ts b/packages/server/dataloader/RethinkDBCache.ts index 7bf0ced8be7..b33a05a28fd 100644 --- a/packages/server/dataloader/RethinkDBCache.ts +++ b/packages/server/dataloader/RethinkDBCache.ts @@ -51,7 +51,7 @@ export default class RethinkDBCache { .table(table) .get(id) // "always" will return the document whether it has changed or not - .update(updater, {returnChanges: 'always'})('changes')(0)('new_val') + .update(updater as any, {returnChanges: 'always'})('changes')(0)('new_val') .default(null) ) }) From b4f9d2760dccd5394216afd355d947a2d7eeb1d2 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 9 Oct 2024 13:53:02 -0700 Subject: [PATCH 69/89] chore: first pass before taskId Signed-off-by: Matt Krick --- .../1728504371818_Notification-phase1.ts | 133 ++++++++++++++++++ .../server/postgres/types/Notification.d.ts | 0 2 files changed, 133 insertions(+) create mode 100644 packages/server/postgres/migrations/1728504371818_Notification-phase1.ts create mode 100644 packages/server/postgres/types/Notification.d.ts diff --git a/packages/server/postgres/migrations/1728504371818_Notification-phase1.ts b/packages/server/postgres/migrations/1728504371818_Notification-phase1.ts new file mode 100644 index 00000000000..96b76e93807 --- /dev/null +++ b/packages/server/postgres/migrations/1728504371818_Notification-phase1.ts @@ -0,0 +1,133 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + await sql` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'NotificationStatusEnum') THEN + CREATE TYPE "NotificationStatusEnum" AS ENUM ( + 'UNREAD', + 'CLICKED', + 'READ' + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'NotificationTypeEnum') THEN + CREATE TYPE "NotificationTypeEnum" AS ENUM ( + 'DISCUSSION_MENTIONED', + 'KICKED_OUT', + 'MEETING_STAGE_TIME_LIMIT_END', + 'PAYMENT_REJECTED', + 'PROMOTE_TO_BILLING_LEADER', + 'RESPONSE_MENTIONED', + 'RESPONSE_REPLIED', + 'MENTIONED', + 'TASK_INVOLVES', + 'TEAM_ARCHIVED', + 'TEAM_INVITATION', + 'TEAMS_LIMIT_EXCEEDED', + 'TEAMS_LIMIT_REMINDER', + 'PROMPT_TO_JOIN_ORG', + 'REQUEST_TO_JOIN_ORG' + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'TaskInvolvementEnum') THEN + CREATE TYPE "TaskInvolvementEnum" AS ENUM ( + 'ASSIGNEE', + 'MENTIONEE' + ); + END IF; + CREATE TABLE IF NOT EXISTS "Notification" ( + "id" VARCHAR(100) NOT NULL PRIMARY KEY, + "status" "NotificationStatusEnum" NOT NULL DEFAULT 'UNREAD', + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "type" "NotificationTypeEnum" NOT NULL, + "userId" VARCHAR(100) NOT NULL, + "changeAuthorId" VARCHAR(100), + "involvement" "TaskInvolvementEnum", + "taskId" VARCHAR(100), + "teamId" VARCHAR(100), + "archivorUserId" VARCHAR(100), + "meetingId" VARCHAR(100), + "orgId" VARCHAR(100), + "last4" SMALLINT, + "brand" VARCHAR(50), + "evictorUserId" VARCHAR(100), + "invitationId" VARCHAR(100), + "responseId" VARCHAR(100), + "authorId" VARCHAR(100), + "commentId" VARCHAR(100), + CONSTRAINT "fk_meetingId" + FOREIGN KEY("meetingId") + REFERENCES "NewMeeting"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_userId" + FOREIGN KEY("userId") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_changeAuthorId" + FOREIGN KEY("changeAuthorId") + REFERENCES "TeamMember"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_taskId" + FOREIGN KEY("taskId") + REFERENCES "Task"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_archivorUserId" + FOREIGN KEY("archivorUserId") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_orgId" + FOREIGN KEY("orgId") + REFERENCES "Organization"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_evictorUserId" + FOREIGN KEY("evictorUserId") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_invitationId" + FOREIGN KEY("invitationId") + REFERENCES "TeamInvitation"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_responseId" + FOREIGN KEY("responseId") + REFERENCES "TeamPromptResponse"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_authorId" + FOREIGN KEY("authorId") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_commentId" + FOREIGN KEY("commentId") + REFERENCES "Comment"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_Notification_userId" ON "Notification"("userId"); + CREATE INDEX IF NOT EXISTS "idx_Notification_teamId" ON "Notification"("teamId") WHERE "teamId" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_Notification_orgId" ON "Notification"("orgId") WHERE "orgId" IS NOT NULL; + END $$; +`.execute(pg) +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "Notification"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/types/Notification.d.ts b/packages/server/postgres/types/Notification.d.ts new file mode 100644 index 00000000000..e69de29bb2d From ea243a14e13b11ab92cc9778821dad1f39353436 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 10 Oct 2024 12:25:32 -0700 Subject: [PATCH 70/89] chore: add writes to PG Signed-off-by: Matt Krick --- .../dataloader/foreignKeyLoaderMakers.ts | 18 ++++ .../dataloader/primaryKeyLoaderMakers.ts | 5 + .../graphql/mutations/changeTaskTeam.ts | 25 ++++- .../server/graphql/mutations/createTask.ts | 7 +- .../mutations/createTaskIntegration.ts | 14 ++- .../server/graphql/mutations/deleteTask.ts | 5 +- .../server/graphql/mutations/endCheckIn.ts | 6 ++ .../graphql/mutations/helpers/addSeedTasks.ts | 4 +- .../mutations/helpers/importTasksForPoker.ts | 6 ++ .../mutations/helpers/removeEmptyTasks.ts | 3 + .../mutations/helpers/removeTeamMember.ts | 19 +++- .../resetRetroMeetingToGroupStage.ts | 3 + .../server/graphql/mutations/updateTask.ts | 18 +++- .../graphql/mutations/updateTaskDueDate.ts | 7 +- .../migrations/1728578190454_Task-phase1.ts | 97 +++++++++++++++++++ packages/server/postgres/select.ts | 26 +++++ .../postgres/types/TaskIntegration.d.ts | 44 +++++++++ .../server/safeMutations/archiveTasksForDB.ts | 11 +++ 18 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 packages/server/postgres/migrations/1728578190454_Task-phase1.ts create mode 100644 packages/server/postgres/types/TaskIntegration.d.ts diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 5f9131e0708..0c296d73386 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -13,6 +13,7 @@ import { selectSlackAuths, selectSlackNotifications, selectSuggestedAction, + selectTasks, selectTeamInvitations, selectTeams, selectTemplateDimension, @@ -294,3 +295,20 @@ export const teamInvitationsByTeamId = foreignKeyLoaderMaker( .execute() } ) + +export const _pgtasksByDiscussionId = foreignKeyLoaderMaker( + '_pgtasks', + 'discussionId', + async (discusisonIds) => { + // include archived cards in the conversation, since it's persistent + return selectTasks().where('discussionId', 'in', discusisonIds).execute() + } +) + +export const _pgtasksByTeamId = foreignKeyLoaderMaker('_pgtasks', 'teamId', async (teamIds) => { + // waraning! contains private tasks + return selectTasks() + .where('teamId', 'in', teamIds) + .where(sql`'archived' = ANY(tags)`) + .execute() +}) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index fe94fd0ea23..f53c962ba2d 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -19,6 +19,7 @@ import { selectSlackAuths, selectSlackNotifications, selectSuggestedAction, + selectTasks, selectTeamInvitations, selectTeamPromptResponses, selectTeams, @@ -144,3 +145,7 @@ export const newFeatures = primaryKeyLoaderMaker((ids: readonly number[]) => { export const teamInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectTeamInvitations().where('id', 'in', ids).execute() }) + +export const _pgtasks = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectTasks().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/changeTaskTeam.ts b/packages/server/graphql/mutations/changeTaskTeam.ts index e229536106c..93396501a5b 100644 --- a/packages/server/graphql/mutations/changeTaskTeam.ts +++ b/packages/server/graphql/mutations/changeTaskTeam.ts @@ -3,6 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import removeEntityKeepText from 'parabol-client/utils/draftjs/removeEntityKeepText' import getRethink from '../../database/rethinkDriver' import Task from '../../database/types/Task' +import getKysely from '../../postgres/getKysely' import {AtlassianAuth} from '../../postgres/queries/getAtlassianAuthByUserIdTeamId' import {GitHubAuth} from '../../postgres/queries/getGitHubAuthByUserIdTeamId' import upsertAtlassianAuths from '../../postgres/queries/upsertAtlassianAuths' @@ -33,6 +34,7 @@ export default { {taskId, teamId}: {taskId: string; teamId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const now = new Date() const operationId = dataLoader.share() @@ -43,7 +45,7 @@ export default { if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) } - const task = await r.table('Task').get(taskId).run() + const task = await dataLoader.get('tasks').load(taskId) if (!task) { return standardError(new Error('Task not found'), {userId: viewerId}) } @@ -155,7 +157,24 @@ export default { .default(null), newTask: r.table('Task').get(taskId).update(updates) }).run() - + await pg + .with('TaskDelete', (qb) => + qb + .deleteFrom('Task') + .where('integrationHash', '=', task.integrationHash) + .where('teamId', '=', teamId) + .limit(1) + .returning('id') + ) + .updateTable('Task') + .set({ + content: rawContent === nextRawContent ? undefined : JSON.stringify(nextRawContent), + teamId, + integration: JSON.stringify(integration) + }) + .where('id', '=', taskId) + .returning(({selectFrom}) => selectFrom('TaskDelete').select('id').as('deletedId')) + .executeTakeFirst() if (deletedConflictingIntegrationTask) { const task = deletedConflictingIntegrationTask as unknown as Task const isPrivate = task.tags.includes('private') @@ -166,7 +185,7 @@ export default { } }) } - + dataLoader.clearAll('tasks') const isPrivate = tags.includes('private') const data = {taskId} const teamMembers = oldTeamMembers.concat(newTeamMembers) diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts index 3aadc09b20a..8d31cb3e193 100644 --- a/packages/server/graphql/mutations/createTask.ts +++ b/packages/server/graphql/mutations/createTask.ts @@ -8,6 +8,7 @@ import getRethink from '../../database/rethinkDriver' import NotificationTaskInvolves from '../../database/types/NotificationTaskInvolves' import Task, {TaskServiceEnum} from '../../database/types/Task' import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache' +import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import publish, {SubOptions} from '../../utils/publish' @@ -152,6 +153,7 @@ export default { info: GraphQLResolveInfo ) { const {authToken, dataLoader, socketId: mutatorId} = context + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const viewerId = getUserId(authToken) @@ -222,7 +224,10 @@ export default { await r({ task: r.table('Task').insert(task) }).run() - + await pg + .insertInto('Task') + .values({...task, integration: JSON.stringify(integration)}) + .execute() handleAddTaskNotifications(teamMembers, task, viewerId, teamId, { operationId, mutatorId diff --git a/packages/server/graphql/mutations/createTaskIntegration.ts b/packages/server/graphql/mutations/createTaskIntegration.ts index d9969f94b9f..4bd681a5860 100644 --- a/packages/server/graphql/mutations/createTaskIntegration.ts +++ b/packages/server/graphql/mutations/createTaskIntegration.ts @@ -5,6 +5,7 @@ import appOrigin from '../../appOrigin' import getRethink from '../../database/rethinkDriver' import TaskIntegrationManagerFactory from '../../integrations/TaskIntegrationManagerFactory' import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import sendToSentry from '../../utils/sendToSentry' @@ -44,7 +45,7 @@ export default { info: GraphQLResolveInfo ) => { const {authToken, dataLoader, socketId: mutatorId} = context - + const pg = getKysely() const r = await getRethink() const now = new Date() const operationId = dataLoader.share() @@ -52,7 +53,7 @@ export default { const viewerId = getUserId(authToken) // AUTH - const task = await r.table('Task').get(taskId).run() + const task = await dataLoader.get('tasks').load(taskId) if (!task) { return standardError(new Error('Task not found'), {userId: viewerId}) } @@ -162,6 +163,15 @@ export default { }) .run() + await pg + .updateTable('Task') + .set({ + integration: JSON.stringify(updateTaskInput.integration), + integrationHash: updateTaskInput.integrationHash + }) + .execute() + + dataLoader.clearAll('tasks') const data = {taskId} teamMembers.forEach(({userId}) => { publish(SubscriptionChannel.TASK, userId, 'CreateTaskIntegrationPayload', data, subOptions) diff --git a/packages/server/graphql/mutations/deleteTask.ts b/packages/server/graphql/mutations/deleteTask.ts index 04288d2d8d5..e433b53b8b3 100644 --- a/packages/server/graphql/mutations/deleteTask.ts +++ b/packages/server/graphql/mutations/deleteTask.ts @@ -1,6 +1,7 @@ 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' @@ -21,13 +22,14 @@ export default { {taskId}: {taskId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) // AUTH - const task = await r.table('Task').get(taskId).run() + const task = await dataLoader.get('tasks').load(taskId) if (!task) { return {error: {message: 'Task not found'}} } @@ -42,6 +44,7 @@ export default { await r({ task: r.table('Task').get(taskId).delete() }).run() + await pg.deleteFrom('Task').where('id', '=', taskId).execute() const {tags, userId: taskUserId} = task const data = {task} diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index b5c65d89ec5..7cb747a7144 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -33,6 +33,7 @@ import updateTeamInsights from './helpers/updateTeamInsights' type SortOrderTask = Pick const updateTaskSortOrders = async (userIds: string[], tasks: SortOrderTask[]) => { + const pg = getKysely() const r = await getRethink() const taskMax = await ( r @@ -61,6 +62,11 @@ const updateTaskSortOrders = async (userIds: string[], tasks: SortOrderTask[]) = }) }) .run() + await Promise.all( + updatedTasks.map((task) => + pg.updateTable('Task').set({sortOrder: task.sortOrder}).where('id', '=', task.id).execute() + ) + ) return tasks } diff --git a/packages/server/graphql/mutations/helpers/addSeedTasks.ts b/packages/server/graphql/mutations/helpers/addSeedTasks.ts index 0bfc175e13c..509ef01f10d 100644 --- a/packages/server/graphql/mutations/helpers/addSeedTasks.ts +++ b/packages/server/graphql/mutations/helpers/addSeedTasks.ts @@ -5,6 +5,7 @@ import appOrigin from '../../../appOrigin' import getRethink from '../../../database/rethinkDriver' import {TaskStatusEnum} from '../../../database/types/Task' import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import {convertHtmlToTaskContent} from '../../../utils/draftjs/convertHtmlToTaskContent' const NORMAL_TASK_STRING = `This is a task card. They can be created here, in a meeting, or via an integration` @@ -37,6 +38,7 @@ function getSeedTasks(teamId: string) { } export default async (userId: string, teamId: string) => { + const pg = getKysely() const r = await getRethink() const now = new Date() @@ -50,6 +52,6 @@ export default async (userId: string, teamId: string) => { userId, updatedAt: now })) - + await pg.insertInto('Task').values(seedTasks).execute() return r.table('Task').insert(seedTasks).run() } diff --git a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts index 3c7d8858474..9d6d15eb564 100644 --- a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts +++ b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts @@ -2,6 +2,7 @@ import IntegrationHash from 'parabol-client/shared/gqlIds/IntegrationHash' import {isNotNull} from 'parabol-client/utils/predicates' import getRethink from '../../../database/rethinkDriver' import ImportedTask from '../../../database/types/ImportedTask' +import getKysely from '../../../postgres/getKysely' import {TUpdatePokerScopeItemInput} from '../updatePokerScope' const importTasksForPoker = async ( @@ -10,6 +11,7 @@ const importTasksForPoker = async ( userId: string, meetingId: string ) => { + const pg = getKysely() const r = await getRethink() const integratedUpdates = additiveUpdates.filter((update) => update.service !== 'PARABOL') const integrationHashes = integratedUpdates.map((update) => update.serviceTaskId) @@ -45,6 +47,10 @@ const importTasksForPoker = async ( .filter(isNotNull) if (newIntegrationUpdates.length > 0) { + await pg + .insertInto('Task') + .values(tasksToAdd.map((t) => ({...t, integration: JSON.stringify(t.integration)}))) + .execute() await r.table('Task').insert(tasksToAdd).run() } const integratedTasks = [...existingTasks, ...tasksToAdd] diff --git a/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts b/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts index f04bed65ac3..ce4dd8a0d41 100644 --- a/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts +++ b/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts @@ -1,7 +1,9 @@ import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' const removeEmptyTasks = async (meetingId: string, teamId: string) => { + const pg = getKysely() const r = await getRethink() const createdTasks = await r .table('Task') @@ -17,6 +19,7 @@ const removeEmptyTasks = async (meetingId: string, teamId: string) => { .filter(({plaintextContent}) => plaintextContent.length === 0) .map(({id}) => id) if (removedTaskIds.length > 0) { + await pg.deleteFrom('Task').where('id', 'in', removedTaskIds).execute() await r.table('Task').getAll(r.args(removedTaskIds)).delete().run() } return removedTaskIds diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 48e7551fa50..b7afbd2de7b 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -1,3 +1,4 @@ +import {sql} from 'kysely' import fromTeamMemberId from 'parabol-client/utils/relay/fromTeamMemberId' import getRethink from '../../../database/rethinkDriver' import {RDatum} from '../../../database/stricterR' @@ -54,7 +55,12 @@ const removeTeamMember = async ( if (willArchive) { await Promise.all([ // archive single-person teams - pg.updateTable('Team').set({isArchived: true}).where('id', '=', teamId).execute(), + pg + .with('TaskDelete', (qb) => qb.deleteFrom('Task').where('teamId', '=', teamId)) + .updateTable('Team') + .set({isArchived: true}) + .where('id', '=', teamId) + .execute(), // delete all tasks belonging to a 1-person team r.table('Task').getAll(teamId, {index: 'teamId'}).delete() ]) @@ -101,11 +107,20 @@ const removeTeamMember = async ( .default([]) as unknown as Task[] }).run() await pg + .with('TaskReassignment', (qb) => + qb + .updateTable('Task') + .set({userId: nextTeamLead.userId}) + .where('userId', '=', userId) + .where('teamId', '=', teamId) + .where('integration', 'is', null) + .where(sql`'archived' != ANY(tags)`) + ) .updateTable('User') .set(({fn, ref, val}) => ({tms: fn('ARRAY_REMOVE', [ref('tms'), val(teamId)])})) .where('id', '=', userId) .execute() - dataLoader.clearAll(['users', 'teamMembers']) + dataLoader.clearAll(['users', 'teamMembers', 'tasks']) const user = await dataLoader.get('users').load(userId) let notificationId: string | undefined diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index 6bd36cb6eb1..73debf3e5f5 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -108,6 +108,9 @@ const resetRetroMeetingToGroupStage = { .with('DeleteComments', (qb) => qb.deleteFrom('Comment').where('discussionId', 'in', discussionIdsToDelete) ) + .with('DeleteTasks', (qb) => + qb.deleteFrom('Task').where('discussionId', 'in', discussionIdsToDelete) + ) .with('ResetGroups', (qb) => qb .updateTable('RetroReflectionGroup') diff --git a/packages/server/graphql/mutations/updateTask.ts b/packages/server/graphql/mutations/updateTask.ts index 63c868fc6dd..b8471bf00b8 100644 --- a/packages/server/graphql/mutations/updateTask.ts +++ b/packages/server/graphql/mutations/updateTask.ts @@ -4,6 +4,7 @@ import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractText import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import getRethink from '../../database/rethinkDriver' import Task, {AreaEnum as TAreaEnum, TaskStatusEnum} from '../../database/types/Task' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -46,6 +47,7 @@ export default { {updatedTask}: UpdateTaskMutationVariables, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const now = new Date() const operationId = dataLoader.share() @@ -56,7 +58,7 @@ export default { const {id: taskId, userId: inputUserId, status, sortOrder, content} = updatedTask const validContent = normalizeRawDraftJS(content) const [task, viewer] = await Promise.all([ - r.table('Task').get(taskId).run(), + dataLoader.get('tasks').load(taskId), dataLoader.get('users').loadNonNull(viewerId) ]) if (!task) { @@ -94,6 +96,20 @@ export default { .update(nextTask, {returnChanges: true})('changes')(0)('new_val') .default(null) as unknown as Task }).run() + await pg + .updateTable('Task') + .set({ + content: content ? validContent : undefined, + plaintextContent: content + ? extractTextFromDraftString(validContent) + : task.plaintextContent, + sortOrder: sortOrder || undefined, + status: status || undefined, + userId: inputUserId || undefined + }) + .where('id', '=', taskId) + .execute() + dataLoader.clearAll('tasks') // TODO: get users in the same location const usersToIgnore = await getUsersToIgnore(viewerId, teamId) if (!newTask) return standardError(new Error('Already updated task'), {userId: viewerId}) diff --git a/packages/server/graphql/mutations/updateTaskDueDate.ts b/packages/server/graphql/mutations/updateTaskDueDate.ts index 40b3a2e85c0..7660dab5521 100644 --- a/packages/server/graphql/mutations/updateTaskDueDate.ts +++ b/packages/server/graphql/mutations/updateTaskDueDate.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import isValidDate from 'parabol-client/utils/isValidDate' import getRethink from '../../database/rethinkDriver' +import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -28,6 +29,7 @@ export default { {taskId, dueDate}: {taskId: string; dueDate: string | null}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -39,7 +41,7 @@ export default { const formattedDueDate = dueDate && new Date(dueDate) const nextDueDate = isValidDate(formattedDueDate) ? formattedDueDate : null const [task, viewer] = await Promise.all([ - r.table('Task').get(taskId).run(), + dataLoader.get('tasks').load(taskId), dataLoader.get('users').loadNonNull(viewerId) ]) if (!task || !isTeamMember(authToken, task.teamId)) { @@ -55,6 +57,9 @@ export default { }) .run() + await pg.updateTable('Task').set({dueDate: nextDueDate}).where('id', '=', taskId).execute() + + dataLoader.clearAll('tasks') const data = {taskId} // send task updated messages diff --git a/packages/server/postgres/migrations/1728578190454_Task-phase1.ts b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts new file mode 100644 index 00000000000..52cc59447a5 --- /dev/null +++ b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts @@ -0,0 +1,97 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {Client} from 'pg' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' +import getPgConfig from '../getPgConfig' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'TaskStatusEnum') THEN + CREATE TYPE "TaskStatusEnum" AS ENUM ( + 'active', + 'stuck', + 'done', + 'future' + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'TaskTagEnum') THEN + CREATE TYPE "TaskTagEnum" AS ENUM ( + 'private', + 'archived' + ); + END IF; + CREATE TABLE IF NOT EXISTS "Task" ( + "id" VARCHAR(100) NOT NULL PRIMARY KEY, + "content" VARCHAR(2000) NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "createdBy" VARCHAR(100) NOT NULL, + "doneMeetingId" VARCHAR(100), + "dueDate" TIMESTAMP WITH TIME ZONE, + "integration" JSONB, + "integrationHash" VARCHAR(100), + "meetingId" VARCHAR(100), + "plaintextContent" VARCHAR(2000) NOT NULL, + "sortOrder" DOUBLE PRECISION NOT NULL DEFAULT 0, + "status" "TaskStatusEnum" NOT NULL DEFAULT 'active', + "tags" "TaskTagEnum"[] NOT NULL DEFAULT ARRAY[]::"TaskTagEnum"[], + "teamId" VARCHAR(100) NOT NULL, + "discussionId" VARCHAR(100), + "threadParentId" VARCHAR(100), + "threadSortOrder" INTEGER, + "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), + "userId" VARCHAR(100), + CONSTRAINT "fk_createdBy" + FOREIGN KEY("createdBy") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_doneMeetingId" + FOREIGN KEY("doneMeetingId") + REFERENCES "NewMeeting"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_meetingId" + FOREIGN KEY("meetingId") + REFERENCES "NewMeeting"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_discussionId" + FOREIGN KEY("discussionId") + REFERENCES "Discussion"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_userId" + FOREIGN KEY("userId") + REFERENCES "User"("id") + ON DELETE SET NULL + ); + CREATE INDEX IF NOT EXISTS "idx_Task_createdBy" ON "Task"("createdBy"); + CREATE INDEX IF NOT EXISTS "idx_Task_discussionId" ON "Task"("discussionId") WHERE "discussionId" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_Task_integrationHash" ON "Task"("integrationHash") WHERE "integrationHash" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_Task_meetingId" ON "Task"("meetingId") WHERE "meetingId" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_Task_teamId_updatedAt" ON "Task"("teamId", "updatedAt" DESC); + CREATE INDEX IF NOT EXISTS "idx_Task_threadParentId" ON "Task"("threadParentId") WHERE "threadParentId" IS NOT NULL; + CREATE INDEX IF NOT EXISTS "idx_Task_userId" ON "Task"("userId") WHERE "userId" IS NOT NULL; + DROP TRIGGER IF EXISTS "update_Task_updatedAt" ON "Task"; + CREATE TRIGGER "update_Task_updatedAt" BEFORE UPDATE ON "Task" FOR EACH ROW EXECUTE PROCEDURE "set_updatedAt"(); + + END $$; +`.execute(pg) +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "Task"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 2b3c4421d72..7a679f8b91e 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -4,6 +4,7 @@ import {NewMeetingPhaseTypeEnum} from '../graphql/public/resolverTypes' import getKysely from './getKysely' import {ReactjiDB} from './types' import {AnyMeeting, AnyMeetingMember} from './types/Meeting' +import {AnyTaskIntegration} from './types/TaskIntegration' export const selectTimelineEvent = () => { return getKysely().selectFrom('TimelineEvent').selectAll().$narrowType< | { @@ -285,3 +286,28 @@ export const selectMassInvitations = () => getKysely().selectFrom('MassInvitatio export const selectNewFeatures = () => getKysely().selectFrom('NewFeature').selectAll() export const selectTeamInvitations = () => getKysely().selectFrom('TeamInvitation').selectAll() + +export const selectTasks = () => + getKysely() + .selectFrom('Task') + .select(({fn}) => [ + 'id', + 'content', + 'createdAt', + 'createdBy', + 'doneMeetingId', + 'dueDate', + 'integrationHash', + 'meetingId', + 'plaintextContent', + 'sortOrder', + 'status', + 'tags', + 'teamId', + 'discussionId', + 'threadParentId', + 'threadSortOrder', + 'updatedAt', + 'userId', + fn('to_json', ['integration']).as('integration') + ]) diff --git a/packages/server/postgres/types/TaskIntegration.d.ts b/packages/server/postgres/types/TaskIntegration.d.ts new file mode 100644 index 00000000000..75ca80ad75f --- /dev/null +++ b/packages/server/postgres/types/TaskIntegration.d.ts @@ -0,0 +1,44 @@ +interface BaseTaskIntegration { + service: 'jira' | 'jiraServer' | 'github' | 'gitlab' | 'azureDevOps' + accessUserId: string +} + +interface TaskIntegrationJira extends BaseTaskIntegration { + service: 'jira' + cloudId: string + issueKey: string + projectKey: string +} + +interface TaskIntegrationJiraServer extends BaseTaskIntegration { + service: 'jiraServer' + providerId: number + issueId: string + repositoryId: string +} + +interface TaskIntegrationGitHub extends BaseTaskIntegration { + service: 'github' + nameWithOwner: string + issueNumber: number +} + +interface TaskIntegrationGitLab extends BaseTaskIntegration { + service: 'gitlab' + providerId: string + gid: string +} + +interface TaskIntegrationAzureDevOps extends BaseTaskIntegration { + service: 'azureDevOps' + instanceId: string + issueKey: string + projectKey: string +} + +export type AnyTaskIntegration = + | TaskIntegrationJira + | TaskIntegrationJiraServer + | TaskIntegrationGitHub + | TaskIntegrationGitLab + | TaskIntegrationAzureDevOps diff --git a/packages/server/safeMutations/archiveTasksForDB.ts b/packages/server/safeMutations/archiveTasksForDB.ts index 8efd00a6692..4197a64d685 100644 --- a/packages/server/safeMutations/archiveTasksForDB.ts +++ b/packages/server/safeMutations/archiveTasksForDB.ts @@ -3,9 +3,11 @@ import addTagToTask from 'parabol-client/utils/draftjs/addTagToTask' import getTagsFromEntityMap from 'parabol-client/utils/draftjs/getTagsFromEntityMap' import getRethink from '../database/rethinkDriver' import Task from '../database/types/Task' +import getKysely from '../postgres/getKysely' const archiveTasksForDB = async (tasks: Task[], doneMeetingId?: string) => { if (!tasks || tasks.length === 0) return [] + const pg = getKysely() const r = await getRethink() const tasksToArchive = tasks.map((task) => { const contentState = convertFromRaw(JSON.parse(task.content)) @@ -25,6 +27,15 @@ const archiveTasksForDB = async (tasks: Task[], doneMeetingId?: string) => { id: task.id } }) + await Promise.all( + tasksToArchive.map((t) => + pg + .updateTable('Task') + .set({content: t.content, tags: t.tags, doneMeetingId: t.doneMeetingId}) + .where('id', '=', t.id) + .execute() + ) + ) return r(tasksToArchive) .forEach((task) => { return r From 204a8d5024cdb233eb8f07dd9c9a2ca084a6fe10 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 10 Oct 2024 14:13:15 -0700 Subject: [PATCH 71/89] fix: await create team Signed-off-by: Matt Krick --- .../server/graphql/mutations/helpers/bootstrapNewUser.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts index 16bdec6f6b2..3c4dc2e826e 100644 --- a/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts +++ b/packages/server/graphql/mutations/helpers/bootstrapNewUser.ts @@ -124,11 +124,8 @@ const bootstrapNewUser = async ( } const orgName = `${newUser.preferredName}’s Org` await createNewOrg(orgId, orgName, userId, email, dataLoader) - await Promise.all([ - createTeamAndLeader(newUser as IUser, validNewTeam, dataLoader), - addSeedTasks(userId, teamId), - sendPromptToJoinOrg(newUser, dataLoader) - ]) + await createTeamAndLeader(newUser as IUser, validNewTeam, dataLoader) + await Promise.all([addSeedTasks(userId, teamId), sendPromptToJoinOrg(newUser, dataLoader)]) analytics.newOrg(newUser, orgId, teamId, true) } From badcd09a1ab76e313fbd5dedde8c659f03fd16c5 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 10 Oct 2024 14:28:40 -0700 Subject: [PATCH 72/89] migrate first pass Signed-off-by: Matt Krick --- .../migrations/1728595090540_Task-phase2.ts | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 packages/server/postgres/migrations/1728595090540_Task-phase2.ts diff --git a/packages/server/postgres/migrations/1728595090540_Task-phase2.ts b/packages/server/postgres/migrations/1728595090540_Task-phase2.ts new file mode 100644 index 00000000000..c62b402a7c0 --- /dev/null +++ b/packages/server/postgres/migrations/1728595090540_Task-phase2.ts @@ -0,0 +1,166 @@ +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('Task') + .indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')]) + .run() + await r.table('Task').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'content', + 'createdAt', + 'createdBy', + 'doneMeetingId', + 'dueDate', + 'integration', + 'integrationHash', + 'meetingId', + 'plaintextContent', + 'sortOrder', + 'status', + 'tags', + 'teamId', + 'discussionId', + 'threadParentId', + 'threadSortOrder', + 'updatedAt', + 'userId' + ] as const + type Task = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + let curUpdatedAt = new Date() + let curId = r.minval + + const insertRow = async (row) => { + try { + await pg + .insertInto('Task') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_teamId') { + console.log('Task has no team, skipping insert', row.id) + return + } + if (e.constraint === 'fk_meetingId') { + console.log('Task has no meeting, skipping insert', row.id) + return + } + if (e.constraint === 'fk_discussionId') { + console.log('Task has no discussionId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_createdBy') { + console.log('Task has no createdBy user, skipping insert', row.id) + return + } + if (e.constraint === 'fk_doneMeetingId') { + console.log('Task has no doneMeetingId user, skipping insert', row.id) + return + } + if (e.constraint === 'fk_userId') { + console.log('Task has no userId user, skipping insert', row.id) + return + } + throw e + } + } + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('Task') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as Task[] + + const rowsToInsert = rawRowsToInsert.map((row) => { + const { + id, + content, + createdAt, + createdBy, + doneMeetingId, + dueDate, + integration, + integrationHash, + meetingId, + plaintextContent, + sortOrder, + status, + tags, + teamId, + discussionId, + threadParentId, + threadSortOrder, + updatedAt, + userId + } = row as any + return { + id, + content: JSON.stringify(content), + createdAt, + createdBy, + doneMeetingId, + dueDate, + integration: JSON.stringify(integration), + integrationHash, + meetingId, + plaintextContent, + sortOrder, + status, + tags, + teamId, + discussionId, + threadParentId, + threadSortOrder, + updatedAt, + userId + } + }) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.updatedAt + curId = lastRow.id + await Promise.all(rowsToInsert.map(async (row) => insertRow(row))) + } +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "Task" CASCADE`.execute(pg) +} From 1f7a9f0bec7019efd3f01065ed59c8ab78081be0 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 10 Oct 2024 14:33:59 -0700 Subject: [PATCH 73/89] make content jsonb Signed-off-by: Matt Krick --- .../server/postgres/migrations/1728578190454_Task-phase1.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/postgres/migrations/1728578190454_Task-phase1.ts b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts index 52cc59447a5..d93c5cc0de6 100644 --- a/packages/server/postgres/migrations/1728578190454_Task-phase1.ts +++ b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts @@ -30,7 +30,7 @@ export async function up() { END IF; CREATE TABLE IF NOT EXISTS "Task" ( "id" VARCHAR(100) NOT NULL PRIMARY KEY, - "content" VARCHAR(2000) NOT NULL, + "content" JSONB NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), "createdBy" VARCHAR(100) NOT NULL, "doneMeetingId" VARCHAR(100), From 31d444a46ad47cd5388cb132497e9a2a322907b8 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 11 Oct 2024 08:50:47 -0700 Subject: [PATCH 74/89] migrate old rows Signed-off-by: Matt Krick --- .../postgres/migrations/1728578190454_Task-phase1.ts | 4 ++-- .../postgres/migrations/1728595090540_Task-phase2.ts | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/server/postgres/migrations/1728578190454_Task-phase1.ts b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts index d93c5cc0de6..7fba8b32c38 100644 --- a/packages/server/postgres/migrations/1728578190454_Task-phase1.ts +++ b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts @@ -36,9 +36,9 @@ export async function up() { "doneMeetingId" VARCHAR(100), "dueDate" TIMESTAMP WITH TIME ZONE, "integration" JSONB, - "integrationHash" VARCHAR(100), + "integrationHash" VARCHAR(200), "meetingId" VARCHAR(100), - "plaintextContent" VARCHAR(2000) NOT NULL, + "plaintextContent" VARCHAR(10000) NOT NULL, "sortOrder" DOUBLE PRECISION NOT NULL DEFAULT 0, "status" "TaskStatusEnum" NOT NULL DEFAULT 'active', "tags" "TaskTagEnum"[] NOT NULL DEFAULT ARRAY[]::"TaskTagEnum"[], diff --git a/packages/server/postgres/migrations/1728595090540_Task-phase2.ts b/packages/server/postgres/migrations/1728595090540_Task-phase2.ts index c62b402a7c0..e5e0b160b66 100644 --- a/packages/server/postgres/migrations/1728595090540_Task-phase2.ts +++ b/packages/server/postgres/migrations/1728595090540_Task-phase2.ts @@ -51,7 +51,7 @@ export async function up() { } const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) - let curUpdatedAt = new Date() + let curUpdatedAt = r.minval let curId = r.minval const insertRow = async (row) => { @@ -86,6 +86,10 @@ export async function up() { console.log('Task has no userId user, skipping insert', row.id) return } + if (e.message.includes('invalid input value for enum "TaskTagEnum"')) { + console.log('Task has invalid enum, skipping insert', row.id) + return + } throw e } } @@ -135,14 +139,14 @@ export async function up() { integration: JSON.stringify(integration), integrationHash, meetingId, - plaintextContent, + plaintextContent: plaintextContent.slice(0, 2000), sortOrder, status, tags, teamId, discussionId, threadParentId, - threadSortOrder, + threadSortOrder: threadSortOrder ? Math.round(threadSortOrder) : null, updatedAt, userId } From aaafa384bc14e7c50761ed860d38fcd8286f3f24 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 11 Oct 2024 08:51:35 -0700 Subject: [PATCH 75/89] fix varchar lengths Signed-off-by: Matt Krick --- .../server/postgres/migrations/1728578190454_Task-phase1.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/postgres/migrations/1728578190454_Task-phase1.ts b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts index d93c5cc0de6..7fba8b32c38 100644 --- a/packages/server/postgres/migrations/1728578190454_Task-phase1.ts +++ b/packages/server/postgres/migrations/1728578190454_Task-phase1.ts @@ -36,9 +36,9 @@ export async function up() { "doneMeetingId" VARCHAR(100), "dueDate" TIMESTAMP WITH TIME ZONE, "integration" JSONB, - "integrationHash" VARCHAR(100), + "integrationHash" VARCHAR(200), "meetingId" VARCHAR(100), - "plaintextContent" VARCHAR(2000) NOT NULL, + "plaintextContent" VARCHAR(10000) NOT NULL, "sortOrder" DOUBLE PRECISION NOT NULL DEFAULT 0, "status" "TaskStatusEnum" NOT NULL DEFAULT 'active', "tags" "TaskTagEnum"[] NOT NULL DEFAULT ARRAY[]::"TaskTagEnum"[], From c31c61865294447185177f512dbf495f59bf2190 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 11 Oct 2024 09:51:12 -0700 Subject: [PATCH 76/89] refactor loaders --- .../RethinkForeignKeyLoaderMaker.ts | 14 ---- packages/server/dataloader/RootDataLoader.ts | 17 +---- .../server/dataloader/customLoaderMakers.ts | 75 ++++++++----------- .../dataloader/foreignKeyLoaderMakers.ts | 6 +- .../dataloader/primaryKeyLoaderMakers.ts | 2 +- .../dataloader/rethinkForeignKeyLoader.ts | 25 ------- .../rethinkForeignKeyLoaderMakers.ts | 32 -------- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - 8 files changed, 35 insertions(+), 137 deletions(-) delete mode 100644 packages/server/dataloader/RethinkForeignKeyLoaderMaker.ts delete mode 100644 packages/server/dataloader/rethinkForeignKeyLoader.ts delete mode 100644 packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts diff --git a/packages/server/dataloader/RethinkForeignKeyLoaderMaker.ts b/packages/server/dataloader/RethinkForeignKeyLoaderMaker.ts deleted file mode 100644 index 72570f2644e..00000000000 --- a/packages/server/dataloader/RethinkForeignKeyLoaderMaker.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers' - -/** - * Load rethink entities by foreign key - */ -export default class RethinkForeignKeyLodaerMaker< - T extends keyof typeof rethinkPrimaryKeyLoaderMakers -> { - constructor( - public pk: T, - public field: string, - public fetch: (ids: readonly string[]) => Promise - ) {} -} diff --git a/packages/server/dataloader/RootDataLoader.ts b/packages/server/dataloader/RootDataLoader.ts index 039146acd95..b6f2391e849 100644 --- a/packages/server/dataloader/RootDataLoader.ts +++ b/packages/server/dataloader/RootDataLoader.ts @@ -1,5 +1,4 @@ import DataLoader from 'dataloader' -import RethinkForeignKeyLoaderMaker from './RethinkForeignKeyLoaderMaker' import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' import * as atlassianLoaders from './atlassianLoaders' import * as azureDevOpsLoaders from './azureDevOpsLoaders' @@ -12,7 +11,6 @@ import * as integrationAuthLoaders from './integrationAuthLoaders' import * as jiraServerLoaders from './jiraServerLoaders' import * as pollLoaders from './pollsLoaders' import * as primaryKeyLoaderMakers from './primaryKeyLoaderMakers' -import rethinkForeignKeyLoader from './rethinkForeignKeyLoader' import * as rethinkForeignKeyLoaderMakers from './rethinkForeignKeyLoaderMakers' import rethinkPrimaryKeyLoader from './rethinkPrimaryKeyLoader' import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers' @@ -56,17 +54,7 @@ interface GenericDataLoader { : // can delete below this line after RethinkDB is gone Loader extends RethinkPrimaryKeyLoaderMaker ? ReturnType> - : Loader extends RethinkForeignKeyLoaderMaker - ? ReturnType< - typeof rethinkForeignKeyLoader< - (typeof rethinkPrimaryKeyLoaderMakers)[U] extends RethinkPrimaryKeyLoaderMaker< - infer V - > - ? V - : never - > - > - : never + : never } export type DataLoaderInstance = GenericDataLoader @@ -105,9 +93,6 @@ export default class RootDataLoader< if (loaderMaker instanceof RethinkPrimaryKeyLoaderMaker) { const {table} = loaderMaker loader = rethinkPrimaryKeyLoader(this.dataLoaderOptions, table) - } else if (loaderMaker instanceof RethinkForeignKeyLoaderMaker) { - const {fetch, field, pk} = loaderMaker - loader = rethinkForeignKeyLoader(this, dependsOn, pk, field, fetch) } else { loader = (loaderMaker as any)(this, dependsOn) } diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index dd75986ec34..7a688115a3c 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -1,8 +1,6 @@ import DataLoader from 'dataloader' import {Selectable, SqlBool, sql} from 'kysely' import {PARABOL_AI_USER_ID} from '../../client/utils/constants' -import getRethink from '../database/rethinkDriver' -import {RDatum} from '../database/stricterR' import MeetingTemplate from '../database/types/MeetingTemplate' import Task, {TaskStatusEnum} from '../database/types/Task' import getFileStoreManager from '../fileStorage/getFileStoreManager' @@ -24,7 +22,12 @@ import getLatestTaskEstimates from '../postgres/queries/getLatestTaskEstimates' import getMeetingTaskEstimates, { MeetingTaskEstimatesResult } from '../postgres/queries/getMeetingTaskEstimates' -import {selectMeetingSettings, selectNewMeetings, selectTeams} from '../postgres/select' +import { + selectMeetingSettings, + selectNewMeetings, + selectTasks, + selectTeams +} from '../postgres/select' import {MeetingSettings, OrganizationUser, Team} from '../postgres/types' import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting' import {Logger} from '../utils/Logger' @@ -130,7 +133,6 @@ export const userTasks = (parent: RootDataLoader, dependsOn: RegisterDependsOn) dependsOn('tasks') return new DataLoader( async (keys) => { - const r = await getRethink() const uniqKeys = [] as UserTasksKey[] const keySet = new Set() keys.forEach((key) => { @@ -154,42 +156,24 @@ export const userTasks = (parent: RootDataLoader, dependsOn: RegisterDependsOn) filterQuery, includeUnassigned } = key - const dbAfter = after ? new Date(after) : r.maxval - - let teamTaskPartial = r.table('Task').getAll(r.args(teamIds), {index: 'teamId'}) - if (userIds?.length) { - teamTaskPartial = teamTaskPartial.filter((row: RDatum) => - r(userIds).contains(row('userId')) - ) - } - if (statusFilters?.length) { - teamTaskPartial = teamTaskPartial.filter((row: RDatum) => - r(statusFilters).contains(row('status')) - ) - } - if (filterQuery) { - // TODO: deal with tags like #archived and #private. should strip out of plaintextContent?? - teamTaskPartial = teamTaskPartial.filter( - (row: RDatum) => row('plaintextContent').match(filterQuery) as any - ) - } + const hasUserIds = userIds?.length > 0 + const hasStatusFilters = statusFilters ? statusFilters.length > 0 : false + const teamTasks = await selectTasks() + .where('teamId', 'in', teamIds) + .$if(hasUserIds, (qb) => qb.where('userId', 'in', userIds)) + .$if(hasStatusFilters, (qb) => qb.where('status', 'in', statusFilters!)) + .$if(!!filterQuery, (qb) => qb.where('plaintextContent', 'match', filterQuery!)) + .$if(!!after, (qb) => qb.where('updatedAt', '<', after!)) + .$if(!!archived, (qb) => qb.where(sql`'archived' = ANY(tags)`)) + .$if(!archived, (qb) => qb.where(sql`'archived' != ANY(tags)`)) + .$if(!includeUnassigned, (qb) => qb.where('userId', 'is not', null)) + .orderBy('updatedAt desc') + .limit(first + 1) + .execute() return { key: serializeUserTasksKey(key), - data: await teamTaskPartial - .filter((task: RDatum) => task('updatedAt').lt(dbAfter)) - .filter((task: RDatum) => - archived - ? task('tags').contains('archived') - : task('tags').contains('archived').not() - ) - .filter((task: RDatum) => { - if (includeUnassigned) return true - return task('userId').ne(null) - }) - .orderBy(r.desc('updatedAt')) - .limit(first + 1) - .run() + data: teamTasks } }) ) @@ -535,16 +519,17 @@ export const taskIdsByTeamAndGitHubRepo = ( dependsOn('tasks') return new DataLoader<{teamId: string; nameWithOwner: string}, string[], string>( async (keys) => { - const r = await getRethink() const res = await Promise.all( - keys.map((key) => { + keys.map(async (key) => { const {teamId, nameWithOwner} = key - // This is very expensive! We should move tasks to PG ASAP - return r - .table('Task') - .getAll(teamId, {index: 'teamId'}) - .filter((row: RDatum) => row('integration')('nameWithOwner').eq(nameWithOwner))('id') - .run() + const res = await getKysely() + .selectFrom('Task') + .select('id') + .where('teamId', '=', teamId) + .where('integration', 'is not', null) + .where(sql`"integration"->>'nameWithOwner' = ${nameWithOwner}`) + .execute() + return res.map(({id}) => id) }) ) return res diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 0c296d73386..51a15ecb27c 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -296,8 +296,8 @@ export const teamInvitationsByTeamId = foreignKeyLoaderMaker( } ) -export const _pgtasksByDiscussionId = foreignKeyLoaderMaker( - '_pgtasks', +export const tasksByDiscussionId = foreignKeyLoaderMaker( + 'tasks', 'discussionId', async (discusisonIds) => { // include archived cards in the conversation, since it's persistent @@ -305,7 +305,7 @@ export const _pgtasksByDiscussionId = foreignKeyLoaderMaker( } ) -export const _pgtasksByTeamId = foreignKeyLoaderMaker('_pgtasks', 'teamId', async (teamIds) => { +export const tasksByTeamId = foreignKeyLoaderMaker('tasks', 'teamId', async (teamIds) => { // waraning! contains private tasks return selectTasks() .where('teamId', 'in', teamIds) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index f53c962ba2d..47ee113669d 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -146,6 +146,6 @@ export const teamInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => return selectTeamInvitations().where('id', 'in', ids).execute() }) -export const _pgtasks = primaryKeyLoaderMaker((ids: readonly string[]) => { +export const tasks = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectTasks().where('id', 'in', ids).execute() }) diff --git a/packages/server/dataloader/rethinkForeignKeyLoader.ts b/packages/server/dataloader/rethinkForeignKeyLoader.ts deleted file mode 100644 index dec36574113..00000000000 --- a/packages/server/dataloader/rethinkForeignKeyLoader.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {DBType} from '../database/rethinkDriver' -import RootDataLoader, {RegisterDependsOn} from './RootDataLoader' -import UpdatableCacheDataLoader from './UpdatableCacheDataLoader' -import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers' - -const rethinkForeignKeyLoader = ( - parent: RootDataLoader, - dependsOn: RegisterDependsOn, - primaryKeyLoaderName: keyof typeof rethinkPrimaryKeyLoaderMakers, - field: string, - fetchFn: (ids: readonly string[]) => any[] | Promise -) => { - const standardLoader = parent.get(primaryKeyLoaderName) - dependsOn(primaryKeyLoaderName) - const batchFn = async (ids: readonly string[]) => { - const items = await fetchFn(ids) - items.forEach((item) => { - standardLoader.clear(item.id).prime(item.id, item) - }) - return ids.map((id) => items.filter((item) => item[field] === id)) - } - return new UpdatableCacheDataLoader(batchFn, {...parent.dataLoaderOptions}) -} - -export default rethinkForeignKeyLoader diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts deleted file mode 100644 index e7d6a9342b7..00000000000 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import getRethink from '../database/rethinkDriver' -import {RDatum} from '../database/stricterR' -import RethinkForeignKeyLoaderMaker from './RethinkForeignKeyLoaderMaker' - -export const tasksByDiscussionId = new RethinkForeignKeyLoaderMaker( - 'tasks', - 'discussionId', - async (discusisonIds) => { - const r = await getRethink() - return ( - r - .table('Task') - .getAll(r.args(discusisonIds), {index: 'discussionId'}) - // include archived cards in the conversation, since it's persistent - .run() - ) - } -) - -export const tasksByTeamId = new RethinkForeignKeyLoaderMaker( - 'tasks', - 'teamId', - async (teamIds) => { - const r = await getRethink() - // waraning! contains private tasks - return r - .table('Task') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((task: RDatum) => task('tags').contains('archived').not()) - .run() - } -) diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index 973c2c959d0..ec24d52c90a 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -4,4 +4,3 @@ import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' * all rethink dataloader types which also must exist in {@link rethinkDriver/RethinkSchema} */ export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') -export const tasks = new RethinkPrimaryKeyLoaderMaker('Task') From 4367111e4326c05cf0198c5ed9ef862bd2aea25f Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 11 Oct 2024 09:57:20 -0700 Subject: [PATCH 77/89] fix: changeTaskTeam --- .../server/graphql/mutations/changeTaskTeam.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/server/graphql/mutations/changeTaskTeam.ts b/packages/server/graphql/mutations/changeTaskTeam.ts index 93396501a5b..8b7bc3566ec 100644 --- a/packages/server/graphql/mutations/changeTaskTeam.ts +++ b/packages/server/graphql/mutations/changeTaskTeam.ts @@ -157,15 +157,16 @@ export default { .default(null), newTask: r.table('Task').get(taskId).update(updates) }).run() + if (task.integrationHash) { + await pg + .deleteFrom('Task') + .where('integrationHash', '=', task.integrationHash) + .where('teamId', '=', teamId) + .limit(1) + .returning('id') + .execute() + } await pg - .with('TaskDelete', (qb) => - qb - .deleteFrom('Task') - .where('integrationHash', '=', task.integrationHash) - .where('teamId', '=', teamId) - .limit(1) - .returning('id') - ) .updateTable('Task') .set({ content: rawContent === nextRawContent ? undefined : JSON.stringify(nextRawContent), @@ -173,7 +174,6 @@ export default { integration: JSON.stringify(integration) }) .where('id', '=', taskId) - .returning(({selectFrom}) => selectFrom('TaskDelete').select('id').as('deletedId')) .executeTakeFirst() if (deletedConflictingIntegrationTask) { const task = deletedConflictingIntegrationTask as unknown as Task From 32466742e080d0027d9fff4c86423b579f968cd9 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 11 Oct 2024 15:48:48 -0700 Subject: [PATCH 78/89] remove reads from r Signed-off-by: Matt Krick --- codegen.json | 2 +- packages/server/database/rethinkDriver.ts | 12 --- .../server/database/types/ImportedTask.ts | 30 ------- packages/server/dataloader/RootDataLoader.ts | 2 - .../server/dataloader/customLoaderMakers.ts | 6 +- .../dataloader/foreignKeyLoaderMakers.ts | 10 ++- .../graphql/mutations/changeTaskTeam.ts | 45 +++-------- .../server/graphql/mutations/createTask.ts | 31 ++++---- .../mutations/createTaskIntegration.ts | 13 --- .../server/graphql/mutations/deleteTask.ts | 5 -- .../server/graphql/mutations/endCheckIn.ts | 67 +++++----------- .../graphql/mutations/endSprintPoker.ts | 2 +- .../graphql/mutations/helpers/addSeedTasks.ts | 3 - .../mutations/helpers/importTasksForPoker.ts | 56 +++++++------ .../helpers/publishChangeNotifications.ts | 2 +- .../mutations/helpers/pushEstimateToGitHub.ts | 2 +- .../mutations/helpers/pushEstimateToGitLab.ts | 2 +- .../mutations/helpers/removeEmptyTasks.ts | 30 ++----- .../mutations/helpers/removeTeamMember.ts | 79 +++++++------------ .../mutations/helpers/safeEndRetrospective.ts | 17 ++-- .../resetRetroMeetingToGroupStage.ts | 5 +- .../server/graphql/mutations/updateTask.ts | 37 ++------- .../graphql/mutations/updateTaskDueDate.ts | 11 --- .../private/mutations/hardDeleteUser.ts | 8 +- .../public/mutations/batchArchiveTasks.ts | 2 +- .../public/types/ActionMeetingMember.ts | 17 ++-- .../graphql/public/types/EstimateStage.ts | 2 +- .../graphql/public/types/TeamPromptMeeting.ts | 16 ++-- packages/server/graphql/public/types/User.ts | 59 +++++--------- .../queries/helpers/connectionFromTasks.ts | 2 +- packages/server/graphql/resolvers.ts | 3 +- .../resolvers/resolveThreadableConnection.ts | 7 +- .../server/graphql/types/EndCheckInPayload.ts | 4 +- packages/server/graphql/types/PokerMeeting.ts | 2 +- packages/server/graphql/types/Team.ts | 3 +- .../TaskIntegrationManagerFactory.ts | 4 +- .../postgres/types/TaskIntegration.d.ts | 2 + packages/server/postgres/types/index.d.ts | 2 + .../server/safeMutations/archiveTasksForDB.ts | 21 +---- packages/server/utils/filterTasksByMeeting.ts | 2 +- 40 files changed, 204 insertions(+), 421 deletions(-) delete mode 100644 packages/server/database/types/ImportedTask.ts diff --git a/codegen.json b/codegen.json index 77d538e22cc..97fff950315 100644 --- a/codegen.json +++ b/codegen.json @@ -176,7 +176,7 @@ "StartRetrospectiveSuccess": "./types/StartRetrospectiveSuccess#StartRetrospectiveSuccessSource", "StartTeamPromptSuccess": "./types/StartTeamPromptSuccess#StartTeamPromptSuccessSource", "StripeFailPaymentPayload": "./types/StripeFailPaymentPayload#StripeFailPaymentPayloadSource", - "Task": "../../database/types/Task#default", + "Task": "../../postgres/types/index#Task as TaskDB", "Team": "../../postgres/types/index#Team as TeamDB", "TeamHealthPhase": "./types/TeamHealthPhase#TeamHealthPhaseSource", "TeamHealthStage": "./types/TeamHealthStage#TeamHealthStageSource", diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index 649fd167cc7..e909172b381 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -11,7 +11,6 @@ import NotificationResponseReplied from './types/NotificationResponseReplied' import NotificationTaskInvolves from './types/NotificationTaskInvolves' import NotificationTeamArchived from './types/NotificationTeamArchived' import NotificationTeamInvitation from './types/NotificationTeamInvitation' -import Task from './types/Task' export type RethinkSchema = { Notification: { @@ -28,17 +27,6 @@ export type RethinkSchema = { | NotificationMentioned index: 'userId' } - Task: { - type: Task - index: - | 'integrationId' - | 'tags' - | 'teamId' - | 'teamIdUpdatedAt' - | 'discussionId' - | 'userId' - | 'integrationHash' - } } export type DBType = { diff --git a/packages/server/database/types/ImportedTask.ts b/packages/server/database/types/ImportedTask.ts deleted file mode 100644 index 89d687c295e..00000000000 --- a/packages/server/database/types/ImportedTask.ts +++ /dev/null @@ -1,30 +0,0 @@ -import IntegrationHash from 'parabol-client/shared/gqlIds/IntegrationHash' -import dndNoise from 'parabol-client/utils/dndNoise' -import convertToTaskContent from 'parabol-client/utils/draftjs/convertToTaskContent' -import Task, {TaskIntegration} from './Task' - -interface Input { - meetingId: string - teamId: string - integration: TaskIntegration - userId: string -} - -export default class ImportedTask extends Task { - integrationHash!: string - integration!: TaskIntegration - constructor(input: Input) { - const {meetingId, teamId, integration, userId} = input - super({ - content: convertToTaskContent(`Task imported from ${integration.service} #archived`), - createdBy: userId, - meetingId, - sortOrder: dndNoise(), - status: 'future', - teamId, - integrationHash: IntegrationHash.join(integration), - integration, - userId - }) - } -} diff --git a/packages/server/dataloader/RootDataLoader.ts b/packages/server/dataloader/RootDataLoader.ts index b6f2391e849..9bf6bd6a34e 100644 --- a/packages/server/dataloader/RootDataLoader.ts +++ b/packages/server/dataloader/RootDataLoader.ts @@ -11,7 +11,6 @@ import * as integrationAuthLoaders from './integrationAuthLoaders' import * as jiraServerLoaders from './jiraServerLoaders' import * as pollLoaders from './pollsLoaders' import * as primaryKeyLoaderMakers from './primaryKeyLoaderMakers' -import * as rethinkForeignKeyLoaderMakers from './rethinkForeignKeyLoaderMakers' import rethinkPrimaryKeyLoader from './rethinkPrimaryKeyLoader' import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers' @@ -21,7 +20,6 @@ interface LoaderDict { // Register all loaders const loaderMakers = { - ...rethinkForeignKeyLoaderMakers, ...rethinkPrimaryKeyLoaderMakers, ...primaryKeyLoaderMakers, ...foreignKeyLoaderMakers, diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 7a688115a3c..36d8e244d05 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -2,7 +2,6 @@ import DataLoader from 'dataloader' import {Selectable, SqlBool, sql} from 'kysely' import {PARABOL_AI_USER_ID} from '../../client/utils/constants' import MeetingTemplate from '../database/types/MeetingTemplate' -import Task, {TaskStatusEnum} from '../database/types/Task' import getFileStoreManager from '../fileStorage/getFileStoreManager' import {ReactableEnum} from '../graphql/public/resolverTypes' import {SAMLSource} from '../graphql/public/types/SAML' @@ -28,7 +27,7 @@ import { selectTasks, selectTeams } from '../postgres/select' -import {MeetingSettings, OrganizationUser, Team} from '../postgres/types' +import {MeetingSettings, OrganizationUser, Task, Team} from '../postgres/types' import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting' import {Logger} from '../utils/Logger' import getRedis from '../utils/getRedis' @@ -36,7 +35,6 @@ import isUserVerified from '../utils/isUserVerified' import NullableDataLoader from './NullableDataLoader' import RootDataLoader, {RegisterDependsOn} from './RootDataLoader' import normalizeArrayResults from './normalizeArrayResults' - export interface MeetingSettingsKey { teamId: string meetingType: MeetingTypeEnum @@ -58,7 +56,7 @@ export interface UserTasksKey { userIds: string[] teamIds: string[] archived?: boolean - statusFilters?: TaskStatusEnum[] | null + statusFilters?: Task['status'][] | null filterQuery?: string | null includeUnassigned?: boolean } diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 51a15ecb27c..1edd49840ed 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -309,6 +309,14 @@ export const tasksByTeamId = foreignKeyLoaderMaker('tasks', 'teamId', async (tea // waraning! contains private tasks return selectTasks() .where('teamId', 'in', teamIds) - .where(sql`'archived' = ANY(tags)`) + .where(sql`'archived' != ANY(tags)`) + .execute() +}) + +export const tasksByMeetingId = foreignKeyLoaderMaker('tasks', 'meetingId', async (meetingIds) => { + // waraning! contains private tasks + return selectTasks() + .where('meetingId', 'in', meetingIds) + .where(sql`'archived' != ANY(tags)`) .execute() }) diff --git a/packages/server/graphql/mutations/changeTaskTeam.ts b/packages/server/graphql/mutations/changeTaskTeam.ts index 8b7bc3566ec..0fc63d0302a 100644 --- a/packages/server/graphql/mutations/changeTaskTeam.ts +++ b/packages/server/graphql/mutations/changeTaskTeam.ts @@ -1,8 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import removeEntityKeepText from 'parabol-client/utils/draftjs/removeEntityKeepText' -import getRethink from '../../database/rethinkDriver' -import Task from '../../database/types/Task' import getKysely from '../../postgres/getKysely' import {AtlassianAuth} from '../../postgres/queries/getAtlassianAuthByUserIdTeamId' import {GitHubAuth} from '../../postgres/queries/getGitHubAuthByUserIdTeamId' @@ -35,8 +33,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() - const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -136,35 +132,26 @@ export default { Boolean(userIdsOnlyOnOldTeam.find((userId) => userId === entity.data.userId)) const {rawContent: nextRawContent} = removeEntityKeepText(rawContent, eqFn) - const updates = { - content: rawContent === nextRawContent ? undefined : JSON.stringify(nextRawContent), - updatedAt: now, - teamId, - integration - } - // If there is a task with the same integration hash in the new team, then delete it first. // This is done so there are no duplicates and also solves the issue of the conflicting task being // private or archived. - const {deletedConflictingIntegrationTask} = await r({ - deletedConflictingIntegrationTask: - task.integrationHash && - r - .table('Task') - .getAll(task.integrationHash, {index: 'integrationHash'}) - .filter({teamId}) - .delete({returnChanges: true})('changes')(0)('old_val') - .default(null), - newTask: r.table('Task').get(taskId).update(updates) - }).run() if (task.integrationHash) { - await pg + const deletedTask = await pg .deleteFrom('Task') .where('integrationHash', '=', task.integrationHash) .where('teamId', '=', teamId) .limit(1) .returning('id') - .execute() + .executeTakeFirst() + if (deletedTask) { + const isPrivate = task.tags.includes('private') + const data = {task} + newTeamMembers.forEach(({userId}) => { + if (!isPrivate || userId === task.userId) { + publish(SubscriptionChannel.TASK, userId, 'DeleteTaskPayload', data, subOptions) + } + }) + } } await pg .updateTable('Task') @@ -175,16 +162,6 @@ export default { }) .where('id', '=', taskId) .executeTakeFirst() - if (deletedConflictingIntegrationTask) { - const task = deletedConflictingIntegrationTask as unknown as Task - const isPrivate = task.tags.includes('private') - const data = {task} - newTeamMembers.forEach(({userId}) => { - if (!isPrivate || userId === task.userId) { - publish(SubscriptionChannel.TASK, userId, 'DeleteTaskPayload', data, subOptions) - } - }) - } dataLoader.clearAll('tasks') const isPrivate = tags.includes('private') const data = {taskId} diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts index 8d31cb3e193..92af99686f7 100644 --- a/packages/server/graphql/mutations/createTask.ts +++ b/packages/server/graphql/mutations/createTask.ts @@ -4,11 +4,16 @@ import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntity import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId' +import dndNoise from '../../../client/utils/dndNoise' +import extractTextFromDraftString from '../../../client/utils/draftjs/extractTextFromDraftString' +import getTagsFromEntityMap from '../../../client/utils/draftjs/getTagsFromEntityMap' import getRethink from '../../database/rethinkDriver' import NotificationTaskInvolves from '../../database/types/NotificationTaskInvolves' -import Task, {TaskServiceEnum} from '../../database/types/Task' +import {TaskServiceEnum} from '../../database/types/Task' +import generateUID from '../../generateUID' import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache' import getKysely from '../../postgres/getKysely' +import {Task} from '../../postgres/types/index.d' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import publish, {SubOptions} from '../../utils/publish' @@ -58,7 +63,7 @@ const validateTaskDiscussionId = async ( const handleAddTaskNotifications = async ( teamMembers: any[], - task: Task, + task: Pick, viewerId: string, teamId: string, subOptions: SubOptions @@ -154,7 +159,6 @@ export default { ) { const {authToken, dataLoader, socketId: mutatorId} = context const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const viewerId = getUserId(authToken) @@ -205,29 +209,26 @@ export default { if (integrationRepoId) { updatePrevUsedRepoIntegrationsCache(teamId, integrationRepoId, viewerId) } - const task = new Task({ + const task = { + id: generateUID(), content, + plaintextContent: extractTextFromDraftString(content), createdBy: viewerId, meetingId, - sortOrder, + sortOrder: sortOrder || dndNoise(), status, teamId, discussionId, integrationHash, - integration, + integration: JSON.stringify(integration), threadSortOrder, threadParentId, - userId - }) + userId: userId || null, + tags: getTagsFromEntityMap(JSON.parse(content).entityMap) + } const {id: taskId} = task const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) - await r({ - task: r.table('Task').insert(task) - }).run() - await pg - .insertInto('Task') - .values({...task, integration: JSON.stringify(integration)}) - .execute() + await pg.insertInto('Task').values(task).execute() handleAddTaskNotifications(teamMembers, task, viewerId, teamId, { operationId, mutatorId diff --git a/packages/server/graphql/mutations/createTaskIntegration.ts b/packages/server/graphql/mutations/createTaskIntegration.ts index 4bd681a5860..c6f15a1e70a 100644 --- a/packages/server/graphql/mutations/createTaskIntegration.ts +++ b/packages/server/graphql/mutations/createTaskIntegration.ts @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull, GraphQLResolveInfo} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import makeAppURL from '~/utils/makeAppURL' import appOrigin from '../../appOrigin' -import getRethink from '../../database/rethinkDriver' import TaskIntegrationManagerFactory from '../../integrations/TaskIntegrationManagerFactory' import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache' import getKysely from '../../postgres/getKysely' @@ -46,8 +45,6 @@ export default { ) => { const {authToken, dataLoader, socketId: mutatorId} = context const pg = getKysely() - const r = await getRethink() - const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -153,16 +150,6 @@ export default { } updatePrevUsedRepoIntegrationsCache(teamId, integrationRepoId, viewerId) - - await r - .table('Task') - .get(taskId) - .update({ - ...updateTaskInput, - updatedAt: now - }) - .run() - await pg .updateTable('Task') .set({ diff --git a/packages/server/graphql/mutations/deleteTask.ts b/packages/server/graphql/mutations/deleteTask.ts index e433b53b8b3..98d9d485550 100644 --- a/packages/server/graphql/mutations/deleteTask.ts +++ b/packages/server/graphql/mutations/deleteTask.ts @@ -1,6 +1,5 @@ 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' @@ -23,7 +22,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -41,9 +39,6 @@ export default { // RESOLUTION const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) const subscribedUserIds = teamMembers.map(({userId}) => userId) - await r({ - task: r.table('Task').get(taskId).delete() - }).run() await pg.deleteFrom('Task').where('id', '=', taskId).execute() const {tags, userId: taskUserId} = task diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index 7cb747a7144..5e19d36f823 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -1,18 +1,17 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' +import {sql} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import {AGENDA_ITEMS, DONE, LAST_CALL} from 'parabol-client/utils/constants' +import {AGENDA_ITEMS, LAST_CALL} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' import {positionAfter} from '../../../client/shared/sortOrder' import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck' -import getRethink from '../../database/rethinkDriver' -import {RDatum} from '../../database/stricterR' -import Task from '../../database/types/Task' import TimelineEventCheckinComplete from '../../database/types/TimelineEventCheckinComplete' import {DataLoaderInstance} from '../../dataloader/RootDataLoader' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' -import {AgendaItem} from '../../postgres/types' +import {selectTasks} from '../../postgres/select' +import {AgendaItem, Task} from '../../postgres/types' import {CheckInMeeting} from '../../postgres/types/Meeting' import archiveTasksForDB from '../../safeMutations/archiveTasksForDB' import removeSuggestedAction from '../../safeMutations/removeSuggestedAction' @@ -34,36 +33,20 @@ import updateTeamInsights from './helpers/updateTeamInsights' type SortOrderTask = Pick const updateTaskSortOrders = async (userIds: string[], tasks: SortOrderTask[]) => { const pg = getKysely() - const r = await getRethink() - const taskMax = await ( - r - .table('Task') - .getAll(r.args(userIds), {index: 'userId'}) - .filter((task: RDatum) => task('tags').contains('archived').not()) as any - ) - .max('sortOrder')('sortOrder') - .default(0) - .run() + const taskMaxRes = await pg + .selectFrom('Task') + .select(({fn}) => fn.max('sortOrder').as('maxSortOrder')) + .where('userId', 'in', userIds) + .where(sql`'archived' != ANY(tags)`) + .executeTakeFirst() + const maxSortOrder = Number(taskMaxRes?.maxSortOrder ?? 0) + // mutate what's in the dataloader tasks.forEach((task, idx) => { - task.sortOrder = taskMax + idx + 1 + task.sortOrder = maxSortOrder + idx + 1 }) - const updatedTasks = tasks.map((task) => ({ - id: task.id, - sortOrder: task.sortOrder - })) - await r(updatedTasks) - .forEach((task) => { - return r - .table('Task') - .get(task('id')) - .update({ - sortOrder: task('sortOrder') as unknown as number - }) - }) - .run() await Promise.all( - updatedTasks.map((task) => + tasks.map((task) => pg.updateTable('Task').set({sortOrder: task.sortOrder}).where('id', '=', task.id).execute() ) ) @@ -115,22 +98,14 @@ const summarizeCheckInMeeting = async (meeting: CheckInMeeting, dataLoader: Data const {id: meetingId, teamId, phases} = meeting const pg = getKysely() - const r = await getRethink() const [meetingMembers, tasks, doneTasks, activeAgendaItems] = await Promise.all([ dataLoader.get('meetingMembersByMeetingId').load(meetingId), - r - .table('Task') - .getAll(teamId, {index: 'teamId'}) - .filter({ - meetingId - }) - .run(), - r - .table('Task') - .getAll(teamId, {index: 'teamId'}) - .filter({status: DONE}) - .filter((task: RDatum) => task('tags').contains('archived').not()) - .run(), + dataLoader.get('tasksByMeetingId').load(meetingId), + selectTasks() + .where('teamId', '=', teamId) + .where('status', '=', 'done') + .where(sql`'archived' != ANY(tags)`) + .execute(), dataLoader.get('agendaItemsByTeamId').load(teamId) ]) @@ -227,7 +202,7 @@ export default { dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('teamMembersByTeamId').load(teamId), - removeEmptyTasks(meetingId, teamId), + removeEmptyTasks(meetingId), updateTeamInsights(teamId, dataLoader) ]) // need to wait for removeEmptyTasks before finishing the meeting diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 4f827428b08..4052119850a 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -100,7 +100,7 @@ export default { dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('teamMembersByTeamId').load(teamId), - removeEmptyTasks(meetingId, teamId), + removeEmptyTasks(meetingId), // technically, this template could have mutated while the meeting was going on. but in practice, probably not dataLoader.get('meetingTemplates').loadNonNull(templateId), updateTeamInsights(teamId, dataLoader) diff --git a/packages/server/graphql/mutations/helpers/addSeedTasks.ts b/packages/server/graphql/mutations/helpers/addSeedTasks.ts index 509ef01f10d..c3e064a212b 100644 --- a/packages/server/graphql/mutations/helpers/addSeedTasks.ts +++ b/packages/server/graphql/mutations/helpers/addSeedTasks.ts @@ -2,7 +2,6 @@ import convertToTaskContent from 'parabol-client/utils/draftjs/convertToTaskCont import getTagsFromEntityMap from 'parabol-client/utils/draftjs/getTagsFromEntityMap' import makeAppURL from 'parabol-client/utils/makeAppURL' import appOrigin from '../../../appOrigin' -import getRethink from '../../../database/rethinkDriver' import {TaskStatusEnum} from '../../../database/types/Task' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' @@ -39,7 +38,6 @@ function getSeedTasks(teamId: string) { export default async (userId: string, teamId: string) => { const pg = getKysely() - const r = await getRethink() const now = new Date() const seedTasks = getSeedTasks(teamId).map((proj) => ({ @@ -53,5 +51,4 @@ export default async (userId: string, teamId: string) => { updatedAt: now })) await pg.insertInto('Task').values(seedTasks).execute() - return r.table('Task').insert(seedTasks).run() } diff --git a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts index 9d6d15eb564..9bb678d2e70 100644 --- a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts +++ b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts @@ -1,8 +1,11 @@ import IntegrationHash from 'parabol-client/shared/gqlIds/IntegrationHash' import {isNotNull} from 'parabol-client/utils/predicates' -import getRethink from '../../../database/rethinkDriver' -import ImportedTask from '../../../database/types/ImportedTask' +import dndNoise from '../../../../client/utils/dndNoise' +import convertToTaskContent from '../../../../client/utils/draftjs/convertToTaskContent' +import getTagsFromEntityMap from '../../../../client/utils/draftjs/getTagsFromEntityMap' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' +import {selectTasks} from '../../../postgres/select' import {TUpdatePokerScopeItemInput} from '../updatePokerScope' const importTasksForPoker = async ( @@ -12,14 +15,13 @@ const importTasksForPoker = async ( meetingId: string ) => { const pg = getKysely() - const r = await getRethink() const integratedUpdates = additiveUpdates.filter((update) => update.service !== 'PARABOL') const integrationHashes = integratedUpdates.map((update) => update.serviceTaskId) - const existingTasks = await r - .table('Task') - .getAll(r.args(integrationHashes), {index: 'integrationHash'}) - .filter({teamId, userId}) - .run() + const existingTasks = await selectTasks() + .where('integrationHash', 'in', integrationHashes) + .where('teamId', '=', teamId) + .where('userId', '=', userId) + .execute() const integrationHashToTaskId = {} as Record additiveUpdates.map((update) => { if (update.service === 'PARABOL') { @@ -32,26 +34,32 @@ const importTasksForPoker = async ( const tasksToAdd = newIntegrationUpdates .map((update) => { const {service, serviceTaskId} = update - const integration = IntegrationHash.split(service, serviceTaskId) - if (!integration) return null - return new ImportedTask({ - userId, - integration: { - accessUserId: userId, - ...integration - }, + const integrationSplit = IntegrationHash.split(service, serviceTaskId) + if (!integrationSplit) return null + const integration = { + accessUserId: userId, + ...integrationSplit + } + const integrationHash = IntegrationHash.join(integration) + const plaintextContent = `Task imported from ${integration.service} #archived` + const content = convertToTaskContent(plaintextContent) + return { + id: generateUID(), + content, + plaintextContent, + createdBy: userId, + sortOrder: dndNoise(), + status: 'future' as const, + teamId, + integrationHash, + integration: JSON.stringify(integration), meetingId, - teamId - }) + tags: getTagsFromEntityMap(JSON.parse(content).entityMap) + } }) .filter(isNotNull) - if (newIntegrationUpdates.length > 0) { - await pg - .insertInto('Task') - .values(tasksToAdd.map((t) => ({...t, integration: JSON.stringify(t.integration)}))) - .execute() - await r.table('Task').insert(tasksToAdd).run() + await pg.insertInto('Task').values(tasksToAdd).execute() } const integratedTasks = [...existingTasks, ...tasksToAdd] diff --git a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts index 7dcf3cfdc3a..0feaec154fe 100644 --- a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts +++ b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts @@ -2,7 +2,7 @@ import {ASSIGNEE, MENTIONEE} from 'parabol-client/utils/constants' import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap' import getRethink from '../../../database/rethinkDriver' import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves' -import Task from '../../../database/types/Task' +import {Task} from '../../../postgres/types' import {analytics} from '../../../utils/analytics/analytics' const publishChangeNotifications = async ( diff --git a/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts b/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts index 891895d7a71..289f36fa763 100644 --- a/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts +++ b/packages/server/graphql/mutations/helpers/pushEstimateToGitHub.ts @@ -41,7 +41,7 @@ const pushEstimateToGitHub = async ( const {dimensionName, taskId, value, meetingId} = taskEstimate const {dataLoader} = context const [task, meeting] = await Promise.all([ - dataLoader.get('tasks').load(taskId), + dataLoader.get('tasks').loadNonNull(taskId), dataLoader.get('newMeetings').load(meetingId) ]) diff --git a/packages/server/graphql/mutations/helpers/pushEstimateToGitLab.ts b/packages/server/graphql/mutations/helpers/pushEstimateToGitLab.ts index 8006cb2b350..32d63499cb2 100644 --- a/packages/server/graphql/mutations/helpers/pushEstimateToGitLab.ts +++ b/packages/server/graphql/mutations/helpers/pushEstimateToGitLab.ts @@ -17,7 +17,7 @@ const pushEstimateToGitLab = async ( const {dimensionName, taskId, value, meetingId} = taskEstimate const {dataLoader} = context const [task, meeting] = await Promise.all([ - dataLoader.get('tasks').load(taskId), + dataLoader.get('tasks').loadNonNull(taskId), dataLoader.get('newMeetings').load(meetingId) ]) if (!meeting) return new Error('Meeting does not exist') diff --git a/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts b/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts index ce4dd8a0d41..7e14376118b 100644 --- a/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts +++ b/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts @@ -1,28 +1,14 @@ -import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' -const removeEmptyTasks = async (meetingId: string, teamId: string) => { +const removeEmptyTasks = async (meetingId: string) => { const pg = getKysely() - const r = await getRethink() - const createdTasks = await r - .table('Task') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingId}) - .run() - - const removedTaskIds = createdTasks - .map((task) => ({ - id: task.id, - plaintextContent: extractTextFromDraftString(task.content) - })) - .filter(({plaintextContent}) => plaintextContent.length === 0) - .map(({id}) => id) - if (removedTaskIds.length > 0) { - await pg.deleteFrom('Task').where('id', 'in', removedTaskIds).execute() - await r.table('Task').getAll(r.args(removedTaskIds)).delete().run() - } - return removedTaskIds + const removedTasks = await pg + .deleteFrom('Task') + .where('meetingId', '=', meetingId) + .where(({or, eb}) => or([eb('plaintextContent', '=', ''), eb('plaintextContent', 'is', null)])) + .returning('id') + .execute() + return removedTasks.map(({id}) => id) } export default removeEmptyTasks diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index b7afbd2de7b..5c13821b456 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -1,14 +1,13 @@ import {sql} from 'kysely' import fromTeamMemberId from 'parabol-client/utils/relay/fromTeamMemberId' import getRethink from '../../../database/rethinkDriver' -import {RDatum} from '../../../database/stricterR' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' import CheckInStage from '../../../database/types/CheckInStage' import EstimateStage from '../../../database/types/EstimateStage' import NotificationKickedOut from '../../../database/types/NotificationKickedOut' -import Task from '../../../database/types/Task' import UpdatesStage from '../../../database/types/UpdatesStage' import getKysely from '../../../postgres/getKysely' +import {selectTasks} from '../../../postgres/select' import archiveTasksForDB from '../../../safeMutations/archiveTasksForDB' import errorFilter from '../../errorFilter' import {DataLoaderWorker} from '../../graphql' @@ -53,17 +52,14 @@ const removeTeamMember = async ( : currentTeamLeader if (willArchive) { - await Promise.all([ - // archive single-person teams - pg - .with('TaskDelete', (qb) => qb.deleteFrom('Task').where('teamId', '=', teamId)) - .updateTable('Team') - .set({isArchived: true}) - .where('id', '=', teamId) - .execute(), + await pg // delete all tasks belonging to a 1-person team - r.table('Task').getAll(teamId, {index: 'teamId'}).delete() - ]) + .with('TaskDelete', (qb) => qb.deleteFrom('Task').where('teamId', '=', teamId)) + // archive single-person teams + .updateTable('Team') + .set({isArchived: true}) + .where('id', '=', teamId) + .execute() } else if (isLead) { // assign new leader, remove old leader flag await pg @@ -79,46 +75,26 @@ const removeTeamMember = async ( .where('id', '=', teamMemberId) .execute() // assign active tasks to the team lead - const {integratedTasksToArchive, reassignedTasks} = await r({ - integratedTasksToArchive: r - .table('Task') - .getAll(userId, {index: 'userId'}) - .filter({teamId}) - .filter((task: RDatum) => { - return r.and( - task('tags').contains('archived').not(), - task('integrations').default(null).ne(null) - ) - }) - .coerceTo('array') as unknown as Task[], - reassignedTasks: r - .table('Task') - .getAll(userId, {index: 'userId'}) - .filter({teamId}) - .filter((task: RDatum) => - r.and(task('tags').contains('archived').not(), task('integrations').default(null).eq(null)) - ) - .update( - { - userId: nextTeamLead.userId - }, - {returnChanges: true} - )('changes')('new_val') - .default([]) as unknown as Task[] - }).run() - await pg - .with('TaskReassignment', (qb) => + const integratedTasksToArchive = await selectTasks() + .where('userId', '=', userId) + .where('teamId', '=', teamId) + .where('integration', 'is not', null) + .where(sql`'archived' != ANY(tags)`) + .execute() + const reassignedTasks = await pg + .with('UserUpdate', (qb) => qb - .updateTable('Task') - .set({userId: nextTeamLead.userId}) - .where('userId', '=', userId) - .where('teamId', '=', teamId) - .where('integration', 'is', null) - .where(sql`'archived' != ANY(tags)`) + .updateTable('User') + .set(({fn, ref, val}) => ({tms: fn('ARRAY_REMOVE', [ref('tms'), val(teamId)])})) + .where('id', '=', userId) ) - .updateTable('User') - .set(({fn, ref, val}) => ({tms: fn('ARRAY_REMOVE', [ref('tms'), val(teamId)])})) - .where('id', '=', userId) + .updateTable('Task') + .set({userId: nextTeamLead.userId}) + .where('userId', '=', userId) + .where('teamId', '=', teamId) + .where('integration', 'is', null) + .where(sql`'archived' != ANY(tags)`) + .returning('id') .execute() dataLoader.clearAll(['users', 'teamMembers', 'tasks']) const user = await dataLoader.get('users').load(userId) @@ -130,8 +106,7 @@ const removeTeamMember = async ( await r.table('Notification').insert(notification).run() } - const archivedTasks = await archiveTasksForDB(integratedTasksToArchive) - const archivedTaskIds = archivedTasks.map(({id}) => id) + const archivedTaskIds = await archiveTasksForDB(integratedTasksToArchive) const teamAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) const agendaItemIds = teamAgendaItems .filter((agendaItem) => agendaItem.teamMemberId === teamMemberId) diff --git a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts index c486815af94..fc91ee11214 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -4,7 +4,6 @@ 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 TimelineEventRetroComplete from '../../../database/types/TimelineEventRetroComplete' import getKysely from '../../../postgres/getKysely' import {RetrospectiveMeeting} from '../../../postgres/types/Meeting' @@ -38,7 +37,6 @@ const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: Int const {dataLoader} = context const {id: meetingId, phases, facilitatorUserId, teamId, recallBotId} = meeting const pg = getKysely() - const r = await getRethink() const [reflectionGroups, reflections, sentimentScore] = await Promise.all([ dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId), dataLoader.get('retroReflectionsByMeetingId').load(meetingId), @@ -57,17 +55,16 @@ const summarizeRetroMeeting = async (meeting: RetrospectiveMeeting, context: Int await dataLoader.get('commentCountByDiscussionId').loadMany(discussionIds) ).filter(isValid) const commentCount = commentCounts.reduce((cumSum, count) => cumSum + count, 0) - const taskCount = await r - .table('Task') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .count() - .default(0) - .run() + const taskCountRes = await pg + .selectFrom('Task') + .select(({fn}) => fn.count('id').as('count')) + .where('discussionId', 'in', discussionIds) + .executeTakeFirst() await pg .updateTable('NewMeeting') .set({ commentCount, - taskCount, + taskCount: Number(taskCountRes?.count ?? 0), topicCount: reflectionGroupIds.length, reflectionCount: reflections.length, sentimentScore, @@ -137,7 +134,7 @@ const safeEndRetrospective = async ({ dataLoader.get('meetingMembersByMeetingId').load(meetingId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('teamMembersByTeamId').load(teamId), - removeEmptyTasks(meetingId, teamId), + removeEmptyTasks(meetingId), dataLoader.get('meetingTemplates').loadNonNull(templateId), updateTeamInsights(teamId, dataLoader) ]) diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index 73debf3e5f5..a8cf539613c 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -1,7 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {CHECKIN, DISCUSS, GROUP, REFLECT, VOTE} from '../../../client/utils/constants' -import getRethink from '../../database/rethinkDriver' import DiscussPhase from '../../database/types/DiscussPhase' import GenericMeetingPhase from '../../database/types/GenericMeetingPhase' import getKysely from '../../postgres/getKysely' @@ -27,7 +26,6 @@ const resetRetroMeetingToGroupStage = { {meetingId}: {meetingId: string}, {authToken, socketId: mutatorId, dataLoader}: GQLContext ) => { - const r = await getRethink() const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -126,8 +124,7 @@ const resetRetroMeetingToGroupStage = { .updateTable('NewMeeting') .set({phases: JSON.stringify(newPhases)}) .where('id', '=', meetingId) - .execute(), - r.table('Task').getAll(r.args(discussionIdsToDelete), {index: 'discussionId'}).delete().run() + .execute() ]) dataLoader.clearAll([ 'newMeetings', diff --git a/packages/server/graphql/mutations/updateTask.ts b/packages/server/graphql/mutations/updateTask.ts index b8471bf00b8..78d38f82cab 100644 --- a/packages/server/graphql/mutations/updateTask.ts +++ b/packages/server/graphql/mutations/updateTask.ts @@ -2,8 +2,7 @@ import {GraphQLNonNull, GraphQLObjectType} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' -import getRethink from '../../database/rethinkDriver' -import Task, {AreaEnum as TAreaEnum, TaskStatusEnum} from '../../database/types/Task' +import {AreaEnum as TAreaEnum, TaskStatusEnum} from '../../database/types/Task' import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -48,8 +47,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() - const now = new Date() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -76,43 +73,25 @@ export default { } } // RESOLUTION - const isSortOrderUpdate = - updatedTask.sortOrder !== undefined && Object.keys(updatedTask).length === 2 - const nextTask = new Task({ - ...task, - userId: nextUserId, - status: status || task.status, - sortOrder: sortOrder || task.sortOrder, - content: content ? validContent : task.content, - plaintextContent: content ? extractTextFromDraftString(validContent) : task.plaintextContent, - updatedAt: isSortOrderUpdate ? task.updatedAt : now - }) - const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) - const {newTask} = await r({ - newTask: r - .table('Task') - .get(taskId) - .update(nextTask, {returnChanges: true})('changes')(0)('new_val') - .default(null) as unknown as Task - }).run() - await pg + const updateRes = await pg .updateTable('Task') .set({ content: content ? validContent : undefined, - plaintextContent: content - ? extractTextFromDraftString(validContent) - : task.plaintextContent, + plaintextContent: content ? extractTextFromDraftString(validContent) : undefined, sortOrder: sortOrder || undefined, status: status || undefined, userId: inputUserId || undefined }) .where('id', '=', taskId) - .execute() + .executeTakeFirst() + if (Number(updateRes.numChangedRows) === 0) { + return standardError(new Error('Already updated task'), {userId: viewerId}) + } dataLoader.clearAll('tasks') + const newTask = await dataLoader.get('tasks').loadNonNull(taskId) // TODO: get users in the same location const usersToIgnore = await getUsersToIgnore(viewerId, teamId) - if (!newTask) return standardError(new Error('Already updated task'), {userId: viewerId}) // send task updated messages const isPrivate = newTask.tags.includes('private') diff --git a/packages/server/graphql/mutations/updateTaskDueDate.ts b/packages/server/graphql/mutations/updateTaskDueDate.ts index 7660dab5521..9482c532ea7 100644 --- a/packages/server/graphql/mutations/updateTaskDueDate.ts +++ b/packages/server/graphql/mutations/updateTaskDueDate.ts @@ -1,7 +1,6 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import isValidDate from 'parabol-client/utils/isValidDate' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -30,7 +29,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -49,16 +47,7 @@ export default { } // RESOLUTION - await r - .table('Task') - .get(taskId) - .update({ - dueDate: nextDueDate - }) - .run() - await pg.updateTable('Task').set({dueDate: nextDueDate}).where('id', '=', taskId).execute() - dataLoader.clearAll('tasks') const data = {taskId} diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 63f9571af59..d2f91d82044 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -1,5 +1,4 @@ import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' @@ -86,12 +85,7 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .where('createdBy', '=', userIdToDelete) .execute() await r({ - notification: r.table('Notification').getAll(userIdToDelete, {index: 'userId'}).delete(), - createdTasks: r - .table('Task') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RValue) => row('createdBy').eq(userIdToDelete)) - .delete() + notification: r.table('Notification').getAll(userIdToDelete, {index: 'userId'}).delete() }).run() // now postgres, after FKs are added then triggers should take care of children diff --git a/packages/server/graphql/public/mutations/batchArchiveTasks.ts b/packages/server/graphql/public/mutations/batchArchiveTasks.ts index 66345e4f2fc..5c8e6dd0d5b 100644 --- a/packages/server/graphql/public/mutations/batchArchiveTasks.ts +++ b/packages/server/graphql/public/mutations/batchArchiveTasks.ts @@ -3,7 +3,7 @@ import {getUserId} from '../../../utils/authorization' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import publish from '../../../utils/publish' -import Task from '../../../database/types/Task' +import {Task} from '../../../postgres/types' import archiveTasksForDB from '../../../safeMutations/archiveTasksForDB' import isValid from '../../isValid' import {MutationResolvers} from '../resolverTypes' diff --git a/packages/server/graphql/public/types/ActionMeetingMember.ts b/packages/server/graphql/public/types/ActionMeetingMember.ts index 036dbf7b2b9..1916b3d0351 100644 --- a/packages/server/graphql/public/types/ActionMeetingMember.ts +++ b/packages/server/graphql/public/types/ActionMeetingMember.ts @@ -1,18 +1,17 @@ +import {sql} from 'kysely' import isTaskPrivate from 'parabol-client/utils/isTaskPrivate' -import getRethink from '../../../database/rethinkDriver' -import {RDatum} from '../../../database/stricterR' +import {selectTasks} from '../../../postgres/select' import {ActionMeetingMemberResolvers} from '../resolverTypes' const ActionMeetingMember: ActionMeetingMemberResolvers = { __isTypeOf: ({meetingType}) => meetingType === 'action', doneTasks: async ({meetingId, userId}) => { - const r = await getRethink() - return r - .table('Task') - .getAll(userId, {index: 'userId'}) - .filter({doneMeetingId: meetingId}) - .filter((task: RDatum) => task('tags').contains('private').not()) - .run() + const res = await selectTasks() + .where('userId', '=', userId) + .where('doneMeetingId', '=', meetingId) + .where(sql`'private' != ANY(tags)`) + .execute() + return res }, tasks: async ({meetingId, userId}, _args, {dataLoader}) => { diff --git a/packages/server/graphql/public/types/EstimateStage.ts b/packages/server/graphql/public/types/EstimateStage.ts index dbb12ab652d..53e65c0a7f8 100644 --- a/packages/server/graphql/public/types/EstimateStage.ts +++ b/packages/server/graphql/public/types/EstimateStage.ts @@ -225,7 +225,7 @@ const EstimateStage: EstimateStageResolvers = { }, task: async ({taskId}, _args, {dataLoader}) => { - return dataLoader.get('tasks').load(taskId) + return dataLoader.get('tasks').loadNonNull(taskId) } } diff --git a/packages/server/graphql/public/types/TeamPromptMeeting.ts b/packages/server/graphql/public/types/TeamPromptMeeting.ts index d4c013869a8..87d921b26c7 100644 --- a/packages/server/graphql/public/types/TeamPromptMeeting.ts +++ b/packages/server/graphql/public/types/TeamPromptMeeting.ts @@ -1,4 +1,4 @@ -import getRethink from '../../../database/rethinkDriver' +import getKysely from '../../../postgres/getKysely' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' import {selectNewMeetings} from '../../../postgres/select' import {TeamPromptMeeting as TeamPromptMeetingSource} from '../../../postgres/types/Meeting' @@ -68,6 +68,7 @@ const TeamPromptMeeting: TeamPromptMeetingResolvers = { }, taskCount: async ({id: meetingId}, _args, {dataLoader}) => { + const pg = getKysely() const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) if (meeting.meetingType !== 'teamPrompt') { return 0 @@ -76,13 +77,12 @@ 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('Task') - .getAll(r.args(discussionIds), {index: 'discussionId'}) - .count() - .default(0) - .run() + const taskCountRes = await pg + .selectFrom('Task') + .select(({fn}) => fn.count('id').as('count')) + .where('discussionId', 'in', discussionIds) + .executeTakeFirst() + return Number(taskCountRes?.count ?? 0) }, commentCount: async ({id: meetingId}, _args, {dataLoader}) => { diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index d736cb65a26..ae1f621fe38 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -1,4 +1,5 @@ import base64url from 'base64url' +import {sql} from 'kysely' import ms from 'ms' import DomainJoinRequestId from 'parabol-client/shared/gqlIds/DomainJoinRequestId' import MeetingMemberId from 'parabol-client/shared/gqlIds/MeetingMemberId' @@ -12,10 +13,10 @@ import { } from '../../../../client/utils/constants' import groupReflections from '../../../../client/utils/smartGroup/groupReflections' import getRethink from '../../../database/rethinkDriver' -import {RDatum, RValue} from '../../../database/stricterR' +import {RDatum} from '../../../database/stricterR' import MeetingTemplate from '../../../database/types/MeetingTemplate' -import Task from '../../../database/types/Task' import getKysely from '../../../postgres/getKysely' +import {selectTasks} from '../../../postgres/select' import {getUserId, isSuperUser, isTeamMember} from '../../../utils/authorization' import getDomainFromEmail from '../../../utils/getDomainFromEmail' import getMonthlyStreak from '../../../utils/getMonthlyStreak' @@ -82,8 +83,6 @@ const User: ReqResolvers<'User'> = { }, invoices, archivedTasks: async (_source, {first, after, teamId}, {authToken}) => { - const r = await getRethink() - // AUTH const userId = getUserId(authToken) if (!isTeamMember(authToken, teamId)) { @@ -92,25 +91,14 @@ const User: ReqResolvers<'User'> = { } // RESOLUTION - const teamMemberId = `${userId}::${teamId}` - const dbAfter = after ? new Date(after) : r.maxval - const tasks = await r - .table('Task') - // use a compound index so we can easily paginate later - .between([teamId, r.minval], [teamId, dbAfter], { - index: 'teamIdUpdatedAt' - }) - .filter((task: RValue) => - task('tags') - .contains('archived') - .and( - r.branch(task('tags').contains('private'), task('teamMemberId').eq(teamMemberId), true) - ) - ) - .orderBy(r.desc('updatedAt')) + const tasks = await selectTasks() + .where('teamId', '=', teamId) + .$if(!!after, (qb) => qb.where('updatedAt', '<=', after!)) + .where(sql`'archived' = ANY(tags)`) + .where(({eb, or}) => or([sql`'private' != ANY(tags)`, eb('userId', '=', userId)])) + .orderBy('updatedAt desc') .limit(first + 1) - .coerceTo('array') - .run() + .execute() const nodes = tasks.slice(0, first) const edges = nodes.map((node) => ({ @@ -129,7 +117,7 @@ const User: ReqResolvers<'User'> = { } }, archivedTasksCount: async (_source, {teamId}, {authToken}) => { - const r = await getRethink() + const pg = getKysely() const viewerId = getUserId(authToken) // AUTH @@ -140,21 +128,14 @@ const User: ReqResolvers<'User'> = { } // RESOLUTION - const teamMemberId = `${userId}::${teamId}` - return r - .table('Task') - .between([teamId, r.minval], [teamId, r.maxval], { - index: 'teamIdUpdatedAt' - }) - .filter((task: RValue) => - task('tags') - .contains('archived') - .and( - r.branch(task('tags').contains('private'), task('teamMemberId').eq(teamMemberId), true) - ) - ) - .count() - .run() + const taskCount = await pg + .selectFrom('Task') + .select(({fn}) => fn.count('id').as('count')) + .where('teamId', '=', teamId) + .where(sql`'archived' = ANY(tags)`) + .where(({eb, or}) => or([sql`'private' != ANY(tags)`, eb('userId', '=', userId)])) + .executeTakeFirstOrThrow() + return Number(taskCount.count) }, meeting: async (_source, {meetingId}, {authToken, dataLoader}) => { const viewerId = getUserId(authToken) @@ -253,7 +234,7 @@ const User: ReqResolvers<'User'> = { filterQuery, includeUnassigned }) - const filteredTasks = tasks.filter((task: Task) => { + const filteredTasks = tasks.filter((task) => { if (isTaskPrivate(task.tags) && task.userId !== viewerId) return false return true }) diff --git a/packages/server/graphql/queries/helpers/connectionFromTasks.ts b/packages/server/graphql/queries/helpers/connectionFromTasks.ts index c0301af54c7..b34f65fd485 100644 --- a/packages/server/graphql/queries/helpers/connectionFromTasks.ts +++ b/packages/server/graphql/queries/helpers/connectionFromTasks.ts @@ -1,5 +1,5 @@ import {Threshold} from 'parabol-client/types/constEnums' -import Task from '../../../database/types/Task' +import {Task} from '../../../postgres/types' const connectionFromTasks = ( tasks: T[], diff --git a/packages/server/graphql/resolvers.ts b/packages/server/graphql/resolvers.ts index a038a5f7c95..c1be3d60a38 100644 --- a/packages/server/graphql/resolvers.ts +++ b/packages/server/graphql/resolvers.ts @@ -4,10 +4,9 @@ import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {NewMeetingPhaseTypeEnum} from '../database/types/GenericMeetingPhase' import GenericMeetingStage from '../database/types/GenericMeetingStage' import Organization from '../database/types/Organization' -import Task from '../database/types/Task' import User from '../database/types/User' import {Loaders} from '../dataloader/RootDataLoader' -import {Team, TeamMember} from '../postgres/types' +import {Task, Team, TeamMember} from '../postgres/types' import {AnyMeeting} from '../postgres/types/Meeting' import {getUserId, isSuperUser, isUserBillingLeader} from '../utils/authorization' import {GQLContext} from './graphql' diff --git a/packages/server/graphql/resolvers/resolveThreadableConnection.ts b/packages/server/graphql/resolvers/resolveThreadableConnection.ts index f553886f470..723f1af3d7f 100644 --- a/packages/server/graphql/resolvers/resolveThreadableConnection.ts +++ b/packages/server/graphql/resolvers/resolveThreadableConnection.ts @@ -1,5 +1,4 @@ -import TaskDB from '../../database/types/Task' -import {Comment} from '../../postgres/types' +import {Comment, Task} from '../../postgres/types' import {ThreadableSource} from '../public/types/Threadable' import {DataLoaderWorker} from './../graphql' @@ -22,7 +21,7 @@ const resolveThreadableConnection = async ( const {threadParentId} = threadable if (!threadParentId) { rootThreadables.push(threadable) - } else if ((threadable as TaskDB).status || (threadable as Comment).isActive) { + } else if ((threadable as Task).status || (threadable as Comment).isActive) { // if it's a task or it's a non-deleted comment, add it threadablesByParentId[threadParentId] = threadablesByParentId[threadParentId] || [] threadablesByParentId[threadParentId]!.push(threadable) @@ -33,7 +32,7 @@ const resolveThreadableConnection = async ( rootThreadables.forEach((threadable) => { const {id: threadableId} = threadable const replies = threadablesByParentId[threadableId] - const isActive = (threadable as TaskDB).status || (threadable as Comment).isActive + const isActive = (threadable as Task).status || (threadable as Comment).isActive // (threadable as Poll).deletedAt === null if (!isActive && !replies) return filteredThreadables.push(threadable) diff --git a/packages/server/graphql/types/EndCheckInPayload.ts b/packages/server/graphql/types/EndCheckInPayload.ts index 82a922729aa..81e648962a9 100644 --- a/packages/server/graphql/types/EndCheckInPayload.ts +++ b/packages/server/graphql/types/EndCheckInPayload.ts @@ -1,8 +1,8 @@ import {GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLObjectType} from 'graphql' import isTaskPrivate from 'parabol-client/utils/isTaskPrivate' import {getUserId} from '../../utils/authorization' -import errorFilter from '../errorFilter' import {GQLContext} from '../graphql' +import isValid from '../isValid' import {resolveNewMeeting} from '../resolvers' import ActionMeeting from './ActionMeeting' import Task from './Task' @@ -51,7 +51,7 @@ export const EndCheckInSuccess = new GraphQLObjectType({ if (!updatedTaskIds) return [] const viewerId = getUserId(authToken) const allUpdatedTasks = (await dataLoader.get('tasks').loadMany(updatedTaskIds)).filter( - errorFilter + isValid ) return allUpdatedTasks.filter((task) => { return isTaskPrivate(task.tags) ? task.userId === viewerId : true diff --git a/packages/server/graphql/types/PokerMeeting.ts b/packages/server/graphql/types/PokerMeeting.ts index bbea73c760f..dfb66332669 100644 --- a/packages/server/graphql/types/PokerMeeting.ts +++ b/packages/server/graphql/types/PokerMeeting.ts @@ -38,7 +38,7 @@ const PokerMeeting = new GraphQLObjectType({ } }, resolve: async ({id: meetingId}, {storyId: taskId}, {dataLoader}) => { - const task = await dataLoader.get('tasks').load(taskId) + const task = await dataLoader.get('tasks').loadNonNull(taskId) if (task.meetingId !== meetingId) { Logger.log('naughty storyId supplied to PokerMeeting') return null diff --git a/packages/server/graphql/types/Team.ts b/packages/server/graphql/types/Team.ts index 7a92925c44a..bf24dfb0207 100644 --- a/packages/server/graphql/types/Team.ts +++ b/packages/server/graphql/types/Team.ts @@ -10,7 +10,6 @@ import { import isTaskPrivate from 'parabol-client/utils/isTaskPrivate' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' import {Security, Threshold} from '../../../client/types/constEnums' -import Task from '../../database/types/Task' import ITeam from '../../database/types/Team' import generateRandomString from '../../generateRandomString' import getKysely from '../../postgres/getKysely' @@ -296,7 +295,7 @@ const Team: GraphQLObjectType = new GraphQLObjectType({ } const viewerId = getUserId(authToken) const allTasks = await dataLoader.get('tasksByTeamId').load(teamId) - const tasks = allTasks.filter((task: Task) => { + const tasks = allTasks.filter((task) => { if (!task.userId || (isTaskPrivate(task.tags) && task.userId !== viewerId)) return false return true }) diff --git a/packages/server/integrations/TaskIntegrationManagerFactory.ts b/packages/server/integrations/TaskIntegrationManagerFactory.ts index eeaed5683d7..44f67180886 100644 --- a/packages/server/integrations/TaskIntegrationManagerFactory.ts +++ b/packages/server/integrations/TaskIntegrationManagerFactory.ts @@ -1,11 +1,11 @@ import {GraphQLResolveInfo} from 'graphql' -import {TaskIntegration} from '../database/types/Task' import {DataLoaderWorker, GQLContext} from '../graphql/graphql' import {IntegrationProviderServiceEnumType} from '../graphql/types/IntegrationProviderServiceEnum' import { IntegrationProviderAzureDevOps, IntegrationProviderJiraServer } from '../postgres/queries/getIntegrationProvidersByIds' +import {Task} from '../postgres/types' import AzureDevOpsServerManager from '../utils/AzureDevOpsServerManager' import GitHubServerManager from './github/GitHubServerManager' import GitLabServerManager from './gitlab/GitLabServerManager' @@ -18,7 +18,7 @@ export type CreateTaskResponse = // TODO: include issueId for GitHub in hash or store integration.issueId for all integrations // See https://github.com/ParabolInc/parabol/issues/6252 issueId: string - integration: TaskIntegration + integration: NonNullable } | Error diff --git a/packages/server/postgres/types/TaskIntegration.d.ts b/packages/server/postgres/types/TaskIntegration.d.ts index 75ca80ad75f..06014b1401e 100644 --- a/packages/server/postgres/types/TaskIntegration.d.ts +++ b/packages/server/postgres/types/TaskIntegration.d.ts @@ -42,3 +42,5 @@ export type AnyTaskIntegration = | TaskIntegrationGitHub | TaskIntegrationGitLab | TaskIntegrationAzureDevOps + +export type TaskServiceEnum = AnyTaskIntegration['service'] | 'PARABOL' diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 587e2372564..4698a12b73d 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -16,6 +16,7 @@ import { selectSlackAuths, selectSlackNotifications, selectSuggestedAction, + selectTasks, selectTeamInvitations, selectTeamPromptResponses, selectTeams, @@ -76,3 +77,4 @@ export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect export type NewFeature = ExtractTypeFromQueryBuilderSelect export type TeamInvitation = ExtractTypeFromQueryBuilderSelect +export type Task = ExtractTypeFromQueryBuilderSelect diff --git a/packages/server/safeMutations/archiveTasksForDB.ts b/packages/server/safeMutations/archiveTasksForDB.ts index 4197a64d685..18b005cdf88 100644 --- a/packages/server/safeMutations/archiveTasksForDB.ts +++ b/packages/server/safeMutations/archiveTasksForDB.ts @@ -1,14 +1,12 @@ import {convertFromRaw, convertToRaw} from 'draft-js' import addTagToTask from 'parabol-client/utils/draftjs/addTagToTask' import getTagsFromEntityMap from 'parabol-client/utils/draftjs/getTagsFromEntityMap' -import getRethink from '../database/rethinkDriver' -import Task from '../database/types/Task' import getKysely from '../postgres/getKysely' +import {Task} from '../postgres/types/index.d' const archiveTasksForDB = async (tasks: Task[], doneMeetingId?: string) => { if (!tasks || tasks.length === 0) return [] const pg = getKysely() - const r = await getRethink() const tasksToArchive = tasks.map((task) => { const contentState = convertFromRaw(JSON.parse(task.content)) const nextContentState = addTagToTask(contentState, '#archived') @@ -36,22 +34,7 @@ const archiveTasksForDB = async (tasks: Task[], doneMeetingId?: string) => { .execute() ) ) - return r(tasksToArchive) - .forEach((task) => { - return r - .table('Task') - .get(task('id')) - .update( - { - content: task('content') as unknown, - tags: task('tags'), - doneMeetingId: task('doneMeetingId').default(null) - } as any, - {returnChanges: true} - ) - }) - .default([])('changes')('new_val') - .run() as Promise + return tasksToArchive.map(({id}) => id) } export default archiveTasksForDB diff --git a/packages/server/utils/filterTasksByMeeting.ts b/packages/server/utils/filterTasksByMeeting.ts index 719f534c4ed..e99c5ecba43 100644 --- a/packages/server/utils/filterTasksByMeeting.ts +++ b/packages/server/utils/filterTasksByMeeting.ts @@ -1,5 +1,5 @@ import isTaskPrivate from 'parabol-client/utils/isTaskPrivate' -import Task from '../database/types/Task' +import {Task} from '../postgres/types' const filterTasksByMeeting = (tasks: Task[], meetingId: string, viewerId: string) => { return tasks.filter((task) => { From 569ebaf15a0ca63e018844db8d394fb752391bdd Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Fri, 11 Oct 2024 16:40:07 -0700 Subject: [PATCH 79/89] remove legacy Task db type Signed-off-by: Matt Krick --- codegen.json | 1 + .../components/TaskIntegrationMenuItem.tsx | 2 +- .../modules/demo/ClientGraphQLServer.ts | 2 +- packages/client/modules/demo/initDB.ts | 2 +- .../mutations/BatchArchiveTasksMutation.ts | 2 +- .../client/mutations/UpdateTaskMutation.ts | 2 +- .../mutations/handlers/handleRemoveTasks.ts | 2 +- .../client/shared/gqlIds/IntegrationHash.ts | 5 +- .../database/types/BaseTaskIntegration.ts | 29 -- packages/server/database/types/Task.ts | 114 ------ .../types/TaskIntegrationAzureDevOps.ts | 22 -- .../database/types/TaskIntegrationGitHub.ts | 19 - .../database/types/TaskIntegrationGitLab.ts | 19 - .../database/types/TaskIntegrationJira.ts | 23 -- .../types/TaskIntegrationJiraServer.ts | 22 -- .../server/graphql/mutations/createTask.ts | 2 +- .../graphql/mutations/helpers/addSeedTasks.ts | 5 +- .../mutations/helpers/createTaskInService.ts | 26 +- .../graphql/mutations/setTaskEstimate.ts | 3 +- .../graphql/mutations/updatePokerScope.ts | 2 +- .../server/graphql/mutations/updateTask.ts | 6 +- .../graphql/public/types/EstimateStage.ts | 12 +- packages/server/graphql/public/types/Task.ts | 229 ++++++++++++ .../graphql/public/types/TaskEstimate.ts | 7 + packages/server/graphql/types/Task.ts | 339 +----------------- packages/server/graphql/types/TaskEstimate.ts | 57 --- packages/server/postgres/types/index.d.ts | 2 + packages/server/utils/analytics/analytics.ts | 2 +- 28 files changed, 277 insertions(+), 681 deletions(-) delete mode 100644 packages/server/database/types/BaseTaskIntegration.ts delete mode 100644 packages/server/database/types/Task.ts delete mode 100644 packages/server/database/types/TaskIntegrationAzureDevOps.ts delete mode 100644 packages/server/database/types/TaskIntegrationGitHub.ts delete mode 100644 packages/server/database/types/TaskIntegrationGitLab.ts delete mode 100644 packages/server/database/types/TaskIntegrationJira.ts delete mode 100644 packages/server/database/types/TaskIntegrationJiraServer.ts create mode 100644 packages/server/graphql/public/types/Task.ts create mode 100644 packages/server/graphql/public/types/TaskEstimate.ts delete mode 100644 packages/server/graphql/types/TaskEstimate.ts diff --git a/codegen.json b/codegen.json index 97fff950315..53737da9630 100644 --- a/codegen.json +++ b/codegen.json @@ -50,6 +50,7 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { + "TaskEstimate": "../../postgres/types/index#TaskEstimate", "ReflectTemplatePromptUpdateDescriptionPayload": "./types/ReflectTemplatePromptUpdateDescriptionPayload#ReflectTemplatePromptUpdateDescriptionPayloadSource", "NewFeatureBroadcast": "../../postgres/types/index#NewFeature", "ReflectTemplatePromptUpdateGroupColorPayload": "./types/ReflectTemplatePromptUpdateGroupColorPayload#ReflectTemplatePromptUpdateGroupColorPayloadSource", diff --git a/packages/client/components/TaskIntegrationMenuItem.tsx b/packages/client/components/TaskIntegrationMenuItem.tsx index c8212b3b687..8e4b47c9ec8 100644 --- a/packages/client/components/TaskIntegrationMenuItem.tsx +++ b/packages/client/components/TaskIntegrationMenuItem.tsx @@ -1,5 +1,5 @@ import React, {forwardRef} from 'react' -import {TaskServiceEnum} from '~/../server/database/types/Task' +import {TaskServiceEnum} from '../__generated__/CreateTaskMutation.graphql' import AzureDevOpsSVG from './AzureDevOpsSVG' import GitHubSVG from './GitHubSVG' import GitLabSVG from './GitLabSVG' diff --git a/packages/client/modules/demo/ClientGraphQLServer.ts b/packages/client/modules/demo/ClientGraphQLServer.ts index 4be77ecd626..0351e4381dc 100644 --- a/packages/client/modules/demo/ClientGraphQLServer.ts +++ b/packages/client/modules/demo/ClientGraphQLServer.ts @@ -10,13 +10,13 @@ import {DragReflectionDropTargetTypeEnum} from '~/__generated__/EndDraggingRefle import {PALETTE} from '~/styles/paletteV3' import GoogleAnalyzedEntity from '../../../server/database/types/GoogleAnalyzedEntity' import ReflectPhase from '../../../server/database/types/ReflectPhase' -import ITask from '../../../server/database/types/Task' import {NewMeetingStage} from '../../../server/graphql/private/resolverTypes' import { DiscussPhase, DiscussStage, NewMeetingPhase } from '../../../server/postgres/types/NewMeetingPhase' +import {Task as ITask} from '../../../server/postgres/types/index.d' import { ExternalLinks, MeetingSettingsThreshold, diff --git a/packages/client/modules/demo/initDB.ts b/packages/client/modules/demo/initDB.ts index 4b7d95391ee..7744fcc783d 100644 --- a/packages/client/modules/demo/initDB.ts +++ b/packages/client/modules/demo/initDB.ts @@ -1,6 +1,6 @@ import {SlackNotificationEventEnum} from '~/__generated__/SlackNotificationList_viewer.graphql' import {PALETTE} from '~/styles/paletteV3' -import ITask from '../../../server/database/types/Task' +import {Task as ITask} from '../../../server/postgres/types/index.d' import {RetrospectiveMeeting} from '../../../server/postgres/types/Meeting' import JiraProjectId from '../../shared/gqlIds/JiraProjectId' import demoUserAvatar from '../../styles/theme/images/avatar-user.svg' diff --git a/packages/client/mutations/BatchArchiveTasksMutation.ts b/packages/client/mutations/BatchArchiveTasksMutation.ts index 4963f72303f..f95935aa9ec 100644 --- a/packages/client/mutations/BatchArchiveTasksMutation.ts +++ b/packages/client/mutations/BatchArchiveTasksMutation.ts @@ -1,7 +1,7 @@ import graphql from 'babel-plugin-relay/macro' import {commitMutation} from 'react-relay' import {BatchArchiveTasksMutation_tasks$data} from '~/__generated__/BatchArchiveTasksMutation_tasks.graphql' -import ITask from '../../server/database/types/Task' +import {Task as ITask} from '../../server/postgres/types/index.d' import {BatchArchiveTasksMutation as TBatchArchiveTasksMutation} from '../__generated__/BatchArchiveTasksMutation.graphql' import {SharedUpdater, StandardMutation} from '../types/relayMutations' import getTagsFromEntityMap from '../utils/draftjs/getTagsFromEntityMap' diff --git a/packages/client/mutations/UpdateTaskMutation.ts b/packages/client/mutations/UpdateTaskMutation.ts index d66706865bd..c9ae0540336 100644 --- a/packages/client/mutations/UpdateTaskMutation.ts +++ b/packages/client/mutations/UpdateTaskMutation.ts @@ -1,6 +1,6 @@ import graphql from 'babel-plugin-relay/macro' import {commitMutation} from 'react-relay' -import ITask from '../../server/database/types/Task' +import {Task as ITask} from '../../server/postgres/types/index.d' import {UpdateTaskMutation as TUpdateTaskMutation} from '../__generated__/UpdateTaskMutation.graphql' import {UpdateTaskMutation_task$data} from '../__generated__/UpdateTaskMutation_task.graphql' import { diff --git a/packages/client/mutations/handlers/handleRemoveTasks.ts b/packages/client/mutations/handlers/handleRemoveTasks.ts index 3c517948455..1ee5667aad8 100644 --- a/packages/client/mutations/handlers/handleRemoveTasks.ts +++ b/packages/client/mutations/handlers/handleRemoveTasks.ts @@ -2,8 +2,8 @@ import {RecordSourceSelectorProxy} from 'relay-runtime' import {handleRemoveReply} from '~/mutations/DeleteCommentMutation' import getDiscussionThreadConn from '~/mutations/connections/getDiscussionThreadConn' import {parseQueryParams} from '~/utils/useQueryParameterParser' -import ITask from '../../../server/database/types/Task' import IUser from '../../../server/database/types/User' +import {Task as ITask} from '../../../server/postgres/types/index.d' import safeRemoveNodeFromArray from '../../utils/relay/safeRemoveNodeFromArray' import safeRemoveNodeFromConn from '../../utils/relay/safeRemoveNodeFromConn' import getArchivedTasksConn from '../connections/getArchivedTasksConn' diff --git a/packages/client/shared/gqlIds/IntegrationHash.ts b/packages/client/shared/gqlIds/IntegrationHash.ts index ec3b33ae0c6..4ad7f2ed493 100644 --- a/packages/client/shared/gqlIds/IntegrationHash.ts +++ b/packages/client/shared/gqlIds/IntegrationHash.ts @@ -1,4 +1,5 @@ -import {TaskIntegration, TaskServiceEnum} from '../../../server/database/types/Task' +import {AnyTaskIntegration} from '../../../server/postgres/types/TaskIntegration' +import {TaskServiceEnum} from '../../__generated__/CreateTaskMutation.graphql' import AzureDevOpsIssueId from './AzureDevOpsIssueId' import GitHubIssueId from './GitHubIssueId' import GitLabIssueId from './GitLabIssueId' @@ -6,7 +7,7 @@ import JiraIssueId from './JiraIssueId' import JiraServerIssueId from './JiraServerIssueId' const IntegrationHash = { - join: (integration: TaskIntegration) => { + join: (integration: AnyTaskIntegration) => { switch (integration.service) { case 'github': return GitHubIssueId.join(integration.nameWithOwner, integration.issueNumber) diff --git a/packages/server/database/types/BaseTaskIntegration.ts b/packages/server/database/types/BaseTaskIntegration.ts deleted file mode 100644 index 859289051da..00000000000 --- a/packages/server/database/types/BaseTaskIntegration.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - The goal of a TaskIntegration is to store the smallest amount of information possible - in order to fetch the issue from the integration. - This includes: - - service: the name of the integration - - accessUserId: The user that first accessed the issue (we can use their accecss token) - Class extensions will probably include one or more of the following: - - tenant (cloudId, providerId, etc.) - - repo (repoName, project, etc.) - - issue (issueId, issueNumber, issueKey, etc.) - As a rule of thumb, if the property isn't required to fetch the issue, don't include it -*/ - -import {TaskServiceEnum} from './Task' - -interface Input { - accessUserId: string - service: TaskServiceEnum -} - -export default abstract class BaseTaskIntegration { - service: TaskServiceEnum - accessUserId: string - constructor(input: Input) { - const {accessUserId, service} = input - this.accessUserId = accessUserId - this.service = service - } -} diff --git a/packages/server/database/types/Task.ts b/packages/server/database/types/Task.ts deleted file mode 100644 index 44d795cf3b3..00000000000 --- a/packages/server/database/types/Task.ts +++ /dev/null @@ -1,114 +0,0 @@ -import dndNoise from 'parabol-client/utils/dndNoise' -import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' -import getTagsFromEntityMap from 'parabol-client/utils/draftjs/getTagsFromEntityMap' -import generateUID from '../../generateUID' -import TaskIntegrationAzureDevOps from './TaskIntegrationAzureDevOps' -import TaskIntegrationGitHub from './TaskIntegrationGitHub' -import TaskIntegrationGitLab from './TaskIntegrationGitLab' -import TaskIntegrationJira from './TaskIntegrationJira' -import TaskIntegrationJiraServer from './TaskIntegrationJiraServer' - -export type AreaEnum = 'meeting' | 'teamDash' | 'userDash' -export type TaskStatusEnum = 'active' | 'stuck' | 'done' | 'future' -export type TaskTagEnum = 'private' | 'archived' -export type TaskServiceEnum = - | 'PARABOL' - | 'github' - | 'jira' - | 'jiraServer' - | 'gitlab' - | 'azureDevOps' - -export type TaskIntegration = - | TaskIntegrationJira - | TaskIntegrationGitHub - | TaskIntegrationJiraServer - | TaskIntegrationGitLab - | TaskIntegrationAzureDevOps -export interface TaskInput { - id?: string - content: string - createdAt?: Date | null - createdBy: string - doneMeetingId?: string - dueDate?: Date | null - integration?: TaskIntegration - - integrationHash?: string - meetingId?: string | null - plaintextContent?: string - sortOrder?: number | null - status: TaskStatusEnum - teamId: string - discussionId?: string | null - threadParentId?: string | null - threadSortOrder?: number | null - updatedAt?: Date - userId?: string | null -} - -export default class Task { - id: string - content: string - createdAt: Date - createdBy: string - doneMeetingId?: string - dueDate?: Date | null - integration?: TaskIntegration - integrationHash?: string - meetingId?: string - plaintextContent: string - sortOrder: number - status: TaskStatusEnum - tags: TaskTagEnum[] - teamId: string - discussionId?: string - threadParentId?: string - threadSortOrder?: number | null - updatedAt: Date - userId: string | null - - constructor(input: TaskInput) { - const { - id, - userId, - meetingId, - teamId, - content, - createdAt, - createdBy, - doneMeetingId, - dueDate, - integration, - integrationHash, - plaintextContent, - sortOrder, - status, - threadParentId, - threadSortOrder, - discussionId, - updatedAt - } = input - const {entityMap} = JSON.parse(content) - const tags = getTagsFromEntityMap(entityMap) - this.id = id || generateUID() - this.discussionId = discussionId || undefined - this.content = content - this.createdAt = createdAt || new Date() - this.createdBy = createdBy - this.doneMeetingId = doneMeetingId - this.dueDate = dueDate || undefined - this.integration = integration || undefined - this.integrationHash = integrationHash - this.meetingId = meetingId || undefined - this.plaintextContent = plaintextContent || extractTextFromDraftString(content) - this.sortOrder = sortOrder || dndNoise() - this.status = status - this.tags = tags - this.teamId = teamId - this.threadSortOrder = threadSortOrder || undefined - this.threadParentId = threadParentId || undefined - this.updatedAt = updatedAt || new Date() - this.userId = userId || null - } -} diff --git a/packages/server/database/types/TaskIntegrationAzureDevOps.ts b/packages/server/database/types/TaskIntegrationAzureDevOps.ts deleted file mode 100644 index 9f0804e426e..00000000000 --- a/packages/server/database/types/TaskIntegrationAzureDevOps.ts +++ /dev/null @@ -1,22 +0,0 @@ -import BaseTaskIntegration from './BaseTaskIntegration' - -interface Input { - accessUserId: string - instanceId: string - projectKey: string - issueKey: string -} - -export default class TaskIntegrationAzureDevOps extends BaseTaskIntegration { - instanceId: string - issueKey: string - projectKey: string - service!: 'azureDevOps' - constructor(input: Input) { - const {accessUserId, instanceId, projectKey, issueKey} = input - super({accessUserId, service: 'azureDevOps'}) - this.projectKey = projectKey - this.instanceId = instanceId - this.issueKey = issueKey - } -} diff --git a/packages/server/database/types/TaskIntegrationGitHub.ts b/packages/server/database/types/TaskIntegrationGitHub.ts deleted file mode 100644 index 7942b147066..00000000000 --- a/packages/server/database/types/TaskIntegrationGitHub.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BaseTaskIntegration from './BaseTaskIntegration' - -interface Input { - accessUserId: string - nameWithOwner: string - issueNumber: number -} - -export default class TaskIntegrationGitHub extends BaseTaskIntegration { - nameWithOwner: string - issueNumber: number - service!: 'github' - constructor(input: Input) { - const {accessUserId, nameWithOwner, issueNumber} = input - super({accessUserId, service: 'github'}) - this.nameWithOwner = nameWithOwner - this.issueNumber = issueNumber - } -} diff --git a/packages/server/database/types/TaskIntegrationGitLab.ts b/packages/server/database/types/TaskIntegrationGitLab.ts deleted file mode 100644 index b21840c9d75..00000000000 --- a/packages/server/database/types/TaskIntegrationGitLab.ts +++ /dev/null @@ -1,19 +0,0 @@ -import BaseTaskIntegration from './BaseTaskIntegration' - -interface Input { - accessUserId: string - providerId: string - gid: string -} - -export default class TaskIntegrationGitLab extends BaseTaskIntegration { - providerId: string - gid: string - service!: 'gitlab' - constructor(input: Input) { - const {accessUserId, providerId, gid} = input - super({accessUserId, service: 'gitlab'}) - this.providerId = providerId - this.gid = gid - } -} diff --git a/packages/server/database/types/TaskIntegrationJira.ts b/packages/server/database/types/TaskIntegrationJira.ts deleted file mode 100644 index 43872ef499c..00000000000 --- a/packages/server/database/types/TaskIntegrationJira.ts +++ /dev/null @@ -1,23 +0,0 @@ -import JiraProjectKeyId from '../../../client/shared/gqlIds/JiraProjectKeyId' -import BaseTaskIntegration from './BaseTaskIntegration' - -interface Input { - accessUserId: string - cloudId: string - issueKey: string -} - -export default class TaskIntegrationJira extends BaseTaskIntegration { - cloudId: string - issueKey: string - projectKey: string - service!: 'jira' - constructor(input: Input) { - const {accessUserId, cloudId, issueKey} = input - super({accessUserId, service: 'jira'}) - const projectKey = JiraProjectKeyId.join(issueKey) - this.projectKey = projectKey - this.cloudId = cloudId - this.issueKey = issueKey - } -} diff --git a/packages/server/database/types/TaskIntegrationJiraServer.ts b/packages/server/database/types/TaskIntegrationJiraServer.ts deleted file mode 100644 index 54cc6bc24a7..00000000000 --- a/packages/server/database/types/TaskIntegrationJiraServer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import BaseTaskIntegration from './BaseTaskIntegration' - -interface Input { - accessUserId: string - providerId: number - issueId: string - repositoryId: string -} - -export default class TaskIntegrationJiraServer extends BaseTaskIntegration { - providerId: number - issueId: string - repositoryId: string - service!: 'jiraServer' - constructor(input: Input) { - const {accessUserId, providerId, issueId, repositoryId} = input - super({accessUserId, service: 'jiraServer'}) - this.providerId = providerId - this.issueId = issueId - this.repositoryId = repositoryId - } -} diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts index 92af99686f7..6aa1d8941e9 100644 --- a/packages/server/graphql/mutations/createTask.ts +++ b/packages/server/graphql/mutations/createTask.ts @@ -9,11 +9,11 @@ import extractTextFromDraftString from '../../../client/utils/draftjs/extractTex import getTagsFromEntityMap from '../../../client/utils/draftjs/getTagsFromEntityMap' import getRethink from '../../database/rethinkDriver' import NotificationTaskInvolves from '../../database/types/NotificationTaskInvolves' -import {TaskServiceEnum} from '../../database/types/Task' import generateUID from '../../generateUID' import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache' import getKysely from '../../postgres/getKysely' import {Task} from '../../postgres/types/index.d' +import {TaskServiceEnum} from '../../postgres/types/TaskIntegration' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import publish, {SubOptions} from '../../utils/publish' diff --git a/packages/server/graphql/mutations/helpers/addSeedTasks.ts b/packages/server/graphql/mutations/helpers/addSeedTasks.ts index c3e064a212b..25ade85b187 100644 --- a/packages/server/graphql/mutations/helpers/addSeedTasks.ts +++ b/packages/server/graphql/mutations/helpers/addSeedTasks.ts @@ -2,7 +2,6 @@ import convertToTaskContent from 'parabol-client/utils/draftjs/convertToTaskCont import getTagsFromEntityMap from 'parabol-client/utils/draftjs/getTagsFromEntityMap' import makeAppURL from 'parabol-client/utils/makeAppURL' import appOrigin from '../../../appOrigin' -import {TaskStatusEnum} from '../../../database/types/Task' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {convertHtmlToTaskContent} from '../../../utils/draftjs/convertHtmlToTaskContent' @@ -22,13 +21,13 @@ function getSeedTasks(teamId: string) { return [ { - status: 'active' as TaskStatusEnum, + status: 'active' as const, sortOrder: 1, content: convertToTaskContent(NORMAL_TASK_STRING), plaintextContent: NORMAL_TASK_STRING }, { - status: 'active' as TaskStatusEnum, + status: 'active' as const, sortOrder: 0, content: convertHtmlToTaskContent(integrationTaskHTML), plaintextContent: INTEGRATIONS_TASK_STRING diff --git a/packages/server/graphql/mutations/helpers/createTaskInService.ts b/packages/server/graphql/mutations/helpers/createTaskInService.ts index 378c61ea187..e972d7fb08e 100644 --- a/packages/server/graphql/mutations/helpers/createTaskInService.ts +++ b/packages/server/graphql/mutations/helpers/createTaskInService.ts @@ -7,11 +7,8 @@ import GitHubRepoId from '../../../../client/shared/gqlIds/GitHubRepoId' import IntegrationRepoId from '../../../../client/shared/gqlIds/IntegrationRepoId' import JiraIssueId from '../../../../client/shared/gqlIds/JiraIssueId' import JiraProjectId from '../../../../client/shared/gqlIds/JiraProjectId' +import JiraProjectKeyId from '../../../../client/shared/gqlIds/JiraProjectKeyId' import removeRangesForEntity from '../../../../client/utils/draftjs/removeRangesForEntity' -import TaskIntegrationAzureDevOps from '../../../database/types/TaskIntegrationAzureDevOps' -import TaskIntegrationGitHub from '../../../database/types/TaskIntegrationGitHub' -import TaskIntegrationGitLab from '../../../database/types/TaskIntegrationGitLab' -import TaskIntegrationJira from '../../../database/types/TaskIntegrationJira' import {GQLContext} from '../../graphql' import {CreateTaskIntegrationInput} from '../createTask' import createAzureTask from './createAzureTask' @@ -45,11 +42,13 @@ const createTaskInService = async ( const {issueKey} = jiraTaskRes const integrationRepoId = IntegrationRepoId.join({cloudId, projectKey, service}) return { - integration: new TaskIntegrationJira({ + integration: { + service: 'jira' as const, + projectKey: JiraProjectKeyId.join(issueKey), accessUserId, cloudId, issueKey - }), + }, integrationHash: JiraIssueId.join(cloudId, issueKey), integrationRepoId } @@ -73,11 +72,12 @@ const createTaskInService = async ( const {issueNumber} = githubTaskRes const integrationRepoId = IntegrationRepoId.join({nameWithOwner: serviceProjectHash, service}) return { - integration: new TaskIntegrationGitHub({ + integration: { + service: 'github' as const, accessUserId, nameWithOwner: serviceProjectHash, issueNumber - }), + }, integrationHash: GitHubIssueId.join(serviceProjectHash, issueNumber), integrationRepoId } @@ -101,11 +101,12 @@ const createTaskInService = async ( const integrationRepoId = IntegrationRepoId.join({fullPath, service}) const integrationProviderId = IntegrationProviderId.join(providerId) return { - integration: new TaskIntegrationGitLab({ + integration: { + service: 'gitlab' as const, accessUserId, providerId: integrationProviderId, gid - }), + }, integrationHash: GitLabIssueId.join(integrationProviderId, gid), integrationRepoId } @@ -128,12 +129,13 @@ const createTaskInService = async ( // TODO: fix inconsistencies with projectKey & projectId: https://github.com/ParabolInc/parabol/issues/7073 const integrationRepoId = IntegrationRepoId.join({instanceId, projectId: projectKey, service}) return { - integration: new TaskIntegrationAzureDevOps({ + integration: { + service: 'azureDevOps' as const, instanceId, accessUserId, projectKey, issueKey - }), + }, integrationHash, integrationRepoId } diff --git a/packages/server/graphql/mutations/setTaskEstimate.ts b/packages/server/graphql/mutations/setTaskEstimate.ts index 1f2bdaea1fa..0b705a9f937 100644 --- a/packages/server/graphql/mutations/setTaskEstimate.ts +++ b/packages/server/graphql/mutations/setTaskEstimate.ts @@ -3,7 +3,6 @@ import {SprintPokerDefaults, SubscriptionChannel, Threshold} from 'parabol-clien import makeAppURL from 'parabol-client/utils/makeAppURL' import JiraProjectKeyId from '../../../client/shared/gqlIds/JiraProjectKeyId' import appOrigin from '../../appOrigin' -import TaskIntegrationJiraServer from '../../database/types/TaskIntegrationJiraServer' import JiraServerRestManager from '../../integrations/jiraServer/JiraServerRestManager' import {IntegrationProviderJiraServer} from '../../postgres/queries/getIntegrationProvidersByIds' import insertTaskEstimate from '../../postgres/queries/insertTaskEstimate' @@ -231,7 +230,7 @@ const setTaskEstimate = { const manager = new JiraServerRestManager(auth, provider as IntegrationProviderJiraServer) - const {providerId, repositoryId: projectId} = integration as TaskIntegrationJiraServer + const {providerId, repositoryId: projectId} = integration! const jiraServerIssue = await dataLoader .get('jiraServerIssue') .load({providerId, teamId, userId: accessUserId, issueId}) diff --git a/packages/server/graphql/mutations/updatePokerScope.ts b/packages/server/graphql/mutations/updatePokerScope.ts index f6d7af19549..a80913a9d47 100644 --- a/packages/server/graphql/mutations/updatePokerScope.ts +++ b/packages/server/graphql/mutations/updatePokerScope.ts @@ -3,9 +3,9 @@ import {Insertable} from 'kysely' import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' import {ESTIMATE_TASK_SORT_ORDER} from '../../../client/utils/constants' import EstimateStage from '../../database/types/EstimateStage' -import {TaskServiceEnum} from '../../database/types/Task' import getKysely from '../../postgres/getKysely' import {Discussion} from '../../postgres/pg' +import {TaskServiceEnum} from '../../postgres/types/TaskIntegration' import RedisLockQueue from '../../utils/RedisLockQueue' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' diff --git a/packages/server/graphql/mutations/updateTask.ts b/packages/server/graphql/mutations/updateTask.ts index 78d38f82cab..a0beefc144f 100644 --- a/packages/server/graphql/mutations/updateTask.ts +++ b/packages/server/graphql/mutations/updateTask.ts @@ -2,8 +2,8 @@ import {GraphQLNonNull, GraphQLObjectType} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' -import {AreaEnum as TAreaEnum, TaskStatusEnum} from '../../database/types/Task' import getKysely from '../../postgres/getKysely' +import {Task} from '../../postgres/types/index' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -18,12 +18,12 @@ type UpdateTaskInput = { id: string content?: string | null sortOrder?: number | null - status?: TaskStatusEnum | null + status?: Task['status'] | null userId?: string | null } type UpdateTaskMutationVariables = { updatedTask: UpdateTaskInput - area?: TAreaEnum | null + area?: 'meeting' | 'teamDash' | 'userDash' | null } export default { type: new GraphQLObjectType({ diff --git a/packages/server/graphql/public/types/EstimateStage.ts b/packages/server/graphql/public/types/EstimateStage.ts index 53e65c0a7f8..a601b2228ad 100644 --- a/packages/server/graphql/public/types/EstimateStage.ts +++ b/packages/server/graphql/public/types/EstimateStage.ts @@ -1,7 +1,5 @@ import JiraProjectKeyId from '../../../../client/shared/gqlIds/JiraProjectKeyId' import {SprintPokerDefaults} from '../../../../client/types/constEnums' -import TaskIntegrationAzureDevOps from '../../../database/types/TaskIntegrationAzureDevOps' -import TaskIntegrationJiraServer from '../../../database/types/TaskIntegrationJiraServer' import GitLabServerManager from '../../../integrations/gitlab/GitLabServerManager' import {getUserId} from '../../../utils/authorization' import getRedis from '../../../utils/getRedis' @@ -63,12 +61,7 @@ const EstimateStage: EstimateStageResolvers = { return {name: SprintPokerDefaults.SERVICE_FIELD_COMMENT, type: 'string'} } if (service === 'jiraServer') { - const { - providerId, - repositoryId: projectId, - issueId, - accessUserId - } = integration as TaskIntegrationJiraServer + const {providerId, repositoryId: projectId, issueId, accessUserId} = integration const dimensionName = await getDimensionName(meetingId) const jiraServerIssue = await dataLoader @@ -89,8 +82,7 @@ const EstimateStage: EstimateStageResolvers = { return {name: SprintPokerDefaults.SERVICE_FIELD_COMMENT, type: 'string'} } if (service === 'azureDevOps') { - const {instanceId, projectKey, issueKey, accessUserId} = - integration as TaskIntegrationAzureDevOps + const {instanceId, projectKey, issueKey, accessUserId} = integration const azureDevOpsWorkItem = await dataLoader.get('azureDevOpsWorkItem').load({ teamId, diff --git a/packages/server/graphql/public/types/Task.ts b/packages/server/graphql/public/types/Task.ts new file mode 100644 index 00000000000..645ea20187b --- /dev/null +++ b/packages/server/graphql/public/types/Task.ts @@ -0,0 +1,229 @@ +import AzureDevOpsIssueId from 'parabol-client/shared/gqlIds/AzureDevOpsIssueId' +import JiraServerIssueId from '~/shared/gqlIds/JiraServerIssueId' +import GitHubRepoId from '../../../../client/shared/gqlIds/GitHubRepoId' +import GitLabServerManager from '../../../integrations/gitlab/GitLabServerManager' +import {IGetLatestTaskEstimatesQueryResult} from '../../../postgres/queries/generated/getLatestTaskEstimatesQuery' +import getSimilarTaskEstimate from '../../../postgres/queries/getSimilarTaskEstimate' +import insertTaskEstimate from '../../../postgres/queries/insertTaskEstimate' +import {GetIssueLabelsQuery, GetIssueLabelsQueryVariables} from '../../../types/githubTypes' +import {getUserId} from '../../../utils/authorization' +import getGitHubRequest from '../../../utils/getGitHubRequest' +import getIssueLabels from '../../../utils/githubQueries/getIssueLabels.graphql' +import sendToSentry from '../../../utils/sendToSentry' +import isValid from '../../isValid' +import {ReqResolvers} from './ReqResolvers' + +const Task: Omit, 'replies'> = { + __isTypeOf: ({status}) => !!status, + agendaItem: async ({discussionId}, _args, {dataLoader}) => { + if (!discussionId) return null + const discussion = await dataLoader.get('discussions').load(discussionId) + if (!discussion) return null + const {discussionTopicId, discussionTopicType} = discussion + if (discussionTopicType !== 'agendaItem') return null + return dataLoader.get('agendaItems').loadNonNull(discussionTopicId) + }, + + taskService: ({integration}, _args) => { + return integration?.service ?? null + }, + + createdByUser: ({createdBy}, _args, {dataLoader}) => { + return dataLoader.get('users').loadNonNull(createdBy) + }, + + estimates: async ({id: taskId, integration, teamId}, _args, context, info) => { + const {dataLoader, authToken} = context + const viewerId = getUserId(authToken) + if (integration?.service === 'jira') { + const {accessUserId, cloudId, issueKey} = integration + // this dataloader has the side effect of guaranteeing fresh estimates + await dataLoader + .get('jiraIssue') + .load({teamId, userId: accessUserId, cloudId, issueKey, taskId, viewerId}) + } else if (integration?.service === 'azureDevOps') { + const {accessUserId, instanceId, projectKey, issueKey} = integration + await dataLoader.get('azureDevOpsWorkItem').load({ + teamId, + userId: accessUserId, + instanceId, + workItemId: issueKey, + taskId, + projectId: projectKey, + viewerId + }) + } else if (integration?.service === 'github') { + const {accessUserId, nameWithOwner, issueNumber} = integration + const [githubAuth, estimates] = await Promise.all([ + dataLoader.get('githubAuth').load({userId: accessUserId, teamId}), + dataLoader.get('latestTaskEstimates').load(taskId) + ]) + if (estimates.length === 0) return estimates + // TODO schedule this work to be done & pump in the updates via subcription + if (!githubAuth) return estimates + // fetch fresh estimates from GH + const {accessToken} = githubAuth + const {repoOwner, repoName} = GitHubRepoId.split(nameWithOwner) + const githubRequest = getGitHubRequest(info, context, {accessToken}) + const [labelsData, labelsError] = await githubRequest< + GetIssueLabelsQuery, + GetIssueLabelsQueryVariables + >(getIssueLabels, { + first: 100, + repoName, + repoOwner, + issueNumber + }) + if (!labelsData) { + if (labelsError) { + sendToSentry(labelsError, {userId: accessUserId}) + } + return estimates + } + const labelNodes = labelsData.repository?.issue?.labels?.nodes + if (!labelNodes) return estimates + const ghIssueLabels = labelNodes.map((node) => node?.name).filter(isValid) + await Promise.all( + estimates.map(async (estimate: IGetLatestTaskEstimatesQueryResult) => { + const {githubLabelName, name: dimensionName} = estimate + const existingLabel = ghIssueLabels.includes(githubLabelName!) + if (existingLabel) return + // VERY EXPENSIVE. We do this only if we're darn sure we need to + const taskIds = await dataLoader + .get('taskIdsByTeamAndGitHubRepo') + .load({teamId, nameWithOwner}) + const similarEstimate = await getSimilarTaskEstimate( + taskIds, + dimensionName, + ghIssueLabels + ) + if (!similarEstimate) return + dataLoader.get('latestTaskEstimates').clear(taskId) + return insertTaskEstimate({ + changeSource: 'external', + // keep the link to the discussion alive, if possible + discussionId: estimate.discussionId, + jiraFieldId: undefined, + label: similarEstimate.label, + name: estimate.name, + meetingId: null, + stageId: null, + taskId, + userId: accessUserId, + githubLabelName: similarEstimate.githubLabelName! + }) + }) + ) + } + return dataLoader.get('latestTaskEstimates').load(taskId) + }, + + editors: () => [], + + integration: async ({integration, integrationHash, teamId, id: taskId}, _args, context, info) => { + const {dataLoader, authToken} = context + const viewerId = getUserId(authToken) + if (!integration) return null + const {accessUserId} = integration + if (integration.service === 'jira') { + const {cloudId, issueKey} = integration + return dataLoader + .get('jiraIssue') + .load({teamId, userId: accessUserId, cloudId, issueKey, taskId, viewerId}) + } else if (integration.service === 'jiraServer') { + const {issueId} = JiraServerIssueId.split(integrationHash!) + const issue = await dataLoader.get('jiraServerIssue').load({ + teamId, + userId: accessUserId, + issueId, + providerId: integration.providerId + }) + return issue + ? { + ...issue, + userId: accessUserId, + teamId + } + : null + } else if (integration.service === 'azureDevOps') { + const {projectKey, issueKey} = integration + const {instanceId} = AzureDevOpsIssueId.split(integrationHash!) + return dataLoader.get('azureDevOpsWorkItem').load({ + teamId, + userId: accessUserId, + instanceId, + projectId: projectKey, + viewerId, + workItemId: issueKey + }) + } else if (integration.service === 'github') { + const githubAuth = await dataLoader.get('githubAuth').load({userId: accessUserId, teamId}) + if (!githubAuth) return null + const {accessToken} = githubAuth + const {nameWithOwner, issueNumber} = integration + const {repoOwner, repoName} = GitHubRepoId.split(nameWithOwner) + const query = ` + { + repository(owner: "${repoOwner}", name: "${repoName}") { + issue(number: ${issueNumber}) { + ...info + } + } + }` + const githubRequest = getGitHubRequest(info, context, {accessToken}) + const [data, error] = await githubRequest(query) + if (error) { + sendToSentry(error, {userId: accessUserId}) + } + return data + } else if (integration.service === 'gitlab') { + const {accessUserId} = integration + const gitlabAuth = await dataLoader + .get('freshGitlabAuth') + .load({teamId, userId: accessUserId}) + if (!gitlabAuth?.accessToken) return null + const {providerId} = gitlabAuth + const provider = await dataLoader.get('integrationProviders').load(providerId) + if (!provider?.serverBaseUrl) return null + const {gid} = integration + const query = ` + query { + issue(id: "${gid}"){ + ...info + } + } + ` + const manager = new GitLabServerManager(gitlabAuth, context, info, provider.serverBaseUrl) + const gitlabRequest = manager.getGitLabRequest(info, context) + const [data, error] = await gitlabRequest(query, {}) + if (error) { + sendToSentry(error, {userId: accessUserId}) + } + return data + } + return null + }, + + team: ({teamId}, _args, {dataLoader}) => { + return dataLoader.get('teams').loadNonNull(teamId) + }, + + title: ({plaintextContent}) => { + const firstBreak = plaintextContent.trim().indexOf('\n') + const endIndex = firstBreak > -1 ? firstBreak : plaintextContent.length + return plaintextContent.slice(0, endIndex) + }, + + user: ({userId}, _args, {dataLoader}) => { + if (!userId) return null + return dataLoader.get('users').loadNonNull(userId) + }, + + isHighlighted: async ({id: taskId}, {meetingId}, {dataLoader}) => { + if (!meetingId) return false + const highlightedTaskId = await dataLoader.get('meetingHighlightedTaskId').load(meetingId) + return taskId === highlightedTaskId + } +} + +export default Task diff --git a/packages/server/graphql/public/types/TaskEstimate.ts b/packages/server/graphql/public/types/TaskEstimate.ts new file mode 100644 index 00000000000..71d5f2900bd --- /dev/null +++ b/packages/server/graphql/public/types/TaskEstimate.ts @@ -0,0 +1,7 @@ +import {TaskEstimateResolvers} from '../resolverTypes' + +const TaskEstimate: TaskEstimateResolvers = { + label: ({label}) => label || '' +} + +export default TaskEstimate diff --git a/packages/server/graphql/types/Task.ts b/packages/server/graphql/types/Task.ts index d2a8b1b7ad8..abcbcee05e7 100644 --- a/packages/server/graphql/types/Task.ts +++ b/packages/server/graphql/types/Task.ts @@ -1,344 +1,14 @@ -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import AzureDevOpsIssueId from 'parabol-client/shared/gqlIds/AzureDevOpsIssueId' -import JiraServerIssueId from '~/shared/gqlIds/JiraServerIssueId' -import GitHubRepoId from '../../../client/shared/gqlIds/GitHubRepoId' -import DBTask from '../../database/types/Task' -import GitLabServerManager from '../../integrations/gitlab/GitLabServerManager' -import getSimilarTaskEstimate from '../../postgres/queries/getSimilarTaskEstimate' -import insertTaskEstimate from '../../postgres/queries/insertTaskEstimate' -import {GetIssueLabelsQuery, GetIssueLabelsQueryVariables} from '../../types/githubTypes' -import {getUserId} from '../../utils/authorization' -import getGitHubRequest from '../../utils/getGitHubRequest' -import getIssueLabels from '../../utils/githubQueries/getIssueLabels.graphql' -import sendToSentry from '../../utils/sendToSentry' +import {GraphQLObjectType} from 'graphql' import connectionDefinitions from '../connectionDefinitions' -import {GQLContext} from '../graphql' -import isValid from '../isValid' -import {IGetLatestTaskEstimatesQueryResult} from './../../postgres/queries/generated/getLatestTaskEstimatesQuery' -import AgendaItem from './AgendaItem' import GraphQLISO8601Type from './GraphQLISO8601Type' import PageInfoDateCursor from './PageInfoDateCursor' -import TaskEditorDetails from './TaskEditorDetails' -import TaskEstimate from './TaskEstimate' -import TaskIntegration from './TaskIntegration' -import TaskServiceEnum from './TaskServiceEnum' -import TaskStatusEnum from './TaskStatusEnum' -import Team from './Team' -const Task: GraphQLObjectType = new GraphQLObjectType({ +const Task: GraphQLObjectType = new GraphQLObjectType({ name: 'Task', - description: 'A long-term task shared across the team, assigned to a single user ', - isTypeOf: ({status}) => !!status, - fields: () => ({ - agendaItem: { - type: AgendaItem, - description: 'The agenda item that the task was created in, if any', - resolve: async ({discussionId}, _args: unknown, {dataLoader}) => { - if (!discussionId) return null - const discussion = await dataLoader.get('discussions').load(discussionId) - if (!discussion) return null - const {discussionTopicId, discussionTopicType} = discussion - if (discussionTopicType !== 'agendaItem') return null - return dataLoader.get('agendaItems').load(discussionTopicId) - } - }, - taskService: { - type: TaskServiceEnum, - description: 'Type of the integration if there is one', - resolve: ({integration}: DBTask, _args: unknown) => { - return integration?.service - } - }, - createdBy: { - type: new GraphQLNonNull(GraphQLID), - description: 'The userId that created the item' - }, - createdByUser: { - type: new GraphQLNonNull(require('./User').default), - description: 'The user that created the item', - resolve: ({createdBy}, _args: unknown, {dataLoader}: GQLContext) => { - return dataLoader.get('users').load(createdBy) - } - }, - dueDate: { - type: GraphQLISO8601Type, - description: 'a user-defined due date' - }, - estimates: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(TaskEstimate))), - description: 'A list of the most recent estimates for the task', - resolve: async ({id: taskId, integration, teamId}: DBTask, _args: unknown, context, info) => { - const {dataLoader, authToken} = context - const viewerId = getUserId(authToken) - if (integration?.service === 'jira') { - const {accessUserId, cloudId, issueKey} = integration - // this dataloader has the side effect of guaranteeing fresh estimates - await dataLoader - .get('jiraIssue') - .load({teamId, userId: accessUserId, cloudId, issueKey, taskId, viewerId}) - } else if (integration?.service === 'azureDevOps') { - const {accessUserId, instanceId, projectKey, issueKey} = integration - await dataLoader.get('azureDevOpsWorkItem').load({ - teamId, - userId: accessUserId, - instanceId, - workItemId: issueKey, - taskId, - projectId: projectKey, - viewerId - }) - } else if (integration?.service === 'github') { - const {accessUserId, nameWithOwner, issueNumber} = integration - const [githubAuth, estimates] = await Promise.all([ - dataLoader.get('githubAuth').load({userId: accessUserId, teamId}), - dataLoader.get('latestTaskEstimates').load(taskId) - ]) - if (estimates.length === 0) return estimates - // TODO schedule this work to be done & pump in the updates via subcription - if (!githubAuth) return estimates - // fetch fresh estimates from GH - const {accessToken} = githubAuth - const {repoOwner, repoName} = GitHubRepoId.split(nameWithOwner) - const githubRequest = getGitHubRequest(info, context, {accessToken}) - const [labelsData, labelsError] = await githubRequest< - GetIssueLabelsQuery, - GetIssueLabelsQueryVariables - >(getIssueLabels, { - first: 100, - repoName, - repoOwner, - issueNumber - }) - if (!labelsData) { - if (labelsError) { - sendToSentry(labelsError, {userId: accessUserId}) - } - return estimates - } - const labelNodes = labelsData.repository?.issue?.labels?.nodes - if (!labelNodes) return estimates - const ghIssueLabels = labelNodes.map((node) => node?.name).filter(isValid) - await Promise.all( - estimates.map(async (estimate: IGetLatestTaskEstimatesQueryResult) => { - const {githubLabelName, name: dimensionName} = estimate - const existingLabel = ghIssueLabels.includes(githubLabelName!) - if (existingLabel) return - // VERY EXPENSIVE. We do this only if we're darn sure we need to - const taskIds = await dataLoader - .get('taskIdsByTeamAndGitHubRepo') - .load({teamId, nameWithOwner}) - const similarEstimate = await getSimilarTaskEstimate( - taskIds, - dimensionName, - ghIssueLabels - ) - if (!similarEstimate) return - dataLoader.get('latestTaskEstimates').clear(taskId) - return insertTaskEstimate({ - changeSource: 'external', - // keep the link to the discussion alive, if possible - discussionId: estimate.discussionId, - jiraFieldId: undefined, - label: similarEstimate.label, - name: estimate.name, - meetingId: null, - stageId: null, - taskId, - userId: accessUserId, - githubLabelName: similarEstimate.githubLabelName! - }) - }) - ) - } - return dataLoader.get('latestTaskEstimates').load(taskId) - } - }, - editors: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(TaskEditorDetails))), - description: - 'a list of users currently editing the task (fed by a subscription, so queries return null)', - resolve: (source: any) => source.editors ?? [] - }, - integration: { - type: TaskIntegration, - description: 'The reference to the single source of truth for this task', - resolve: async ( - {integration, integrationHash, teamId, id: taskId}: DBTask, - _args: unknown, - context, - info - ) => { - const {dataLoader, authToken} = context - const viewerId = getUserId(authToken) - if (!integration) return null - const {accessUserId} = integration - if (integration.service === 'jira') { - const {cloudId, issueKey} = integration - return dataLoader - .get('jiraIssue') - .load({teamId, userId: accessUserId, cloudId, issueKey, taskId, viewerId}) - } else if (integration.service === 'jiraServer') { - const {issueId} = JiraServerIssueId.split(integrationHash!) - const issue = await dataLoader.get('jiraServerIssue').load({ - teamId, - userId: accessUserId, - issueId, - providerId: integration.providerId - }) - return issue - ? { - ...issue, - userId: accessUserId, - teamId - } - : null - } else if (integration.service === 'azureDevOps') { - const {projectKey, issueKey} = integration - const {instanceId} = AzureDevOpsIssueId.split(integrationHash!) - return dataLoader.get('azureDevOpsWorkItem').load({ - teamId, - userId: accessUserId, - instanceId, - projectId: projectKey, - viewerId, - workItemId: issueKey - }) - } else if (integration.service === 'github') { - const githubAuth = await dataLoader.get('githubAuth').load({userId: accessUserId, teamId}) - if (!githubAuth) return null - const {accessToken} = githubAuth - const {nameWithOwner, issueNumber} = integration - const {repoOwner, repoName} = GitHubRepoId.split(nameWithOwner) - const query = ` - { - repository(owner: "${repoOwner}", name: "${repoName}") { - issue(number: ${issueNumber}) { - ...info - } - } - }` - const githubRequest = getGitHubRequest(info, context, {accessToken}) - const [data, error] = await githubRequest(query) - if (error) { - sendToSentry(error, {userId: accessUserId}) - } - return data - } else if (integration.service === 'gitlab') { - const {accessUserId} = integration - const gitlabAuth = await dataLoader - .get('freshGitlabAuth') - .load({teamId, userId: accessUserId}) - if (!gitlabAuth?.accessToken) return null - const {providerId} = gitlabAuth - const provider = await dataLoader.get('integrationProviders').load(providerId) - if (!provider?.serverBaseUrl) return null - const {gid} = integration - const query = ` - query { - issue(id: "${gid}"){ - ...info - } - } - ` - const manager = new GitLabServerManager(gitlabAuth, context, info, provider.serverBaseUrl) - const gitlabRequest = manager.getGitLabRequest(info, context) - const [data, error] = await gitlabRequest(query, {}) - if (error) { - sendToSentry(error, {userId: accessUserId}) - } - return data - } - return null - } - }, - integrationHash: { - type: GraphQLID, - description: 'A hash of the integrated task' - }, - meetingId: { - type: GraphQLID, - description: 'the foreign key for the meeting the task was created in' - }, - doneMeetingId: { - type: GraphQLID, - description: 'the foreign key for the meeting the task was marked as complete' - }, - plaintextContent: { - type: new GraphQLNonNull(GraphQLString), - description: 'the plain text content of the task' - }, - sortOrder: { - type: new GraphQLNonNull(GraphQLFloat), - description: 'the shared sort order for tasks on the team dash & user dash' - }, - status: { - type: new GraphQLNonNull(TaskStatusEnum), - description: 'The status of the task' - }, - tags: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))), - description: 'The tags associated with the task' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The id of the team (indexed). Needed for subscribing to archived tasks' - }, - team: { - type: new GraphQLNonNull(Team), - description: 'The team this task belongs to', - resolve: ({teamId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('teams').load(teamId) - } - }, - title: { - type: new GraphQLNonNull(GraphQLString), - description: 'The first block of the content', - resolve: ({plaintextContent}) => { - const firstBreak = plaintextContent.trim().indexOf('\n') - const endIndex = firstBreak > -1 ? firstBreak : plaintextContent.length - return plaintextContent.slice(0, endIndex) - } - }, - userId: { - type: GraphQLID, - description: - '* The userId, index useful for server-side methods getting all tasks under a user. This can be null if the task is not assigned to anyone.' - }, - user: { - type: require('./User').default, - description: 'The user the task is assigned to. Null if it is not assigned to anyone.', - resolve: ({userId}, _args: unknown, {dataLoader}) => { - if (!userId) return null - return dataLoader.get('users').load(userId) - } - }, - isHighlighted: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'The owner hovers over the task in their solo update of a checkin', - args: { - meetingId: { - type: GraphQLID, - description: 'Meeting for which the highlight is checked' - } - }, - resolve: async ({id: taskId}, {meetingId}: {meetingId?: string | null}, {dataLoader}) => { - if (!meetingId) return false - const highlightedTaskId = await dataLoader.get('meetingHighlightedTaskId').load(meetingId) - return taskId === highlightedTaskId - } - } - }) + fields: {} }) -const {connectionType, edgeType} = connectionDefinitions({ +const {connectionType} = connectionDefinitions({ name: Task.name, nodeType: Task, edgeFields: () => ({ @@ -355,5 +25,4 @@ const {connectionType, edgeType} = connectionDefinitions({ }) export const TaskConnection = connectionType -export const TaskEdge = edgeType export default Task diff --git a/packages/server/graphql/types/TaskEstimate.ts b/packages/server/graphql/types/TaskEstimate.ts deleted file mode 100644 index dc09b2d4ba9..00000000000 --- a/packages/server/graphql/types/TaskEstimate.ts +++ /dev/null @@ -1,57 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' -import ChangeSourceEnum from './ChangeSourceEnum' -import GraphQLISO8601Type from './GraphQLISO8601Type' - -const TaskEstimate = new GraphQLObjectType({ - name: 'TaskEstimate', - description: 'An estimate for a Task that was voted on and scored in a poker meeting', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'The ID of the estimate' - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the estimate was created' - }, - changeSource: { - type: new GraphQLNonNull(ChangeSourceEnum), - description: 'The source that a change came in through' - }, - name: { - type: new GraphQLNonNull(GraphQLString), - description: 'The name of the estimate dimension' - }, - label: { - type: new GraphQLNonNull(GraphQLString), - description: 'The human-readable label for the estimate', - resolve: ({label}) => label || '' - }, - taskId: { - type: new GraphQLNonNull(GraphQLID), - description: '*The taskId that the estimate refers to' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The userId that added the estimate' - }, - meetingId: { - type: GraphQLID, - description: '*The meetingId that the estimate occured in, if any' - }, - stageId: { - type: GraphQLID, - description: 'The meeting stageId the estimate occurred in, if any' - }, - discussionId: { - type: GraphQLID, - description: 'The discussionId where the estimated was discussed' - }, - jiraFieldId: { - type: GraphQLID, - description: 'If the task comes from jira, this is the jira field that the estimate refers to' - } - }) -}) -export default TaskEstimate diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 4698a12b73d..1101524bf79 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -2,6 +2,7 @@ import {SelectQueryBuilder, Selectable} from 'kysely' import { Discussion as DiscussionPG, OrganizationUser as OrganizationUserPG, + TaskEstimate as TaskEstimatePG, TeamMember as TeamMemberPG } from '../pg.d' import { @@ -78,3 +79,4 @@ export type NewMeeting = ExtractTypeFromQueryBuilderSelect export type TeamInvitation = ExtractTypeFromQueryBuilderSelect export type Task = ExtractTypeFromQueryBuilderSelect +export type TaskEstimate = Selectable diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 0c32e1b0881..7a5cbb0817e 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -4,7 +4,6 @@ import TeamPromptResponseId from '../../../client/shared/gqlIds/TeamPromptRespon import {PARABOL_AI_USER_ID} from '../../../client/utils/constants' import {TeamLimitsEmailType} from '../../billing/helpers/sendTeamsLimitEmail' import MeetingTemplate from '../../database/types/MeetingTemplate' -import {TaskServiceEnum} from '../../database/types/Task' import {DataLoaderWorker} from '../../graphql/graphql' import {ModifyType, ReactableEnum} from '../../graphql/public/resolverTypes' import {IntegrationProviderServiceEnumType} from '../../graphql/types/IntegrationProviderServiceEnum' @@ -16,6 +15,7 @@ import { RetrospectiveMeeting } from '../../postgres/types/Meeting' import {MeetingSeries} from '../../postgres/types/MeetingSeries' +import {TaskServiceEnum} from '../../postgres/types/TaskIntegration' import {AmplitudeAnalytics} from './amplitude/AmplitudeAnalytics' import {createMeetingProperties} from './helpers' export type AnalyticsUser = { From b78f67d06d6f55e998f2ff84881cc53fbaf960d4 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 15 Oct 2024 09:21:49 -0700 Subject: [PATCH 80/89] self review Signed-off-by: Matt Krick --- packages/server/graphql/mutations/createTaskIntegration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/graphql/mutations/createTaskIntegration.ts b/packages/server/graphql/mutations/createTaskIntegration.ts index 4bd681a5860..4b069473115 100644 --- a/packages/server/graphql/mutations/createTaskIntegration.ts +++ b/packages/server/graphql/mutations/createTaskIntegration.ts @@ -169,6 +169,7 @@ export default { integration: JSON.stringify(updateTaskInput.integration), integrationHash: updateTaskInput.integrationHash }) + .where('id', '=', taskId) .execute() dataLoader.clearAll('tasks') From 95445788dff8516e74e05defecd585e62c13a3c0 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 15 Oct 2024 12:26:40 -0700 Subject: [PATCH 81/89] fix tags and archived filter Signed-off-by: Matt Krick --- packages/server/dataloader/customLoaderMakers.ts | 5 ++--- packages/server/dataloader/foreignKeyLoaderMakers.ts | 4 ++-- packages/server/graphql/mutations/createTask.ts | 4 ++-- packages/server/graphql/mutations/endCheckIn.ts | 4 ++-- .../server/graphql/mutations/helpers/removeTeamMember.ts | 4 ++-- packages/server/graphql/mutations/updateTask.ts | 4 +++- packages/server/postgres/select.ts | 7 ++++--- packages/server/postgres/types/index.d.ts | 2 ++ 8 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 36d8e244d05..286eeadd236 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -160,15 +160,14 @@ export const userTasks = (parent: RootDataLoader, dependsOn: RegisterDependsOn) .where('teamId', 'in', teamIds) .$if(hasUserIds, (qb) => qb.where('userId', 'in', userIds)) .$if(hasStatusFilters, (qb) => qb.where('status', 'in', statusFilters!)) - .$if(!!filterQuery, (qb) => qb.where('plaintextContent', 'match', filterQuery!)) + .$if(!!filterQuery, (qb) => qb.where('plaintextContent', 'ilike', `%${filterQuery}%`)) .$if(!!after, (qb) => qb.where('updatedAt', '<', after!)) .$if(!!archived, (qb) => qb.where(sql`'archived' = ANY(tags)`)) - .$if(!archived, (qb) => qb.where(sql`'archived' != ANY(tags)`)) + .$if(!archived, (qb) => qb.where(sql`'archived' != ALL(tags)`)) .$if(!includeUnassigned, (qb) => qb.where('userId', 'is not', null)) .orderBy('updatedAt desc') .limit(first + 1) .execute() - return { key: serializeUserTasksKey(key), data: teamTasks diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 1edd49840ed..eff5c1d3ab2 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -309,7 +309,7 @@ export const tasksByTeamId = foreignKeyLoaderMaker('tasks', 'teamId', async (tea // waraning! contains private tasks return selectTasks() .where('teamId', 'in', teamIds) - .where(sql`'archived' != ANY(tags)`) + .where(sql`'archived' != ALL(tags)`) .execute() }) @@ -317,6 +317,6 @@ export const tasksByMeetingId = foreignKeyLoaderMaker('tasks', 'meetingId', asyn // waraning! contains private tasks return selectTasks() .where('meetingId', 'in', meetingIds) - .where(sql`'archived' != ANY(tags)`) + .where(sql`'archived' != ALL(tags)`) .execute() }) diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts index 6aa1d8941e9..79cc7b56371 100644 --- a/packages/server/graphql/mutations/createTask.ts +++ b/packages/server/graphql/mutations/createTask.ts @@ -12,7 +12,7 @@ import NotificationTaskInvolves from '../../database/types/NotificationTaskInvol import generateUID from '../../generateUID' import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache' import getKysely from '../../postgres/getKysely' -import {Task} from '../../postgres/types/index.d' +import {Task, TaskTag} from '../../postgres/types/index.d' import {TaskServiceEnum} from '../../postgres/types/TaskIntegration' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -224,7 +224,7 @@ export default { threadSortOrder, threadParentId, userId: userId || null, - tags: getTagsFromEntityMap(JSON.parse(content).entityMap) + tags: getTagsFromEntityMap(JSON.parse(content).entityMap) } const {id: taskId} = task const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index 5e19d36f823..b212051d08a 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -37,7 +37,7 @@ const updateTaskSortOrders = async (userIds: string[], tasks: SortOrderTask[]) = .selectFrom('Task') .select(({fn}) => fn.max('sortOrder').as('maxSortOrder')) .where('userId', 'in', userIds) - .where(sql`'archived' != ANY(tags)`) + .where(sql`'archived' != ALL(tags)`) .executeTakeFirst() const maxSortOrder = Number(taskMaxRes?.maxSortOrder ?? 0) @@ -104,7 +104,7 @@ const summarizeCheckInMeeting = async (meeting: CheckInMeeting, dataLoader: Data selectTasks() .where('teamId', '=', teamId) .where('status', '=', 'done') - .where(sql`'archived' != ANY(tags)`) + .where(sql`'archived' != ALL(tags)`) .execute(), dataLoader.get('agendaItemsByTeamId').load(teamId) ]) diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 5c13821b456..44ad0e0d858 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -79,7 +79,7 @@ const removeTeamMember = async ( .where('userId', '=', userId) .where('teamId', '=', teamId) .where('integration', 'is not', null) - .where(sql`'archived' != ANY(tags)`) + .where(sql`'archived' != ALL(tags)`) .execute() const reassignedTasks = await pg .with('UserUpdate', (qb) => @@ -93,7 +93,7 @@ const removeTeamMember = async ( .where('userId', '=', userId) .where('teamId', '=', teamId) .where('integration', 'is', null) - .where(sql`'archived' != ANY(tags)`) + .where(sql`'archived' != ALL(tags)`) .returning('id') .execute() dataLoader.clearAll(['users', 'teamMembers', 'tasks']) diff --git a/packages/server/graphql/mutations/updateTask.ts b/packages/server/graphql/mutations/updateTask.ts index a0beefc144f..b9ac62a1415 100644 --- a/packages/server/graphql/mutations/updateTask.ts +++ b/packages/server/graphql/mutations/updateTask.ts @@ -2,6 +2,7 @@ import {GraphQLNonNull, GraphQLObjectType} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS' +import getTagsFromEntityMap from '../../../client/utils/draftjs/getTagsFromEntityMap' import getKysely from '../../postgres/getKysely' import {Task} from '../../postgres/types/index' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -81,7 +82,8 @@ export default { plaintextContent: content ? extractTextFromDraftString(validContent) : undefined, sortOrder: sortOrder || undefined, status: status || undefined, - userId: inputUserId || undefined + userId: inputUserId || undefined, + tags: content ? getTagsFromEntityMap(JSON.parse(validContent).entityMap) : undefined }) .where('id', '=', taskId) .executeTakeFirst() diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 7a679f8b91e..f73831e9abe 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -2,7 +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' +import {ReactjiDB, TaskTag} from './types' import {AnyMeeting, AnyMeetingMember} from './types/Meeting' import {AnyTaskIntegration} from './types/TaskIntegration' export const selectTimelineEvent = () => { @@ -292,7 +292,6 @@ export const selectTasks = () => .selectFrom('Task') .select(({fn}) => [ 'id', - 'content', 'createdAt', 'createdBy', 'doneMeetingId', @@ -302,12 +301,14 @@ export const selectTasks = () => 'plaintextContent', 'sortOrder', 'status', - 'tags', 'teamId', 'discussionId', 'threadParentId', 'threadSortOrder', 'updatedAt', 'userId', + // this is to match the previous behavior, no reason we couldn't export as json in the future + sql`content::text`.as('content'), + fn('to_json', ['tags']).as('tags'), fn('to_json', ['integration']).as('integration') ]) diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 1101524bf79..385035df432 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -42,6 +42,8 @@ export type AutogroupReflectionGroupType = { reflectionIds: string[] } +export type TaskTag = 'private' | 'archived' + export interface Organization extends ExtractTypeFromQueryBuilderSelect {} export type OrganizationUser = Selectable From a5dc4ef9131ef06aeba2ab8109ed9c474e585180 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 15 Oct 2024 13:19:20 -0700 Subject: [PATCH 82/89] fix task query for privates Signed-off-by: Matt Krick --- .../mutations/helpers/importTasksForPoker.ts | 2 +- .../resetRetroMeetingToGroupStage.ts | 43 ++++++++++--------- .../public/types/ActionMeetingMember.ts | 2 +- packages/server/graphql/public/types/User.ts | 4 +- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts index 9bb678d2e70..155f5a4740b 100644 --- a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts +++ b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts @@ -58,7 +58,7 @@ const importTasksForPoker = async ( } }) .filter(isNotNull) - if (newIntegrationUpdates.length > 0) { + if (tasksToAdd.length > 0) { await pg.insertInto('Task').values(tasksToAdd).execute() } const integratedTasks = [...existingTasks, ...tasksToAdd] diff --git a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts index a8cf539613c..5a52a2f05e8 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -101,31 +101,32 @@ const resetRetroMeetingToGroupStage = { // bc we return the reflection groups cached by data loader in the fragment reflectionGroups.forEach((rg) => (rg.voterIds = [])) - await Promise.all([ - pg + if (discussionIdsToDelete.length > 0) { + await pg .with('DeleteComments', (qb) => qb.deleteFrom('Comment').where('discussionId', 'in', discussionIdsToDelete) ) - .with('DeleteTasks', (qb) => - qb.deleteFrom('Task').where('discussionId', 'in', discussionIdsToDelete) - ) - .with('ResetGroups', (qb) => - qb - .updateTable('RetroReflectionGroup') - .set({voterIds: [], discussionPromptQuestion: null}) - .where('id', 'in', reflectionGroupIds) - ) - .with('ResetMeetingMember', (qb) => - qb - .updateTable('MeetingMember') - .set({votesRemaining: meeting.totalVotes}) - .where('meetingId', '=', meetingId) - ) - .updateTable('NewMeeting') - .set({phases: JSON.stringify(newPhases)}) - .where('id', '=', meetingId) + .deleteFrom('Task') + .where('discussionId', 'in', discussionIdsToDelete) .execute() - ]) + } + await pg + .with('ResetGroups', (qb) => + qb + .updateTable('RetroReflectionGroup') + .set({voterIds: [], discussionPromptQuestion: null}) + .where('id', 'in', reflectionGroupIds) + ) + .with('ResetMeetingMember', (qb) => + qb + .updateTable('MeetingMember') + .set({votesRemaining: meeting.totalVotes}) + .where('meetingId', '=', meetingId) + ) + .updateTable('NewMeeting') + .set({phases: JSON.stringify(newPhases)}) + .where('id', '=', meetingId) + .execute() dataLoader.clearAll([ 'newMeetings', 'comments', diff --git a/packages/server/graphql/public/types/ActionMeetingMember.ts b/packages/server/graphql/public/types/ActionMeetingMember.ts index 1916b3d0351..ce27294b5db 100644 --- a/packages/server/graphql/public/types/ActionMeetingMember.ts +++ b/packages/server/graphql/public/types/ActionMeetingMember.ts @@ -9,7 +9,7 @@ const ActionMeetingMember: ActionMeetingMemberResolvers = { const res = await selectTasks() .where('userId', '=', userId) .where('doneMeetingId', '=', meetingId) - .where(sql`'private' != ANY(tags)`) + .where(sql`'private' != ALL(tags)`) .execute() return res }, diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index ae1f621fe38..ab06417cd2a 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -95,7 +95,7 @@ const User: ReqResolvers<'User'> = { .where('teamId', '=', teamId) .$if(!!after, (qb) => qb.where('updatedAt', '<=', after!)) .where(sql`'archived' = ANY(tags)`) - .where(({eb, or}) => or([sql`'private' != ANY(tags)`, eb('userId', '=', userId)])) + .where(({eb, or}) => or([sql`'private' != ALL(tags)`, eb('userId', '=', userId)])) .orderBy('updatedAt desc') .limit(first + 1) .execute() @@ -133,7 +133,7 @@ const User: ReqResolvers<'User'> = { .select(({fn}) => fn.count('id').as('count')) .where('teamId', '=', teamId) .where(sql`'archived' = ANY(tags)`) - .where(({eb, or}) => or([sql`'private' != ANY(tags)`, eb('userId', '=', userId)])) + .where(({eb, or}) => or([sql`'private' != ALL(tags)`, eb('userId', '=', userId)])) .executeTakeFirstOrThrow() return Number(taskCount.count) }, From 9db13f237022db77944723e93e8eaee8e7e70d61 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 16 Oct 2024 09:58:35 -0700 Subject: [PATCH 83/89] chore: add writes to PG Signed-off-by: Matt Krick --- .../helpers/removeTeamsLimitObjects.ts | 7 + .../server/billing/helpers/teamLimitsCheck.ts | 3 +- .../types/NotificationPaymentRejected.ts | 4 +- .../database/types/processTeamsLimitsJob.ts | 2 +- .../server/graphql/mutations/archiveTeam.ts | 3 + .../server/graphql/mutations/createTask.ts | 5 +- .../mutations/helpers/inviteToTeamHelper.ts | 1 + .../helpers/notifications/SlackNotifier.ts | 5 +- .../helpers/publishChangeNotifications.ts | 3 + .../mutations/helpers/removeTeamMember.ts | 1 + .../server/graphql/mutations/moveTeamToOrg.ts | 19 ++- .../mutations/setNotificationStatus.ts | 3 + .../__tests__/intranetJobsQuery.test.js | 2 + .../private/mutations/runScheduledJobs.ts | 2 + .../private/mutations/stripeFailPayment.ts | 3 + .../graphql/public/mutations/addComment.ts | 3 + .../helpers/publishTeamPromptMentions.ts | 9 +- .../public/mutations/requestToJoinDomain.ts | 1 + .../public/mutations/setOrgUserRole.ts | 2 + ...s => 1728595099540_Notification-phase1.ts} | 46 +++++-- .../queries/updateMeetingTemplateOrgId.ts | 11 -- packages/server/postgres/select.ts | 4 + .../server/postgres/types/Notification.d.ts | 129 ++++++++++++++++++ .../safeMutations/acceptTeamInvitation.ts | 10 +- packages/server/utils/sendPromptToJoinOrg.ts | 3 + 25 files changed, 247 insertions(+), 34 deletions(-) rename packages/server/postgres/migrations/{1728504371818_Notification-phase1.ts => 1728595099540_Notification-phase1.ts} (82%) delete mode 100644 packages/server/postgres/queries/updateMeetingTemplateOrgId.ts diff --git a/packages/server/billing/helpers/removeTeamsLimitObjects.ts b/packages/server/billing/helpers/removeTeamsLimitObjects.ts index 3b24d5c45c6..14d46788aac 100644 --- a/packages/server/billing/helpers/removeTeamsLimitObjects.ts +++ b/packages/server/billing/helpers/removeTeamsLimitObjects.ts @@ -12,6 +12,13 @@ const removeTeamsLimitObjects = async (orgId: string, dataLoader: DataLoaderWork // Remove team limits jobs and existing notifications const [, updateNotificationsChanges] = await Promise.all([ pg + .with('NotificationUpdate', (qb) => + qb + .updateTable('Notification') + .set({status: 'CLICKED'}) + .where('orgId', '=', orgId) + .where('type', 'in', removeNotificationTypes) + ) .deleteFrom('ScheduledJob') .where('orgId', '=', orgId) .where('type', 'in', removeJobTypes) diff --git a/packages/server/billing/helpers/teamLimitsCheck.ts b/packages/server/billing/helpers/teamLimitsCheck.ts index 3f3f1750e3a..94757589f45 100644 --- a/packages/server/billing/helpers/teamLimitsCheck.ts +++ b/packages/server/billing/helpers/teamLimitsCheck.ts @@ -47,6 +47,7 @@ const sendWebsiteNotifications = async ( userIds: string[], dataLoader: DataLoaderWorker ) => { + const pg = getKysely() const {id: orgId, name: orgName, picture: orgPicture} = organization const operationId = dataLoader.share() const subOptions = {operationId} @@ -60,7 +61,7 @@ const sendWebsiteNotifications = async ( }) await r.table('Notification').insert(notificationsToInsert).run() - + await pg.insertInto('Notification').values(notificationsToInsert).execute() notificationsToInsert.forEach((notification) => { publishNotification(notification, subOptions) }) diff --git a/packages/server/database/types/NotificationPaymentRejected.ts b/packages/server/database/types/NotificationPaymentRejected.ts index 53a779c47d3..707a65f5634 100644 --- a/packages/server/database/types/NotificationPaymentRejected.ts +++ b/packages/server/database/types/NotificationPaymentRejected.ts @@ -10,14 +10,14 @@ interface Input { export default class NotificationPaymentRejected extends Notification { readonly type = 'PAYMENT_REJECTED' orgId: string - last4: string + last4: number brand: string constructor(input: Input) { const {orgId, last4, brand, userId} = input super({userId, type: 'PAYMENT_REJECTED'}) this.orgId = orgId - this.last4 = String(last4) + this.last4 = Number(last4) this.brand = brand } } diff --git a/packages/server/database/types/processTeamsLimitsJob.ts b/packages/server/database/types/processTeamsLimitsJob.ts index 6dfb2b8e8df..bead3705f80 100644 --- a/packages/server/database/types/processTeamsLimitsJob.ts +++ b/packages/server/database/types/processTeamsLimitsJob.ts @@ -46,7 +46,7 @@ const processTeamsLimitsJob = async (job: ScheduledTeamLimitsJob, dataLoader: Da }) await r.table('Notification').insert(notificationsToInsert).run() - + await getKysely().insertInto('Notification').values(notificationsToInsert).execute() const operationId = dataLoader.share() const subOptions = {operationId} notificationsToInsert.forEach((notification) => { diff --git a/packages/server/graphql/mutations/archiveTeam.ts b/packages/server/graphql/mutations/archiveTeam.ts index 6b447ab44c1..edfca0f7108 100644 --- a/packages/server/graphql/mutations/archiveTeam.ts +++ b/packages/server/graphql/mutations/archiveTeam.ts @@ -4,6 +4,7 @@ import TeamMemberId from '../../../client/shared/gqlIds/TeamMemberId' import {maybeRemoveRestrictions} from '../../billing/helpers/teamLimitsCheck' import getRethink from '../../database/rethinkDriver' import NotificationTeamArchived from '../../database/types/NotificationTeamArchived' +import getKysely from '../../postgres/getKysely' import removeMeetingTemplatesForTeam from '../../postgres/queries/removeMeetingTemplatesForTeam' import safeArchiveTeam from '../../safeMutations/safeArchiveTeam' import {analytics} from '../../utils/analytics/analytics' @@ -30,6 +31,7 @@ export default { {teamId}: {teamId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -70,6 +72,7 @@ export default { if (notifications.length) { await r.table('Notification').insert(notifications).run() + await pg.insertInto('Notification').values(notifications).execute() } const data = { diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts index 79cc7b56371..a6013bc1c54 100644 --- a/packages/server/graphql/mutations/createTask.ts +++ b/packages/server/graphql/mutations/createTask.ts @@ -68,6 +68,7 @@ const handleAddTaskNotifications = async ( teamId: string, subOptions: SubOptions ) => { + const pg = getKysely() const r = await getRethink() const {id: taskId, content, tags, userId} = task const usersIdsToIgnore = await getUsersToIgnore(viewerId, teamId) @@ -107,8 +108,8 @@ const handleAddTaskNotifications = async ( const data = {taskId, notifications: notificationsToAdd} if (notificationsToAdd.length) { - // don't await to speed up task creation - r.table('Notification').insert(notificationsToAdd).run() + await r.table('Notification').insert(notificationsToAdd).run() + await pg.insertInto('Notification').values(notificationsToAdd).execute() notificationsToAdd.forEach((notification) => { publish( SubscriptionChannel.NOTIFICATION, diff --git a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts index 81688c94fbc..e617f31810c 100644 --- a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts +++ b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts @@ -154,6 +154,7 @@ const inviteToTeamHelper = async ( }) if (notificationsToInsert.length > 0) { await r.table('Notification').insert(notificationsToInsert).run() + await pg.insertInto('Notification').values(notificationsToInsert).execute() } const bestMeeting = await getBestInvitationMeeting(teamId, meetingId ?? undefined, dataLoader) diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts index 183e4ab19d1..aa64b773863 100644 --- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts @@ -6,7 +6,6 @@ import {phaseLabelLookup} from 'parabol-client/utils/meetings/lookups' import TeamPromptResponseId from '../../../../../client/shared/gqlIds/TeamPromptResponseId' import {ErrorResponse, PostMessageResponse} from '../../../../../client/utils/SlackManager' import appOrigin from '../../../../appOrigin' -import {RethinkSchema} from '../../../../database/rethinkDriver' import SlackAuth from '../../../../database/types/SlackAuth' import {SlackNotificationAuth} from '../../../../dataloader/integrationAuthLoaders' import getKysely from '../../../../postgres/getKysely' @@ -14,6 +13,7 @@ import {getTeamPromptResponsesByMeetingId} from '../../../../postgres/queries/ge import {SlackNotification, Team, TeamPromptResponse} from '../../../../postgres/types' import User from '../../../../postgres/types/IUser' import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting' +import {AnyNotification} from '../../../../postgres/types/Notification' import SlackServerManager from '../../../../utils/SlackServerManager' import {analytics} from '../../../../utils/analytics/analytics' import {toEpochSeconds} from '../../../../utils/epochTime' @@ -245,7 +245,7 @@ const addStandupResponsesToThread = async ( const getSlackMessageForNotification = async ( dataLoader: DataLoaderWorker, - notification: RethinkSchema['Notification']['type'], + notification: AnyNotification, meeting: AnyMeeting, userId: string ) => { @@ -277,6 +277,7 @@ const getSlackMessageForNotification = async ( buttonText: 'See the discussion' } } else if (notification.type === 'RESPONSE_MENTIONED') { + // Notification Phase 3 do not split the responseId const responseId = TeamPromptResponseId.split(notification.responseId) const response = await dataLoader.get('teamPromptResponses').loadNonNull(responseId) const author = await dataLoader.get('users').loadNonNull(response.userId) diff --git a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts index 0feaec154fe..b1e75a9b2d5 100644 --- a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts +++ b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts @@ -2,6 +2,7 @@ import {ASSIGNEE, MENTIONEE} from 'parabol-client/utils/constants' import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap' import getRethink from '../../../database/rethinkDriver' import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves' +import getKysely from '../../../postgres/getKysely' import {Task} from '../../../postgres/types' import {analytics} from '../../../utils/analytics/analytics' @@ -11,6 +12,7 @@ const publishChangeNotifications = async ( changeUser: {id: string; email: string}, usersToIgnore: string[] ) => { + const pg = getKysely() const r = await getRethink() const changeAuthorId = `${changeUser.id}::${task.teamId}` const {entityMap: oldEntityMap, blocks: oldBlocks} = JSON.parse(oldTask.content) @@ -84,6 +86,7 @@ const publishChangeNotifications = async ( // update changes in the db if (notificationsToAdd.length) { await r.table('Notification').insert(notificationsToAdd).run() + await pg.insertInto('Notification').values(notificationsToAdd).execute() } return {notificationsToAdd} } diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 44ad0e0d858..3e8e7307962 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -104,6 +104,7 @@ const removeTeamMember = async ( const notification = new NotificationKickedOut({teamId, userId, evictorUserId}) notificationId = notification.id await r.table('Notification').insert(notification).run() + await pg.insertInto('Notification').values(notification).execute() } const archivedTaskIds = await archiveTasksForDB(integratedTasksToArchive) diff --git a/packages/server/graphql/mutations/moveTeamToOrg.ts b/packages/server/graphql/mutations/moveTeamToOrg.ts index dbae4a63713..1caf980f8a6 100644 --- a/packages/server/graphql/mutations/moveTeamToOrg.ts +++ b/packages/server/graphql/mutations/moveTeamToOrg.ts @@ -4,8 +4,6 @@ import adjustUserCount from '../../billing/helpers/adjustUserCount' import getRethink from '../../database/rethinkDriver' import {RDatum} from '../../database/stricterR' import getKysely from '../../postgres/getKysely' -import updateMeetingTemplateOrgId from '../../postgres/queries/updateMeetingTemplateOrgId' -import updateTeamByTeamId from '../../postgres/queries/updateTeamByTeamId' import safeArchiveEmptyStarterOrganization from '../../safeMutations/safeArchiveEmptyStarterOrganization' import {Logger} from '../../utils/Logger' import {getUserId, isSuperUser} from '../../utils/authorization' @@ -96,8 +94,21 @@ const moveToOrg = async ( .filter((notification: RDatum) => notification('orgId').default(null).ne(null)) .update({orgId}) .run(), - updateMeetingTemplateOrgId(currentOrgId, orgId), - updateTeamByTeamId(updates, teamId) + pg + .with('NotificationUpdate', (qb) => + qb + .updateTable('Notification') + .set({orgId}) + .where('teamId', '=', teamId) + .where('orgId', 'is not', null) + ) + .with('MeetingTemplateUpdate', (qb) => + qb.updateTable('MeetingTemplate').set({orgId}).where('orgId', '=', currentOrgId) + ) + .updateTable('Team') + .set(updates) + .where('id', '=', teamId) + .execute() ]) dataLoader.clearAll('teams') // if no teams remain on the org, remove it diff --git a/packages/server/graphql/mutations/setNotificationStatus.ts b/packages/server/graphql/mutations/setNotificationStatus.ts index caf4ccaf386..21647608b3e 100644 --- a/packages/server/graphql/mutations/setNotificationStatus.ts +++ b/packages/server/graphql/mutations/setNotificationStatus.ts @@ -1,6 +1,7 @@ import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' 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' @@ -27,6 +28,7 @@ export default { {notificationId, status}: {notificationId: string; status: NotificationStatusEnumType}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { + const pg = getKysely() const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -41,6 +43,7 @@ export default { // RESOLUTION await r.table('Notification').get(notificationId).update({status}).run() + await pg.updateTable('Notification').set({status}).where('id', '=', notificationId).execute() // mutate dataloader cache notification.status = status diff --git a/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js b/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js index 6893a79d2fd..8b3ceed96c2 100644 --- a/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js +++ b/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js @@ -4,6 +4,7 @@ import MockDB from '../../../../__tests__/setup/MockDB' import {__anHourAgo, __now, __overADayAgo} from '../../../../__tests__/setup/mockTimes' import getRethink from '../../../../database/rethinkDriver' import {sendBatchEmail} from '../../../../email/sendEmail' +import getKysely from '../../../../postgres/getKysely' import sendBatchNotificationEmails from '../sendBatchNotificationEmails' // Manage side-effects @@ -20,6 +21,7 @@ describe('sendBatchNotificationEmails', () => { // global DB state, there's no getting around this. const r = await getRethink() await r.table('Notification').delete() + await sql`TRUNCATE TABLE "Notification"`.execute(getKysely()) }) it('requires the superuser role', async () => { diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts index 49651b3cef9..66bf0898eea 100644 --- a/packages/server/graphql/private/mutations/runScheduledJobs.ts +++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts @@ -33,8 +33,10 @@ const processMeetingStageTimeLimits = async ( meetingId, userId: facilitatorUserId! }) + const pg = getKysely() const r = await getRethink() await r.table('Notification').insert(notification).run() + await pg.insertInto('Notification').values(notification).execute() publish(SubscriptionChannel.NOTIFICATION, facilitatorUserId!, 'MeetingStageTimeLimitPayload', { notification }) diff --git a/packages/server/graphql/private/mutations/stripeFailPayment.ts b/packages/server/graphql/private/mutations/stripeFailPayment.ts index 4281862845c..07b41564634 100644 --- a/packages/server/graphql/private/mutations/stripeFailPayment.ts +++ b/packages/server/graphql/private/mutations/stripeFailPayment.ts @@ -3,6 +3,7 @@ import Stripe from 'stripe' import terminateSubscription from '../../../billing/helpers/terminateSubscription' import getRethink from '../../../database/rethinkDriver' import NotificationPaymentRejected from '../../../database/types/NotificationPaymentRejected' +import getKysely from '../../../postgres/getKysely' import {isSuperUser} from '../../../utils/authorization' import publish from '../../../utils/publish' import {getStripeManager} from '../../../utils/stripe' @@ -25,6 +26,7 @@ const stripeFailPayment: MutationResolvers['stripeFailPayment'] = async ( throw new Error('Don’t be rude.') } + const pg = getKysely() const r = await getRethink() const manager = getStripeManager() @@ -103,6 +105,7 @@ const stripeFailPayment: MutationResolvers['stripeFailPayment'] = async ( await r({ insert: r.table('Notification').insert(notifications) }).run() + await pg.insertInto('Notification').values(notifications).execute() notifications.forEach((notification) => { const data = {orgId, notificationId: notification.id} diff --git a/packages/server/graphql/public/mutations/addComment.ts b/packages/server/graphql/public/mutations/addComment.ts index 616b0118b2e..978c4981bb6 100644 --- a/packages/server/graphql/public/mutations/addComment.ts +++ b/packages/server/graphql/public/mutations/addComment.ts @@ -73,6 +73,7 @@ const addComment: MutationResolvers['addComment'] = async ( {comment}, {authToken, dataLoader, socketId: mutatorId} ) => { + const pg = getKysely() const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() @@ -130,6 +131,7 @@ const addComment: MutationResolvers['addComment'] = async ( }) await r.table('Notification').insert(notification).run() + await pg.insertInto('Notification').values(notification).execute() IntegrationNotifier.sendNotificationToUser?.(dataLoader, notification.id, notification.userId) publishNotification(notification, subOptions) @@ -146,6 +148,7 @@ const addComment: MutationResolvers['addComment'] = async ( if (notificationsToAdd.length) { await r.table('Notification').insert(notificationsToAdd).run() + await pg.insertInto('Notification').values(notificationsToAdd).execute() notificationsToAdd.forEach((notification) => { publishNotification(notification, subOptions) }) diff --git a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts index 6ca6ece3237..30591488c5f 100644 --- a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts +++ b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts @@ -2,6 +2,7 @@ import {JSONContent} from '@tiptap/core' import TeamPromptResponseId from '../../../../../client/shared/gqlIds/TeamPromptResponseId' import getRethink from '../../../../database/rethinkDriver' import NotificationResponseMentioned from '../../../../database/types/NotificationResponseMentioned' +import getKysely from '../../../../postgres/getKysely' import {TeamPromptResponse} from '../../../../postgres/types' const getMentionedUserIdsFromContent = (content: JSONContent): string[] => { @@ -47,8 +48,14 @@ const createTeamPromptMentionNotifications = async ( }) const r = await getRethink() + const pg = getKysely() await r.table('Notification').insert(notificationsToAdd).run() - + await pg + .insertInto('Notification') + .values( + notificationsToAdd.map((n) => ({...n, responseId: TeamPromptResponseId.split(n.responseId)})) + ) + .execute() return notificationsToAdd } diff --git a/packages/server/graphql/public/mutations/requestToJoinDomain.ts b/packages/server/graphql/public/mutations/requestToJoinDomain.ts index da16dd57dee..e279a04156f 100644 --- a/packages/server/graphql/public/mutations/requestToJoinDomain.ts +++ b/packages/server/graphql/public/mutations/requestToJoinDomain.ts @@ -68,6 +68,7 @@ const requestToJoinDomain: MutationResolvers['requestToJoinDomain'] = async ( }) await r.table('Notification').insert(notificationsToInsert).run() + await pg.insertInto('Notification').values(notificationsToInsert).execute() notificationsToInsert.forEach((notification) => { publishNotification(notification, subOptions) diff --git a/packages/server/graphql/public/mutations/setOrgUserRole.ts b/packages/server/graphql/public/mutations/setOrgUserRole.ts index 64f378c95b6..14a2f67cf46 100644 --- a/packages/server/graphql/public/mutations/setOrgUserRole.ts +++ b/packages/server/graphql/public/mutations/setOrgUserRole.ts @@ -9,10 +9,12 @@ import standardError from '../../../utils/standardError' import {MutationResolvers} from '../resolverTypes' const addNotifications = async (orgId: string, userId: string) => { + const pg = getKysely() const r = await getRethink() const promotionNotification = new NotificationPromoteToBillingLeader({orgId, userId}) const {id: promotionNotificationId} = promotionNotification await r.table('Notification').insert(promotionNotification).run() + await pg.insertInto('Notification').values(promotionNotification).execute() return [promotionNotificationId] } diff --git a/packages/server/postgres/migrations/1728504371818_Notification-phase1.ts b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts similarity index 82% rename from packages/server/postgres/migrations/1728504371818_Notification-phase1.ts rename to packages/server/postgres/migrations/1728595099540_Notification-phase1.ts index 96b76e93807..3dfb4c71eb2 100644 --- a/packages/server/postgres/migrations/1728504371818_Notification-phase1.ts +++ b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts @@ -53,20 +53,48 @@ export async function up() { "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), "type" "NotificationTypeEnum" NOT NULL, "userId" VARCHAR(100) NOT NULL, - "changeAuthorId" VARCHAR(100), - "involvement" "TaskInvolvementEnum", - "taskId" VARCHAR(100), - "teamId" VARCHAR(100), - "archivorUserId" VARCHAR(100), + -- DISCUSSION_MENTIONED "meetingId" VARCHAR(100), + "authorId" VARCHAR(100), + "commentId" VARCHAR(100), + "discussionId" VARCHAR(100), + -- KICKED_OUT + "teamId" VARCHAR(100), + "evictorUserId" VARCHAR(100), + -- MENTIONED + "senderName" VARCHAR(100), + "senderPicture" VARCHAR(2056), + "senderUserId" VARCHAR(100), + "meetingName" VARCHAR(100), + "retroReflectionId" VARCHAR(100), + "retroDiscussStageIdx" SMALLINT, + -- PAYMENT_REJECTED "orgId" VARCHAR(100), "last4" SMALLINT, "brand" VARCHAR(50), - "evictorUserId" VARCHAR(100), + -- PROMPT_TO_JOIN_ORG + "activeDomain" VARCHAR(100), + -- REQUEST_TO_JOIN_ORG + "domainJoinRequestId" INTEGER, + "email" "citext", + "name" VARCHAR(100), + "picture" VARCHAR(2056), + "requestCreatedBy" VARCHAR(100), + -- RESPONSE_MENTIONED + "responseId" INTEGER, + -- TASK_INVOLVES + "changeAuthorId" VARCHAR(100), + "involvement" "TaskInvolvementEnum", + "taskId" VARCHAR(100), + -- TEAM_ARCHIVED + "archivorUserId" VARCHAR(100), + -- TEAM_INVITATION "invitationId" VARCHAR(100), - "responseId" VARCHAR(100), - "authorId" VARCHAR(100), - "commentId" VARCHAR(100), + -- TEAMS_LIMIT_EXCEEDED + "orgName" VARCHAR(100), + "orgPicture" VARCHAR(2056), + -- TEAMS_LIMIT_REMINDER + "scheduledLockAt" TIMESTAMP WITH TIME ZONE, CONSTRAINT "fk_meetingId" FOREIGN KEY("meetingId") REFERENCES "NewMeeting"("id") diff --git a/packages/server/postgres/queries/updateMeetingTemplateOrgId.ts b/packages/server/postgres/queries/updateMeetingTemplateOrgId.ts deleted file mode 100644 index f1a1fde24d9..00000000000 --- a/packages/server/postgres/queries/updateMeetingTemplateOrgId.ts +++ /dev/null @@ -1,11 +0,0 @@ -import getPg from '../getPg' - -const updateMeetingTemplateOrgId = async (oldOrgId: string, newOrgId: string) => { - const pg = getPg() - await pg.query(`UPDATE "MeetingTemplate" SET "orgId" = $1 WHERE "orgId" = $2;`, [ - newOrgId, - oldOrgId - ]) -} - -export default updateMeetingTemplateOrgId diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index f73831e9abe..79ed12a07d7 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -4,6 +4,7 @@ import {NewMeetingPhaseTypeEnum} from '../graphql/public/resolverTypes' import getKysely from './getKysely' import {ReactjiDB, TaskTag} from './types' import {AnyMeeting, AnyMeetingMember} from './types/Meeting' +import {AnyNotification} from './types/Notification' import {AnyTaskIntegration} from './types/TaskIntegration' export const selectTimelineEvent = () => { return getKysely().selectFrom('TimelineEvent').selectAll().$narrowType< @@ -312,3 +313,6 @@ export const selectTasks = () => fn('to_json', ['tags']).as('tags'), fn('to_json', ['integration']).as('integration') ]) + +export const selectNotifications = () => + getKysely().selectFrom('Notification').selectAll().$narrowType() diff --git a/packages/server/postgres/types/Notification.d.ts b/packages/server/postgres/types/Notification.d.ts index e69de29bb2d..cc10388b8ee 100644 --- a/packages/server/postgres/types/Notification.d.ts +++ b/packages/server/postgres/types/Notification.d.ts @@ -0,0 +1,129 @@ +import type {Notification} from '../pg.d' + +interface BaseNotification { + id: string + status: Notification['status'] + type: Notification['type'] + userId: string +} + +interface DiscussionMentionedNotification extends BaseNotification { + type: 'DISCUSSION_MENTIONED' + meetingId: string + authorId: string + commentId: string + discussionId: string +} + +interface KickedOutNotification extends BaseNotification { + type: 'KICKED_OUT' + teamId: string + evictorUserId: string +} + +interface MeetingStageTimeLimitEndNotification extends BaseNotification { + type: 'MEETING_STAGE_TIME_LIMIT_END' + meetingId: string +} + +interface MentionedNotification extends BaseNotification { + type: 'MENTIONED' + senderName: string | null + senderPicture: string | null + senderUserId: string + meetingName: string + meetingId: string + retroReflectionId?: string | null + retroDiscussStageIdx?: number | null +} + +interface PaymentRejectedNotification extends BaseNotification { + type: 'PAYMENT_REJECTED' + orgId: string + last4: string + brand: string +} + +interface PromoteToBillingLeaderNotification extends BaseNotification { + type: 'PROMOTE_TO_BILLING_LEADER' + orgId: string +} + +interface PromptToJoinOrgNotification extends BaseNotification { + type: 'PROMPT_TO_JOIN_ORG' + activeDomain: string +} + +interface RequestToJoinOrgNotification extends BaseNotification { + type: 'REQUEST_TO_JOIN_ORG' + domainJoinRequestId: number + email: string + name: string + picture: string + requestCreatedBy: string +} + +interface ResponseMentionedNotification extends BaseNotification { + type: 'RESPONSE_MENTIONED' + responseId: string + meetingId: string +} + +interface ResponseRepliedNotification extends BaseNotification { + type: 'RESPONSE_REPLIED' + meetingId: string + authorId: string + commentId: string +} + +interface TaskInvolvesNotification extends BaseNotification { + type: 'TASK_INVOLVES' + changeAuthorId: string + involvement: TaskInvolvement + taskId: string + teamId: string +} + +interface TeamArchivedNotification extends BaseNotification { + type: 'TEAM_ARCHIVED' + archivorUserId: string + teamId: string +} + +interface TeamInvitationNotification extends BaseNotification { + type: 'TEAM_INVITATION' + invitationId: string + teamId: string +} + +interface TeamsLimitExceededNotification extends BaseNotification { + type: 'TEAMS_LIMIT_EXCEEDED' + orgId: string + orgName: string + orgPicture: string | null +} + +interface TeamsLimitReminderNotification extends BaseNotification { + type: 'TEAMS_LIMIT_REMINDER' + orgId: string + orgName: string + orgPicture: string | null + scheduledLockAt: Date +} + +type AnyNotification = + | DiscussionMentionedNotification + | KickedOutNotification + | MeetingStageTimeLimitEndNotification + | MentionedNotification + | PaymentRejectedNotification + | PromoteToBillingLeaderNotification + | PromptToJoinOrgNotification + | RequestToJoinOrgNotification + | ResponseMentionedNotification + | ResponseRepliedNotification + | TaskInvolvesNotification + | TeamArchivedNotification + | TeamInvitationNotification + | TeamsLimitExceededNotification + | TeamsLimitReminderNotification diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index c2cd852458d..ade90d9b059 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -86,6 +86,14 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data .default([]) .run(), pg + .with('NotificationUpdate', (qc) => + qc + .updateTable('Notification') + .set({status: 'CLICKED'}) + .where('userId', '=', userId) + .where('teamId', '=', teamId) + .where('type', '=', 'TEAM_INVITATION') + ) .with('UserUpdate', (qc) => qc .updateTable('User') @@ -113,7 +121,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false})) .execute() ]) - dataLoader.clearAll(['teamMembers', 'users']) + dataLoader.clearAll(['teamMembers', 'users', 'notifications']) if (!organizationUser) { // clear the cache, adjustUserCount will mutate these dataLoader.get('organizationUsersByUserIdOrgId').clear({userId, orgId}) diff --git a/packages/server/utils/sendPromptToJoinOrg.ts b/packages/server/utils/sendPromptToJoinOrg.ts index 1c8422dc79c..d97dbec630e 100644 --- a/packages/server/utils/sendPromptToJoinOrg.ts +++ b/packages/server/utils/sendPromptToJoinOrg.ts @@ -2,11 +2,13 @@ import getRethink from '../database/rethinkDriver' import NotificationPromptToJoinOrg from '../database/types/NotificationPromptToJoinOrg' import User from '../database/types/User' import {DataLoaderWorker} from '../graphql/graphql' +import getKysely from '../postgres/getKysely' import getDomainFromEmail from './getDomainFromEmail' import isRequestToJoinDomainAllowed from './isRequestToJoinDomainAllowed' const sendPromptToJoinOrg = async (newUser: User, dataLoader: DataLoaderWorker) => { const {id: userId, email} = newUser + const pg = getKysely() const r = await getRethink() const activeDomain = getDomainFromEmail(email) @@ -21,6 +23,7 @@ const sendPromptToJoinOrg = async (newUser: User, dataLoader: DataLoaderWorker) }) await r.table('Notification').insert(notificationToInsert).run() + await pg.insertInto('Notification').values(notificationToInsert).execute() } export default sendPromptToJoinOrg From d6828a896bdf55fe928bcf29f7fad4f752b7de75 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 16 Oct 2024 10:55:03 -0700 Subject: [PATCH 84/89] migrate r to pg Signed-off-by: Matt Krick --- .../1728595099540_Notification-phase1.ts | 20 ++ .../1729098152007_Notification-phase2.ts | 193 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 packages/server/postgres/migrations/1729098152007_Notification-phase2.ts diff --git a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts index 3dfb4c71eb2..4b6db64edec 100644 --- a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts +++ b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts @@ -142,6 +142,26 @@ export async function up() { CONSTRAINT "fk_teamId" FOREIGN KEY("teamId") REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_discussionId" + FOREIGN KEY("discussionId") + REFERENCES "Discussion"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_senderUserId" + FOREIGN KEY("senderUserId") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_retroReflectionId" + FOREIGN KEY("retroReflectionId") + REFERENCES "RetroReflection"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_domainJoinRequestId" + FOREIGN KEY("domainJoinRequestId") + REFERENCES "DomainJoinRequest"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_requestCreatedBy" + FOREIGN KEY("requestCreatedBy") + REFERENCES "User"("id") ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "idx_Notification_userId" ON "Notification"("userId"); diff --git a/packages/server/postgres/migrations/1729098152007_Notification-phase2.ts b/packages/server/postgres/migrations/1729098152007_Notification-phase2.ts new file mode 100644 index 00000000000..0332d5e7361 --- /dev/null +++ b/packages/server/postgres/migrations/1729098152007_Notification-phase2.ts @@ -0,0 +1,193 @@ +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('Notification') + .indexCreate('updatedAtId', (row: any) => [row('createdAt'), row('id')]) + .run() + await r.table('Notification').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'status', + 'createdAt', + 'type', + 'userId', + 'meetingId', + 'authorId', + 'commentId', + 'discussionId', + 'teamId', + 'evictorUserId', + 'senderName', + 'senderPicture', + 'senderUserId', + 'meetingName', + 'retroReflectionId', + 'retroDiscussStageIdx', + 'orgId', + 'last4', + 'brand', + 'activeDomain', + 'domainJoinRequestId', + 'email', + 'name', + 'picture', + 'requestCreatedBy', + 'responseId', + 'changeAuthorId', + 'involvement', + 'taskId', + 'archivorUserId', + 'invitationId', + 'orgName', + 'orgPicture', + 'scheduledLockAt' + ] as const + type Notification = { + [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 + + const insertRow = async (row) => { + if (!row.type) { + console.log('Notification has no type, skipping insert', row.id) + return + } + try { + await pg + .insertInto('Notification') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_meetingId') { + console.log('Notification has no meeting, skipping insert', row.id) + return + } + if (e.constraint === 'fk_userId') { + console.log('Notification has no user, skipping insert', row.id) + return + } + if (e.constraint === 'fk_changeAuthorId') { + console.log('Notification has no fk_changeAuthorId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_taskId') { + console.log('Notification has no fk_taskId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_archivorUserId') { + console.log('Notification has no fk_archivorUserId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_orgId') { + console.log('Notification has no fk_orgId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_evictorUserId') { + console.log('Notification has no fk_evictorUserId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_invitationId') { + console.log('Notification has no fk_invitationId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_responseId') { + console.log('Notification has no fk_responseId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_authorId') { + console.log('Notification has no fk_authorId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_commentId') { + console.log('Notification has no fk_commentId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_teamId') { + console.log('Notification has no fk_teamId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_discussionId') { + console.log('Notification has no fk_discussionId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_senderUserId') { + console.log('Notification has no fk_senderUserId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_retroReflectionId') { + console.log('Notification has no fk_retroReflectionId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_domainJoinRequestId') { + console.log('Notification has no fk_domainJoinRequestId, skipping insert', row.id) + return + } + if (e.constraint === 'fk_requestCreatedBy') { + console.log('Notification has no fk_requestCreatedBy, skipping insert', row.id) + return + } + throw e + } + } + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('Notification') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as Notification[] + + const rowsToInsert = rawRowsToInsert.map((row) => { + const {responseId, ...rest} = row as any + return { + ...rest, + responseId: responseId ? Number(responseId.split(':')[1]) : null + } + }) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.createdAt + curId = lastRow.id + await Promise.all(rowsToInsert.map(async (row) => insertRow(row))) + } +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "Notification" CASCADE`.execute(pg) +} From 3a1a80e5fa3d74d87c5bb51e66dafdef4b20d0d0 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 16 Oct 2024 10:58:30 -0700 Subject: [PATCH 85/89] update mig Signed-off-by: Matt Krick --- .../1728595099540_Notification-phase1.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts index 3dfb4c71eb2..4b6db64edec 100644 --- a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts +++ b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts @@ -142,6 +142,26 @@ export async function up() { CONSTRAINT "fk_teamId" FOREIGN KEY("teamId") REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_discussionId" + FOREIGN KEY("discussionId") + REFERENCES "Discussion"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_senderUserId" + FOREIGN KEY("senderUserId") + REFERENCES "User"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_retroReflectionId" + FOREIGN KEY("retroReflectionId") + REFERENCES "RetroReflection"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_domainJoinRequestId" + FOREIGN KEY("domainJoinRequestId") + REFERENCES "DomainJoinRequest"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_requestCreatedBy" + FOREIGN KEY("requestCreatedBy") + REFERENCES "User"("id") ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "idx_Notification_userId" ON "Notification"("userId"); From 6a970dfac23d622c06f01d3f48489f426bc83502 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 16 Oct 2024 13:08:46 -0700 Subject: [PATCH 86/89] remove reads from r Signed-off-by: Matt Krick --- codegen.json | 25 ++--- .../helpers/removeTeamsLimitObjects.ts | 39 +++----- .../server/billing/helpers/teamLimitsCheck.ts | 20 ++-- packages/server/database/rethinkDriver.ts | 27 +----- .../server/database/types/Notification.ts | 37 -------- .../types/NotificationDiscussionMentioned.ts | 26 ------ .../database/types/NotificationKickedOut.ts | 19 ---- .../NotificationMeetingStageTimeLimitEnd.ts | 16 ---- .../database/types/NotificationMentioned.ts | 45 --------- .../types/NotificationPaymentRejected.ts | 23 ----- .../NotificationPromoteToBillingLeader.ts | 17 ---- .../types/NotificationPromptToJoinOrg.ts | 16 ---- .../types/NotificationRequestToJoinOrg.ts | 28 ------ .../types/NotificationResponseMentioned.ts | 20 ---- .../types/NotificationResponseReplied.ts | 23 ----- .../types/NotificationTaskInvolves.ts | 28 ------ .../types/NotificationTeamArchived.ts | 19 ---- .../types/NotificationTeamInvitation.ts | 19 ---- .../types/NotificationTeamsLimitExceeded.ts | 22 ----- .../types/NotificationTeamsLimitReminder.ts | 25 ----- .../database/types/processTeamsLimitsJob.ts | 22 ++--- packages/server/dataloader/LocalCache.ts | 10 +- .../server/dataloader/NullableDataLoader.ts | 8 +- packages/server/dataloader/RedisCache.ts | 4 +- .../RethinkPrimaryKeyLoaderMaker.ts | 8 -- packages/server/dataloader/RootDataLoader.ts | 24 +---- .../dataloader/foreignKeyLoaderMaker.ts | 4 +- .../server/dataloader/getLoaderNameByTable.ts | 14 --- .../dataloader/primaryKeyLoaderMakers.ts | 5 + .../rethinkPrimaryKeyLoaderMakers.ts | 6 -- .../server/graphql/mutations/archiveTeam.ts | 16 ++-- .../server/graphql/mutations/createTask.ts | 44 +++++---- .../helpers/handleTeamInviteToken.ts | 10 +- .../mutations/helpers/inviteToTeamHelper.ts | 31 +++---- .../helpers/notifications/SlackNotifier.ts | 9 +- .../helpers/publishChangeNotifications.ts | 59 ++++++------ .../mutations/helpers/removeTeamMember.ts | 13 ++- .../server/graphql/mutations/moveTeamToOrg.ts | 41 +++----- .../mutations/setNotificationStatus.ts | 3 - .../__tests__/intranetJobsQuery.test.js | 3 - .../private/mutations/hardDeleteUser.ts | 5 - .../private/mutations/runScheduledJobs.ts | 11 +-- .../mutations/sendBatchNotificationEmails.ts | 30 ++---- .../private/mutations/stripeFailPayment.ts | 18 ++-- .../graphql/public/mutations/addComment.ts | 31 +++---- .../mutations/helpers/publishNotification.ts | 6 +- .../helpers/publishTeamPromptMentions.ts | 28 ++---- .../mutations/helpers/updateNotification.ts | 3 +- .../public/mutations/requestToJoinDomain.ts | 25 +++-- .../public/mutations/setOrgUserRole.ts | 18 ++-- .../types/AcceptTeamInvitationPayload.ts | 5 +- .../graphql/public/types/AddedNotification.ts | 2 +- .../public/types/ArchiveTeamPayload.ts | 15 ++- .../graphql/public/types/CreateTaskPayload.ts | 9 +- .../public/types/InviteToTeamPayload.ts | 8 +- .../public/types/NotifyResponseMentioned.ts | 5 +- .../public/types/PokerMeetingSettings.ts | 3 +- .../public/types/RemoveTeamMemberPayload.ts | 8 +- .../types/SetNotificationStatusPayload.ts | 2 +- .../public/types/SetOrgUserRoleSuccess.ts | 4 +- .../public/types/StripeFailPaymentPayload.ts | 8 +- .../graphql/public/types/UpdateTaskPayload.ts | 10 +- .../public/types/UpdatedNotification.ts | 2 +- packages/server/graphql/public/types/User.ts | 28 ++---- .../graphql/types/RemoveOrgUserPayload.ts | 4 +- .../1728595099540_Notification-phase1.ts | 1 + .../server/postgres/types/Notification.d.ts | 40 ++++---- .../safeMutations/acceptTeamInvitation.ts | 93 ++++++++----------- packages/server/utils/sendPromptToJoinOrg.ts | 21 ++--- 69 files changed, 361 insertions(+), 910 deletions(-) delete mode 100644 packages/server/database/types/Notification.ts delete mode 100644 packages/server/database/types/NotificationDiscussionMentioned.ts delete mode 100644 packages/server/database/types/NotificationKickedOut.ts delete mode 100644 packages/server/database/types/NotificationMeetingStageTimeLimitEnd.ts delete mode 100644 packages/server/database/types/NotificationMentioned.ts delete mode 100644 packages/server/database/types/NotificationPaymentRejected.ts delete mode 100644 packages/server/database/types/NotificationPromoteToBillingLeader.ts delete mode 100644 packages/server/database/types/NotificationPromptToJoinOrg.ts delete mode 100644 packages/server/database/types/NotificationRequestToJoinOrg.ts delete mode 100644 packages/server/database/types/NotificationResponseMentioned.ts delete mode 100644 packages/server/database/types/NotificationResponseReplied.ts delete mode 100644 packages/server/database/types/NotificationTaskInvolves.ts delete mode 100644 packages/server/database/types/NotificationTeamArchived.ts delete mode 100644 packages/server/database/types/NotificationTeamInvitation.ts delete mode 100644 packages/server/database/types/NotificationTeamsLimitExceeded.ts delete mode 100644 packages/server/database/types/NotificationTeamsLimitReminder.ts delete mode 100644 packages/server/dataloader/RethinkPrimaryKeyLoaderMaker.ts delete mode 100644 packages/server/dataloader/getLoaderNameByTable.ts delete mode 100644 packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts diff --git a/codegen.json b/codegen.json index 53737da9630..881ee17070e 100644 --- a/codegen.json +++ b/codegen.json @@ -128,18 +128,19 @@ "NewMeeting": "../../postgres/types/Meeting#AnyMeeting", "NewMeetingPhase": "./types/NewMeetingPhase#NewMeetingPhaseSource", "NewMeetingStage": "./types/NewMeetingStage#NewMeetingStageSource", - "NotificationMeetingStageTimeLimitEnd": "../../database/types/NotificationMeetingStageTimeLimitEnd#default as NotificationMeetingStageTimeLimitEndDB", - "NotificationTeamInvitation": "../../database/types/NotificationTeamInvitation#default as NotificationTeamInvitationDB", - "NotifyDiscussionMentioned": "../../database/types/NotificationDiscussionMentioned#default as NotificationDiscussionMentionedDB", - "NotifyKickedOut": "../../database/types/NotificationKickedOut#default", - "NotifyMentioned": "../../database/types/NotificationMentioned#default as NotificationMentionedDB", - "NotifyPaymentRejected": "../../database/types/NotificationPaymentRejected#default", - "NotifyPromoteToOrgLeader": "../../database/types/NotificationPromoteToBillingLeader#default", - "NotifyRequestToJoinOrg": "../../database/types/NotificationRequestToJoinOrg#default", - "NotifyResponseMentioned": "../../database/types/NotificationResponseMentioned#default as NotificationResponseMentionedDB", - "NotifyResponseReplied": "../../database/types/NotifyResponseReplied#default as NotifyResponseRepliedDB", - "NotifyTaskInvolves": "../../database/types/NotificationTaskInvolves#default", - "NotifyTeamArchived": "../../database/types/NotificationTeamArchived#default", + "Notification": "../../postgres/types/Notification.d#AnyNotification as AnyNotificationDB", + "NotificationMeetingStageTimeLimitEnd": "../../postgres/types/Notification.d#MeetingStageTimeLimitEndNotification as MeetingStageTimeLimitEndNotificationDB", + "NotificationTeamInvitation": "../../postgres/types/Notification.d#TeamInvitationNotification as TeamInvitationNotificationDB", + "NotifyDiscussionMentioned": "../../postgres/types/Notification.d#DiscussionMentionedNotification as DiscussionMentionedNotificationDB", + "NotifyKickedOut": "../../postgres/types/Notification.d#KickedOutNotification as KickedOutNotificationDB", + "NotifyMentioned": "../../postgres/types/Notification.d#MentionedNotification as MentionedNotificationDB", + "NotifyPaymentRejected": "../../postgres/types/Notification.d#PaymentRejectedNotification as PaymentRejectedNotificationDB", + "NotifyPromoteToOrgLeader": "../../postgres/types/Notification.d#PromoteToBillingLeaderNotification as PromoteToBillingLeaderNotificationDB", + "NotifyRequestToJoinOrg": "../../postgres/types/Notification.d#RequestToJoinOrgNotification as RequestToJoinOrgNotificationDB", + "NotifyResponseMentioned": "../../postgres/types/Notification.d#ResponseMentionedNotification as ResponseMentionedNotificationDB", + "NotifyResponseReplied": "../../postgres/types/Notification.d#ResponseRepliedNotification as ResponseRepliedNotificationDB", + "NotifyTaskInvolves": "../../postgres/types/Notification.d#TaskInvolvesNotification as TaskInvolvesNotificationDB", + "NotifyTeamArchived": "../../postgres/types/Notification.d#TeamArchivedNotification as TeamArchivedNotificationDB", "Organization": "../../postgres/types/index#Organization as OrganizationDB", "TemplateScaleValue": "./types/TemplateScaleValue#TemplateScaleValueSource as TemplateScaleValueSourceDB", "SuggestedAction": "../../postgres/types/index#SuggestedAction as SuggestedActionDB", diff --git a/packages/server/billing/helpers/removeTeamsLimitObjects.ts b/packages/server/billing/helpers/removeTeamsLimitObjects.ts index 14d46788aac..56ee4cf58db 100644 --- a/packages/server/billing/helpers/removeTeamsLimitObjects.ts +++ b/packages/server/billing/helpers/removeTeamsLimitObjects.ts @@ -1,5 +1,3 @@ -import {r} from 'rethinkdb-ts' -import {RValue} from '../../database/stricterR' import {DataLoaderWorker} from '../../graphql/graphql' import updateNotification from '../../graphql/public/mutations/helpers/updateNotification' import getKysely from '../../postgres/getKysely' @@ -10,37 +8,22 @@ const removeTeamsLimitObjects = async (orgId: string, dataLoader: DataLoaderWork const pg = getKysely() // Remove team limits jobs and existing notifications - const [, updateNotificationsChanges] = await Promise.all([ - pg - .with('NotificationUpdate', (qb) => - qb - .updateTable('Notification') - .set({status: 'CLICKED'}) - .where('orgId', '=', orgId) - .where('type', 'in', removeNotificationTypes) - ) - .deleteFrom('ScheduledJob') - .where('orgId', '=', orgId) - .where('type', 'in', removeJobTypes) - .execute(), - r - .table('Notification') - .getAll(orgId, {index: 'orgId'}) - .filter((row: RValue) => r.expr(removeNotificationTypes).contains(row('type'))) - .update( - // not really clicked, but no longer important - {status: 'CLICKED'}, - {returnChanges: true} - )('changes') - .default([]) - .run() - ]) + const updateNotificationsChanges = await pg + .with('ScheduledJobDelete', (qb) => + qb.deleteFrom('ScheduledJob').where('orgId', '=', orgId).where('type', 'in', removeJobTypes) + ) + .updateTable('Notification') + .set({status: 'CLICKED'}) + .where('orgId', '=', orgId) + .where('type', 'in', removeNotificationTypes) + .returning(['id', 'userId']) + .execute() const operationId = dataLoader.share() const subOptions = {operationId} updateNotificationsChanges?.forEach((change) => { - updateNotification(change.new_val, subOptions) + updateNotification(change, subOptions) }) } diff --git a/packages/server/billing/helpers/teamLimitsCheck.ts b/packages/server/billing/helpers/teamLimitsCheck.ts index 94757589f45..fb16cfdca80 100644 --- a/packages/server/billing/helpers/teamLimitsCheck.ts +++ b/packages/server/billing/helpers/teamLimitsCheck.ts @@ -2,9 +2,8 @@ import ms from 'ms' import {Threshold} from 'parabol-client/types/constEnums' // Uncomment for easier testing // import { ThresholdTest as Threshold } from "~/types/constEnums"; -import {r} from 'rethinkdb-ts' -import NotificationTeamsLimitExceeded from '../../database/types/NotificationTeamsLimitExceeded' import scheduleTeamLimitsJobs from '../../database/types/scheduleTeamLimitsJobs' +import generateUID from '../../generateUID' import {DataLoaderWorker} from '../../graphql/graphql' import publishNotification from '../../graphql/public/mutations/helpers/publishNotification' import getActiveTeamCountByTeamIds from '../../graphql/public/types/helpers/getActiveTeamCountByTeamIds' @@ -51,16 +50,15 @@ const sendWebsiteNotifications = async ( const {id: orgId, name: orgName, picture: orgPicture} = organization const operationId = dataLoader.share() const subOptions = {operationId} - const notificationsToInsert = userIds.map((userId) => { - return new NotificationTeamsLimitExceeded({ - userId, - orgId, - orgName, - orgPicture - }) - }) + const notificationsToInsert = userIds.map((userId) => ({ + id: generateUID(), + type: 'TEAMS_LIMIT_EXCEEDED' as const, + userId, + orgId, + orgName, + orgPicture + })) - await r.table('Notification').insert(notificationsToInsert).run() await pg.insertInto('Notification').values(notificationsToInsert).execute() notificationsToInsert.forEach((notification) => { publishNotification(notification, subOptions) diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index e909172b381..1b2078fdcfc 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -1,33 +1,8 @@ import {MasterPool, r} from 'rethinkdb-ts' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' -import NotificationKickedOut from './types/NotificationKickedOut' -import NotificationMeetingStageTimeLimitEnd from './types/NotificationMeetingStageTimeLimitEnd' -import NotificationMentioned from './types/NotificationMentioned' -import NotificationPaymentRejected from './types/NotificationPaymentRejected' -import NotificationPromoteToBillingLeader from './types/NotificationPromoteToBillingLeader' -import NotificationResponseMentioned from './types/NotificationResponseMentioned' -import NotificationResponseReplied from './types/NotificationResponseReplied' -import NotificationTaskInvolves from './types/NotificationTaskInvolves' -import NotificationTeamArchived from './types/NotificationTeamArchived' -import NotificationTeamInvitation from './types/NotificationTeamInvitation' -export type RethinkSchema = { - Notification: { - type: - | NotificationTaskInvolves - | NotificationTeamArchived - | NotificationMeetingStageTimeLimitEnd - | NotificationPaymentRejected - | NotificationKickedOut - | NotificationPromoteToBillingLeader - | NotificationTeamInvitation - | NotificationResponseMentioned - | NotificationResponseReplied - | NotificationMentioned - index: 'userId' - } -} +export type RethinkSchema = {} export type DBType = { [P in keyof RethinkSchema]: any diff --git a/packages/server/database/types/Notification.ts b/packages/server/database/types/Notification.ts deleted file mode 100644 index fcea79d8f4e..00000000000 --- a/packages/server/database/types/Notification.ts +++ /dev/null @@ -1,37 +0,0 @@ -import generateUID from '../../generateUID' -import {NotificationStatusEnumType} from '../../graphql/types/NotificationStatusEnum' - -export type NotificationEnum = - | 'DISCUSSION_MENTIONED' - | 'KICKED_OUT' - | 'MEETING_STAGE_TIME_LIMIT_END' - | 'PAYMENT_REJECTED' - | 'PROMOTE_TO_BILLING_LEADER' - | 'RESPONSE_MENTIONED' - | 'RESPONSE_REPLIED' - | 'MENTIONED' - | 'TASK_INVOLVES' - | 'TEAM_ARCHIVED' - | 'TEAM_INVITATION' - | 'TEAMS_LIMIT_EXCEEDED' - | 'TEAMS_LIMIT_REMINDER' - | 'PROMPT_TO_JOIN_ORG' - | 'REQUEST_TO_JOIN_ORG' - -export interface NotificationInput { - type: NotificationEnum - userId: string -} - -export default abstract class Notification { - id = generateUID() - status: NotificationStatusEnumType = 'UNREAD' - createdAt = new Date() - readonly type: NotificationEnum - userId: string - - constructor({type, userId}: NotificationInput) { - this.type = type - this.userId = userId - } -} diff --git a/packages/server/database/types/NotificationDiscussionMentioned.ts b/packages/server/database/types/NotificationDiscussionMentioned.ts deleted file mode 100644 index aa0a26b14cc..00000000000 --- a/packages/server/database/types/NotificationDiscussionMentioned.ts +++ /dev/null @@ -1,26 +0,0 @@ -import Notification from './Notification' - -interface Input { - meetingId: string - authorId: string - userId: string - commentId: string - discussionId: string -} - -export default class NotificationDiscussionMentioned extends Notification { - readonly type = 'DISCUSSION_MENTIONED' - meetingId: string - authorId: string - commentId: string - discussionId: string - - constructor(input: Input) { - const {meetingId, authorId, userId, commentId, discussionId} = input - super({userId, type: 'DISCUSSION_MENTIONED'}) - this.meetingId = meetingId - this.authorId = authorId - this.commentId = commentId - this.discussionId = discussionId - } -} diff --git a/packages/server/database/types/NotificationKickedOut.ts b/packages/server/database/types/NotificationKickedOut.ts deleted file mode 100644 index f8396284921..00000000000 --- a/packages/server/database/types/NotificationKickedOut.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Notification from './Notification' - -interface Input { - teamId: string - userId: string - evictorUserId: string -} - -export default class NotificationKickedOut extends Notification { - readonly type = 'KICKED_OUT' - teamId: string - evictorUserId: string - constructor(input: Input) { - const {evictorUserId, teamId, userId} = input - super({userId, type: 'KICKED_OUT'}) - this.teamId = teamId - this.evictorUserId = evictorUserId - } -} diff --git a/packages/server/database/types/NotificationMeetingStageTimeLimitEnd.ts b/packages/server/database/types/NotificationMeetingStageTimeLimitEnd.ts deleted file mode 100644 index 79e245fb0e5..00000000000 --- a/packages/server/database/types/NotificationMeetingStageTimeLimitEnd.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Notification from './Notification' - -interface Input { - meetingId: string - userId: string -} - -export default class NotificationMeetingStageTimeLimitEnd extends Notification { - readonly type = 'MEETING_STAGE_TIME_LIMIT_END' - meetingId: string - constructor(input: Input) { - const {meetingId, userId} = input - super({userId, type: 'MEETING_STAGE_TIME_LIMIT_END'}) - this.meetingId = meetingId - } -} diff --git a/packages/server/database/types/NotificationMentioned.ts b/packages/server/database/types/NotificationMentioned.ts deleted file mode 100644 index c35dff270bb..00000000000 --- a/packages/server/database/types/NotificationMentioned.ts +++ /dev/null @@ -1,45 +0,0 @@ -import Notification from './Notification' - -interface Input { - userId: string - senderName: string | null - senderPicture: string | null - senderUserId: string - meetingName: string - meetingId: string - retroReflectionId?: string | null - retroDiscussStageIdx?: number | null -} - -// TODO: replace NotificationResponseMentioned and NotificationResponseReplied with NotificationMentioned -export default class NotificationMentioned extends Notification { - readonly type = 'MENTIONED' - senderName: string | null - senderPicture: string | null - senderUserId: string - meetingName: string - meetingId: string - retroReflectionId?: string | null - retroDiscussStageIdx?: number | null - - constructor(input: Input) { - const { - userId, - senderName, - senderPicture, - senderUserId, - meetingName, - meetingId, - retroReflectionId, - retroDiscussStageIdx - } = input - super({userId, type: 'MENTIONED'}) - this.senderName = senderName - this.senderPicture = senderPicture - this.senderUserId = senderUserId - this.meetingName = meetingName - this.meetingId = meetingId - this.retroReflectionId = retroReflectionId - this.retroDiscussStageIdx = retroDiscussStageIdx - } -} diff --git a/packages/server/database/types/NotificationPaymentRejected.ts b/packages/server/database/types/NotificationPaymentRejected.ts deleted file mode 100644 index 707a65f5634..00000000000 --- a/packages/server/database/types/NotificationPaymentRejected.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Notification from './Notification' - -interface Input { - orgId: string - last4: string | number - brand: string - userId: string -} - -export default class NotificationPaymentRejected extends Notification { - readonly type = 'PAYMENT_REJECTED' - orgId: string - last4: number - brand: string - - constructor(input: Input) { - const {orgId, last4, brand, userId} = input - super({userId, type: 'PAYMENT_REJECTED'}) - this.orgId = orgId - this.last4 = Number(last4) - this.brand = brand - } -} diff --git a/packages/server/database/types/NotificationPromoteToBillingLeader.ts b/packages/server/database/types/NotificationPromoteToBillingLeader.ts deleted file mode 100644 index adbaed292d7..00000000000 --- a/packages/server/database/types/NotificationPromoteToBillingLeader.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Notification from './Notification' - -interface Input { - orgId: string - userId: string -} - -export default class NotificationPromoteToBillingLeader extends Notification { - readonly type = 'PROMOTE_TO_BILLING_LEADER' - orgId: string - - constructor(input: Input) { - const {orgId, userId} = input - super({userId, type: 'PROMOTE_TO_BILLING_LEADER'}) - this.orgId = orgId - } -} diff --git a/packages/server/database/types/NotificationPromptToJoinOrg.ts b/packages/server/database/types/NotificationPromptToJoinOrg.ts deleted file mode 100644 index 010813bb191..00000000000 --- a/packages/server/database/types/NotificationPromptToJoinOrg.ts +++ /dev/null @@ -1,16 +0,0 @@ -import Notification from './Notification' - -interface Input { - activeDomain: string - userId: string -} - -export default class NotificationPromptToJoinOrg extends Notification { - readonly type = 'PROMPT_TO_JOIN_ORG' - activeDomain: string - constructor(input: Input) { - const {userId, activeDomain} = input - super({userId, type: 'PROMPT_TO_JOIN_ORG'}) - this.activeDomain = activeDomain - } -} diff --git a/packages/server/database/types/NotificationRequestToJoinOrg.ts b/packages/server/database/types/NotificationRequestToJoinOrg.ts deleted file mode 100644 index bb9c56e0695..00000000000 --- a/packages/server/database/types/NotificationRequestToJoinOrg.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Notification from './Notification' - -interface Input { - domainJoinRequestId: number - email: string - name: string - picture: string - userId: string - requestCreatedBy: string -} - -export default class NotificationRequestToJoinOrg extends Notification { - readonly type = 'REQUEST_TO_JOIN_ORG' - domainJoinRequestId: number - email: string - name: string - picture: string - requestCreatedBy: string - constructor(input: Input) { - const {domainJoinRequestId, requestCreatedBy, email, name, picture, userId} = input - super({userId, type: 'REQUEST_TO_JOIN_ORG'}) - this.domainJoinRequestId = domainJoinRequestId - this.email = email - this.name = name - this.picture = picture - this.requestCreatedBy = requestCreatedBy - } -} diff --git a/packages/server/database/types/NotificationResponseMentioned.ts b/packages/server/database/types/NotificationResponseMentioned.ts deleted file mode 100644 index 3368b54babb..00000000000 --- a/packages/server/database/types/NotificationResponseMentioned.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Notification from './Notification' - -interface Input { - responseId: string - meetingId: string - userId: string -} - -export default class NotificationResponseMentioned extends Notification { - readonly type = 'RESPONSE_MENTIONED' - responseId: string - meetingId: string - - constructor(input: Input) { - const {responseId, meetingId, userId} = input - super({userId, type: 'RESPONSE_MENTIONED'}) - this.responseId = responseId - this.meetingId = meetingId - } -} diff --git a/packages/server/database/types/NotificationResponseReplied.ts b/packages/server/database/types/NotificationResponseReplied.ts deleted file mode 100644 index 8f254a43990..00000000000 --- a/packages/server/database/types/NotificationResponseReplied.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Notification from './Notification' - -interface Input { - meetingId: string - authorId: string - userId: string - commentId: string -} - -export default class NotificationResponseReplied extends Notification { - readonly type = 'RESPONSE_REPLIED' - meetingId: string - authorId: string - commentId: string - - constructor(input: Input) { - const {meetingId, authorId, userId, commentId} = input - super({userId, type: 'RESPONSE_REPLIED'}) - this.meetingId = meetingId - this.authorId = authorId - this.commentId = commentId - } -} diff --git a/packages/server/database/types/NotificationTaskInvolves.ts b/packages/server/database/types/NotificationTaskInvolves.ts deleted file mode 100644 index 44fd3e83b09..00000000000 --- a/packages/server/database/types/NotificationTaskInvolves.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Notification from './Notification' - -export type TaskInvolvement = 'ASSIGNEE' | 'MENTIONEE' - -interface Input { - changeAuthorId: string - involvement: TaskInvolvement - taskId: string - teamId: string - userId: string -} - -export default class NotificationTaskInvolves extends Notification { - readonly type = 'TASK_INVOLVES' - changeAuthorId: string - involvement: TaskInvolvement - taskId: string - teamId: string - - constructor(input: Input) { - const {teamId, changeAuthorId, involvement, taskId, userId} = input - super({userId, type: 'TASK_INVOLVES'}) - this.changeAuthorId = changeAuthorId - this.involvement = involvement - this.taskId = taskId - this.teamId = teamId - } -} diff --git a/packages/server/database/types/NotificationTeamArchived.ts b/packages/server/database/types/NotificationTeamArchived.ts deleted file mode 100644 index 3b48290d046..00000000000 --- a/packages/server/database/types/NotificationTeamArchived.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Notification from './Notification' - -interface Input { - archivorUserId: string - teamId: string - userId: string -} - -export default class NotificationTeamArchived extends Notification { - readonly type = 'TEAM_ARCHIVED' - archivorUserId: string - teamId: string - constructor(input: Input) { - const {archivorUserId, teamId, userId} = input - super({userId, type: 'TEAM_ARCHIVED'}) - this.archivorUserId = archivorUserId - this.teamId = teamId - } -} diff --git a/packages/server/database/types/NotificationTeamInvitation.ts b/packages/server/database/types/NotificationTeamInvitation.ts deleted file mode 100644 index e0995ae8393..00000000000 --- a/packages/server/database/types/NotificationTeamInvitation.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Notification from './Notification' - -interface Input { - invitationId: string - teamId: string - userId: string -} - -export default class NotificationTeamInvitation extends Notification { - readonly type = 'TEAM_INVITATION' - invitationId: string - teamId: string - constructor(input: Input) { - const {invitationId, teamId, userId} = input - super({userId, type: 'TEAM_INVITATION'}) - this.invitationId = invitationId - this.teamId = teamId - } -} diff --git a/packages/server/database/types/NotificationTeamsLimitExceeded.ts b/packages/server/database/types/NotificationTeamsLimitExceeded.ts deleted file mode 100644 index 8a070846a9c..00000000000 --- a/packages/server/database/types/NotificationTeamsLimitExceeded.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Notification from './Notification' - -interface Input { - orgId: string - orgName: string - orgPicture?: string | null - userId: string -} - -export default class NotificationTeamsLimitExceeded extends Notification { - readonly type = 'TEAMS_LIMIT_EXCEEDED' - orgId: string - orgName: string - orgPicture?: string | null - constructor(input: Input) { - const {userId, orgId, orgName, orgPicture} = input - super({userId, type: 'TEAMS_LIMIT_EXCEEDED'}) - this.orgId = orgId - this.orgName = orgName - this.orgPicture = orgPicture - } -} diff --git a/packages/server/database/types/NotificationTeamsLimitReminder.ts b/packages/server/database/types/NotificationTeamsLimitReminder.ts deleted file mode 100644 index a03c33edb9c..00000000000 --- a/packages/server/database/types/NotificationTeamsLimitReminder.ts +++ /dev/null @@ -1,25 +0,0 @@ -import Notification from './Notification' - -interface Input { - orgId: string - orgName: string - orgPicture?: string | null - userId: string - scheduledLockAt: Date -} - -export default class NotificationTeamsLimitReminder extends Notification { - readonly type = 'TEAMS_LIMIT_REMINDER' - orgId: string - orgName: string - orgPicture?: string | null - scheduledLockAt: Date - constructor(input: Input) { - const {userId, orgId, orgName, orgPicture, scheduledLockAt} = input - super({userId, type: 'TEAMS_LIMIT_REMINDER'}) - this.orgId = orgId - this.scheduledLockAt = scheduledLockAt - this.orgName = orgName - this.orgPicture = orgPicture - } -} diff --git a/packages/server/database/types/processTeamsLimitsJob.ts b/packages/server/database/types/processTeamsLimitsJob.ts index bead3705f80..58c5bde0dd8 100644 --- a/packages/server/database/types/processTeamsLimitsJob.ts +++ b/packages/server/database/types/processTeamsLimitsJob.ts @@ -1,10 +1,9 @@ -import {r} from 'rethinkdb-ts' import sendTeamsLimitEmail from '../../billing/helpers/sendTeamsLimitEmail' +import generateUID from '../../generateUID' import {DataLoaderWorker} from '../../graphql/graphql' import isValid from '../../graphql/isValid' import publishNotification from '../../graphql/public/mutations/helpers/publishNotification' import getKysely from '../../postgres/getKysely' -import NotificationTeamsLimitReminder from './NotificationTeamsLimitReminder' import ScheduledTeamLimitsJob from './ScheduledTeamLimitsJob' const processTeamsLimitsJob = async (job: ScheduledTeamLimitsJob, dataLoader: DataLoaderWorker) => { @@ -35,17 +34,16 @@ const processTeamsLimitsJob = async (job: ScheduledTeamLimitsJob, dataLoader: Da .execute() organization.lockedAt = lockedAt } else if (type === 'WARN_ORGANIZATION') { - const notificationsToInsert = billingLeadersIds.map((userId) => { - return new NotificationTeamsLimitReminder({ - userId, - orgId, - orgName, - orgPicture, - scheduledLockAt - }) - }) + const notificationsToInsert = billingLeadersIds.map((userId) => ({ + id: generateUID(), + type: 'TEAMS_LIMIT_REMINDER' as const, + userId, + orgId, + orgName, + orgPicture, + scheduledLockAt + })) - await r.table('Notification').insert(notificationsToInsert).run() await getKysely().insertInto('Notification').values(notificationsToInsert).execute() const operationId = dataLoader.share() const subOptions = {operationId} diff --git a/packages/server/dataloader/LocalCache.ts b/packages/server/dataloader/LocalCache.ts index 90bbd22fb7c..ab73c74efa0 100644 --- a/packages/server/dataloader/LocalCache.ts +++ b/packages/server/dataloader/LocalCache.ts @@ -1,6 +1,6 @@ import {DBType} from '../database/rethinkDriver' import RedisCache, {CacheType} from './RedisCache' -import {RWrite, Updater} from './RethinkDBCache' +import {Updater} from './RethinkDBCache' const resolvedPromise = Promise.resolve() @@ -95,7 +95,7 @@ export default class LocalCache { writes.forEach(({resolve, table, id}, idx) => { const key = `${table}:${id}` const result = results[idx] - this.primeLocal(key, result) + this.primeLocal(key, result!) resolve(result) }) } @@ -107,7 +107,7 @@ export default class LocalCache { } async prime(table: T, docs: CacheType[T][]) { - docs.forEach((doc) => { + docs.forEach((doc: any) => { const key = `${table}:${doc.id}` this.primeLocal(key, doc) }) @@ -149,10 +149,10 @@ export default class LocalCache { }) return Promise.all(loadPromises) } - async write

(table: P, id: string, updater: Updater) { + async write

(table: P, id: string, updater: any) { if (this.hasWriteDispatched) { this.hasWriteDispatched = false - this.writes = [] as (RWrite & {resolve: (payload: any) => void})[] + this.writes = [] as any[] resolvedPromise.then(() => { process.nextTick(this.dispatchWriteBatch) }) diff --git a/packages/server/dataloader/NullableDataLoader.ts b/packages/server/dataloader/NullableDataLoader.ts index 0cfb4393e1a..1e3fc17b064 100644 --- a/packages/server/dataloader/NullableDataLoader.ts +++ b/packages/server/dataloader/NullableDataLoader.ts @@ -17,16 +17,16 @@ class NullableDataLoader extends UpdatableCacheDataL super(batchLoadFn, options) } - load(key: Key) { - return super.load(key) as Promise<(Value & NarrowType) | undefined> + load(key: Key) { + return super.load(key) as Promise } - async loadNonNull(key: Key): Promise { + async loadNonNull(key: Key) { const value = await this.load(key) if (value === undefined) { throw new Error('Non-nullable value is undefined') } - return value + return value as NarrowType } } diff --git a/packages/server/dataloader/RedisCache.ts b/packages/server/dataloader/RedisCache.ts index 2a2d01f08a3..89d9064d921 100644 --- a/packages/server/dataloader/RedisCache.ts +++ b/packages/server/dataloader/RedisCache.ts @@ -10,7 +10,7 @@ export type RedisType = { [P in keyof typeof customRedisQueries]: Unpromise>[0] } -export type CacheType = RedisType & DBType +export type CacheType = RedisType const TTL = ms('3h') @@ -134,7 +134,7 @@ export default class RedisCache { return this.getRedis().del(key) } prime = async (table: T, docs: CacheType[T][]) => { - const writes = docs.map((doc) => { + const writes = docs.map((doc: any) => { return msetpx(`${table}:${doc.id}`, doc) }) await this.getRedis().multi(writes).exec() diff --git a/packages/server/dataloader/RethinkPrimaryKeyLoaderMaker.ts b/packages/server/dataloader/RethinkPrimaryKeyLoaderMaker.ts deleted file mode 100644 index c8d21cb3548..00000000000 --- a/packages/server/dataloader/RethinkPrimaryKeyLoaderMaker.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {DBType} from '../database/rethinkDriver' - -/** - * Used to register rethink types in the dataloader - */ -export default class RethinkPrimaryKeyLoaderMaker { - constructor(public table: T) {} -} diff --git a/packages/server/dataloader/RootDataLoader.ts b/packages/server/dataloader/RootDataLoader.ts index 9bf6bd6a34e..587d3e67826 100644 --- a/packages/server/dataloader/RootDataLoader.ts +++ b/packages/server/dataloader/RootDataLoader.ts @@ -1,5 +1,4 @@ import DataLoader from 'dataloader' -import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' import * as atlassianLoaders from './atlassianLoaders' import * as azureDevOpsLoaders from './azureDevOpsLoaders' import * as customLoaderMakers from './customLoaderMakers' @@ -11,8 +10,6 @@ import * as integrationAuthLoaders from './integrationAuthLoaders' import * as jiraServerLoaders from './jiraServerLoaders' import * as pollLoaders from './pollsLoaders' import * as primaryKeyLoaderMakers from './primaryKeyLoaderMakers' -import rethinkPrimaryKeyLoader from './rethinkPrimaryKeyLoader' -import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers' interface LoaderDict { [loaderName: string]: DataLoader @@ -20,13 +17,11 @@ interface LoaderDict { // Register all loaders const loaderMakers = { - ...rethinkPrimaryKeyLoaderMakers, ...primaryKeyLoaderMakers, ...foreignKeyLoaderMakers, ...customLoaderMakers, ...atlassianLoaders, ...jiraServerLoaders, - ...customLoaderMakers, ...githubLoaders, ...gitlabLoaders, ...gcalLoaders, @@ -37,9 +32,8 @@ const loaderMakers = { export type Loaders = keyof typeof loaderMakers -export type AllPrimaryLoaders = - | keyof typeof primaryKeyLoaderMakers - | keyof typeof rethinkPrimaryKeyLoaderMakers +export type AllPrimaryLoaders = keyof typeof primaryKeyLoaderMakers + export type RegisterDependsOn = (primaryLoaders: AllPrimaryLoaders | AllPrimaryLoaders[]) => void // The RethinkDB logic is a leaky abstraction! It will be gone soon & this will be generic enough to put in its own package @@ -47,12 +41,7 @@ interface GenericDataLoader { clearAll(pkLoaderName: TPrimaryLoaderNames | TPrimaryLoaderNames[]): void get( loaderName: LoaderName - ): Loader extends (...args: any[]) => any - ? ReturnType - : // can delete below this line after RethinkDB is gone - Loader extends RethinkPrimaryKeyLoaderMaker - ? ReturnType> - : never + ): Loader extends (...args: any[]) => any ? ReturnType : never } export type DataLoaderInstance = GenericDataLoader @@ -88,12 +77,7 @@ export default class RootDataLoader< ;(this.dependentLoaders[primaryLoader] ??= []).push(loaderName) }) } - if (loaderMaker instanceof RethinkPrimaryKeyLoaderMaker) { - const {table} = loaderMaker - loader = rethinkPrimaryKeyLoader(this.dataLoaderOptions, table) - } else { - loader = (loaderMaker as any)(this, dependsOn) - } + loader = (loaderMaker as any)(this, dependsOn) this.loaders[loaderName] = loader! return loader as any } diff --git a/packages/server/dataloader/foreignKeyLoaderMaker.ts b/packages/server/dataloader/foreignKeyLoaderMaker.ts index 88594e3d8e2..69e042e8aa7 100644 --- a/packages/server/dataloader/foreignKeyLoaderMaker.ts +++ b/packages/server/dataloader/foreignKeyLoaderMaker.ts @@ -1,4 +1,5 @@ import DataLoader from 'dataloader' +import NullableDataLoader from './NullableDataLoader' import RootDataLoader, {RegisterDependsOn} from './RootDataLoader' import UpdatableCacheDataLoader from './UpdatableCacheDataLoader' import * as primaryKeyLoaderMakers from './primaryKeyLoaderMakers' @@ -7,7 +8,7 @@ type LoaderMakers = typeof primaryKeyLoaderMakers type LoaderKeys = keyof LoaderMakers type Loader = ReturnType type LoaderType = - Loader extends DataLoader ? NonNullable : any + Loader extends NullableDataLoader ? NonNullable : any /** * Used to register loaders for types by foreign key. @@ -16,6 +17,7 @@ type LoaderType = * When an item is loaded via this loader, the primary loader will be primed with the result as well. * It reflects a one to many relationship, i.e. for each key passed, an array will be returned. */ + export function foreignKeyLoaderMaker< LoaderName extends LoaderKeys, T extends LoaderType, diff --git a/packages/server/dataloader/getLoaderNameByTable.ts b/packages/server/dataloader/getLoaderNameByTable.ts deleted file mode 100644 index 0449dd2a631..00000000000 --- a/packages/server/dataloader/getLoaderNameByTable.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as rethinkPrimaryKeyLoaderMakers from './rethinkPrimaryKeyLoaderMakers' - -const loadersByTable = {} as Record -Object.keys(rethinkPrimaryKeyLoaderMakers).forEach((loaderName) => { - const loader = - rethinkPrimaryKeyLoaderMakers[loaderName as keyof typeof rethinkPrimaryKeyLoaderMakers] - loadersByTable[loader.table] = loaderName -}) - -const getLoaderNameByTable = (table: string) => { - return loadersByTable[table] -} - -export default getLoaderNameByTable diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 47ee113669d..2e0c20e33a0 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -13,6 +13,7 @@ import { selectMeetingSettings, selectNewFeatures, selectNewMeetings, + selectNotifications, selectOrganizations, selectReflectPrompts, selectRetroReflections, @@ -149,3 +150,7 @@ export const teamInvitations = primaryKeyLoaderMaker((ids: readonly string[]) => export const tasks = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectTasks().where('id', 'in', ids).execute() }) + +export const notifications = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectNotifications().where('id', 'in', ids).execute() +}) diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts deleted file mode 100644 index ec24d52c90a..00000000000 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ /dev/null @@ -1,6 +0,0 @@ -import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' - -/** - * all rethink dataloader types which also must exist in {@link rethinkDriver/RethinkSchema} - */ -export const notifications = new RethinkPrimaryKeyLoaderMaker('Notification') diff --git a/packages/server/graphql/mutations/archiveTeam.ts b/packages/server/graphql/mutations/archiveTeam.ts index edfca0f7108..c9ccd474666 100644 --- a/packages/server/graphql/mutations/archiveTeam.ts +++ b/packages/server/graphql/mutations/archiveTeam.ts @@ -2,8 +2,7 @@ import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import TeamMemberId from '../../../client/shared/gqlIds/TeamMemberId' import {maybeRemoveRestrictions} from '../../billing/helpers/teamLimitsCheck' -import getRethink from '../../database/rethinkDriver' -import NotificationTeamArchived from '../../database/types/NotificationTeamArchived' +import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' import removeMeetingTemplatesForTeam from '../../postgres/queries/removeMeetingTemplatesForTeam' import safeArchiveTeam from '../../safeMutations/safeArchiveTeam' @@ -32,7 +31,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -65,13 +63,15 @@ export default { const notifications = users .map((user) => user?.id) .filter((userId) => userId !== undefined && userId !== viewerId) - .map( - (notifiedUserId) => - new NotificationTeamArchived({userId: notifiedUserId!, teamId, archivorUserId: viewerId}) - ) + .map((notifiedUserId) => ({ + id: generateUID(), + type: 'TEAM_ARCHIVED' as const, + userId: notifiedUserId!, + teamId, + archivorUserId: viewerId + })) if (notifications.length) { - await r.table('Notification').insert(notifications).run() await pg.insertInto('Notification').values(notifications).execute() } diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts index a6013bc1c54..98958388cd4 100644 --- a/packages/server/graphql/mutations/createTask.ts +++ b/packages/server/graphql/mutations/createTask.ts @@ -1,4 +1,5 @@ import {GraphQLNonNull, GraphQLObjectType, GraphQLResolveInfo} from 'graphql' +import {Insertable} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap' import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId' @@ -7,11 +8,10 @@ import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId' import dndNoise from '../../../client/utils/dndNoise' import extractTextFromDraftString from '../../../client/utils/draftjs/extractTextFromDraftString' import getTagsFromEntityMap from '../../../client/utils/draftjs/getTagsFromEntityMap' -import getRethink from '../../database/rethinkDriver' -import NotificationTaskInvolves from '../../database/types/NotificationTaskInvolves' import generateUID from '../../generateUID' import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache' import getKysely from '../../postgres/getKysely' +import {Notification} from '../../postgres/pg' import {Task, TaskTag} from '../../postgres/types/index.d' import {TaskServiceEnum} from '../../postgres/types/TaskIntegration' import {analytics} from '../../utils/analytics/analytics' @@ -69,24 +69,23 @@ const handleAddTaskNotifications = async ( subOptions: SubOptions ) => { const pg = getKysely() - const r = await getRethink() const {id: taskId, content, tags, userId} = task const usersIdsToIgnore = await getUsersToIgnore(viewerId, teamId) // Handle notifications // Almost always you start out with a blank card assigned to you (except for filtered team dash) const changeAuthorId = toTeamMemberId(teamId, viewerId) - const notificationsToAdd = [] as NotificationTaskInvolves[] + const notificationsToAdd = [] as Insertable[] if (userId && viewerId !== userId && !usersIdsToIgnore.includes(userId)) { - notificationsToAdd.push( - new NotificationTaskInvolves({ - involvement: 'ASSIGNEE', - taskId, - changeAuthorId, - teamId, - userId - }) - ) + notificationsToAdd.push({ + id: generateUID(), + type: 'TASK_INVOLVES' as const, + involvement: 'ASSIGNEE' as const, + taskId, + changeAuthorId, + teamId, + userId + }) } const {entityMap} = JSON.parse(content) @@ -95,20 +94,19 @@ const handleAddTaskNotifications = async ( (mention) => mention !== viewerId && mention !== userId && !usersIdsToIgnore.includes(mention) ) .forEach((mentioneeUserId) => { - notificationsToAdd.push( - new NotificationTaskInvolves({ - userId: mentioneeUserId, - involvement: 'MENTIONEE', - taskId, - changeAuthorId, - teamId - }) - ) + notificationsToAdd.push({ + id: generateUID(), + type: 'TASK_INVOLVES' as const, + userId: mentioneeUserId, + involvement: 'MENTIONEE', + taskId, + changeAuthorId, + teamId + }) }) const data = {taskId, notifications: notificationsToAdd} if (notificationsToAdd.length) { - await r.table('Notification').insert(notificationsToAdd).run() await pg.insertInto('Notification').values(notificationsToAdd).execute() notificationsToAdd.forEach((notification) => { publish( diff --git a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts index 8205be8c520..f32aa5781a0 100644 --- a/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts +++ b/packages/server/graphql/mutations/helpers/handleTeamInviteToken.ts @@ -1,6 +1,6 @@ import {InvitationTokenError} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' import getKysely from '../../../postgres/getKysely' +import {selectNotifications} from '../../../postgres/select' const handleTeamInviteToken = async ( invitationToken: string, @@ -8,7 +8,6 @@ const handleTeamInviteToken = async ( tms: string[], notificationId?: string ) => { - const r = await getRethink() const pg = getKysely() const invitation = await pg .selectFrom('TeamInvitation') @@ -21,9 +20,12 @@ const handleTeamInviteToken = async ( if (expiresAt.getTime() < Date.now()) { // using the notification has no expiry const notification = notificationId - ? await r.table('Notification').get(notificationId).run() + ? await selectNotifications() + .where('id', '=', notificationId) + .where('userId', '=', viewerId) + .executeTakeFirst() : undefined - if (!notification || notification.userId !== viewerId) { + if (!notification) { return {error: InvitationTokenError.EXPIRED} } } diff --git a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts index e617f31810c..319c90e78fd 100644 --- a/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts +++ b/packages/server/graphql/mutations/helpers/inviteToTeamHelper.ts @@ -5,8 +5,6 @@ import {EMAIL_CORS_OPTIONS} from '../../../../client/types/cors' import makeAppURL from '../../../../client/utils/makeAppURL' import {isNotNull} from '../../../../client/utils/predicates' import appOrigin from '../../../appOrigin' -import getRethink from '../../../database/rethinkDriver' -import NotificationTeamInvitation from '../../../database/types/NotificationTeamInvitation' import getMailManager from '../../../email/getMailManager' import teamInviteEmailCreator from '../../../email/teamInviteEmailCreator' import generateUID from '../../../generateUID' @@ -34,7 +32,6 @@ const inviteToTeamHelper = async ( ) => { const {authToken, dataLoader, socketId: mutatorId} = context const viewerId = getUserId(authToken) - const r = await getRethink() const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -139,21 +136,21 @@ const inviteToTeamHelper = async ( removedSuggestedActionId = await removeSuggestedAction(viewerId, 'inviteYourTeam') } // insert notification records - const notificationsToInsert = [] as NotificationTeamInvitation[] - teamInvitationsToInsert.forEach((invitation) => { - const user = users.find((user) => user.email === invitation.email) - if (user) { - notificationsToInsert.push( - new NotificationTeamInvitation({ - userId: user.id, - invitationId: invitation.id, - teamId - }) - ) - } - }) + const notificationsToInsert = teamInvitationsToInsert + .map((invitation) => { + const user = users.find((user) => user.email === invitation.email) + if (!user) return null + return { + id: generateUID(), + type: 'TEAM_INVITATION' as const, + userId: user.id, + invitationId: invitation.id, + teamId + } + }) + .filter(isValid) + if (notificationsToInsert.length > 0) { - await r.table('Notification').insert(notificationsToInsert).run() await pg.insertInto('Notification').values(notificationsToInsert).execute() } diff --git a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts index aa64b773863..d21f0c7653b 100644 --- a/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/SlackNotifier.ts @@ -277,8 +277,7 @@ const getSlackMessageForNotification = async ( buttonText: 'See the discussion' } } else if (notification.type === 'RESPONSE_MENTIONED') { - // Notification Phase 3 do not split the responseId - const responseId = TeamPromptResponseId.split(notification.responseId) + const responseId = notification.responseId const response = await dataLoader.get('teamPromptResponses').loadNonNull(responseId) const author = await dataLoader.get('users').loadNonNull(response.userId) const title = `*${author.preferredName}* mentioned you in their response in *${meeting.name}*` @@ -288,7 +287,7 @@ const getSlackMessageForNotification = async ( utm_source: 'slack standup notification', utm_medium: 'product', utm_campaign: 'notifications', - responseId: notification.responseId + responseId: TeamPromptResponseId.join(notification.responseId) } } @@ -300,6 +299,8 @@ const getSlackMessageForNotification = async ( buttonText: 'See their response' } } else if (notification.type === 'MENTIONED') { + // This type is no longer created anywhere in the app but is still in the DB. + // We should remove this logic & the remaining DB notifications const authorName = notification.senderName ?? 'Someone' const {meetingId} = notification @@ -682,7 +683,7 @@ export const SlackNotifier = { notificationId: string, userId: string ) { - const notification = await dataLoader.get('notifications').load(notificationId) + const notification = await dataLoader.get('notifications').loadNonNull(notificationId) if ( notification.type !== 'RESPONSE_MENTIONED' && notification.type !== 'RESPONSE_REPLIED' && diff --git a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts index b1e75a9b2d5..17851cdbb78 100644 --- a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts +++ b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts @@ -1,9 +1,9 @@ -import {ASSIGNEE, MENTIONEE} from 'parabol-client/utils/constants' import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap' -import getRethink from '../../../database/rethinkDriver' -import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' +import {selectNotifications} from '../../../postgres/select' import {Task} from '../../../postgres/types' +import {TaskInvolvesNotification} from '../../../postgres/types/Notification' import {analytics} from '../../../utils/analytics/analytics' const publishChangeNotifications = async ( @@ -13,7 +13,6 @@ const publishChangeNotifications = async ( usersToIgnore: string[] ) => { const pg = getKysely() - const r = await getRethink() const changeAuthorId = `${changeUser.id}::${task.teamId}` const {entityMap: oldEntityMap, blocks: oldBlocks} = JSON.parse(oldTask.content) const {entityMap, blocks} = JSON.parse(task.content) @@ -35,16 +34,15 @@ const publishChangeNotifications = async ( // it isn't someone in a meeting !usersToIgnore.includes(userId) ) - .map( - (userId) => - new NotificationTaskInvolves({ - userId, - involvement: MENTIONEE, - taskId: task.id, - changeAuthorId, - teamId: task.teamId - }) - ) + .map((userId) => ({ + id: generateUID(), + type: 'TASK_INVOLVES' as const, + userId, + involvement: 'MENTIONEE' as TaskInvolvesNotification['involvement'], + taskId: task.id, + changeAuthorId, + teamId: task.teamId + })) mentions.forEach((mentionedUserId) => { analytics.mentionedOnTask(changeUser, mentionedUserId, task.teamId) @@ -52,15 +50,15 @@ const publishChangeNotifications = async ( // add in the assignee changes if (oldTask.userId && oldTask.userId !== task.userId) { if (task.userId && task.userId !== changeUser.id && !usersToIgnore.includes(task.userId)) { - notificationsToAdd.push( - new NotificationTaskInvolves({ - userId: task.userId, - involvement: ASSIGNEE, - taskId: task.id, - changeAuthorId, - teamId: task.teamId - }) - ) + notificationsToAdd.push({ + id: generateUID(), + type: 'TASK_INVOLVES' as const, + userId: task.userId, + involvement: 'ASSIGNEE' as const, + taskId: task.id, + changeAuthorId, + teamId: task.teamId + }) } userIdsToRemove.push(oldTask.userId) } @@ -71,21 +69,18 @@ const publishChangeNotifications = async ( const contentLen = blocks[0] ? blocks[0].text.length : 0 if (contentLen > oldContentLen && task.userId) { const maybeInvolvedUserIds = mentions.concat(task.userId) - const existingTaskNotifications = (await r - .table('Notification') - .getAll(r.args(maybeInvolvedUserIds), {index: 'userId'}) - .filter({ - taskId: task.id, - type: 'TASK_INVOLVES' - }) - .run()) as NotificationTaskInvolves[] + const existingTaskNotifications = await selectNotifications() + .where('userId', 'in', maybeInvolvedUserIds) + .where('type', '=', 'TASK_INVOLVES') + .where('taskId', '=', task.id) + .$narrowType() + .execute() notificationsToAdd.push(...existingTaskNotifications) } } // update changes in the db if (notificationsToAdd.length) { - await r.table('Notification').insert(notificationsToAdd).run() await pg.insertInto('Notification').values(notificationsToAdd).execute() } return {notificationsToAdd} diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index 3e8e7307962..c98b2955fc0 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -1,11 +1,10 @@ import {sql} from 'kysely' import fromTeamMemberId from 'parabol-client/utils/relay/fromTeamMemberId' -import getRethink from '../../../database/rethinkDriver' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' import CheckInStage from '../../../database/types/CheckInStage' import EstimateStage from '../../../database/types/EstimateStage' -import NotificationKickedOut from '../../../database/types/NotificationKickedOut' import UpdatesStage from '../../../database/types/UpdatesStage' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {selectTasks} from '../../../postgres/select' import archiveTasksForDB from '../../../safeMutations/archiveTasksForDB' @@ -25,7 +24,6 @@ const removeTeamMember = async ( dataLoader: DataLoaderWorker ) => { const {evictorUserId} = options - const r = await getRethink() const pg = getKysely() const {userId, teamId} = fromTeamMemberId(teamMemberId) // see if they were a leader, make a new guy leader so later we can reassign tasks @@ -101,9 +99,14 @@ const removeTeamMember = async ( let notificationId: string | undefined if (evictorUserId) { - const notification = new NotificationKickedOut({teamId, userId, evictorUserId}) + const notification = { + id: generateUID(), + type: 'KICKED_OUT' as const, + teamId, + userId, + evictorUserId + } notificationId = notification.id - await r.table('Notification').insert(notification).run() await pg.insertInto('Notification').values(notification).execute() } diff --git a/packages/server/graphql/mutations/moveTeamToOrg.ts b/packages/server/graphql/mutations/moveTeamToOrg.ts index 1caf980f8a6..2747ad99b65 100644 --- a/packages/server/graphql/mutations/moveTeamToOrg.ts +++ b/packages/server/graphql/mutations/moveTeamToOrg.ts @@ -1,8 +1,6 @@ import {GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString} from 'graphql' import {InvoiceItemType} from 'parabol-client/types/constEnums' import adjustUserCount from '../../billing/helpers/adjustUserCount' -import getRethink from '../../database/rethinkDriver' -import {RDatum} from '../../database/stricterR' import getKysely from '../../postgres/getKysely' import safeArchiveEmptyStarterOrganization from '../../safeMutations/safeArchiveEmptyStarterOrganization' import {Logger} from '../../utils/Logger' @@ -19,7 +17,6 @@ const moveToOrg = async ( authToken: any, dataLoader: DataLoaderWorker ) => { - const r = await getRethink() const pg = getKysely() // AUTH @@ -87,29 +84,21 @@ const moveToOrg = async ( const newToOrgUserIds = teamMemberUserIds.filter( (userId) => !existingOrgUsers.find((orgUser) => orgUser.userId === userId) ) - await Promise.all([ - r - .table('Notification') - .filter({teamId}) - .filter((notification: RDatum) => notification('orgId').default(null).ne(null)) - .update({orgId}) - .run(), - pg - .with('NotificationUpdate', (qb) => - qb - .updateTable('Notification') - .set({orgId}) - .where('teamId', '=', teamId) - .where('orgId', 'is not', null) - ) - .with('MeetingTemplateUpdate', (qb) => - qb.updateTable('MeetingTemplate').set({orgId}).where('orgId', '=', currentOrgId) - ) - .updateTable('Team') - .set(updates) - .where('id', '=', teamId) - .execute() - ]) + await pg + .with('NotificationUpdate', (qb) => + qb + .updateTable('Notification') + .set({orgId}) + .where('teamId', '=', teamId) + .where('orgId', 'is not', null) + ) + .with('MeetingTemplateUpdate', (qb) => + qb.updateTable('MeetingTemplate').set({orgId}).where('orgId', '=', currentOrgId) + ) + .updateTable('Team') + .set(updates) + .where('id', '=', teamId) + .execute() dataLoader.clearAll('teams') // if no teams remain on the org, remove it await safeArchiveEmptyStarterOrganization(currentOrgId, dataLoader) diff --git a/packages/server/graphql/mutations/setNotificationStatus.ts b/packages/server/graphql/mutations/setNotificationStatus.ts index 21647608b3e..c35988e3986 100644 --- a/packages/server/graphql/mutations/setNotificationStatus.ts +++ b/packages/server/graphql/mutations/setNotificationStatus.ts @@ -1,6 +1,5 @@ import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' import getKysely from '../../postgres/getKysely' import {getUserId} from '../../utils/authorization' import publish from '../../utils/publish' @@ -29,7 +28,6 @@ export default { {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { const pg = getKysely() - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -42,7 +40,6 @@ export default { } // RESOLUTION - await r.table('Notification').get(notificationId).update({status}).run() await pg.updateTable('Notification').set({status}).where('id', '=', notificationId).execute() // mutate dataloader cache notification.status = status diff --git a/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js b/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js index 8b3ceed96c2..80b79c68c13 100644 --- a/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js +++ b/packages/server/graphql/private/mutations/__tests__/intranetJobsQuery.test.js @@ -2,7 +2,6 @@ import mockAuthToken from '../../../../__tests__/setup/mockAuthToken' import MockDB from '../../../../__tests__/setup/MockDB' import {__anHourAgo, __now, __overADayAgo} from '../../../../__tests__/setup/mockTimes' -import getRethink from '../../../../database/rethinkDriver' import {sendBatchEmail} from '../../../../email/sendEmail' import getKysely from '../../../../postgres/getKysely' import sendBatchNotificationEmails from '../sendBatchNotificationEmails' @@ -19,8 +18,6 @@ describe('sendBatchNotificationEmails', () => { // Unfortunately, other tests are not cleaning up after themselves. Since // "sending everyone with pending notifications an email" relies on the // global DB state, there's no getting around this. - const r = await getRethink() - await r.table('Notification').delete() await sql`TRUNCATE TABLE "Notification"`.execute(getKysely()) }) diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index d2f91d82044..e830f4d95dc 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -1,4 +1,3 @@ -import getRethink from '../../../database/rethinkDriver' import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' import getKysely from '../../../postgres/getKysely' import {getUserByEmail} from '../../../postgres/queries/getUsersByEmails' @@ -51,7 +50,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( if (!userId && !email) { return {error: {message: 'Provide a userId or email'}} } - const r = await getRethink() const pg = getKysely() const user = userId ? await getUserById(userId) : email ? await getUserByEmail(email) : null @@ -84,9 +82,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .where('teamId', 'in', teamIds) .where('createdBy', '=', userIdToDelete) .execute() - await r({ - notification: r.table('Notification').getAll(userIdToDelete, {index: 'userId'}).delete() - }).run() // now postgres, after FKs are added then triggers should take care of children // TODO when we're done migrating to PG, these should have constraints that ON DELETE CASCADE diff --git a/packages/server/graphql/private/mutations/runScheduledJobs.ts b/packages/server/graphql/private/mutations/runScheduledJobs.ts index 66bf0898eea..465f13bb2df 100644 --- a/packages/server/graphql/private/mutations/runScheduledJobs.ts +++ b/packages/server/graphql/private/mutations/runScheduledJobs.ts @@ -1,10 +1,9 @@ import {Selectable} from 'kysely' import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' -import NotificationMeetingStageTimeLimitEnd from '../../../database/types/NotificationMeetingStageTimeLimitEnd' import ScheduledJobMeetingStageTimeLimit from '../../../database/types/ScheduledJobMetingStageTimeLimit' import ScheduledTeamLimitsJob from '../../../database/types/ScheduledTeamLimitsJob' import processTeamsLimitsJob from '../../../database/types/processTeamsLimitsJob' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {DB} from '../../../postgres/pg' import {Logger} from '../../../utils/Logger' @@ -29,13 +28,13 @@ const processMeetingStageTimeLimits = async ( const {teamId, facilitatorUserId} = meeting IntegrationNotifier.endTimeLimit(dataLoader, meetingId, teamId) - const notification = new NotificationMeetingStageTimeLimitEnd({ + const notification = { + id: generateUID(), + type: 'MEETING_STAGE_TIME_LIMIT_END' as const, meetingId, userId: facilitatorUserId! - }) + } const pg = getKysely() - const r = await getRethink() - await r.table('Notification').insert(notification).run() await pg.insertInto('Notification').values(notification).execute() publish(SubscriptionChannel.NOTIFICATION, facilitatorUserId!, 'MeetingStageTimeLimitPayload', { notification diff --git a/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts b/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts index d320cd90028..8df8ea6a54d 100644 --- a/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts +++ b/packages/server/graphql/private/mutations/sendBatchNotificationEmails.ts @@ -1,11 +1,10 @@ import ms from 'ms' import appOrigin from '../../../appOrigin' -import getRethink from '../../../database/rethinkDriver' -import {RValue} from '../../../database/stricterR' import AuthToken from '../../../database/types/AuthToken' import ServerEnvironment from '../../../email/ServerEnvironment' import getMailManager from '../../../email/getMailManager' import notificationSummaryCreator from '../../../email/notificationSummaryCreator' +import getKysely from '../../../postgres/getKysely' import isValid from '../../isValid' import {MutationResolvers} from '../resolverTypes' @@ -14,36 +13,27 @@ const sendBatchNotificationEmails: MutationResolvers['sendBatchNotificationEmail _args, {dataLoader} ) => { + const pg = getKysely() // RESOLUTION // Note - this may be a lot of data one day. userNotifications is an array // of all the users who have not logged in within the last 24 hours and their // associated notifications. - const r = await getRethink() const now = Date.now() const yesterday = new Date(now - ms('1d')) - const userNotificationCount = (await ( - r - .table('Notification') - // Only include notifications which occurred within the last day - .filter((row: RValue) => row('createdAt').gt(yesterday)) - .filter({status: 'UNREAD'}) - // de-dup users - .group('userId') as any - ) - .count() - .ungroup() - .map((group: RValue) => ({ - userId: group('group'), - notificationCount: group('reduction') - })) - .run()) as {userId: string; notificationCount: number}[] + const userNotificationCount = await pg + .selectFrom('Notification') + .select(({fn}) => ['userId', fn.count('id').as('notificationCount')]) + .where('createdAt', '>', yesterday) + .where('status', '=', 'UNREAD') + .groupBy('userId') + .execute() // :TODO: (jmtaber129): Filter out team invitations for users who are already on the team. // :TODO: (jmtaber129): Filter out "stage timer" notifications if the meeting has already // progressed to the next stage. const userNotificationMap = new Map( - userNotificationCount.map((value) => [value.userId, value.notificationCount]) + userNotificationCount.map((value) => [value.userId, Number(value.notificationCount)]) ) const users = (await dataLoader.get('users').loadMany([...userNotificationMap.keys()])).filter( isValid diff --git a/packages/server/graphql/private/mutations/stripeFailPayment.ts b/packages/server/graphql/private/mutations/stripeFailPayment.ts index 07b41564634..bd0354f4eb8 100644 --- a/packages/server/graphql/private/mutations/stripeFailPayment.ts +++ b/packages/server/graphql/private/mutations/stripeFailPayment.ts @@ -1,8 +1,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import Stripe from 'stripe' import terminateSubscription from '../../../billing/helpers/terminateSubscription' -import getRethink from '../../../database/rethinkDriver' -import NotificationPaymentRejected from '../../../database/types/NotificationPaymentRejected' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {isSuperUser} from '../../../utils/authorization' import publish from '../../../utils/publish' @@ -27,7 +26,6 @@ const stripeFailPayment: MutationResolvers['stripeFailPayment'] = async ( } const pg = getKysely() - const r = await getRethink() const manager = getStripeManager() // VALIDATION @@ -98,13 +96,15 @@ const stripeFailPayment: MutationResolvers['stripeFailPayment'] = async ( } const {last4, brand} = creditCard - const notifications = billingLeaderUserIds.map( - (userId) => new NotificationPaymentRejected({orgId, last4, brand, userId}) - ) + const notifications = billingLeaderUserIds.map((userId) => ({ + id: generateUID(), + type: 'PAYMENT_REJECTED' as const, + orgId, + last4: Number(last4), + brand, + userId + })) - await r({ - insert: r.table('Notification').insert(notifications) - }).run() await pg.insertInto('Notification').values(notifications).execute() notifications.forEach((notification) => { diff --git a/packages/server/graphql/public/mutations/addComment.ts b/packages/server/graphql/public/mutations/addComment.ts index 978c4981bb6..6fd9250283f 100644 --- a/packages/server/graphql/public/mutations/addComment.ts +++ b/packages/server/graphql/public/mutations/addComment.ts @@ -4,13 +4,10 @@ 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 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' @@ -56,16 +53,15 @@ const getMentionNotifications = ( // relevant page. return true }) - .map( - (mentioneeUserId) => - new NotificationDiscussionMentioned({ - userId: mentioneeUserId, - meetingId: meetingId, - authorId: viewerId, - commentId, - discussionId: discussion.id - }) - ) + .map((mentioneeUserId) => ({ + id: generateUID(), + type: 'DISCUSSION_MENTIONED' as const, + userId: mentioneeUserId, + meetingId: meetingId, + authorId: viewerId, + commentId, + discussionId: discussion.id + })) } const addComment: MutationResolvers['addComment'] = async ( @@ -74,7 +70,6 @@ const addComment: MutationResolvers['addComment'] = async ( {authToken, dataLoader, socketId: mutatorId} ) => { const pg = getKysely() - const r = await getRethink() const viewerId = getUserId(authToken) const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -123,14 +118,15 @@ const addComment: MutationResolvers['addComment'] = async ( const {userId: responseUserId} = TeamMemberId.split(discussion.discussionTopicId) if (responseUserId !== viewerId) { - const notification = new NotificationResponseReplied({ + const notification = { + id: generateUID(), + type: 'RESPONSE_REPLIED' as const, userId: responseUserId, meetingId: meetingId, authorId: viewerId, commentId - }) + } - await r.table('Notification').insert(notification).run() await pg.insertInto('Notification').values(notification).execute() IntegrationNotifier.sendNotificationToUser?.(dataLoader, notification.id, notification.userId) @@ -147,7 +143,6 @@ const addComment: MutationResolvers['addComment'] = async ( ) if (notificationsToAdd.length) { - await r.table('Notification').insert(notificationsToAdd).run() await pg.insertInto('Notification').values(notificationsToAdd).execute() notificationsToAdd.forEach((notification) => { publishNotification(notification, subOptions) diff --git a/packages/server/graphql/public/mutations/helpers/publishNotification.ts b/packages/server/graphql/public/mutations/helpers/publishNotification.ts index 2abfe2b1c0b..23b8cff7d70 100644 --- a/packages/server/graphql/public/mutations/helpers/publishNotification.ts +++ b/packages/server/graphql/public/mutations/helpers/publishNotification.ts @@ -1,8 +1,10 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import Notification from '../../../../database/types/Notification' import publish, {SubOptions} from '../../../../utils/publish' -const publishNotification = (notification: Notification, subOptions: SubOptions) => { +const publishNotification = ( + notification: {id: string; userId: string}, + subOptions: SubOptions +) => { publish( SubscriptionChannel.NOTIFICATION, notification.userId, diff --git a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts index 30591488c5f..cb928928d40 100644 --- a/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts +++ b/packages/server/graphql/public/mutations/helpers/publishTeamPromptMentions.ts @@ -1,7 +1,5 @@ import {JSONContent} from '@tiptap/core' -import TeamPromptResponseId from '../../../../../client/shared/gqlIds/TeamPromptResponseId' -import getRethink from '../../../../database/rethinkDriver' -import NotificationResponseMentioned from '../../../../database/types/NotificationResponseMentioned' +import generateUID from '../../../../generateUID' import getKysely from '../../../../postgres/getKysely' import {TeamPromptResponse} from '../../../../postgres/types' @@ -38,24 +36,16 @@ const createTeamPromptMentionNotifications = async ( return [] } - const notificationsToAdd = addedMentions.map((mention) => { - return new NotificationResponseMentioned({ - userId: mention, - // hack to turn the DB id into the GQL ID. The GDL ID should only be used in GQL resolvers, but i didn't catch this before it got built - responseId: TeamPromptResponseId.join(newResponse.id), - meetingId: newResponse.meetingId - }) - }) + const notificationsToAdd = addedMentions.map((mention) => ({ + id: generateUID(), + type: 'RESPONSE_MENTIONED' as const, + userId: mention, + responseId: newResponse.id, + meetingId: newResponse.meetingId + })) - const r = await getRethink() const pg = getKysely() - await r.table('Notification').insert(notificationsToAdd).run() - await pg - .insertInto('Notification') - .values( - notificationsToAdd.map((n) => ({...n, responseId: TeamPromptResponseId.split(n.responseId)})) - ) - .execute() + await pg.insertInto('Notification').values(notificationsToAdd).execute() return notificationsToAdd } diff --git a/packages/server/graphql/public/mutations/helpers/updateNotification.ts b/packages/server/graphql/public/mutations/helpers/updateNotification.ts index b1f14ffa3e5..58e1d2f498c 100644 --- a/packages/server/graphql/public/mutations/helpers/updateNotification.ts +++ b/packages/server/graphql/public/mutations/helpers/updateNotification.ts @@ -1,8 +1,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import Notification from '../../../../database/types/Notification' import publish, {SubOptions} from '../../../../utils/publish' -const updateNotification = (notification: Notification, subOptions: SubOptions) => { +const updateNotification = (notification: {id: string; userId: string}, subOptions: SubOptions) => { publish( SubscriptionChannel.NOTIFICATION, notification.userId, diff --git a/packages/server/graphql/public/mutations/requestToJoinDomain.ts b/packages/server/graphql/public/mutations/requestToJoinDomain.ts index e279a04156f..6e5262a10f2 100644 --- a/packages/server/graphql/public/mutations/requestToJoinDomain.ts +++ b/packages/server/graphql/public/mutations/requestToJoinDomain.ts @@ -1,6 +1,5 @@ import ms from 'ms' -import getRethink from '../../../database/rethinkDriver' -import NotificationRequestToJoinOrg from '../../../database/types/NotificationRequestToJoinOrg' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {getUserId} from '../../../utils/authorization' import getDomainFromEmail from '../../../utils/getDomainFromEmail' @@ -17,7 +16,6 @@ const requestToJoinDomain: MutationResolvers['requestToJoinDomain'] = async ( _args, {authToken, dataLoader} ) => { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId} const pg = getKysely() @@ -56,18 +54,17 @@ const requestToJoinDomain: MutationResolvers['requestToJoinDomain'] = async ( const leadTeamMembers = teamMembers.filter(({isLead}) => isLead) const leadUserIds = [...new Set(leadTeamMembers.map(({userId}) => userId))] - const notificationsToInsert = leadUserIds.map((userId) => { - return new NotificationRequestToJoinOrg({ - userId, - email: viewer.email, - name: viewer.preferredName, - picture: viewer.picture, - requestCreatedBy: viewerId, - domainJoinRequestId: insertResult.id - }) - }) + const notificationsToInsert = leadUserIds.map((userId) => ({ + id: generateUID(), + type: 'REQUEST_TO_JOIN_ORG' as const, + userId, + email: viewer.email, + name: viewer.preferredName, + picture: viewer.picture, + requestCreatedBy: viewerId, + domainJoinRequestId: insertResult.id + })) - await r.table('Notification').insert(notificationsToInsert).run() await pg.insertInto('Notification').values(notificationsToInsert).execute() notificationsToInsert.forEach((notification) => { diff --git a/packages/server/graphql/public/mutations/setOrgUserRole.ts b/packages/server/graphql/public/mutations/setOrgUserRole.ts index 14a2f67cf46..d107658d119 100644 --- a/packages/server/graphql/public/mutations/setOrgUserRole.ts +++ b/packages/server/graphql/public/mutations/setOrgUserRole.ts @@ -1,6 +1,5 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../../database/rethinkDriver' -import NotificationPromoteToBillingLeader from '../../../database/types/NotificationPromoteToBillingLeader' +import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' import {analytics} from '../../../utils/analytics/analytics' import {getUserId, isSuperUser, isUserBillingLeader} from '../../../utils/authorization' @@ -10,11 +9,16 @@ import {MutationResolvers} from '../resolverTypes' const addNotifications = async (orgId: string, userId: string) => { const pg = getKysely() - const r = await getRethink() - const promotionNotification = new NotificationPromoteToBillingLeader({orgId, userId}) - const {id: promotionNotificationId} = promotionNotification - await r.table('Notification').insert(promotionNotification).run() - await pg.insertInto('Notification').values(promotionNotification).execute() + const promotionNotificationId = generateUID() + await pg + .insertInto('Notification') + .values({ + id: promotionNotificationId, + type: 'PROMOTE_TO_BILLING_LEADER', + orgId, + userId + }) + .execute() return [promotionNotificationId] } diff --git a/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts b/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts index ff21995cf78..33620c041df 100644 --- a/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts +++ b/packages/server/graphql/public/types/AcceptTeamInvitationPayload.ts @@ -1,4 +1,3 @@ -import NotificationTeamInvitation from '../../../database/types/NotificationTeamInvitation' import {getUserId, isTeamMember} from '../../../utils/authorization' import standardError from '../../../utils/standardError' import {GQLContext} from '../../graphql' @@ -45,7 +44,9 @@ const AcceptTeamInvitationPayload: AcceptTeamInvitationPayloadResolvers = { } const teamInvitationNotifications = ( await dataLoader.get('notifications').loadMany(invitationNotificationIds) - ).filter(isValid) as NotificationTeamInvitation[] + ) + .filter(isValid) + .filter((n) => n.type === 'TEAM_INVITATION') return teamInvitationNotifications }, diff --git a/packages/server/graphql/public/types/AddedNotification.ts b/packages/server/graphql/public/types/AddedNotification.ts index 5a5406c2da2..e96176cef7c 100644 --- a/packages/server/graphql/public/types/AddedNotification.ts +++ b/packages/server/graphql/public/types/AddedNotification.ts @@ -6,7 +6,7 @@ export type AddedNotificationSource = {addedNotificationId: string} const AddedNotification: AddedNotificationResolvers = { addedNotification: async ({addedNotificationId}, _args: unknown, {dataLoader, authToken}) => { const viewerId = getUserId(authToken) - const notification = await dataLoader.get('notifications').load(addedNotificationId) + const notification = await dataLoader.get('notifications').loadNonNull(addedNotificationId) if (notification.userId !== viewerId) { throw new Error( `Viewer ID does not match notification user ID: notification ${addedNotificationId} for user ${notification.userId} published to user ${viewerId}` diff --git a/packages/server/graphql/public/types/ArchiveTeamPayload.ts b/packages/server/graphql/public/types/ArchiveTeamPayload.ts index be91d0e6765..af6079cdb75 100644 --- a/packages/server/graphql/public/types/ArchiveTeamPayload.ts +++ b/packages/server/graphql/public/types/ArchiveTeamPayload.ts @@ -1,6 +1,5 @@ -import NotificationTeamArchived from '../../../database/types/NotificationTeamArchived' import {getUserId} from '../../../utils/authorization' -import errorFilter from '../../errorFilter' +import isValid from '../../isValid' import {ArchiveTeamPayloadResolvers} from '../resolverTypes' export type ArchiveTeamPayloadSource = { @@ -16,15 +15,13 @@ const ArchiveTeamPayload: ArchiveTeamPayloadResolvers = { }, notification: async ({notificationIds}, _args, {authToken, dataLoader}) => { if (!notificationIds) return null - const notifications = (await dataLoader.get('notifications').loadMany(notificationIds)).filter( - errorFilter - ) const viewerId = getUserId(authToken) - const archivedNotification = notifications.find( - (notification) => notification.userId === viewerId - ) + const archivedNotification = (await dataLoader.get('notifications').loadMany(notificationIds)) + .filter(isValid) + .filter((notification) => notification.type === 'TEAM_ARCHIVED') + .find((notification) => notification.userId === viewerId) if (!archivedNotification) return null - return archivedNotification as NotificationTeamArchived + return archivedNotification } } diff --git a/packages/server/graphql/public/types/CreateTaskPayload.ts b/packages/server/graphql/public/types/CreateTaskPayload.ts index e18f2f6d822..45a144bf13d 100644 --- a/packages/server/graphql/public/types/CreateTaskPayload.ts +++ b/packages/server/graphql/public/types/CreateTaskPayload.ts @@ -1,6 +1,5 @@ -import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves' import {getUserId} from '../../../utils/authorization' -import errorFilter from '../../errorFilter' +import isValid from '../../isValid' import {CreateTaskPayloadResolvers} from '../resolverTypes' export type CreateTaskPayloadSource = { @@ -28,9 +27,9 @@ const CreateTaskPayload: CreateTaskPayloadResolvers = { involvementNotification: async ({notificationIds}, _args, {authToken, dataLoader}) => { if (!notificationIds) return null - const notifications = (await dataLoader.get('notifications').loadMany(notificationIds)).filter( - errorFilter - ) as NotificationTaskInvolves[] + const notifications = (await dataLoader.get('notifications').loadMany(notificationIds)) + .filter(isValid) + .filter((n) => n.type === 'TASK_INVOLVES') const viewerId = getUserId(authToken) return notifications.find((notification) => notification.userId === viewerId) || null } diff --git a/packages/server/graphql/public/types/InviteToTeamPayload.ts b/packages/server/graphql/public/types/InviteToTeamPayload.ts index 231c82f6469..50e2967f5fe 100644 --- a/packages/server/graphql/public/types/InviteToTeamPayload.ts +++ b/packages/server/graphql/public/types/InviteToTeamPayload.ts @@ -1,4 +1,4 @@ -import NotificationTeamInvitation from '../../../database/types/NotificationTeamInvitation' +import {TeamInvitationNotification} from '../../../postgres/types/Notification' import {InviteToTeamPayloadResolvers} from '../resolverTypes' export type InviteToTeamPayloadSource = { @@ -14,8 +14,10 @@ const InviteToTeamPayload: InviteToTeamPayloadResolvers = { }, teamInvitationNotification: async ({teamInvitationNotificationId}, _args, {dataLoader}) => { if (!teamInvitationNotificationId) return null - const teamInvitation = await dataLoader.get('notifications').load(teamInvitationNotificationId) - return teamInvitation as NotificationTeamInvitation + const teamInvitation = await dataLoader + .get('notifications') + .loadNonNull(teamInvitationNotificationId) + return teamInvitation } } diff --git a/packages/server/graphql/public/types/NotifyResponseMentioned.ts b/packages/server/graphql/public/types/NotifyResponseMentioned.ts index 06a3a6cd992..584abebbd2a 100644 --- a/packages/server/graphql/public/types/NotifyResponseMentioned.ts +++ b/packages/server/graphql/public/types/NotifyResponseMentioned.ts @@ -1,4 +1,3 @@ -import TeamPromptResponseId from '../../../../client/shared/gqlIds/TeamPromptResponseId' import {NotifyResponseMentionedResolvers} from '../resolverTypes' const NotifyResponseMentioned: NotifyResponseMentionedResolvers = { @@ -9,9 +8,7 @@ const NotifyResponseMentioned: NotifyResponseMentionedResolvers = { return meeting }, response: ({responseId}, _args, {dataLoader}) => { - // Hack, in a perfect world, this notification would have the numeric DB ID saved on it - const dbId = TeamPromptResponseId.split(responseId) - return dataLoader.get('teamPromptResponses').loadNonNull(dbId) + return dataLoader.get('teamPromptResponses').loadNonNull(responseId) } } diff --git a/packages/server/graphql/public/types/PokerMeetingSettings.ts b/packages/server/graphql/public/types/PokerMeetingSettings.ts index 40df579d5a2..145e1c939a4 100644 --- a/packages/server/graphql/public/types/PokerMeetingSettings.ts +++ b/packages/server/graphql/public/types/PokerMeetingSettings.ts @@ -1,4 +1,3 @@ -import MeetingTemplate from '../../../database/types/MeetingTemplate' import db from '../../../db' import {ORG_HOTNESS_FACTOR, TEAM_HOTNESS_FACTOR} from '../../../utils/getTemplateScore' import connectionFromTemplateArray from '../../queries/helpers/connectionFromTemplateArray' @@ -23,7 +22,7 @@ const PokerMeetingSettings: PokerMeetingSettingsResolvers = { const {orgId} = team const templates = await dataLoader.get('meetingTemplatesByOrgId').load(orgId) const organizationTemplates = templates.filter( - (template: MeetingTemplate) => + (template) => template.scope !== 'TEAM' && template.teamId !== teamId && template.type === 'poker' ) const scoredTemplates = await getScoredTemplates(organizationTemplates, ORG_HOTNESS_FACTOR) diff --git a/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts b/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts index dedf7714255..f5e607ed40a 100644 --- a/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts +++ b/packages/server/graphql/public/types/RemoveTeamMemberPayload.ts @@ -1,5 +1,5 @@ import nullIfEmpty from 'parabol-client/utils/nullIfEmpty' -import NotificationKickedOut from '../../../database/types/NotificationKickedOut' +import {KickedOutNotification} from '../../../postgres/types/Notification' import {getUserId} from '../../../utils/authorization' import {GQLContext} from '../../graphql' import isValid from '../../isValid' @@ -38,9 +38,11 @@ const RemoveTeamMemberPayload: RemoveTeamMemberPayloadResolvers = { kickOutNotification: async ({notificationId}, _args, {authToken, dataLoader}) => { if (!notificationId) return null const viewerId = getUserId(authToken) - const notification = await dataLoader.get('notifications').load(notificationId) + const notification = await dataLoader + .get('notifications') + .load(notificationId) if (!notification || notification.userId !== viewerId) return null - return notification as NotificationKickedOut + return notification } } diff --git a/packages/server/graphql/public/types/SetNotificationStatusPayload.ts b/packages/server/graphql/public/types/SetNotificationStatusPayload.ts index 18b287143ec..5ae28c86755 100644 --- a/packages/server/graphql/public/types/SetNotificationStatusPayload.ts +++ b/packages/server/graphql/public/types/SetNotificationStatusPayload.ts @@ -6,7 +6,7 @@ export type SetNotificationStatusPayloadSource = { const SetNotificationStatusPayload: SetNotificationStatusPayloadResolvers = { notification: ({notificationId}, _args, {dataLoader}) => { - return dataLoader.get('notifications').load(notificationId) + return dataLoader.get('notifications').loadNonNull(notificationId) } } diff --git a/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts b/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts index 3954347afdf..6ca671dc943 100644 --- a/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts +++ b/packages/server/graphql/public/types/SetOrgUserRoleSuccess.ts @@ -1,5 +1,5 @@ import {getUserId} from '../../../utils/authorization' -import errorFilter from '../../errorFilter' +import isValid from '../../isValid' import {SetOrgUserRoleSuccessResolvers} from '../resolverTypes' export type SetOrgUserRoleSuccessSource = { @@ -20,7 +20,7 @@ const SetOrgUserRoleSuccess: SetOrgUserRoleSuccessResolvers = { const viewerId = getUserId(authToken) const notifications = ( await dataLoader.get('notifications').loadMany(notificationIdsAdded) - ).filter(errorFilter) + ).filter(isValid) return notifications.filter((notification) => notification.userId === viewerId) } } diff --git a/packages/server/graphql/public/types/StripeFailPaymentPayload.ts b/packages/server/graphql/public/types/StripeFailPaymentPayload.ts index e04fa093e86..73c4c915c25 100644 --- a/packages/server/graphql/public/types/StripeFailPaymentPayload.ts +++ b/packages/server/graphql/public/types/StripeFailPaymentPayload.ts @@ -1,4 +1,4 @@ -import NotificationPaymentRejected from '../../../database/types/NotificationPaymentRejected' +import {PaymentRejectedNotification} from '../../../postgres/types/Notification' import {StripeFailPaymentPayloadResolvers} from '../resolverTypes' export type StripeFailPaymentPayloadSource = { @@ -11,8 +11,10 @@ const StripeFailPaymentPayload: StripeFailPaymentPayloadResolvers = { return dataLoader.get('organizations').loadNonNull(orgId) }, notification: async ({notificationId}, _args, {dataLoader}) => { - const notification = await dataLoader.get('notifications').load(notificationId) - return notification as NotificationPaymentRejected + const notification = await dataLoader + .get('notifications') + .loadNonNull(notificationId) + return notification } } diff --git a/packages/server/graphql/public/types/UpdateTaskPayload.ts b/packages/server/graphql/public/types/UpdateTaskPayload.ts index b9b886dc629..59e5f35fc53 100644 --- a/packages/server/graphql/public/types/UpdateTaskPayload.ts +++ b/packages/server/graphql/public/types/UpdateTaskPayload.ts @@ -1,12 +1,11 @@ -import Notification from '../../../database/types/Notification' -import NotificationTaskInvolves from '../../../database/types/NotificationTaskInvolves' +import {TaskInvolvesNotification} from '../../../postgres/types/Notification' import {getUserId} from '../../../utils/authorization' import {UpdateTaskPayloadResolvers} from '../resolverTypes' export type UpdateTaskPayloadSource = { taskId: string isPrivatized: boolean - notificationsToAdd?: NotificationTaskInvolves[] + notificationsToAdd?: TaskInvolvesNotification[] } const UpdateTaskPayload: UpdateTaskPayloadResolvers = { @@ -25,10 +24,7 @@ const UpdateTaskPayload: UpdateTaskPayloadResolvers = { addedNotification: async ({notificationsToAdd}, _args, {authToken}) => { const viewerId = getUserId(authToken) - return ( - notificationsToAdd?.find((notification: Notification) => notification.userId === viewerId) ?? - null - ) + return notificationsToAdd?.find((notification) => notification.userId === viewerId) ?? null } } diff --git a/packages/server/graphql/public/types/UpdatedNotification.ts b/packages/server/graphql/public/types/UpdatedNotification.ts index 588854c3145..708e8b8418f 100644 --- a/packages/server/graphql/public/types/UpdatedNotification.ts +++ b/packages/server/graphql/public/types/UpdatedNotification.ts @@ -6,7 +6,7 @@ export type UpdatedNotificationSource = {updatedNotificationId: string} const UpdatedNotification: UpdatedNotificationResolvers = { updatedNotification: async ({updatedNotificationId}, _args: unknown, {dataLoader, authToken}) => { const viewerId = getUserId(authToken) - const notification = await dataLoader.get('notifications').load(updatedNotificationId) + const notification = await dataLoader.get('notifications').loadNonNull(updatedNotificationId) if (notification.userId !== viewerId) { throw new Error( `Viewer ID does not match notification user ID: notification ${updatedNotificationId} published to user ${viewerId}` diff --git a/packages/server/graphql/public/types/User.ts b/packages/server/graphql/public/types/User.ts index ab06417cd2a..6cf83e0eb6d 100644 --- a/packages/server/graphql/public/types/User.ts +++ b/packages/server/graphql/public/types/User.ts @@ -12,11 +12,9 @@ import { MAX_RESULT_GROUP_SIZE } from '../../../../client/utils/constants' import groupReflections from '../../../../client/utils/smartGroup/groupReflections' -import getRethink from '../../../database/rethinkDriver' -import {RDatum} from '../../../database/stricterR' import MeetingTemplate from '../../../database/types/MeetingTemplate' import getKysely from '../../../postgres/getKysely' -import {selectTasks} from '../../../postgres/select' +import {selectNotifications, selectTasks} from '../../../postgres/select' import {getUserId, isSuperUser, isTeamMember} from '../../../utils/authorization' import getDomainFromEmail from '../../../utils/getDomainFromEmail' import getMonthlyStreak from '../../../utils/getMonthlyStreak' @@ -156,26 +154,16 @@ const User: ReqResolvers<'User'> = { return meeting }, notifications: async (_source, {first, after, types}, {authToken}) => { - const r = await getRethink() - // AUTH const userId = getUserId(authToken) - const dbAfter = after || r.maxval - // RESOLUTION + const hasTypes = types ? types.length > 0 : false // TODO consider moving the requestedFields to all queries - const nodesPlus1 = await r - .table('Notification') - .getAll(userId, {index: 'userId'}) - .orderBy(r.desc('createdAt')) - .filter((row: RDatum) => { - if (types) { - return row('createdAt') - .lt(dbAfter) - .and(r.expr(types).contains(row('type'))) - } - return row('createdAt').lt(dbAfter) - }) + const nodesPlus1 = await selectNotifications() + .where('userId', '=', userId) + .$if(hasTypes, (qb) => qb.where('type', 'in', types!)) + .$if(!!after, (qb) => qb.where('createdAt', '<', after!)) + .orderBy('createdAt desc') .limit(first + 1) - .run() + .execute() const nodes = nodesPlus1.slice(0, first) const edges = nodes.map((node) => ({ diff --git a/packages/server/graphql/types/RemoveOrgUserPayload.ts b/packages/server/graphql/types/RemoveOrgUserPayload.ts index 667e339e306..1f840c53419 100644 --- a/packages/server/graphql/types/RemoveOrgUserPayload.ts +++ b/packages/server/graphql/types/RemoveOrgUserPayload.ts @@ -1,7 +1,7 @@ import {GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' import {getUserId} from '../../utils/authorization' -import errorFilter from '../errorFilter' import {GQLContext} from '../graphql' +import isValid from '../isValid' import { resolveFilterByTeam, resolveOrganization, @@ -64,7 +64,7 @@ const RemoveOrgUserPayload = new GraphQLObjectType({ const viewerId = getUserId(authToken) const notifications = ( await dataLoader.get('notifications').loadMany(kickOutNotificationIds) - ).filter(errorFilter) + ).filter(isValid) return notifications.filter((notification) => notification.userId === viewerId) } }, diff --git a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts index 4b6db64edec..11b3d8456e6 100644 --- a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts +++ b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts @@ -165,6 +165,7 @@ export async function up() { ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "idx_Notification_userId" ON "Notification"("userId"); + CREATE INDEX IF NOT EXISTS "idx_Notification_createdAt" ON "Notification"("createdAt"); CREATE INDEX IF NOT EXISTS "idx_Notification_teamId" ON "Notification"("teamId") WHERE "teamId" IS NOT NULL; CREATE INDEX IF NOT EXISTS "idx_Notification_orgId" ON "Notification"("orgId") WHERE "orgId" IS NOT NULL; END $$; diff --git a/packages/server/postgres/types/Notification.d.ts b/packages/server/postgres/types/Notification.d.ts index cc10388b8ee..14a71b0700d 100644 --- a/packages/server/postgres/types/Notification.d.ts +++ b/packages/server/postgres/types/Notification.d.ts @@ -2,12 +2,12 @@ import type {Notification} from '../pg.d' interface BaseNotification { id: string - status: Notification['status'] + status: 'CLICKED' | 'READ' | 'UNREAD' type: Notification['type'] userId: string } -interface DiscussionMentionedNotification extends BaseNotification { +export interface DiscussionMentionedNotification extends BaseNotification { type: 'DISCUSSION_MENTIONED' meetingId: string authorId: string @@ -15,46 +15,46 @@ interface DiscussionMentionedNotification extends BaseNotification { discussionId: string } -interface KickedOutNotification extends BaseNotification { +export interface KickedOutNotification extends BaseNotification { type: 'KICKED_OUT' teamId: string evictorUserId: string } -interface MeetingStageTimeLimitEndNotification extends BaseNotification { +export interface MeetingStageTimeLimitEndNotification extends BaseNotification { type: 'MEETING_STAGE_TIME_LIMIT_END' meetingId: string } -interface MentionedNotification extends BaseNotification { +export interface MentionedNotification extends BaseNotification { type: 'MENTIONED' senderName: string | null senderPicture: string | null senderUserId: string meetingName: string meetingId: string - retroReflectionId?: string | null - retroDiscussStageIdx?: number | null + retroReflectionId: string | null + retroDiscussStageIdx: number | null } -interface PaymentRejectedNotification extends BaseNotification { +export interface PaymentRejectedNotification extends BaseNotification { type: 'PAYMENT_REJECTED' orgId: string - last4: string + last4: number brand: string } -interface PromoteToBillingLeaderNotification extends BaseNotification { +export interface PromoteToBillingLeaderNotification extends BaseNotification { type: 'PROMOTE_TO_BILLING_LEADER' orgId: string } -interface PromptToJoinOrgNotification extends BaseNotification { +export interface PromptToJoinOrgNotification extends BaseNotification { type: 'PROMPT_TO_JOIN_ORG' activeDomain: string } -interface RequestToJoinOrgNotification extends BaseNotification { +export interface RequestToJoinOrgNotification extends BaseNotification { type: 'REQUEST_TO_JOIN_ORG' domainJoinRequestId: number email: string @@ -63,20 +63,20 @@ interface RequestToJoinOrgNotification extends BaseNotification { requestCreatedBy: string } -interface ResponseMentionedNotification extends BaseNotification { +export interface ResponseMentionedNotification extends BaseNotification { type: 'RESPONSE_MENTIONED' - responseId: string + responseId: number meetingId: string } -interface ResponseRepliedNotification extends BaseNotification { +export interface ResponseRepliedNotification extends BaseNotification { type: 'RESPONSE_REPLIED' meetingId: string authorId: string commentId: string } -interface TaskInvolvesNotification extends BaseNotification { +export interface TaskInvolvesNotification extends BaseNotification { type: 'TASK_INVOLVES' changeAuthorId: string involvement: TaskInvolvement @@ -84,26 +84,26 @@ interface TaskInvolvesNotification extends BaseNotification { teamId: string } -interface TeamArchivedNotification extends BaseNotification { +export interface TeamArchivedNotification extends BaseNotification { type: 'TEAM_ARCHIVED' archivorUserId: string teamId: string } -interface TeamInvitationNotification extends BaseNotification { +export interface TeamInvitationNotification extends BaseNotification { type: 'TEAM_INVITATION' invitationId: string teamId: string } -interface TeamsLimitExceededNotification extends BaseNotification { +export interface TeamsLimitExceededNotification extends BaseNotification { type: 'TEAMS_LIMIT_EXCEEDED' orgId: string orgName: string orgPicture: string | null } -interface TeamsLimitReminderNotification extends BaseNotification { +export interface TeamsLimitReminderNotification extends BaseNotification { type: 'TEAMS_LIMIT_REMINDER' orgId: string orgName: string diff --git a/packages/server/safeMutations/acceptTeamInvitation.ts b/packages/server/safeMutations/acceptTeamInvitation.ts index ade90d9b059..d5b9a4507eb 100644 --- a/packages/server/safeMutations/acceptTeamInvitation.ts +++ b/packages/server/safeMutations/acceptTeamInvitation.ts @@ -2,7 +2,6 @@ import {sql} from 'kysely' import {InvoiceItemType} from 'parabol-client/types/constEnums' import TeamMemberId from '../../client/shared/gqlIds/TeamMemberId' import adjustUserCount from '../billing/helpers/adjustUserCount' -import getRethink from '../database/rethinkDriver' import {DataLoaderInstance} from '../dataloader/RootDataLoader' import generateUID from '../generateUID' import {DataLoaderWorker} from '../graphql/graphql' @@ -61,7 +60,6 @@ const handleFirstAcceptedInvitation = async ( } const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: DataLoaderWorker) => { - const r = await getRethink() const pg = getKysely() const {id: teamId, orgId} = team const [user, organizationUser] = await Promise.all([ @@ -70,57 +68,42 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data ]) const {email, picture, preferredName} = user const teamLeadUserIdWithNewActions = await handleFirstAcceptedInvitation(team, dataLoader) - const [invitationNotificationIds] = await Promise.all([ - r - .table('Notification') - .getAll(userId, {index: 'userId'}) - .filter({ - type: 'TEAM_INVITATION', - teamId - }) - .update( - // not really clicked, but no longer important - {status: 'CLICKED'}, - {returnChanges: true} - )('changes')('new_val')('id') - .default([]) - .run(), - pg - .with('NotificationUpdate', (qc) => - qc - .updateTable('Notification') - .set({status: 'CLICKED'}) - .where('userId', '=', userId) - .where('teamId', '=', teamId) - .where('type', '=', 'TEAM_INVITATION') - ) - .with('UserUpdate', (qc) => - qc - .updateTable('User') - .set({tms: sql`arr_append_uniq("tms", ${teamId})`}) - .where('id', '=', userId) - ) - .with('TeamInvitationUpdate', (qb) => - // redeem all invitations, otherwise if they have 2 someone could join after they've been kicked out - qb - .updateTable('TeamInvitation') - .set({acceptedAt: sql`CURRENT_TIMESTAMP`, acceptedBy: userId}) - .where('email', '=', email) - .where('teamId', '=', teamId) - ) - .insertInto('TeamMember') - .values({ - id: TeamMemberId.join(teamId, userId), - teamId, - userId, - picture, - preferredName, - email, - openDrawer: 'manageTeam' - }) - .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false})) - .execute() - ]) + const invitationNotifications = await pg + .with('TeamMemberInsert', (qc) => + qc + .insertInto('TeamMember') + .values({ + id: TeamMemberId.join(teamId, userId), + teamId, + userId, + picture, + preferredName, + email, + openDrawer: 'manageTeam' + }) + .onConflict((oc) => oc.column('id').doUpdateSet({isNotRemoved: true, isLead: false})) + ) + .with('UserUpdate', (qc) => + qc + .updateTable('User') + .set({tms: sql`arr_append_uniq("tms", ${teamId})`}) + .where('id', '=', userId) + ) + .with('TeamInvitationUpdate', (qb) => + // redeem all invitations, otherwise if they have 2 someone could join after they've been kicked out + qb + .updateTable('TeamInvitation') + .set({acceptedAt: sql`CURRENT_TIMESTAMP`, acceptedBy: userId}) + .where('email', '=', email) + .where('teamId', '=', teamId) + ) + .updateTable('Notification') + .set({status: 'CLICKED'}) + .where('userId', '=', userId) + .where('teamId', '=', teamId) + .where('type', '=', 'TEAM_INVITATION') + .returning('id') + .execute() dataLoader.clearAll(['teamMembers', 'users', 'notifications']) if (!organizationUser) { // clear the cache, adjustUserCount will mutate these @@ -133,7 +116,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data } await setUserTierForUserIds([userId]) } - + const invitationNotificationIds = invitationNotifications.map(({id}) => id) // if accepted to team, don't count it towards the global denial count await pg .deleteFrom('PushInvitation') @@ -142,7 +125,7 @@ const acceptTeamInvitation = async (team: Team, userId: string, dataLoader: Data .execute() return { teamLeadUserIdWithNewActions, - invitationNotificationIds: invitationNotificationIds as string[] + invitationNotificationIds } } diff --git a/packages/server/utils/sendPromptToJoinOrg.ts b/packages/server/utils/sendPromptToJoinOrg.ts index d97dbec630e..5c3ea988538 100644 --- a/packages/server/utils/sendPromptToJoinOrg.ts +++ b/packages/server/utils/sendPromptToJoinOrg.ts @@ -1,6 +1,5 @@ -import getRethink from '../database/rethinkDriver' -import NotificationPromptToJoinOrg from '../database/types/NotificationPromptToJoinOrg' import User from '../database/types/User' +import generateUID from '../generateUID' import {DataLoaderWorker} from '../graphql/graphql' import getKysely from '../postgres/getKysely' import getDomainFromEmail from './getDomainFromEmail' @@ -9,21 +8,21 @@ import isRequestToJoinDomainAllowed from './isRequestToJoinDomainAllowed' const sendPromptToJoinOrg = async (newUser: User, dataLoader: DataLoaderWorker) => { const {id: userId, email} = newUser const pg = getKysely() - const r = await getRethink() - const activeDomain = getDomainFromEmail(email) if (!(await isRequestToJoinDomainAllowed(activeDomain, newUser, dataLoader))) { return } - const notificationToInsert = new NotificationPromptToJoinOrg({ - userId, - activeDomain - }) - - await r.table('Notification').insert(notificationToInsert).run() - await pg.insertInto('Notification').values(notificationToInsert).execute() + await pg + .insertInto('Notification') + .values({ + id: generateUID(), + type: 'PROMPT_TO_JOIN_ORG', + userId, + activeDomain + }) + .execute() } export default sendPromptToJoinOrg From 91a8d7d2f0a7a118c1873cab5c4bd7c8dc77df5c Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 16 Oct 2024 13:13:29 -0700 Subject: [PATCH 87/89] add createdAt index --- .../postgres/migrations/1728595099540_Notification-phase1.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts index 4b6db64edec..11b3d8456e6 100644 --- a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts +++ b/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts @@ -165,6 +165,7 @@ export async function up() { ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS "idx_Notification_userId" ON "Notification"("userId"); + CREATE INDEX IF NOT EXISTS "idx_Notification_createdAt" ON "Notification"("createdAt"); CREATE INDEX IF NOT EXISTS "idx_Notification_teamId" ON "Notification"("teamId") WHERE "teamId" IS NOT NULL; CREATE INDEX IF NOT EXISTS "idx_Notification_orgId" ON "Notification"("orgId") WHERE "orgId" IS NOT NULL; END $$; From b9ed21ad9288f8bc17edc08b4faaed1a7dde7ffa Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 16 Oct 2024 13:22:55 -0700 Subject: [PATCH 88/89] fix migration name --- ...otification-phase1.ts => 1728596443080_Notification-phase1.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/server/postgres/migrations/{1728595099540_Notification-phase1.ts => 1728596443080_Notification-phase1.ts} (100%) diff --git a/packages/server/postgres/migrations/1728595099540_Notification-phase1.ts b/packages/server/postgres/migrations/1728596443080_Notification-phase1.ts similarity index 100% rename from packages/server/postgres/migrations/1728595099540_Notification-phase1.ts rename to packages/server/postgres/migrations/1728596443080_Notification-phase1.ts From 55317e1933bea9dce9b9a9deadfc5376a28ea077 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 17 Oct 2024 15:55:50 -0700 Subject: [PATCH 89/89] create meeting before linked discussions --- .../helpers/safeCreateRetrospective.ts | 14 ++++++- .../mutations/helpers/safeCreateTeamPrompt.ts | 38 ++++++++++++------- .../private/mutations/processRecurrence.ts | 16 +++----- .../public/mutations/startRetrospective.ts | 15 ++------ .../public/mutations/startTeamPrompt.ts | 14 +------ 5 files changed, 49 insertions(+), 48 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts index 8a9f2e8ef0e..58e85eb4556 100644 --- a/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeCreateRetrospective.ts @@ -1,5 +1,6 @@ import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import {MeetingTypeEnum, RetrospectiveMeeting} from '../../../postgres/types/Meeting' import {RetroMeetingPhase} from '../../../postgres/types/NewMeetingPhase' import {DataLoaderWorker} from '../../graphql' @@ -20,6 +21,7 @@ const safeCreateRetrospective = async ( }, dataLoader: DataLoaderWorker ) => { + const pg = getKysely() const {teamId, facilitatorUserId, name} = meetingSettings const meetingType: MeetingTypeEnum = 'retrospective' const [meetingCount, team] = await Promise.all([ @@ -40,7 +42,7 @@ const safeCreateRetrospective = async ( dataLoader ) - return new MeetingRetrospective({ + const meeting = new MeetingRetrospective({ id: meetingId, meetingCount, phases, @@ -48,6 +50,16 @@ const safeCreateRetrospective = async ( ...meetingSettings, name }) as RetrospectiveMeeting + try { + await pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(meeting.phases)}) + .execute() + } catch (e) { + // meeting already started + return null + } + return meeting } export default safeCreateRetrospective diff --git a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts index 7bb25288232..a123cb02c6f 100644 --- a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts @@ -15,6 +15,7 @@ const safeCreateTeamPrompt = async ( dataLoader: DataLoaderWorker, meetingOverrideProps = {} ) => { + const pg = getKysely() const meetingType: MeetingTypeEnum = 'teamPrompt' const meetingCount = await dataLoader.get('meetingCount').load({teamId, meetingType}) const meetingId = generateUID() @@ -22,20 +23,15 @@ const safeCreateTeamPrompt = async ( const teamMemberIds = teamMembers.map(({id}) => id) const teamPromptResponsesPhase = new TeamPromptResponsesPhase(teamMemberIds) const {stages: teamPromptStages} = teamPromptResponsesPhase - await getKysely() - .insertInto('Discussion') - .values( - teamPromptStages.map((stage) => ({ - id: stage.discussionId, - teamId, - meetingId, - discussionTopicId: stage.teamMemberId, - discussionTopicType: 'teamPromptResponse' - })) - ) - .execute() + const discussions = teamPromptStages.map((stage) => ({ + id: stage.discussionId, + teamId, + meetingId, + discussionTopicId: stage.teamMemberId, + discussionTopicType: 'teamPromptResponse' as const + })) primePhases([teamPromptResponsesPhase]) - return new MeetingTeamPrompt({ + const meeting = new MeetingTeamPrompt({ id: meetingId, name, teamId, @@ -45,6 +41,22 @@ const safeCreateTeamPrompt = async ( meetingPrompt: DEFAULT_PROMPT, // :TODO: (jmtaber129): Get this from meeting settings. ...meetingOverrideProps }) as TeamPromptMeeting + try { + await pg + .insertInto('NewMeeting') + .values({...meeting, phases: JSON.stringify(meeting.phases)}) + .execute() + } catch { + // can't insert, meeting already exists? + return null + } + await pg + .with('DiscussionInsert', (qb) => qb.insertInto('Discussion').values(discussions)) + .updateTable('Team') + .set({lastMeetingType: 'teamPrompt'}) + .where('id', '=', teamId) + .execute() + return meeting } export default safeCreateTeamPrompt diff --git a/packages/server/graphql/private/mutations/processRecurrence.ts b/packages/server/graphql/private/mutations/processRecurrence.ts index 737ab494653..3ea6b08b9ac 100644 --- a/packages/server/graphql/private/mutations/processRecurrence.ts +++ b/packages/server/graphql/private/mutations/processRecurrence.ts @@ -6,7 +6,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {DateTime, RRuleSet} from 'rrule-rust' import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId' import {fromDateTime, toDateTime} from '../../../../client/shared/rruleUtil' -import getKysely from '../../../postgres/getKysely' import {getActiveMeetingSeries} from '../../../postgres/queries/getActiveMeetingSeries' import {selectNewMeetings} from '../../../postgres/select' import {RetrospectiveMeeting, TeamPromptMeeting} from '../../../postgres/types/Meeting' @@ -32,7 +31,6 @@ const startRecurringMeeting = async ( dataLoader: DataLoaderWorker, subOptions: SubOptions ) => { - const pg = getKysely() const {id: meetingSeriesId, teamId, facilitatorId, meetingType} = meetingSeries // AUTH @@ -59,10 +57,9 @@ const startRecurringMeeting = async ( meetingSeriesId: meetingSeries.id, meetingPrompt: teamPromptMeeting?.meetingPrompt ?? DEFAULT_PROMPT }) - await pg - .insertInto('NewMeeting') - .values({...meeting, phases: JSON.stringify(meeting.phases)}) - .execute() + if (!meeting) { + return {error: {message: 'Unable to create meeting. Perhaps one was just created?'}} + } const data = {teamId, meetingId: meeting.id} publish(SubscriptionChannel.TEAM, teamId, 'StartTeamPromptSuccess', data, subOptions) return meeting @@ -87,10 +84,9 @@ const startRecurringMeeting = async ( }, dataLoader ) - await pg - .insertInto('NewMeeting') - .values({...meeting, phases: JSON.stringify(meeting.phases)}) - .execute() + if (!meeting) { + return {error: {message: 'Unable to create meeting. Perhaps one was just created?'}} + } const data = {teamId, meetingId: meeting.id} publish(SubscriptionChannel.TEAM, teamId, 'StartRetrospectiveSuccess', data, subOptions) return meeting diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index f45d27aae82..36b42743c63 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -67,19 +67,12 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( }, dataLoader ) - const meetingId = meeting.id - - const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) - const [newMeetingRes] = await Promise.allSettled([ - pg - .insertInto('NewMeeting') - .values({...meeting, phases: JSON.stringify(meeting.phases)}) - .execute(), - updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) - ]) - if (newMeetingRes.status === 'rejected') { + if (!meeting) { return {error: {message: 'Meeting already started'}} } + const meetingId = meeting.id + const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) + await updateMeetingTemplateLastUsedAt(selectedTemplateId, teamId) const meetingMember = createMeetingMember(meeting, { userId: viewerId, diff --git a/packages/server/graphql/public/mutations/startTeamPrompt.ts b/packages/server/graphql/public/mutations/startTeamPrompt.ts index bf0b8b32612..2dd22822eea 100644 --- a/packages/server/graphql/public/mutations/startTeamPrompt.ts +++ b/packages/server/graphql/public/mutations/startTeamPrompt.ts @@ -20,7 +20,6 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async ( {teamId, name, rrule, gcalInput}, {authToken, dataLoader, socketId: mutatorId} ) => { - const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} @@ -48,18 +47,7 @@ const startTeamPrompt: MutationResolvers['startTeamPrompt'] = async ( const meetingName = createMeetingSeriesTitle(name || 'Standup', new Date(), 'UTC') const eventName = rrule ? name || 'Standup' : meetingName const meeting = await safeCreateTeamPrompt(meetingName, teamId, viewerId, dataLoader) - - const [newMeetingRes] = await Promise.allSettled([ - pg - .with('NewMeetingInsert', (qb) => - qb.insertInto('NewMeeting').values({...meeting, phases: JSON.stringify(meeting.phases)}) - ) - .updateTable('Team') - .set({lastMeetingType: 'teamPrompt'}) - .where('id', '=', teamId) - .execute() - ]) - if (newMeetingRes.status === 'rejected') { + if (!meeting) { return {error: {message: 'Meeting already started'}} } const {id: meetingId} = meeting