diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 419139c591f..208d8d55836 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.48.0" + ".": "7.48.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 46dc19fd412..c9d71c846bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.48.1](https://github.com/ParabolInc/parabol/compare/v7.48.0...v7.48.1) (2024-09-27) + + +### Fixed + +* stop series when team is no more ([#10268](https://github.com/ParabolInc/parabol/issues/10268)) ([203835e](https://github.com/ParabolInc/parabol/commit/203835e4296f17f51c8d6f968b41f7fb64e820ab)) + + +### Changed + +* **rethinkdb:** NewMeeting: Phase 1a ([#10216](https://github.com/ParabolInc/parabol/issues/10216)) ([6273411](https://github.com/ParabolInc/parabol/commit/6273411f5c5e7e03dc569b3359a49902b88dc11c)) +* **rethinkdb:** NewMeeting: Phase 1b ([#10250](https://github.com/ParabolInc/parabol/issues/10250)) ([8070a7e](https://github.com/ParabolInc/parabol/commit/8070a7e82d156d7b587742f1bde8279419ea85db)) + ## [7.48.0](https://github.com/ParabolInc/parabol/compare/v7.47.5...v7.48.0) (2024-09-24) 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/package.json b/package.json index b627a19d5f8..d01cbb8008f 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.48.0", + "version": "7.48.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index f6518c68cf3..9161d0b3e7b 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.48.0", + "version": "7.48.1", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.48.0" + "parabol-server": "7.48.1" } } 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/package.json b/packages/client/package.json index cf4761c89af..1f1002e9280 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.48.0", + "version": "7.48.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" 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/package.json b/packages/embedder/package.json index 6deba5caa56..de7e56e253b 100644 --- a/packages/embedder/package.json +++ b/packages/embedder/package.json @@ -1,6 +1,6 @@ { "name": "parabol-embedder", - "version": "7.48.0", + "version": "7.48.1", "description": "A service that computes embedding vectors from Parabol objects", "author": "Jordan Husney ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/embedder#readme", 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/gql-executor/package.json b/packages/gql-executor/package.json index 3776b5aeb90..2fe0e45de08 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.48.0", + "version": "7.48.1", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -26,8 +26,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.48.0", - "parabol-server": "7.48.0", + "parabol-client": "7.48.1", + "parabol-server": "7.48.1", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index c074a1defdd..bb59e09999f 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.48.0", + "version": "7.48.1", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/__tests__/processRecurrence.test.ts b/packages/server/__tests__/processRecurrence.test.ts index bcbf2ab7fbd..86edc1b87fb 100644 --- a/packages/server/__tests__/processRecurrence.test.ts +++ b/packages/server/__tests__/processRecurrence.test.ts @@ -10,6 +10,7 @@ import ReflectPhase from '../database/types/ReflectPhase' import TeamPromptResponsesPhase from '../database/types/TeamPromptResponsesPhase' import generateUID from '../generateUID' import {insertMeetingSeries as insertMeetingSeriesQuery} from '../postgres/queries/insertMeetingSeries' +import {RetroMeetingPhase} from '../postgres/types/NewMeetingPhase' import {getUserTeams, sendIntranet, signUp} from './common' const PROCESS_RECURRENCE = ` @@ -273,7 +274,10 @@ RRULE:FREQ=WEEKLY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR,SA,SU` id: meetingId, teamId, meetingCount: 0, - phases: [new ReflectPhase(teamId, []), new DiscussPhase(undefined)], + phases: [ + new ReflectPhase(teamId, []) as RetroMeetingPhase, + new DiscussPhase(undefined) as RetroMeetingPhase + ], facilitatorUserId: userId, scheduledEndTime: new Date(Date.now() - ms('5m')), meetingSeriesId, 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/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 00ef82e4031..a809dae9d0a 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,36 @@ 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 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 +625,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 +674,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 +918,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/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/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 4ad865c40b0..dd11494d64d 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,12 +101,13 @@ 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. */ 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: MeetingAction, dataLoader: DataL 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: MeetingAction, dataLoader: DataL ) .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,12 +188,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 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}) + } const {endedAt, facilitatorStageId, phases, teamId} = meeting // VALIDATION @@ -201,7 +211,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,20 +223,35 @@ export default { {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingAction - + .run() + await pg + .updateTable('NewMeeting') + .set({ + endedAt: now, + phases: JSON.stringify(phases), + usedReactjis: JSON.stringify(insights.usedReactjis), + engagement: insights.engagement + }) + .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') { + 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), 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 @@ -248,7 +273,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 82136fa8f9d..4d2e36bfd99 100644 --- a/packages/server/graphql/mutations/endRetrospective.ts +++ b/packages/server/graphql/mutations/endRetrospective.ts @@ -1,6 +1,4 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' -import getRethink from '../../database/rethinkDriver' -import MeetingRetrospective from '../../database/types/MeetingRetrospective' import {getUserId, isTeamMember} from '../../utils/authorization' import standardError from '../../utils/standardError' import {GQLContext} from '../graphql' @@ -17,18 +15,16 @@ 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()) as MeetingRetrospective | null + 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}) + } const {endedAt, teamId} = meeting // VALIDATION diff --git a/packages/server/graphql/mutations/endSprintPoker.ts b/packages/server/graphql/mutations/endSprintPoker.ts index 00db314a2ab..f057e37a57c 100644 --- a/packages/server/graphql/mutations/endSprintPoker.ts +++ b/packages/server/graphql/mutations/endSprintPoker.ts @@ -1,11 +1,10 @@ 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' 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 +41,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 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}) + } const {endedAt, facilitatorStageId, phases, teamId} = meeting // VALIDATION @@ -82,7 +80,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,18 +94,34 @@ export default { {returnChanges: true, nonAtomic: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingPoker + .run() + await getKysely() + .updateTable('NewMeeting') + .set({ + endedAt: sql`CURRENT_TIMESTAMP`, + phases: JSON.stringify(phases), + commentCount, + storyCount, + usedReactjis: JSON.stringify(insights.usedReactjis), + engagement: insights.engagement + }) + .where('id', '=', meetingId) + .executeTakeFirst() + dataLoader.clearAll('newMeetings') 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), 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/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/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index 735ba4be831..b7a34bc7b6f 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') @@ -47,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/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/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/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..bc4aa19f9a4 100644 --- a/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts +++ b/packages/server/graphql/mutations/helpers/endMeeting/sendNewMeetingSummary.ts @@ -1,23 +1,31 @@ +import {sql} from 'kysely' import getRethink from '../../../../database/rethinkDriver' -import Meeting from '../../../../database/types/Meeting' 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' export default async function sendNewMeetingSummary( - newMeeting: Meeting, + newMeeting: AnyMeeting, context: Pick ) { 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/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..df1d1ea0427 100644 --- a/packages/server/graphql/mutations/helpers/generateGroups.ts +++ b/packages/server/graphql/mutations/helpers/generateGroups.ts @@ -1,7 +1,7 @@ import {SubscriptionChannel} from '../../../../client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import {AutogroupReflectionGroupType} from '../../../database/types/MeetingRetrospective' -import {RetroReflection} from '../../../postgres/types' +import getKysely from '../../../postgres/getKysely' +import {AutogroupReflectionGroupType, RetroReflection} from '../../../postgres/types' import {Logger} from '../../../utils/Logger' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import {analytics} from '../../../utils/analytics/analytics' @@ -54,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/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..5a88154ce8b 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() @@ -63,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) @@ -72,7 +76,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 +120,7 @@ const handleCompletedStage = async ( dataLoader: DataLoaderWorker ) => { if (meeting.meetingType === 'retrospective') { - return handleCompletedRetrospectiveStage(stage, meeting as MeetingRetrospective, dataLoader) + return handleCompletedRetrospectiveStage(stage, meeting, dataLoader) } return {} } 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/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..d38ed8dffc4 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,16 @@ 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..891895d7a71 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'} @@ -150,7 +152,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/removeEmptyTasks.ts b/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts index c09acc2e2a9..f04bed65ac3 100644 --- a/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts +++ b/packages/server/graphql/mutations/helpers/removeEmptyTasks.ts @@ -1,9 +1,8 @@ import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString' import getRethink from '../../../database/rethinkDriver' -const removeEmptyTasks = async (meetingId: string) => { +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 64f1dda66a0..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 @@ -40,11 +42,16 @@ 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) } } }) + 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/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..7bb25288232 100644 --- a/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeCreateTeamPrompt.ts @@ -1,9 +1,8 @@ -import {ParabolR} from '../../../database/rethinkDriver' 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' @@ -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) @@ -52,7 +44,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..6a0b1e21689 100644 --- a/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts +++ b/packages/server/graphql/mutations/helpers/safeEndRetrospective.ts @@ -1,12 +1,13 @@ +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' 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 +34,15 @@ 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 pg = getKysely() 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,24 +50,39 @@ 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 = ( 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, @@ -76,7 +93,7 @@ const summarizeRetroMeeting = async (meeting: MeetingRetrospective, 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) @@ -93,7 +110,7 @@ const safeEndRetrospective = async ({ context, now }: { - meeting: MeetingRetrospective + meeting: RetrospectiveMeeting context: InternalContext now: Date }) => { @@ -115,7 +132,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,21 +144,35 @@ const safeEndRetrospective = async ({ {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingRetrospective - + .run() + await getKysely() + .updateTable('NewMeeting') + .set({ + endedAt: sql`CURRENT_TIMESTAMP`, + phases: JSON.stringify(phases), + usedReactjis: JSON.stringify(insights.usedReactjis), + engagement: insights.engagement + }) + .where('id', '=', meetingId) + .executeTakeFirst() + dataLoader.clearAll('newMeetings') 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([ 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 e88ed41daf1..33364edf2fb 100644 --- a/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts +++ b/packages/server/graphql/mutations/helpers/safeEndTeamPrompt.ts @@ -1,10 +1,11 @@ +import {sql} from 'kysely' 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,12 +18,13 @@ 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 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: MeetingTeamPrompt, 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) @@ -51,13 +53,14 @@ const safeEndTeamPrompt = async ({ context, subOptions }: { - meeting: MeetingTeamPrompt + meeting: TeamPromptMeeting now: Date viewerId?: string r: ParabolR context: InternalContext subOptions: SubOptions }) => { + const pg = getKysely() const {dataLoader} = context const {endedAt, id: meetingId, teamId} = meeting @@ -66,7 +69,15 @@ const safeEndTeamPrompt = async ({ // RESOLUTION const insights = await gatherInsights(meeting, dataLoader) - const completedTeamPrompt = (await r + 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) .update( @@ -77,7 +88,7 @@ const safeEndTeamPrompt = async ({ {returnChanges: true} )('changes')(0)('new_val') .default(null) - .run()) as unknown as MeetingTeamPrompt + .run() if (!completedTeamPrompt) { return standardError(new Error('Completed team prompt meeting does not exist'), { @@ -85,6 +96,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), @@ -103,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/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..e830e298608 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -5,9 +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 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 +12,8 @@ import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStag 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' @@ -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 return new RetroMeetingMember({ teamId, userId, @@ -92,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) @@ -161,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/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..5dafb36786b 100644 --- a/packages/server/graphql/mutations/pokerResetDimension.ts +++ b/packages/server/graphql/mutations/pokerResetDimension.ts @@ -1,9 +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 MeetingPoker from '../../database/types/MeetingPoker' 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' @@ -28,15 +29,19 @@ 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} //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'}} @@ -63,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'}} } @@ -75,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 c640ae4964b..1c0f22fc51d 100644 --- a/packages/server/graphql/mutations/pokerRevealVotes.ts +++ b/packages/server/graphql/mutations/pokerRevealVotes.ts @@ -1,10 +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 MeetingPoker from '../../database/types/MeetingPoker' 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' @@ -27,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} @@ -36,12 +38,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'}} @@ -65,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'}} } @@ -91,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 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 259a838413a..6985db0c274 100644 --- a/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts +++ b/packages/server/graphql/mutations/resetRetroMeetingToGroupStage.ts @@ -4,8 +4,8 @@ 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 {RetroMeetingPhase} from '../../postgres/types/NewMeetingPhase' import {getUserId} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -34,7 +34,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) if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) const {createdBy, facilitatorUserId, phases, meetingType} = meeting if (meetingType !== 'retrospective') { @@ -91,7 +91,7 @@ const resetRetroMeetingToGroupStage = { default: throw new Error(`Unhandled phaseType: ${phase.phaseType}`) } - }) + }) as RetroMeetingPhase[] primePhases(newPhases, resetToPhaseIndex) meeting.phases = newPhases @@ -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/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..abb727757e7 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' @@ -96,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} @@ -115,15 +117,9 @@ 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( + const phases = await createNewMeetingPhases( viewerId, teamId, meetingId, @@ -149,14 +145,18 @@ export default { facilitatorUserId: viewerId, templateId: selectedTemplateId, templateRefId - }) + }) 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/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/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 823a6381383..f104dc07282 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' @@ -43,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) @@ -56,12 +56,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 +72,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') @@ -159,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) @@ -171,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 db5c0beca24..329ce6646fd 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -4,7 +4,6 @@ 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 {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -44,12 +43,15 @@ const updateRetroMaxVotes = { const subOptions = {mutatorId, operationId} //AUTH - const meeting = (await r.table('NewMeeting').get(meetingId).run()) as MeetingRetrospective + const meeting = await dataLoader.get('newMeetings').load(meetingId) if (!meeting) { return {error: {message: 'Meeting not found'}} } + if (meeting.meetingType !== 'retrospective') { + return {error: {message: `Meeting not retrospective`}} + } const { endedAt, meetingType, @@ -138,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, @@ -155,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 76330da4d8a..79e4012c8a4 100644 --- a/packages/server/graphql/mutations/voteForPokerStory.ts +++ b/packages/server/graphql/mutations/voteForPokerStory.ts @@ -3,8 +3,9 @@ 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 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' @@ -12,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( @@ -24,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({ @@ -71,20 +119,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..6afe994dee6 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 MeetingRetrospective from '../../database/types/MeetingRetrospective' 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 MeetingRetrospective + const meeting = await dataLoader.get('newMeetings').load(meetingId) + 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 8dbb9b5a071..0d5d21ede25 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' @@ -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 MeetingRetrospective[] + .run() 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 @@ -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 @@ -168,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 32670a43866..6d0b2099e3c 100644 --- a/packages/server/graphql/private/mutations/processRecurrence.ts +++ b/packages/server/graphql/private/mutations/processRecurrence.ts @@ -3,13 +3,12 @@ import tracer from 'dd-trace' 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 MeetingRetrospective, { - isMeetingRetrospective -} from '../../../database/types/MeetingRetrospective' -import MeetingTeamPrompt, {isMeetingTeamPrompt} from '../../../database/types/MeetingTeamPrompt' +import getKysely from '../../../postgres/getKysely' 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' @@ -23,6 +22,7 @@ import safeCreateRetrospective from '../../mutations/helpers/safeCreateRetrospec import safeCreateTeamPrompt, {DEFAULT_PROMPT} from '../../mutations/helpers/safeCreateTeamPrompt' import safeEndRetrospective from '../../mutations/helpers/safeEndRetrospective' import safeEndTeamPrompt from '../../mutations/helpers/safeEndTeamPrompt' +import {stopMeetingSeries} from '../../public/mutations/updateRecurrenceSettings' import {MutationResolvers} from '../resolverTypes' const startRecurringMeeting = async ( @@ -31,6 +31,7 @@ const startRecurringMeeting = async ( dataLoader: DataLoaderWorker, subOptions: SubOptions ) => { + const pg = getKysely() const r = await getRethink() const {id: meetingSeriesId, teamId, facilitatorId, meetingType} = meetingSeries @@ -52,26 +53,23 @@ 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 meeting = await safeCreateTeamPrompt( - meetingName, - teamId, - facilitatorId, - r, - dataLoader, - { - scheduledEndTime, - meetingSeriesId: meetingSeries.id, - meetingPrompt: teamPromptMeeting?.meetingPrompt ?? DEFAULT_PROMPT - } - ) + const teamPromptMeeting = lastMeeting as TeamPromptMeeting | null + 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) return meeting } else if (meetingSeries.meetingType === 'retrospective') { const {totalVotes, maxVotesPerGroup, disableAnonymity, templateId} = - (lastMeeting as MeetingRetrospective) ?? { + (lastMeeting as RetrospectiveMeeting) ?? { templateId: meetingSettings.selectedTemplateId, ...meetingSettings } @@ -91,6 +89,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 @@ -130,9 +132,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'), { @@ -156,14 +158,22 @@ const processRecurrence: MutationResolvers['processRecurrence'] = async ( await tracer.trace('processRecurrence.startActiveMeetingSeries', async () => Promise.allSettled( activeMeetingSeries.map(async (meetingSeries) => { - const seriesTeam = await dataLoader.get('teams').loadNonNull(meetingSeries.teamId) - if (seriesTeam.isArchived || !seriesTeam.isPaid) { + const {teamId, id: meetingSeriesId, recurrenceRule, facilitatorId} = meetingSeries + const teamMemberId = TeamMemberId.join(teamId, facilitatorId) + const [seriesTeam, facilitatorTeamMember] = await Promise.all([ + dataLoader.get('teams').loadNonNull(teamId), + dataLoader.get('teamMembers').loadNonNull(teamMemberId) + ]) + if (seriesTeam.isArchived || !facilitatorTeamMember.isNotRemoved) { + return await stopMeetingSeries(meetingSeries) + } + if (!seriesTeam.isPaid) { return } const [seriesOrg, lastMeeting] = await Promise.all([ dataLoader.get('organizations').loadNonNull(seriesTeam.orgId), - dataLoader.get('lastMeetingByMeetingSeriesId').load(meetingSeries.id) + dataLoader.get('lastMeetingByMeetingSeriesId').load(meetingSeriesId) ]) if (seriesOrg.lockedAt) { @@ -172,7 +182,7 @@ const processRecurrence: MutationResolvers['processRecurrence'] = async ( // For meetings that should still be active, start the meeting and set its end time. // Any subscriptions are handled by the shared meeting start code - const rrule = RRuleSet.parse(meetingSeries.recurrenceRule) + const rrule = RRuleSet.parse(recurrenceRule) // Only get meetings that should currently be active, i.e. meetings that should have started // within the last 24 hours, started after the last meeting in the series, and started before @@ -188,7 +198,7 @@ const processRecurrence: MutationResolvers['processRecurrence'] = async ( ) for (const startTime of newMeetingsStartTimes) { const err = await tracer.trace('startRecurringMeeting', async (span) => { - span?.addTags({meetingSeriesId: meetingSeries.id}) + span?.addTags({meetingSeriesId}) return startRecurringMeeting( meetingSeries, fromDateTime(startTime.toString(), rrule.tzid).toDate(), 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..ad29fab70d5 100644 --- a/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts +++ b/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts @@ -1,4 +1,4 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import isValid from '../../isValid' import {GenerateMeetingSummarySuccessResolvers} from '../resolverTypes' export type GenerateMeetingSummarySuccessSource = { @@ -7,10 +7,9 @@ export type GenerateMeetingSummarySuccessSource = { const GenerateMeetingSummarySuccess: GenerateMeetingSummarySuccessResolvers = { meetings: async ({meetingIds}, _args, {dataLoader}) => { - const meetings = (await dataLoader - .get('newMeetings') - .loadMany(meetingIds)) as MeetingRetrospective[] - return meetings + 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 6862a934f00..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 MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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 MeetingRetrospective + 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/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/endTeamPrompt.ts b/packages/server/graphql/public/mutations/endTeamPrompt.ts index 692ee81d339..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 MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' 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 MeetingTeamPrompt | 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 c80339a30f9..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 MeetingRetrospective from '../../../../database/types/MeetingRetrospective' 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 MeetingRetrospective[] + .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 4cb74174c5b..47782e177f9 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -1,6 +1,5 @@ import yaml from 'js-yaml' import getRethink from '../../../../database/rethinkDriver' -import MeetingRetrospective from '../../../../database/types/MeetingRetrospective' import getKysely from '../../../../postgres/getKysely' import OpenAIServerManager from '../../../../utils/OpenAIServerManager' import sendToSentry from '../../../../utils/sendToSentry' @@ -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 MeetingRetrospective + 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/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 c52fc4361a3..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,7 +77,12 @@ const resetReflectionGroups: MutationResolvers['resetReflectionGroups'] = async .get(meetingId) .replace(r.row.without('resetReflectionGroups') as any) .run() - meeting.resetReflectionGroups = undefined + await pg + .updateTable('NewMeeting') + .set({resetReflectionGroups: null}) + .where('id', '=', meetingId) + .execute() + 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/revealTeamHealthVotes.ts b/packages/server/graphql/public/mutations/revealTeamHealthVotes.ts index ba88c567f35..86bbbb99744 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} @@ -41,7 +44,10 @@ const revealTeamHealthVotes: MutationResolvers['revealTeamHealthVotes'] = async // VALIDATION const teamHealthPhase = getPhase(phases, 'TEAM_HEALTH') const {stages} = teamHealthPhase - const stage = stages.find((stage) => stage.id === stageId) + 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'}} } @@ -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 d186c7940af..68198ba59cf 100644 --- a/packages/server/graphql/public/mutations/setTeamHealthVote.ts +++ b/packages/server/graphql/public/mutations/setTeamHealthVote.ts @@ -3,12 +3,41 @@ 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) => { + 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, '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() + }) 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..3714e6a2042 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' @@ -21,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() @@ -39,16 +41,10 @@ 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( + const phases = await createNewMeetingPhases( viewerId, teamId, meetingId, @@ -64,9 +60,12 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( meetingCount, 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) => { @@ -78,6 +77,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/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/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/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/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 a0be265f8de..de34566210a 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 getKysely from '../../../postgres/getKysely' import {getUserId, isTeamMember} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import publish from '../../../utils/publish' @@ -12,12 +12,14 @@ 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() const subOptions = {mutatorId, operationId} - const meeting = (await dataLoader.get('newMeetings').load(meetingId)) as MeetingRetrospective + 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}) } @@ -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 a41aea04e18..2692015636f 100644 --- a/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts +++ b/packages/server/graphql/public/mutations/updateRecurrenceSettings.ts @@ -1,11 +1,12 @@ 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' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' import {MeetingSeries} from '../../../postgres/types/MeetingSeries' import {analytics} from '../../../utils/analytics/analytics' @@ -22,7 +23,7 @@ export const startNewMeetingSeries = async ( teamId: string meetingType: MeetingTypeEnum name: string - facilitatorUserId: string + facilitatorUserId: string | null }, recurrenceRule: RRuleSet, meetingSeriesName?: string | null @@ -34,8 +35,11 @@ export const startNewMeetingSeries = async ( name: meetingName, facilitatorUserId: facilitatorId } = meeting + const pg = getKysely() 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 @@ -56,7 +60,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 @@ -64,6 +72,7 @@ export const startNewMeetingSeries = async ( } const updateMeetingSeries = async (meetingSeries: MeetingSeries, newRecurrenceRule: RRuleSet) => { + const pg = getKysely() const r = await getRethink() const {id: meetingSeriesId} = meetingSeries @@ -76,23 +85,42 @@ 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) => { +export const stopMeetingSeries = async (meetingSeries: MeetingSeries) => { + const pg = getKysely() const r = await getRethink() - const now = new Date() - - await updateMeetingSeriesQuery({cancelledAt: now}, meetingSeries.id) + 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 r .table('NewMeeting') .getAll(meetingSeries.id, {index: 'meetingSeriesId'}) @@ -117,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} @@ -162,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( @@ -178,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/graphql/public/types/AddTranscriptionBotSuccess.ts b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts index 880fb7e8538..44839c75df1 100644 --- a/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts +++ b/packages/server/graphql/public/types/AddTranscriptionBotSuccess.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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 MeetingRetrospective + 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 9c62aced1b3..1c86eb2efce 100644 --- a/packages/server/graphql/public/types/AutogroupSuccess.ts +++ b/packages/server/graphql/public/types/AutogroupSuccess.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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 MeetingRetrospective + if (meeting.meetingType !== 'retrospective') throw new Error('Not a retrospective meeting') + return meeting } } 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..a15db891039 100644 --- a/packages/server/graphql/public/types/GenerateGroupsSuccess.ts +++ b/packages/server/graphql/public/types/GenerateGroupsSuccess.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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 MeetingRetrospective + 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 ac099aa2295..a22c5a7e1cd 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 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 MeetingRetrospective[] - 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/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..e11232d3cc6 100644 --- a/packages/server/graphql/public/types/ReflectPhase.ts +++ b/packages/server/graphql/public/types/ReflectPhase.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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 MeetingRetrospective + 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 e15430efc9d..be7d98e9b8c 100644 --- a/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts +++ b/packages/server/graphql/public/types/ResetReflectionGroupsSuccess.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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 MeetingRetrospective + 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 cd98f071fb3..3d4aa611c20 100644 --- a/packages/server/graphql/public/types/RetroDiscussStage.ts +++ b/packages/server/graphql/public/types/RetroDiscussStage.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import ReflectionGroup from '../../../database/types/ReflectionGroup' import {RetroDiscussStageResolvers} from '../resolverTypes' @@ -27,7 +26,8 @@ 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) + 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 80c967c6797..1443aba8d35 100644 --- a/packages/server/graphql/public/types/RetroReflection.ts +++ b/packages/server/graphql/public/types/RetroReflection.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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 MeetingRetrospective + 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 2c0834b7635..c5435b3a00e 100644 --- a/packages/server/graphql/public/types/RetroReflectionGroup.ts +++ b/packages/server/graphql/public/types/RetroReflectionGroup.ts @@ -1,5 +1,4 @@ import {Selectable} from 'kysely' -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import {RetroReflectionGroup as TRetroReflectionGroup} from '../../../postgres/pg' 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 MeetingRetrospective + 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/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..261edb42fec 100644 --- a/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts +++ b/packages/server/graphql/public/types/StartRetrospectiveSuccess.ts @@ -1,4 +1,3 @@ -import MeetingRetrospective from '../../../database/types/MeetingRetrospective' 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/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..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 MeetingTeamPrompt from '../../../database/types/MeetingTeamPrompt' import {getTeamPromptResponsesByMeetingId} from '../../../postgres/queries/getTeamPromptResponsesByMeetingIds' +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 MeetingTeamPrompt + 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 MeetingTeamPrompt + return meetings[0] as TeamPromptMeetingSource }, 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/package.json b/packages/server/package.json index 6591639beee..fdf9c945831 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.48.0", + "version": "7.48.1", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -122,7 +122,7 @@ "oauth-1.0a": "^2.2.6", "openai": "^4.53.0", "oy-vey": "^0.12.1", - "parabol-client": "7.48.0", + "parabol-client": "7.48.1", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts b/packages/server/postgres/migrations/1726174453131_NewMeeting-phase1.ts new file mode 100644 index 00000000000..87318cabb7a --- /dev/null +++ b/packages/server/postgres/migrations/1726174453131_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) NOT NULL, + "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 CASCADE, + CONSTRAINT "fk_meetingSeriesId" + FOREIGN KEY("meetingSeriesId") + REFERENCES "MeetingSeries"("id") + ON DELETE SET NULL, + CONSTRAINT "fk_templateId" + FOREIGN KEY("templateId") + REFERENCES "MeetingTemplate"("id") + 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"); + 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; + 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() +} 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..5bdf1e5a27d --- /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 createdAt + IF EXISTS ( + SELECT 1 FROM "NewMeeting" + WHERE "teamId" = NEW."teamId" + AND "meetingType" = NEW."meetingType" + 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; + -- If no conflict, allow the insert + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + 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(); + `.execute(pg) +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`DROP TRIGGER IF EXISTS "check_meeting_overlap" ON "NewMeeting";`.execute(pg) +} 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/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..1e9f965b50c 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} from '../../postgres/types/Meeting' export const createMeetingProperties = ( - meeting: Meeting, + meeting: AnyMeeting, meetingMembers?: MeetingMember[], template?: MeetingTemplate ) => { @@ -28,8 +27,6 @@ export const createMeetingProperties = ( meetingTemplateCategory: template?.mainCategory, meetingSeriesId: meeting.meetingSeriesId, disableAnonymity: - meetingType === 'retrospective' - ? (meeting as MeetingRetrospective).disableAnonymity ?? false - : undefined + meetingType === 'retrospective' ? meeting.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