From ecaeb924a61b9d0242a6800c3c7bb0aa1edbedcc Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 7 Aug 2024 16:48:50 -0700 Subject: [PATCH] write MeetingSettings to PG Signed-off-by: Matt Krick --- packages/server/database/rethinkDriver.ts | 23 ------ .../server/dataloader/customLoaderMakers.ts | 35 +++++++-- .../dataloader/primaryKeyLoaderMakers.ts | 5 ++ .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../mutations/helpers/createTeamAndLeader.ts | 6 ++ .../graphql/mutations/removePokerTemplate.ts | 15 ++-- .../mutations/removeReflectTemplate.ts | 16 ++-- .../graphql/mutations/selectTemplate.ts | 9 ++- .../graphql/mutations/updateRetroMaxVotes.ts | 10 +++ .../public/mutations/setMeetingSettings.ts | 49 +++++++++++-- .../public/mutations/startRetrospective.ts | 10 ++- .../helpers/resolveSelectedTemplate.ts | 7 ++ .../1723061869934_MeetingSettings-phase1.ts | 73 +++++++++++++++++++ packages/server/postgres/select.ts | 39 ++++++++++ packages/server/postgres/types/index.d.ts | 3 + packages/server/utils/analytics/analytics.ts | 2 +- 16 files changed, 252 insertions(+), 51 deletions(-) create mode 100644 packages/server/postgres/migrations/1723061869934_MeetingSettings-phase1.ts diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index 38e4268d917..5088452eb4b 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -3,13 +3,10 @@ import SlackAuth from '../database/types/SlackAuth' import SlackNotification from '../database/types/SlackNotification' import TeamInvitation from '../database/types/TeamInvitation' import {AnyMeeting, AnyMeetingSettings, AnyMeetingTeamMember} from '../postgres/types/Meeting' -import {ScheduledJobUnion} from '../types/custom' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' import AgendaItem from './types/AgendaItem' -import AtlassianAuth from './types/AtlassianAuth' import Comment from './types/Comment' -import FailedAuthRequest from './types/FailedAuthRequest' import Invoice from './types/Invoice' import InvoiceItemHook from './types/InvoiceItemHook' import MassInvitation from './types/MassInvitation' @@ -33,10 +30,6 @@ export type RethinkSchema = { type: AgendaItem index: 'teamId' | 'meetingId' } - AtlassianAuth: { - type: AtlassianAuth - index: 'atlassianUserId' | 'userId' | 'teamId' - } Comment: { type: Comment index: 'discussionId' @@ -45,18 +38,6 @@ export type RethinkSchema = { type: RetrospectivePrompt index: 'teamId' | 'templateId' } - EmailVerification: { - type: any - index: 'email' | 'token' - } - FailedAuthRequest: { - type: FailedAuthRequest - index: 'email' | 'ip' - } - GQLRequest: { - type: any - index: 'id' - } Invoice: { type: Invoice index: 'orgIdStartAt' @@ -112,10 +93,6 @@ export type RethinkSchema = { type: PushInvitation index: 'userId' } - ScheduledJob: { - type: ScheduledJobUnion - index: 'runAt' | 'type' - } SlackAuth: { type: SlackAuth index: 'teamId' | 'userId' diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 38e013297ef..6dd43e5141e 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -2,7 +2,7 @@ import DataLoader from 'dataloader' import tracer from 'dd-trace' import {Selectable, SqlBool, sql} from 'kysely' import {PARABOL_AI_USER_ID} from '../../client/utils/constants' -import getRethink, {RethinkSchema} from '../database/rethinkDriver' +import getRethink from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' import MeetingSettingsTeamPrompt from '../database/types/MeetingSettingsTeamPrompt' import MeetingTemplate from '../database/types/MeetingTemplate' @@ -26,8 +26,8 @@ import getLatestTaskEstimates from '../postgres/queries/getLatestTaskEstimates' import getMeetingTaskEstimates, { MeetingTaskEstimatesResult } from '../postgres/queries/getMeetingTaskEstimates' -import {selectTeams} from '../postgres/select' -import {OrganizationUser, Team} from '../postgres/types' +import {selectMeetingSettings, selectTeams} from '../postgres/select' +import {MeetingSettings, OrganizationUser, Team} from '../postgres/types' import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting' import {Logger} from '../utils/Logger' import getRedis from '../utils/getRedis' @@ -288,7 +288,7 @@ export const githubDimensionFieldMaps = (parent: RootDataLoader) => { export const meetingSettingsByType = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { dependsOn('meetingSettings') - return new DataLoader( + return new DataLoader( async (keys) => { const r = await getRethink() const types = {} as Record @@ -313,7 +313,7 @@ export const meetingSettingsByType = (parent: RootDataLoader, dependsOn: Registe const {teamId, meetingType} = key // until we decide the final shape of the team prompt settings, let's return a temporary hardcoded value if (meetingType === 'teamPrompt') { - return new MeetingSettingsTeamPrompt({teamId}) + return new MeetingSettingsTeamPrompt({teamId}) as any } return docs.find((doc) => doc.teamId === teamId && doc.meetingType === meetingType)! }) @@ -325,6 +325,31 @@ export const meetingSettingsByType = (parent: RootDataLoader, dependsOn: Registe ) } +export const _PGmeetingSettingsByType = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { + dependsOn('meetingSettings') + return new DataLoader( + async (keys) => { + const res = await selectMeetingSettings() + .where(({eb, refTuple, tuple}) => + eb( + refTuple('teamId', 'meetingType'), + 'in', + keys.map((key) => tuple(key.teamId, key.meetingType)) + ) + ) + .execute() + return keys.map( + (key) => + res.find((doc) => doc.teamId === key.teamId && doc.meetingType === key.meetingType)! + ) + }, + { + ...parent.dataLoaderOptions, + cacheKeyFn: (key) => `${key.teamId}:${key.meetingType}` + } + ) +} + export const organizationApprovedDomainsByOrgId = (parent: RootDataLoader) => { return new DataLoader( async (orgIds) => { diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 155e7f34489..a5e46c9e45b 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -6,6 +6,7 @@ import getMeetingTemplatesByIds from '../postgres/queries/getMeetingTemplatesByI import getTemplateRefsByIds from '../postgres/queries/getTemplateRefsByIds' import {getUsersByIds} from '../postgres/queries/getUsersByIds' import { + selectMeetingSettings, selectOrganizations, selectRetroReflections, selectSuggestedAction, @@ -85,3 +86,7 @@ export const templateDimensions = primaryKeyLoaderMaker((ids: readonly string[]) export const suggestedActions = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectSuggestedAction().where('id', 'in', ids).execute() }) + +export const _PGmeetingSettings = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectMeetingSettings().where('id', 'in', ids).execute() +}) diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index 5d015a2042a..a0fa13be86a 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -4,7 +4,6 @@ import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' * all rethink dataloader types which also must exist in {@link rethinkDriver/RethinkSchema} */ export const agendaItems = new RethinkPrimaryKeyLoaderMaker('AgendaItem') -export const atlassianAuths = new RethinkPrimaryKeyLoaderMaker('AtlassianAuth') export const comments = new RethinkPrimaryKeyLoaderMaker('Comment') export const reflectPrompts = new RethinkPrimaryKeyLoaderMaker('ReflectPrompt') export const massInvitations = new RethinkPrimaryKeyLoaderMaker('MassInvitation') diff --git a/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts b/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts index f9c9bdf6d73..c7e672d1c69 100644 --- a/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts +++ b/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts @@ -30,6 +30,7 @@ export default async function createTeamAndLeader( const organization = await dataLoader.get('organizations').loadNonNull(orgId) const {tier, trialStartDate} = organization const verifiedTeam = new Team({...newTeam, createdBy: userId, tier, trialStartDate}) + const meetingSettings = [ new MeetingSettingsRetrospective({teamId}), new MeetingSettingsAction({teamId}), @@ -77,6 +78,11 @@ export default async function createTeamAndLeader( .values(suggestedAction) .onConflict((oc) => oc.columns(['userId', 'type']).doNothing()) ) + .with('MeetingSettingsInsert', (qc) => + qc + .insertInto('MeetingSettings') + .values(meetingSettings.map((s) => ({...s, jiraSearchQueries: null}))) + ) .insertInto('TimelineEvent') .values(timelineEvent) .execute(), diff --git a/packages/server/graphql/mutations/removePokerTemplate.ts b/packages/server/graphql/mutations/removePokerTemplate.ts index 660b5622633..7d1b9f399a8 100644 --- a/packages/server/graphql/mutations/removePokerTemplate.ts +++ b/packages/server/graphql/mutations/removePokerTemplate.ts @@ -42,12 +42,9 @@ const removePokerTemplate = { const {teamId} = template const [templates, settings] = await Promise.all([ dataLoader.get('meetingTemplatesByType').load({meetingType: 'poker', teamId}), - r - .table('MeetingSettings') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType: 'poker'}) - .nth(0) - .run() as unknown as MeetingSettingsPoker + dataLoader + .get('meetingSettingsByType') + .load({meetingType: 'poker', teamId}) as any as MeetingSettingsPoker ]) // RESOLUTION @@ -66,6 +63,11 @@ const removePokerTemplate = { if (settings.selectedTemplateId === templateId) { const nextTemplate = templates.find((template) => template.id !== templateId) const nextTemplateId = nextTemplate?.id ?? SprintPokerDefaults.DEFAULT_TEMPLATE_ID + await getKysely() + .updateTable('MeetingSettings') + .set({selectedTemplateId: nextTemplateId}) + .where('id', '=', settingsId) + .execute() await r .table('MeetingSettings') .get(settingsId) @@ -73,6 +75,7 @@ const removePokerTemplate = { selectedTemplateId: nextTemplateId }) .run() + dataLoader.clearAll('meetingSettings') } const data = {templateId, settingsId} diff --git a/packages/server/graphql/mutations/removeReflectTemplate.ts b/packages/server/graphql/mutations/removeReflectTemplate.ts index 2a7f9bc9896..251f0f631a2 100644 --- a/packages/server/graphql/mutations/removeReflectTemplate.ts +++ b/packages/server/graphql/mutations/removeReflectTemplate.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import MeetingSettingsRetrospective from '../../database/types/MeetingSettingsRetrospective' +import getKysely from '../../postgres/getKysely' import removeMeetingTemplate from '../../postgres/queries/removeMeetingTemplate' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -41,12 +42,9 @@ const removeReflectTemplate = { const {teamId} = template const [templates, settings] = await Promise.all([ dataLoader.get('meetingTemplatesByType').load({meetingType: 'retrospective', teamId}), - r - .table('MeetingSettings') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType: 'retrospective'}) - .nth(0) - .run() as unknown as MeetingSettingsRetrospective + dataLoader + .get('meetingSettingsByType') + .load({meetingType: 'retrospective', teamId}) as any as MeetingSettingsRetrospective ]) // RESOLUTION @@ -69,6 +67,11 @@ const removeReflectTemplate = { if (settings.selectedTemplateId === templateId) { const nextTemplate = templates.find((template) => template.id !== templateId) const nextTemplateId = nextTemplate?.id ?? 'workingStuckTemplate' + await getKysely() + .updateTable('MeetingSettings') + .set({selectedTemplateId: nextTemplateId}) + .where('id', '=', settingsId) + .execute() await r .table('MeetingSettings') .get(settingsId) @@ -76,6 +79,7 @@ const removeReflectTemplate = { selectedTemplateId: nextTemplateId }) .run() + dataLoader.clearAll('meetingSettings') } const data = {templateId, settingsId} diff --git a/packages/server/graphql/mutations/selectTemplate.ts b/packages/server/graphql/mutations/selectTemplate.ts index 5e0855e6912..861f9a37545 100644 --- a/packages/server/graphql/mutations/selectTemplate.ts +++ b/packages/server/graphql/mutations/selectTemplate.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import MeetingTemplate from '../../database/types/MeetingTemplate' +import getKysely from '../../postgres/getKysely' import {Logger} from '../../utils/Logger' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -67,7 +68,13 @@ const selectTemplate = { )('changes')(0)('old_val')('id') .default(null) .run() - + await getKysely() + .updateTable('MeetingSettings') + .set({selectedTemplateId}) + .where('teamId', '=', teamId) + .where('meetingType', '=', template.type) + .returning('id') + .executeTakeFirst() // No need to check if a non-null 'meetingSettingsId' was returned - the Activity Library client // will always attempt to update the template, even if it's already selected, and we don't need // to return a 'meetingSettingsId' if no updates took place. diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index 3ec7c8d1f2c..e687a9b9bb8 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -5,6 +5,7 @@ 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' import standardError from '../../utils/standardError' @@ -136,6 +137,15 @@ const updateRetroMaxVotes = { // RESOLUTION await Promise.all([ + getKysely() + .updateTable('MeetingSettings') + .set({ + totalVotes, + maxVotesPerGroup + }) + .where('teamId', '=', teamId) + .where('meetingType', '=', 'retrospective') + .execute(), r .table('MeetingSettings') .getAll(teamId, {index: 'teamId'}) diff --git a/packages/server/graphql/public/mutations/setMeetingSettings.ts b/packages/server/graphql/public/mutations/setMeetingSettings.ts index 0b0cc44a899..92c339d1b41 100644 --- a/packages/server/graphql/public/mutations/setMeetingSettings.ts +++ b/packages/server/graphql/public/mutations/setMeetingSettings.ts @@ -2,11 +2,16 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {isNotNull} from 'parabol-client/utils/predicates' import getRethink from '../../../database/rethinkDriver' import {RValue} from '../../../database/stricterR' -import {analytics, MeetingSettings} from '../../../utils/analytics/analytics' +import getKysely from '../../../postgres/getKysely' +import {MeetingSettings} from '../../../postgres/types' +import { + analytics, + MeetingSettings as MeetingSettingsAnalytics +} from '../../../utils/analytics/analytics' import {getUserId} from '../../../utils/authorization' import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' -import {MutationResolvers} from '../resolverTypes' +import {MutationResolvers, NewMeetingPhaseTypeEnum} from '../resolverTypes' const setMeetingSettings: MutationResolvers['setMeetingSettings'] = async ( _source, @@ -19,13 +24,13 @@ const setMeetingSettings: MutationResolvers['setMeetingSettings'] = async ( // AUTH const viewerId = getUserId(authToken) - const settings = await r.table('MeetingSettings').get(settingsId).run() + const settings = (await dataLoader.get('meetingSettings').load(settingsId)) as MeetingSettings if (!settings) { return standardError(new Error('Settings not found'), {userId: viewerId}) } // RESOLUTION - const {teamId, meetingType} = settings + const {teamId, meetingType, phaseTypes} = settings const [team, viewer] = await Promise.all([ dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('users').loadNonNull(viewerId) @@ -34,7 +39,27 @@ const setMeetingSettings: MutationResolvers['setMeetingSettings'] = async ( const {featureFlags} = organization const hasTranscriptFlag = featureFlags?.includes('zoomTranscription') - const meetingSettings = {} as MeetingSettings + const meetingSettings = {} as MeetingSettingsAnalytics + const firstPhases: NewMeetingPhaseTypeEnum[] = [] + if (checkinEnabled || (checkinEnabled !== false && phaseTypes.includes('checkin'))) { + firstPhases.push('checkin') + } + if (teamHealthEnabled || (teamHealthEnabled !== false && phaseTypes.includes('TEAM_HEALTH'))) { + firstPhases.push('TEAM_HEALTH') + } + const nextSettings = { + phaseTypes: [ + ...firstPhases, + ...phaseTypes.filter((phase) => phase !== 'checkin' && phase !== 'TEAM_HEALTH') + ], + disableAnonymity: isNotNull(disableAnonymity) ? disableAnonymity : settings.disableAnonymity, + videoMeetingURL: hasTranscriptFlag + ? isNotNull(videoMeetingURL) + ? videoMeetingURL + : settings.videoMeetingURL + : null + } + await r .table('MeetingSettings') .get(settingsId) @@ -74,8 +99,20 @@ const setMeetingSettings: MutationResolvers['setMeetingSettings'] = async ( }) .run() + await getKysely() + .updateTable('MeetingSettings') + .set(nextSettings) + .where('id', '=', settings.id) + .execute() + dataLoader.clearAll('meetingSettings') + const data = {settingsId} - analytics.meetingSettingsChanged(viewer, teamId, meetingType, meetingSettings) + analytics.meetingSettingsChanged(viewer, teamId, meetingType, { + disableAnonymity: nextSettings.disableAnonymity, + videoMeetingURL: nextSettings.videoMeetingURL, + hasIcebreaker: nextSettings.phaseTypes.includes('checkin'), + hasTeamHealth: nextSettings.phaseTypes.includes('TEAM_HEALTH') + }) publish(SubscriptionChannel.TEAM, teamId, 'SetMeetingSettingsPayload', data, subOptions) return data } diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index b4c7ba3ddad..81b5a500f0d 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -24,6 +24,7 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( {authToken, socketId: mutatorId, dataLoader} ) => { const r = await getRethink() + const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const DUPLICATE_THRESHOLD = 3000 @@ -113,7 +114,13 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( .update({ videoMeetingURL: null }) - .run() + .run(), + videoMeetingURL && + pg + .updateTable('MeetingSettings') + .set({videoMeetingURL: null}) + .where('id', '=', meetingSettingsId) + .execute() ]) if (meetingSeries) { // meeting was modified if a new meeting series was created @@ -133,7 +140,6 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( dataLoader }) if (meetingSeries && gcalSeriesId) { - const pg = getKysely() await pg .updateTable('MeetingSeries') .set({gcalSeriesId}) diff --git a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts index 4f09128912e..5d556ffdf3b 100644 --- a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts +++ b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts @@ -1,6 +1,7 @@ import getRethink from '../../../database/rethinkDriver' import MeetingSettingsPoker from '../../../database/types/MeetingSettingsPoker' import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' +import getKysely from '../../../postgres/getKysely' import {GQLContext} from '../../graphql' const resolveSelectedTemplate = @@ -23,6 +24,12 @@ const resolveSelectedTemplate = .get(settingsId) .update({selectedTemplateId: fallbackTemplateId}) .run() + await getKysely() + .updateTable('MeetingSettings') + .set({selectedTemplateId: fallbackTemplateId}) + .where('id', '=', settingsId) + .execute() + return dataLoader.get('meetingTemplates').loadNonNull(fallbackTemplateId) } diff --git a/packages/server/postgres/migrations/1723061869934_MeetingSettings-phase1.ts b/packages/server/postgres/migrations/1723061869934_MeetingSettings-phase1.ts new file mode 100644 index 00000000000..d8f1741cd7d --- /dev/null +++ b/packages/server/postgres/migrations/1723061869934_MeetingSettings-phase1.ts @@ -0,0 +1,73 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'NewMeetingPhaseTypeEnum') THEN + CREATE TYPE "NewMeetingPhaseTypeEnum" AS ENUM ( + 'ESTIMATE', + 'SCOPE', + 'SUMMARY', + 'agendaitems', + 'checkin', + 'TEAM_HEALTH', + 'discuss', + 'firstcall', + 'group', + 'lastcall', + 'lobby', + 'reflect', + 'updates', + 'vote', + 'RESPONSES' + ); + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'MeetingTypeEnum') THEN + CREATE TYPE "MeetingTypeEnum" AS ENUM ( + 'action', + 'retrospective', + 'poker', + 'teamPrompt' + ); + END IF; + CREATE TABLE IF NOT EXISTS "MeetingSettings" ( + "id" VARCHAR(100) PRIMARY KEY, + "phaseTypes" "NewMeetingPhaseTypeEnum"[] NOT NULL, + "meetingType" "MeetingTypeEnum" NOT NULL, + "teamId" VARCHAR(100) NOT NULL, + "selectedTemplateId" VARCHAR(100), + "jiraSearchQueries" JSONB, + "maxVotesPerGroup" SMALLINT, + "totalVotes" SMALLINT, + "disableAnonymity" BOOLEAN, + "videoMeetingURL" VARCHAR(2056), + UNIQUE("teamId", "meetingType"), + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_selectedTemplateId" + FOREIGN KEY("selectedTemplateId") + REFERENCES "MeetingTemplate"("id") + ON DELETE SET NULL + ); + CREATE INDEX IF NOT EXISTS "idx_MeetingSettings_teamId" ON "MeetingSettings"("teamId"); + END $$; +`) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE "MeetingSettings"; + DROP TYPE "NewMeetingPhaseTypeEnum"; + DROP TYPE "MeetingTypeEnum"; + ` /* Do undo magic */) + await client.end() +} diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 53a2052c047..8c12cbf97d9 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -1,5 +1,6 @@ import type {JSONContent} from '@tiptap/core' import {NotNull, sql} from 'kysely' +import {NewMeetingPhaseTypeEnum} from '../graphql/public/resolverTypes' import getKysely from './getKysely' export const selectTimelineEvent = () => { @@ -169,3 +170,41 @@ export const selectTeamPromptResponses = () => ]) .$narrowType<{content: JSONContent}>() .select(({fn}) => [fn('to_json', ['reactjis']).as('reactjis')]) + +export type JiraSearchQuery = { + id: string + queryString: string + isJQL: boolean + projectKeyFilters?: string[] + lastUsedAt: Date +} + +export const selectMeetingSettings = () => + getKysely() + .selectFrom('MeetingSettings') + .select([ + 'id', + 'phaseTypes', + 'meetingType', + 'teamId', + 'selectedTemplateId', + 'jiraSearchQueries', + 'maxVotesPerGroup', + 'totalVotes', + 'disableAnonymity', + 'videoMeetingURL' + ]) + .$narrowType< + // NewMeeetingPhaseTypeEnum[] should be inferred from kysely-codegen, but it's not + | {meetingType: NotNull; phaseTypes: NewMeetingPhaseTypeEnum[]} + | { + meetingType: 'retrospective' + phaseTypes: NewMeetingPhaseTypeEnum[] + maxVotesPerGroup: NotNull + totalVotes: NotNull + disableAnonymity: NotNull + } + >() + .select(({fn}) => [ + fn('to_json', ['jiraSearchQueries']).as('jiraSearchQueries') + ]) diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index af90f4583a8..f5260cea94f 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -6,6 +6,7 @@ import { TeamMember as TeamMemberPG } from '../pg.d' import { + selectMeetingSettings, selectOrganizations, selectRetroReflections, selectSuggestedAction, @@ -39,3 +40,5 @@ export type TemplateScale = ExtractTypeFromQueryBuilderSelect + +export type MeetingSettings = ExtractTypeFromQueryBuilderSelect diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 6da67df7976..9424b16a716 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -91,7 +91,7 @@ export type TaskEstimateProperties = { export type MeetingSettings = { hasIcebreaker?: boolean hasTeamHealth?: boolean - disableAnonymity?: boolean + disableAnonymity?: boolean | null videoMeetingURL?: string | null }