From ecaeb924a61b9d0242a6800c3c7bb0aa1edbedcc Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 7 Aug 2024 16:48:50 -0700 Subject: [PATCH 1/9] 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 } From 7702035dd3a519c131ac8c039acd87ac527f8467 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 7 Aug 2024 17:19:03 -0700 Subject: [PATCH 2/9] migrate existing records Signed-off-by: Matt Krick --- .../1723074637531_MeetingSettings-phase2.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 packages/server/postgres/migrations/1723074637531_MeetingSettings-phase2.ts diff --git a/packages/server/postgres/migrations/1723074637531_MeetingSettings-phase2.ts b/packages/server/postgres/migrations/1723074637531_MeetingSettings-phase2.ts new file mode 100644 index 00000000000..1bb1e99f0a0 --- /dev/null +++ b/packages/server/postgres/migrations/1723074637531_MeetingSettings-phase2.ts @@ -0,0 +1,102 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + const MAX_PG_PARAMS = 65545 + + const PG_COLS = [ + 'id', + 'phaseTypes', + 'meetingType', + 'teamId', + 'selectedTemplateId', + 'jiraSearchQueries', + 'maxVotesPerGroup', + 'totalVotes', + 'disableAnonymity', + 'videoMeetingURL' + ] as const + type MeetingSettings = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + const startAt = new Date() + let curId = r.minval + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, curId) + let rawRowsToInsert = (await r + .table('MeetingSettings') + .between(curId, r.maxval, { + index: 'id', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'id'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as MeetingSettings[] + if (rawRowsToInsert.length === 0) { + // since we don't have a createdAt, it's possible new rows were created while this was running. + // Grab those new teams & get their settings, too + const newTeams = await pg + .selectFrom('Team') + .select('id') + .where('createdAt', '>', startAt) + .execute() + const newTeamIds = newTeams.map((team) => team.id) + console.log('got new TeamIds!', newTeamIds) + if (newTeamIds.length === 0) break + rawRowsToInsert = (await r + .table('MeetingSettings') + .getAll(r.args(newTeamIds)) + .pluck(...PG_COLS) + .run()) as MeetingSettings[] + } + const rowsToInsert = rawRowsToInsert.map((row) => ({ + ...row + })) + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curId = lastRow.id + await Promise.all( + rowsToInsert.map(async (row) => { + try { + await pg + .insertInto('MeetingSettings') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_teamId') { + // console.log(`Skipping ${row.id} because it has no team`) + return + } + if (e.constraint === 'fk_selectedTemplateId') { + // console.log(`Skipping ${row.id} because it has no template`) + return + } + console.log(e, row) + } + }) + ) + } +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "MeetingSettings" CASCADE`.execute(pg) +} From d412307b0a8370b6f9ccd2a38f31d513fbea394b Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 7 Aug 2024 18:14:54 -0700 Subject: [PATCH 3/9] remove MeetingSettings from rethinkdb Signed-off-by: Matt Krick --- codegen.json | 7 +- packages/client/modules/demo/initDB.ts | 3 +- packages/server/database/rethinkDriver.ts | 6 +- .../server/database/types/MeetingSettings.ts | 25 ------ .../database/types/MeetingSettingsAction.ts | 20 ----- .../database/types/MeetingSettingsPoker.ts | 23 ----- .../types/MeetingSettingsRetrospective.ts | 49 ----------- .../types/MeetingSettingsTeamPrompt.ts | 14 --- .../server/dataloader/customLoaderMakers.ts | 40 --------- .../dataloader/primaryKeyLoaderMakers.ts | 2 +- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../mutations/helpers/createTeamAndLeader.ts | 43 +++++---- .../graphql/mutations/removePokerTemplate.ts | 87 ------------------- .../mutations/removeReflectTemplate.ts | 12 +-- .../graphql/mutations/selectTemplate.ts | 18 +--- .../graphql/mutations/startSprintPoker.ts | 6 +- .../graphql/mutations/updateRetroMaxVotes.ts | 9 -- .../private/mutations/processRecurrence.ts | 3 +- .../public/mutations/removePokerTemplate.ts | 64 ++++++++++++++ .../public/mutations/setMeetingSettings.ts | 48 +--------- .../public/mutations/startRetrospective.ts | 18 +--- .../public/types/PokerMeetingSettings.ts | 2 +- .../types/RemovePokerTemplatePayload.ts | 25 ++++++ .../helpers/resolveSelectedTemplate.ts | 29 +++---- packages/server/graphql/rootMutation.ts | 2 - .../graphql/types/PokerMeetingSettings.ts | 8 -- .../types/RemovePokerTemplatePayload.ts | 30 ------- packages/server/postgres/select.ts | 3 +- packages/server/postgres/types/Meeting.d.ts | 10 --- packages/server/postgres/types/index.d.ts | 2 + 30 files changed, 149 insertions(+), 460 deletions(-) delete mode 100644 packages/server/database/types/MeetingSettings.ts delete mode 100644 packages/server/database/types/MeetingSettingsAction.ts delete mode 100644 packages/server/database/types/MeetingSettingsPoker.ts delete mode 100644 packages/server/database/types/MeetingSettingsRetrospective.ts delete mode 100644 packages/server/database/types/MeetingSettingsTeamPrompt.ts delete mode 100644 packages/server/graphql/mutations/removePokerTemplate.ts create mode 100644 packages/server/graphql/public/mutations/removePokerTemplate.ts create mode 100644 packages/server/graphql/public/types/RemovePokerTemplatePayload.ts delete mode 100644 packages/server/graphql/types/PokerMeetingSettings.ts delete mode 100644 packages/server/graphql/types/RemovePokerTemplatePayload.ts diff --git a/codegen.json b/codegen.json index c1edd8f890f..83eaf7bf530 100644 --- a/codegen.json +++ b/codegen.json @@ -46,6 +46,11 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { + "TeamMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB", + "TeamPromptMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB", + "PokerMeetingSettings": "../../postgres/types/index#PokerMeetingSettings as PokerMeetingSettingsDB", + "RetrospectiveMeetingSettings": "../../postgres/types/index#RetrospectiveMeetingSettings as RetrospectiveMeetingSettingsDB", + "RemovePokerTemplatePayload": "./types/RemovePokerTemplatePayload#RemovePokerTemplatePayloadSource", "JiraRemoteAvatarUrls": "./types/JiraRemoteAvatarUrls#JiraRemoteAvatarUrlsSource", "TemplateDimensionRef": "./types/TemplateDimensionRef#TemplateDimensionRefSource", "UpdateIntegrationProviderSuccess": "./types/UpdateIntegrationProviderSuccess#UpdateIntegrationProviderSuccessSource", @@ -139,7 +144,6 @@ "RetroReflectionGroup": "./types/RetroReflectionGroup#RetroReflectionGroupSource", "RetrospectiveMeeting": "../../database/types/MeetingRetrospective#default", "RetrospectiveMeetingMember": "../../database/types/RetroMeetingMember#default", - "RetrospectiveMeetingSettings": "../../database/types/MeetingSettingsRetrospective#default", "SAML": "./types/SAML#SAMLSource", "SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource", "SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource", @@ -153,7 +157,6 @@ "StripeFailPaymentPayload": "./types/StripeFailPaymentPayload#StripeFailPaymentPayloadSource", "Task": "../../database/types/Task#default", "Team": "../../postgres/types/index#Team as TeamDB", - "TeamPromptMeetingSettings": "../../database/types/MeetingSettingsTeamPrompt#default as MeetingSettingsTeamPromptDB", "TeamHealthPhase": "./types/TeamHealthPhase#TeamHealthPhaseSource", "TeamHealthStage": "./types/TeamHealthStage#TeamHealthStageSource", "TeamInvitation": "../../database/types/TeamInvitation#default", diff --git a/packages/client/modules/demo/initDB.ts b/packages/client/modules/demo/initDB.ts index d452d9ee3af..f719a3f9270 100644 --- a/packages/client/modules/demo/initDB.ts +++ b/packages/client/modules/demo/initDB.ts @@ -1,7 +1,6 @@ import {SlackNotificationEventEnum} from '~/__generated__/SlackNotificationList_viewer.graphql' import {PALETTE} from '~/styles/paletteV3' import RetrospectiveMeeting from '../../../server/database/types/MeetingRetrospective' -import RetrospectiveMeetingSettings from '../../../server/database/types/MeetingSettingsRetrospective' import ITask from '../../../server/database/types/Task' import JiraProjectId from '../../shared/gqlIds/JiraProjectId' import demoUserAvatar from '../../styles/theme/images/avatar-user.svg' @@ -35,7 +34,7 @@ type IRetrospectiveMeeting = Omit< votesRemaining: number } -type IRetrospectiveMeetingSettings = RetrospectiveMeetingSettings & { +type IRetrospectiveMeetingSettings = { team: any } diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index 5088452eb4b..70f9c25fc3e 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -2,7 +2,7 @@ import {MasterPool, r} from 'rethinkdb-ts' 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 {AnyMeeting, AnyMeetingTeamMember} from '../postgres/types/Meeting' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' import AgendaItem from './types/AgendaItem' @@ -50,10 +50,6 @@ export type RethinkSchema = { type: MassInvitation index: 'teamMemberId' } - MeetingSettings: { - type: AnyMeetingSettings - index: 'teamId' - } MeetingMember: { type: AnyMeetingTeamMember index: 'meetingId' | 'teamId' | 'userId' diff --git a/packages/server/database/types/MeetingSettings.ts b/packages/server/database/types/MeetingSettings.ts deleted file mode 100644 index b66143e0e25..00000000000 --- a/packages/server/database/types/MeetingSettings.ts +++ /dev/null @@ -1,25 +0,0 @@ -import generateUID from '../../generateUID' -import {MeetingTypeEnum} from '../../graphql/public/resolverTypes' -import {NewMeetingPhaseTypeEnum} from './GenericMeetingPhase' - -interface Input { - id?: string - phaseTypes: NewMeetingPhaseTypeEnum[] - meetingType: MeetingTypeEnum - teamId: string -} - -export default abstract class MeetingSettings { - id: string - phaseTypes: NewMeetingPhaseTypeEnum[] - meetingType: MeetingTypeEnum - teamId: string - - constructor(input: Input) { - const {id, meetingType, phaseTypes, teamId} = input - this.id = id ?? generateUID() - this.meetingType = meetingType - this.teamId = teamId - this.phaseTypes = phaseTypes - } -} diff --git a/packages/server/database/types/MeetingSettingsAction.ts b/packages/server/database/types/MeetingSettingsAction.ts deleted file mode 100644 index 381b519c44d..00000000000 --- a/packages/server/database/types/MeetingSettingsAction.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {NewMeetingPhaseTypeEnum} from './GenericMeetingPhase' -import MeetingSettings from './MeetingSettings' -interface Input { - teamId: string - id?: string -} - -const phaseTypes = [ - 'checkin', - 'updates', - 'firstcall', - 'agendaitems', - 'lastcall' -] as NewMeetingPhaseTypeEnum[] -export default class MeetingSettingsAction extends MeetingSettings { - constructor(input: Input) { - const {teamId, id} = input - super({teamId, id, meetingType: 'action', phaseTypes}) - } -} diff --git a/packages/server/database/types/MeetingSettingsPoker.ts b/packages/server/database/types/MeetingSettingsPoker.ts deleted file mode 100644 index 5f379770230..00000000000 --- a/packages/server/database/types/MeetingSettingsPoker.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {NewMeetingPhaseTypeEnum} from './GenericMeetingPhase' -import JiraSearchQuery from './JiraSearchQuery' -import MeetingSettings from './MeetingSettings' - -interface Input { - teamId: string - id?: string - maxVotesPerGroup?: number - totalVotes?: number - selectedTemplateId?: string -} - -const phaseTypes = ['checkin', 'SCOPE', 'ESTIMATE'] as NewMeetingPhaseTypeEnum[] - -export default class MeetingSettingsPoker extends MeetingSettings { - selectedTemplateId: string - jiraSearchQueries?: JiraSearchQuery[] - constructor(input: Input) { - const {teamId, id, selectedTemplateId} = input - super({teamId, id, meetingType: 'poker', phaseTypes}) - this.selectedTemplateId = selectedTemplateId || 'estimatedEffortTemplate' - } -} diff --git a/packages/server/database/types/MeetingSettingsRetrospective.ts b/packages/server/database/types/MeetingSettingsRetrospective.ts deleted file mode 100644 index a91f3c9315f..00000000000 --- a/packages/server/database/types/MeetingSettingsRetrospective.ts +++ /dev/null @@ -1,49 +0,0 @@ -import {MeetingSettingsThreshold} from '../../../client/types/constEnums' -import {NewMeetingPhaseTypeEnum} from './GenericMeetingPhase' -import MeetingSettings from './MeetingSettings' - -interface Input { - teamId: string - id?: string - maxVotesPerGroup?: number - totalVotes?: number - selectedTemplateId?: string - disableAnonymity?: boolean - videoMeetingURL?: string -} - -const phaseTypes = [ - 'checkin', - 'TEAM_HEALTH', - 'reflect', - 'group', - 'vote', - 'discuss' -] as NewMeetingPhaseTypeEnum[] - -export default class MeetingSettingsRetrospective extends MeetingSettings { - maxVotesPerGroup: number - totalVotes: number - selectedTemplateId: string - disableAnonymity: boolean - videoMeetingURL?: string | null - - constructor(input: Input) { - const { - teamId, - id, - maxVotesPerGroup, - selectedTemplateId, - totalVotes, - disableAnonymity, - videoMeetingURL - } = input - super({teamId, id, meetingType: 'retrospective', phaseTypes}) - this.maxVotesPerGroup = - maxVotesPerGroup ?? MeetingSettingsThreshold.RETROSPECTIVE_MAX_VOTES_PER_GROUP_DEFAULT - this.totalVotes = totalVotes ?? MeetingSettingsThreshold.RETROSPECTIVE_TOTAL_VOTES_DEFAULT - this.selectedTemplateId = selectedTemplateId || 'workingStuckTemplate' - this.disableAnonymity = disableAnonymity ?? false - this.videoMeetingURL = videoMeetingURL - } -} diff --git a/packages/server/database/types/MeetingSettingsTeamPrompt.ts b/packages/server/database/types/MeetingSettingsTeamPrompt.ts deleted file mode 100644 index 1caed27eecd..00000000000 --- a/packages/server/database/types/MeetingSettingsTeamPrompt.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {NewMeetingPhaseTypeEnum} from './GenericMeetingPhase' -import MeetingSettings from './MeetingSettings' -interface Input { - teamId: string - id?: string -} - -const phaseTypes = ['RESPONSES'] as NewMeetingPhaseTypeEnum[] -export default class MeetingSettingsTeamPrompt extends MeetingSettings { - constructor(input: Input) { - const {teamId, id} = input - super({teamId, id, meetingType: 'teamPrompt', phaseTypes}) - } -} diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index 6dd43e5141e..8a5b883dce3 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -4,7 +4,6 @@ import {Selectable, SqlBool, sql} from 'kysely' import {PARABOL_AI_USER_ID} from '../../client/utils/constants' import getRethink from '../database/rethinkDriver' import {RDatum} from '../database/stricterR' -import MeetingSettingsTeamPrompt from '../database/types/MeetingSettingsTeamPrompt' import MeetingTemplate from '../database/types/MeetingTemplate' import Task, {TaskStatusEnum} from '../database/types/Task' import getFileStoreManager from '../fileStorage/getFileStoreManager' @@ -287,45 +286,6 @@ export const githubDimensionFieldMaps = (parent: RootDataLoader) => { } export const meetingSettingsByType = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { - dependsOn('meetingSettings') - return new DataLoader( - async (keys) => { - const r = await getRethink() - const types = {} as Record - keys.forEach((key) => { - const {meetingType} = key - types[meetingType] = types[meetingType] || [] - types[meetingType]!.push(key.teamId) - }) - const entries = Object.entries(types) as [MeetingTypeEnum, string[]][] - const resultsByType = await Promise.all( - entries.map((entry) => { - const [meetingType, teamIds] = entry - return r - .table('MeetingSettings') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter({meetingType: meetingType}) - .run() - }) - ) - const docs = resultsByType.flat() - return keys.map((key) => { - 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}) as any - } - return docs.find((doc) => doc.teamId === teamId && doc.meetingType === meetingType)! - }) - }, - { - ...parent.dataLoaderOptions, - cacheKeyFn: (key) => `${key.teamId}:${key.meetingType}` - } - ) -} - -export const _PGmeetingSettingsByType = (parent: RootDataLoader, dependsOn: RegisterDependsOn) => { dependsOn('meetingSettings') return new DataLoader( async (keys) => { diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index a5e46c9e45b..0aa989dd98a 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -87,6 +87,6 @@ export const suggestedActions = primaryKeyLoaderMaker((ids: readonly string[]) = return selectSuggestedAction().where('id', 'in', ids).execute() }) -export const _PGmeetingSettings = primaryKeyLoaderMaker((ids: readonly string[]) => { +export const meetingSettings = 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 a0fa13be86a..b16d76e3adf 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -7,7 +7,6 @@ export const agendaItems = new RethinkPrimaryKeyLoaderMaker('AgendaItem') export const comments = new RethinkPrimaryKeyLoaderMaker('Comment') export const reflectPrompts = new RethinkPrimaryKeyLoaderMaker('ReflectPrompt') export const massInvitations = new RethinkPrimaryKeyLoaderMaker('MassInvitation') -export const meetingSettings = new RethinkPrimaryKeyLoaderMaker('MeetingSettings') export const meetingMembers = new RethinkPrimaryKeyLoaderMaker('MeetingMember') export const newMeetings = new RethinkPrimaryKeyLoaderMaker('NewMeeting') export const newFeatures = new RethinkPrimaryKeyLoaderMaker('NewFeature') diff --git a/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts b/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts index c7e672d1c69..e0cfd12c4fc 100644 --- a/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts +++ b/packages/server/graphql/mutations/helpers/createTeamAndLeader.ts @@ -1,9 +1,6 @@ import {sql} from 'kysely' import TeamMemberId from '../../../../client/shared/gqlIds/TeamMemberId' -import getRethink from '../../../database/rethinkDriver' -import MeetingSettingsAction from '../../../database/types/MeetingSettingsAction' -import MeetingSettingsPoker from '../../../database/types/MeetingSettingsPoker' -import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' +import {MeetingSettingsThreshold} from '../../../../client/types/constEnums' import Team from '../../../database/types/Team' import TimelineEventCreatedTeam from '../../../database/types/TimelineEventCreatedTeam' import {DataLoaderInstance} from '../../../dataloader/RootDataLoader' @@ -24,18 +21,12 @@ export default async function createTeamAndLeader( newTeam: ValidNewTeam, dataLoader: DataLoaderInstance ) { - const r = await getRethink() const {id: userId, picture, preferredName, email} = user const {id: teamId, orgId} = newTeam 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}), - new MeetingSettingsPoker({teamId}) - ] const timelineEvent = new TimelineEventCreatedTeam({ createdAt: new Date(Date.now() + 5), userId, @@ -79,15 +70,35 @@ export default async function createTeamAndLeader( .onConflict((oc) => oc.columns(['userId', 'type']).doNothing()) ) .with('MeetingSettingsInsert', (qc) => - qc - .insertInto('MeetingSettings') - .values(meetingSettings.map((s) => ({...s, jiraSearchQueries: null}))) + qc.insertInto('MeetingSettings').values([ + { + id: generateUID(), + teamId, + meetingType: 'retrospective', + phaseTypes: ['checkin', 'TEAM_HEALTH', 'reflect', 'group', 'vote', 'discuss'], + disableAnonymity: false, + maxVotesPerGroup: MeetingSettingsThreshold.RETROSPECTIVE_MAX_VOTES_PER_GROUP_DEFAULT, + totalVotes: MeetingSettingsThreshold.RETROSPECTIVE_TOTAL_VOTES_DEFAULT, + selectedTemplateId: 'workingStuckTemplate' + }, + { + id: generateUID(), + teamId, + meetingType: 'action', + phaseTypes: ['checkin', 'updates', 'firstcall', 'agendaitems', 'lastcall'] + }, + { + id: generateUID(), + teamId, + meetingType: 'poker', + phaseTypes: ['checkin', 'SCOPE', 'ESTIMATE'], + selectedTemplateId: 'estimatedEffortTemplate' + } + ]) ) .insertInto('TimelineEvent') .values(timelineEvent) - .execute(), - // add meeting settings - r.table('MeetingSettings').insert(meetingSettings).run() + .execute() ]) dataLoader.clearAll(['teams', 'users', 'teamMembers', 'timelineEvents', 'meetingSettings']) } diff --git a/packages/server/graphql/mutations/removePokerTemplate.ts b/packages/server/graphql/mutations/removePokerTemplate.ts deleted file mode 100644 index 7d1b9f399a8..00000000000 --- a/packages/server/graphql/mutations/removePokerTemplate.ts +++ /dev/null @@ -1,87 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SprintPokerDefaults, SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import MeetingSettingsPoker from '../../database/types/MeetingSettingsPoker' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import RemovePokerTemplatePayload from '../types/RemovePokerTemplatePayload' - -const removePokerTemplate = { - description: 'Remove a poker meeting template', - type: new GraphQLNonNull(RemovePokerTemplatePayload), - args: { - templateId: { - type: new GraphQLNonNull(GraphQLID) - } - }, - async resolve( - _source: unknown, - {templateId}: {templateId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const now = new Date() - const pg = getKysely() - const operationId = dataLoader.share() - const subOptions = {operationId, mutatorId} - const template = await dataLoader.get('meetingTemplates').load(templateId) - const viewerId = getUserId(authToken) - - // AUTH - if (!template || !template.isActive) { - return standardError(new Error('Template not found'), {userId: viewerId}) - } - if (!isTeamMember(authToken, template.teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const {teamId} = template - const [templates, settings] = await Promise.all([ - dataLoader.get('meetingTemplatesByType').load({meetingType: 'poker', teamId}), - dataLoader - .get('meetingSettingsByType') - .load({meetingType: 'poker', teamId}) as any as MeetingSettingsPoker - ]) - - // RESOLUTION - const {id: settingsId} = settings - template.isActive = false - await pg - .with('MeetingTemplateDelete', (qc) => - qc.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) - ) - .updateTable('TemplateDimension') - .set({removedAt: now}) - .where('templateId', '=', templateId) - .execute() - dataLoader.clearAll(['meetingTemplates', 'templateDimensions']) - - 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) - .update({ - selectedTemplateId: nextTemplateId - }) - .run() - dataLoader.clearAll('meetingSettings') - } - - const data = {templateId, settingsId} - publish(SubscriptionChannel.TEAM, teamId, 'RemovePokerTemplatePayload', data, subOptions) - return data - } -} - -export default removePokerTemplate diff --git a/packages/server/graphql/mutations/removeReflectTemplate.ts b/packages/server/graphql/mutations/removeReflectTemplate.ts index 251f0f631a2..3357551a51a 100644 --- a/packages/server/graphql/mutations/removeReflectTemplate.ts +++ b/packages/server/graphql/mutations/removeReflectTemplate.ts @@ -1,7 +1,6 @@ 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' @@ -42,9 +41,7 @@ const removeReflectTemplate = { const {teamId} = template const [templates, settings] = await Promise.all([ dataLoader.get('meetingTemplatesByType').load({meetingType: 'retrospective', teamId}), - dataLoader - .get('meetingSettingsByType') - .load({meetingType: 'retrospective', teamId}) as any as MeetingSettingsRetrospective + dataLoader.get('meetingSettingsByType').load({meetingType: 'retrospective', teamId}) ]) // RESOLUTION @@ -72,13 +69,6 @@ const removeReflectTemplate = { .set({selectedTemplateId: nextTemplateId}) .where('id', '=', settingsId) .execute() - await r - .table('MeetingSettings') - .get(settingsId) - .update({ - selectedTemplateId: nextTemplateId - }) - .run() dataLoader.clearAll('meetingSettings') } diff --git a/packages/server/graphql/mutations/selectTemplate.ts b/packages/server/graphql/mutations/selectTemplate.ts index 861f9a37545..6434070c22a 100644 --- a/packages/server/graphql/mutations/selectTemplate.ts +++ b/packages/server/graphql/mutations/selectTemplate.ts @@ -1,6 +1,5 @@ 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' @@ -26,7 +25,6 @@ const selectTemplate = { {selectedTemplateId, teamId}: {selectedTemplateId: string; teamId: string}, {authToken, dataLoader, socketId: mutatorId}: GQLContext ) { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} const viewerId = getUserId(authToken) @@ -54,21 +52,7 @@ const selectTemplate = { } // RESOLUTION - const meetingSettingsId = await r - .table('MeetingSettings') - .getAll(teamId, {index: 'teamId'}) - .filter({ - meetingType: template.type - }) - .update( - { - selectedTemplateId - }, - {returnChanges: true} - )('changes')(0)('old_val')('id') - .default(null) - .run() - await getKysely() + const meetingSettingsId = await getKysely() .updateTable('MeetingSettings') .set({selectedTemplateId}) .where('teamId', '=', teamId) diff --git a/packages/server/graphql/mutations/startSprintPoker.ts b/packages/server/graphql/mutations/startSprintPoker.ts index 3b7b7766c32..0fd4ecd04f9 100644 --- a/packages/server/graphql/mutations/startSprintPoker.ts +++ b/packages/server/graphql/mutations/startSprintPoker.ts @@ -3,7 +3,6 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import toTeamMemberId from '../../../client/utils/relay/toTeamMemberId' import getRethink from '../../database/rethinkDriver' import MeetingPoker from '../../database/types/MeetingPoker' -import MeetingSettingsPoker from '../../database/types/MeetingSettingsPoker' import PokerMeetingMember from '../../database/types/PokerMeetingMember' import generateUID from '../../generateUID' import getKysely from '../../postgres/getKysely' @@ -135,7 +134,10 @@ export default { const meetingSettings = await dataLoader .get('meetingSettingsByType') .load({teamId, meetingType: 'poker'}) - const {selectedTemplateId} = meetingSettings as MeetingSettingsPoker + const {selectedTemplateId} = meetingSettings + if (!selectedTemplateId) { + throw new Error('selectedTemplateId is required') + } const templateRefId = await freezeTemplateAsRef(selectedTemplateId, dataLoader) const meeting = new MeetingPoker({ diff --git a/packages/server/graphql/mutations/updateRetroMaxVotes.ts b/packages/server/graphql/mutations/updateRetroMaxVotes.ts index e687a9b9bb8..db5c0beca24 100644 --- a/packages/server/graphql/mutations/updateRetroMaxVotes.ts +++ b/packages/server/graphql/mutations/updateRetroMaxVotes.ts @@ -146,15 +146,6 @@ const updateRetroMaxVotes = { .where('teamId', '=', teamId) .where('meetingType', '=', 'retrospective') .execute(), - r - .table('MeetingSettings') - .getAll(teamId, {index: 'teamId'}) - .filter({meetingType: 'retrospective'}) - .update({ - totalVotes, - maxVotesPerGroup - }) - .run(), r .table('NewMeeting') .get(meetingId) diff --git a/packages/server/graphql/private/mutations/processRecurrence.ts b/packages/server/graphql/private/mutations/processRecurrence.ts index 22bdd3b5815..8b1828eadd8 100644 --- a/packages/server/graphql/private/mutations/processRecurrence.ts +++ b/packages/server/graphql/private/mutations/processRecurrence.ts @@ -7,7 +7,6 @@ import getRethink from '../../../database/rethinkDriver' import MeetingRetrospective, { isMeetingRetrospective } from '../../../database/types/MeetingRetrospective' -import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' import MeetingTeamPrompt, {isMeetingTeamPrompt} from '../../../database/types/MeetingTeamPrompt' import {getActiveMeetingSeries} from '../../../postgres/queries/getActiveMeetingSeries' import {MeetingSeries} from '../../../postgres/types/MeetingSeries' @@ -79,7 +78,7 @@ const startRecurringMeeting = async ( } else if (meetingSeries.meetingType === 'retrospective') { const {totalVotes, maxVotesPerGroup, disableAnonymity, templateId} = (lastMeeting as MeetingRetrospective) ?? { - templateId: (meetingSettings as MeetingSettingsRetrospective).selectedTemplateId, + templateId: meetingSettings.selectedTemplateId, ...meetingSettings } const meeting = await safeCreateRetrospective( diff --git a/packages/server/graphql/public/mutations/removePokerTemplate.ts b/packages/server/graphql/public/mutations/removePokerTemplate.ts new file mode 100644 index 00000000000..2916695a05c --- /dev/null +++ b/packages/server/graphql/public/mutations/removePokerTemplate.ts @@ -0,0 +1,64 @@ +import {SprintPokerDefaults, SubscriptionChannel} from 'parabol-client/types/constEnums' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const removePokerTemplate: MutationResolvers['removePokerTemplate'] = async ( + _source, + {templateId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const now = new Date() + const pg = getKysely() + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} + const template = await dataLoader.get('meetingTemplates').load(templateId) + const viewerId = getUserId(authToken) + + // AUTH + if (!template || !template.isActive) { + return standardError(new Error('Template not found'), {userId: viewerId}) + } + if (!isTeamMember(authToken, template.teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const {teamId} = template + const [templates, settings] = await Promise.all([ + dataLoader.get('meetingTemplatesByType').load({meetingType: 'poker', teamId}), + dataLoader.get('meetingSettingsByType').load({meetingType: 'poker', teamId}) + ]) + + // RESOLUTION + const {id: settingsId} = settings + template.isActive = false + await pg + .with('MeetingTemplateDelete', (qc) => + qc.updateTable('MeetingTemplate').set({isActive: false}).where('id', '=', templateId) + ) + .updateTable('TemplateDimension') + .set({removedAt: now}) + .where('templateId', '=', templateId) + .execute() + dataLoader.clearAll(['meetingTemplates', 'templateDimensions']) + + 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() + dataLoader.clearAll('meetingSettings') + } + + const data = {templateId, settingsId} + publish(SubscriptionChannel.TEAM, teamId, 'RemovePokerTemplatePayload', data, subOptions) + return data +} + +export default removePokerTemplate diff --git a/packages/server/graphql/public/mutations/setMeetingSettings.ts b/packages/server/graphql/public/mutations/setMeetingSettings.ts index 92c339d1b41..7dda9b23d33 100644 --- a/packages/server/graphql/public/mutations/setMeetingSettings.ts +++ b/packages/server/graphql/public/mutations/setMeetingSettings.ts @@ -1,13 +1,8 @@ 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 getKysely from '../../../postgres/getKysely' import {MeetingSettings} from '../../../postgres/types' -import { - analytics, - MeetingSettings as MeetingSettingsAnalytics -} from '../../../utils/analytics/analytics' +import {analytics} from '../../../utils/analytics/analytics' import {getUserId} from '../../../utils/authorization' import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' @@ -18,7 +13,6 @@ const setMeetingSettings: MutationResolvers['setMeetingSettings'] = async ( {settingsId, checkinEnabled, teamHealthEnabled, disableAnonymity, videoMeetingURL}, {authToken, dataLoader, socketId: mutatorId} ) => { - const r = await getRethink() const operationId = dataLoader.share() const subOptions = {operationId, mutatorId} @@ -39,7 +33,6 @@ const setMeetingSettings: MutationResolvers['setMeetingSettings'] = async ( const {featureFlags} = organization const hasTranscriptFlag = featureFlags?.includes('zoomTranscription') - const meetingSettings = {} as MeetingSettingsAnalytics const firstPhases: NewMeetingPhaseTypeEnum[] = [] if (checkinEnabled || (checkinEnabled !== false && phaseTypes.includes('checkin'))) { firstPhases.push('checkin') @@ -60,45 +53,6 @@ const setMeetingSettings: MutationResolvers['setMeetingSettings'] = async ( : null } - await r - .table('MeetingSettings') - .get(settingsId) - .update((row: RValue) => { - const updatedSettings: {[key: string]: any} = {} - if (isNotNull(checkinEnabled)) { - updatedSettings.phaseTypes = r.branch( - row('phaseTypes').contains('checkin'), - checkinEnabled ? row('phaseTypes') : row('phaseTypes').difference(['checkin']), - checkinEnabled ? row('phaseTypes').prepend('checkin') : row('phaseTypes') - ) - meetingSettings.hasIcebreaker = checkinEnabled - } - - if (isNotNull(teamHealthEnabled)) { - updatedSettings.phaseTypes = r.branch( - row('phaseTypes').contains('TEAM_HEALTH'), - teamHealthEnabled ? row('phaseTypes') : row('phaseTypes').difference(['TEAM_HEALTH']), - row('phaseTypes').contains('checkin'), - teamHealthEnabled ? row('phaseTypes').insertAt(1, 'TEAM_HEALTH') : row('phaseTypes'), - teamHealthEnabled ? row('phaseTypes').prepend('TEAM_HEALTH') : row('phaseTypes') - ) - meetingSettings.hasTeamHealth = teamHealthEnabled - } - - if (isNotNull(disableAnonymity)) { - updatedSettings.disableAnonymity = disableAnonymity - meetingSettings.disableAnonymity = disableAnonymity - } - - if (hasTranscriptFlag) { - updatedSettings.videoMeetingURL = videoMeetingURL - meetingSettings.videoMeetingURL = videoMeetingURL - } - - return updatedSettings - }) - .run() - await getKysely() .updateTable('MeetingSettings') .set(nextSettings) diff --git a/packages/server/graphql/public/mutations/startRetrospective.ts b/packages/server/graphql/public/mutations/startRetrospective.ts index 81b5a500f0d..541e983f79b 100644 --- a/packages/server/graphql/public/mutations/startRetrospective.ts +++ b/packages/server/graphql/public/mutations/startRetrospective.ts @@ -1,6 +1,5 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../../database/rethinkDriver' -import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' import RetroMeetingMember from '../../../database/types/RetroMeetingMember' import getKysely from '../../../postgres/getKysely' import updateMeetingTemplateLastUsedAt from '../../../postgres/queries/updateMeetingTemplateLastUsedAt' @@ -40,9 +39,7 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( const meetingType: MeetingTypeEnum = 'retrospective' const [viewer, meetingSettings, meetingCount] = await Promise.all([ dataLoader.get('users').loadNonNull(viewerId), - dataLoader - .get('meetingSettingsByType') - .load({teamId, meetingType}) as Promise, + dataLoader.get('meetingSettingsByType').load({teamId, meetingType}), dataLoader.get('meetingCount').load({teamId, meetingType}) ]) @@ -50,11 +47,10 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( id: meetingSettingsId, totalVotes, maxVotesPerGroup, - selectedTemplateId, disableAnonymity, videoMeetingURL - } = meetingSettings - + } = meetingSettings as typeof meetingSettings & {meetingType: 'retrospective'} + const selectedTemplateId = meetingSettings.selectedTemplateId || 'workingStuckTemplate' const meetingName = !name ? `Retro #${meetingCount + 1}` : rrule @@ -107,14 +103,6 @@ const startRetrospective: MutationResolvers['startRetrospective'] = async ( ) .run(), updateTeamByTeamId(updates, teamId), - videoMeetingURL && - r - .table('MeetingSettings') - .get(meetingSettingsId) - .update({ - videoMeetingURL: null - }) - .run(), videoMeetingURL && pg .updateTable('MeetingSettings') diff --git a/packages/server/graphql/public/types/PokerMeetingSettings.ts b/packages/server/graphql/public/types/PokerMeetingSettings.ts index 98eef614ec9..40df579d5a2 100644 --- a/packages/server/graphql/public/types/PokerMeetingSettings.ts +++ b/packages/server/graphql/public/types/PokerMeetingSettings.ts @@ -8,7 +8,7 @@ import {PokerMeetingSettingsResolvers} from '../resolverTypes' const PokerMeetingSettings: PokerMeetingSettingsResolvers = { __isTypeOf: ({meetingType}) => meetingType === 'poker', - selectedTemplate: resolveSelectedTemplate('estimatedEffortTemplate'), + selectedTemplate: resolveSelectedTemplate<'poker'>('estimatedEffortTemplate'), teamTemplates: async ({teamId}, _args, {dataLoader}) => { const templates = await dataLoader diff --git a/packages/server/graphql/public/types/RemovePokerTemplatePayload.ts b/packages/server/graphql/public/types/RemovePokerTemplatePayload.ts new file mode 100644 index 00000000000..3afa1ee5bda --- /dev/null +++ b/packages/server/graphql/public/types/RemovePokerTemplatePayload.ts @@ -0,0 +1,25 @@ +import {RemovePokerTemplatePayloadResolvers} from '../resolverTypes' + +export type RemovePokerTemplatePayloadSource = + | { + templateId: string + settingsId: string + } + | {error: {message: string}} + +const RemovePokerTemplatePayload: RemovePokerTemplatePayloadResolvers = { + pokerTemplate: (source, _args, {dataLoader}) => { + if ('error' in source) return null + const {templateId} = source + return dataLoader.get('meetingTemplates').loadNonNull(templateId) + }, + + pokerMeetingSettings: async (source, _args, {dataLoader}) => { + if ('error' in source) return null + const {settingsId} = source + const settings = await dataLoader.get('meetingSettings').loadNonNull(settingsId) + return settings as typeof settings & {meetingType: 'poker'} + } +} + +export default RemovePokerTemplatePayload diff --git a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts index 5d556ffdf3b..4a8209d5629 100644 --- a/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts +++ b/packages/server/graphql/queries/helpers/resolveSelectedTemplate.ts @@ -1,36 +1,27 @@ -import getRethink from '../../../database/rethinkDriver' -import MeetingSettingsPoker from '../../../database/types/MeetingSettingsPoker' -import MeetingSettingsRetrospective from '../../../database/types/MeetingSettingsRetrospective' import getKysely from '../../../postgres/getKysely' +import {MeetingSettings} from '../../../postgres/types' import {GQLContext} from '../../graphql' const resolveSelectedTemplate = - (fallbackTemplateId: string) => - async ( - source: MeetingSettingsPoker | MeetingSettingsRetrospective, - _args: unknown, - {dataLoader}: GQLContext - ) => { + (fallbackTemplateId: string) => + async (source: MeetingSettings, _args: unknown, {dataLoader}: GQLContext) => { const {id: settingsId, selectedTemplateId} = source - const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) - if (template) { - return template + if (selectedTemplateId) { + const template = await dataLoader.get('meetingTemplates').load(selectedTemplateId) + if (template) { + return template + } } // there may be holes in our template deletion or reselection logic, so doing this to be safe source.selectedTemplateId = fallbackTemplateId - const r = await getRethink() - await r - .table('MeetingSettings') - .get(settingsId) - .update({selectedTemplateId: fallbackTemplateId}) - .run() await getKysely() .updateTable('MeetingSettings') .set({selectedTemplateId: fallbackTemplateId}) .where('id', '=', settingsId) .execute() - return dataLoader.get('meetingTemplates').loadNonNull(fallbackTemplateId) + const res = await dataLoader.get('meetingTemplates').loadNonNull(fallbackTemplateId) + return res as typeof res & {type: TMeetingTypeEnum} } export default resolveSelectedTemplate diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 74dcd4d81de..91654c128b6 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -68,7 +68,6 @@ import removeAtlassianAuth from './mutations/removeAtlassianAuth' import removeGitHubAuth from './mutations/removeGitHubAuth' import removeIntegrationProvider from './mutations/removeIntegrationProvider' import removeOrgUser from './mutations/removeOrgUser' -import removePokerTemplate from './mutations/removePokerTemplate' import removePokerTemplateDimension from './mutations/removePokerTemplateDimension' import removePokerTemplateScale from './mutations/removePokerTemplateScale' import removePokerTemplateScaleValue from './mutations/removePokerTemplateScaleValue' @@ -178,7 +177,6 @@ export default new GraphQLObjectType({ removeAtlassianAuth, removeGitHubAuth, removeOrgUser, - removePokerTemplate, removeReflectTemplate, removeReflectTemplatePrompt, removePokerTemplateDimension, diff --git a/packages/server/graphql/types/PokerMeetingSettings.ts b/packages/server/graphql/types/PokerMeetingSettings.ts deleted file mode 100644 index a63971cd4df..00000000000 --- a/packages/server/graphql/types/PokerMeetingSettings.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {GraphQLObjectType} from 'graphql' - -const PokerMeetingSettings = new GraphQLObjectType({ - name: 'PokerMeetingSettings', - fields: {} -}) - -export default PokerMeetingSettings diff --git a/packages/server/graphql/types/RemovePokerTemplatePayload.ts b/packages/server/graphql/types/RemovePokerTemplatePayload.ts deleted file mode 100644 index fadc2591d22..00000000000 --- a/packages/server/graphql/types/RemovePokerTemplatePayload.ts +++ /dev/null @@ -1,30 +0,0 @@ -import {GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import PokerMeetingSettings from './PokerMeetingSettings' -import PokerTemplate from './PokerTemplate' -import StandardMutationError from './StandardMutationError' - -const RemovePokerTemplatePayload = new GraphQLObjectType({ - name: 'RemovePokerTemplatePayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - pokerTemplate: { - type: PokerTemplate, - resolve: ({templateId}, _args: unknown, {dataLoader}) => { - if (!templateId) return null - return dataLoader.get('meetingTemplates').load(templateId) - } - }, - pokerMeetingSettings: { - type: PokerMeetingSettings, - resolve: ({settingsId}, _args: unknown, {dataLoader}) => { - if (!settingsId) return null - return dataLoader.get('meetingSettings').load(settingsId) - } - } - }) -}) - -export default RemovePokerTemplatePayload diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 8c12cbf97d9..7fd142dd542 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -188,7 +188,6 @@ export const selectMeetingSettings = () => 'meetingType', 'teamId', 'selectedTemplateId', - 'jiraSearchQueries', 'maxVotesPerGroup', 'totalVotes', 'disableAnonymity', @@ -196,7 +195,7 @@ export const selectMeetingSettings = () => ]) .$narrowType< // NewMeeetingPhaseTypeEnum[] should be inferred from kysely-codegen, but it's not - | {meetingType: NotNull; phaseTypes: NewMeetingPhaseTypeEnum[]} + | {meetingType: 'action' | 'poker' | 'teamPrompt'; phaseTypes: NewMeetingPhaseTypeEnum[]} | { meetingType: 'retrospective' phaseTypes: NewMeetingPhaseTypeEnum[] diff --git a/packages/server/postgres/types/Meeting.d.ts b/packages/server/postgres/types/Meeting.d.ts index 5feb066120d..65d7995ce18 100644 --- a/packages/server/postgres/types/Meeting.d.ts +++ b/packages/server/postgres/types/Meeting.d.ts @@ -2,10 +2,6 @@ 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 MeetingSettingsAction from '../../database/types/MeetingSettingsAction' -import MeetingSettingsPoker from '../../database/types/MeetingSettingsPoker' -import MeetingSettingsRetrospective from '../../database/types/MeetingSettingsRetrospective' -import MeetingSettingsTeamPrompt from '../../database/types/MeetingSettingsTeamPrompt' import MeetingTeamPrompt from '../../database/types/MeetingTeamPrompt' import PokerMeetingMember from '../../database/types/PokerMeetingMember' import RetroMeetingMember from '../../database/types/RetroMeetingMember' @@ -21,9 +17,3 @@ export type AnyMeetingTeamMember = | RetroMeetingMember | ActionMeetingMember | TeamPromptMeetingMember - -export type AnyMeetingSettings = - | MeetingSettingsRetrospective - | MeetingSettingsAction - | MeetingSettingsPoker - | MeetingSettingsTeamPrompt diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index f5260cea94f..f6706233236 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -42,3 +42,5 @@ export type TemplateScale = ExtractTypeFromQueryBuilderSelect export type MeetingSettings = ExtractTypeFromQueryBuilderSelect +export type PokerMeetingSettings = MeetingSettings & {meetingType: 'poker'} +export type RetrospectiveMeetingSettings = MeetingSettings & {meetingType: 'retrospective'} From 07e28114f024db8611503383a753b90df8d36706 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Mon, 12 Aug 2024 12:32:04 -0700 Subject: [PATCH 4/9] fix: jsonify phaseTypes Signed-off-by: Matt Krick --- packages/server/postgres/select.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index dc02429686d..77559cab3ce 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -181,7 +181,6 @@ export const selectMeetingSettings = () => .selectFrom('MeetingSettings') .select([ 'id', - 'phaseTypes', 'meetingType', 'teamId', 'selectedTemplateId', @@ -192,15 +191,15 @@ export const selectMeetingSettings = () => ]) .$narrowType< // NewMeeetingPhaseTypeEnum[] should be inferred from kysely-codegen, but it's not - | {meetingType: 'action' | 'poker' | 'teamPrompt'; phaseTypes: NewMeetingPhaseTypeEnum[]} + | {meetingType: 'action' | 'poker' | 'teamPrompt'} | { meetingType: 'retrospective' - phaseTypes: NewMeetingPhaseTypeEnum[] maxVotesPerGroup: NotNull totalVotes: NotNull disableAnonymity: NotNull } >() .select(({fn}) => [ - fn('to_json', ['jiraSearchQueries']).as('jiraSearchQueries') + fn('to_json', ['jiraSearchQueries']).as('jiraSearchQueries'), + fn('to_json', ['phaseTypes']).as('phaseTypes') ]) From 8241c148f4a6da907753586dd45de0414a1ef707 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 14 Aug 2024 14:56:31 -0700 Subject: [PATCH 5/9] write to PG Signed-off-by: Matt Krick --- .../1723491538114_AgendaItem-phase1.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/server/postgres/migrations/1723491538114_AgendaItem-phase1.ts diff --git a/packages/server/postgres/migrations/1723491538114_AgendaItem-phase1.ts b/packages/server/postgres/migrations/1723491538114_AgendaItem-phase1.ts new file mode 100644 index 00000000000..cfa935f2a7c --- /dev/null +++ b/packages/server/postgres/migrations/1723491538114_AgendaItem-phase1.ts @@ -0,0 +1,49 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DO $$ + BEGIN + CREATE TABLE IF NOT EXISTS "AgendaItem" ( + "id" VARCHAR(100) PRIMARY KEY, + "content" VARCHAR(64) NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT TRUE, + "isComplete" BOOLEAN NOT NULL DEFAULT FALSE, + "sortOrder" VARCHAR(64) NOT NULL COLLATE "C", + "teamId" VARCHAR(100) NOT NULL, + "teamMemberId" VARCHAR(100) NOT NULL, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + "meetingId" VARCHAR(100), + "pinned" BOOLEAN NOT NULL DEFAULT FALSE, + "pinnedParentId" VARCHAR(100), + CONSTRAINT "fk_teamId" + FOREIGN KEY("teamId") + REFERENCES "Team"("id") + ON DELETE CASCADE, + CONSTRAINT "fk_teamMemberId" + FOREIGN KEY("teamMemberId") + REFERENCES "TeamMember"("id") + ON DELETE CASCADE + ); + CREATE INDEX IF NOT EXISTS "idx_AgendaItem_teamId" ON "AgendaItem"("teamId"); + CREATE INDEX IF NOT EXISTS "idx_AgendaItem_meetingId" ON "AgendaItem"("meetingId"); + CREATE INDEX IF NOT EXISTS "idx_AgendaItem_teamMemberId" ON "AgendaItem"("teamMemberId"); + DROP TRIGGER IF EXISTS "update_AgendaItem_updatedAt" ON "AgendaItem"; + CREATE TRIGGER "update_AgendaItem_updatedAt" BEFORE UPDATE ON "AgendaItem" 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 "AgendaItem"; + ` /* Do undo magic */) + await client.end() +} From 4ead2185b919b2aeed221cb5a3f1e58d21f09a3d Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 14 Aug 2024 14:56:54 -0700 Subject: [PATCH 6/9] write to PG Signed-off-by: Matt Krick --- .../AgendaListAndInput/AgendaListAndInput.tsx | 1 - .../dataloader/foreignKeyLoaderMakers.ts | 21 +++++++++ .../dataloader/primaryKeyLoaderMakers.ts | 5 +++ .../server/graphql/mutations/addAgendaItem.ts | 21 ++++++++- .../server/graphql/mutations/endCheckIn.ts | 44 +++++++++++++------ .../addAgendaItemToActiveActionMeeting.ts | 2 + .../mutations/helpers/removeTeamMember.ts | 12 +++-- .../graphql/mutations/removeAgendaItem.ts | 7 +++ .../graphql/mutations/updateAgendaItem.ts | 24 ++++++++++ .../graphql/public/mutations/startCheckIn.ts | 8 +++- packages/server/postgres/select.ts | 2 + 11 files changed, 123 insertions(+), 24 deletions(-) diff --git a/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx b/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx index f338fe6a7a0..5733d314b27 100644 --- a/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx +++ b/packages/client/modules/teamDashboard/components/AgendaListAndInput/AgendaListAndInput.tsx @@ -55,7 +55,6 @@ const AgendaListAndInput = (props: Props) => { agendaItems { id content - sortOrder ...AgendaList_agendaItems } } diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 98146663a5d..51e21b4ca0b 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -1,6 +1,7 @@ import getKysely from '../postgres/getKysely' import {getTeamPromptResponsesByMeetingIds} from '../postgres/queries/getTeamPromptResponsesByMeetingIds' import { + selectAgendaItems, selectOrganizations, selectRetroReflections, selectSuggestedAction, @@ -170,3 +171,23 @@ export const teamPromptResponsesByMeetingId = foreignKeyLoaderMaker( 'meetingId', getTeamPromptResponsesByMeetingIds ) + +export const _pgagendaItemsByTeamId = foreignKeyLoaderMaker( + '_pgagendaItems', + 'teamId', + async (teamIds) => { + return selectAgendaItems() + .where('teamId', 'in', teamIds) + .where('isActive', '=', true) + .orderBy('sortOrder') + .execute() + } +) + +export const _pgagendaItemsByMeetingId = foreignKeyLoaderMaker( + '_pgagendaItems', + 'meetingId', + async (meetingIds) => { + return selectAgendaItems().where('meetingId', 'in', meetingIds).orderBy('sortOrder').execute() + } +) diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 0aa989dd98a..0849187482d 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 { + selectAgendaItems, selectMeetingSettings, selectOrganizations, selectRetroReflections, @@ -90,3 +91,7 @@ export const suggestedActions = primaryKeyLoaderMaker((ids: readonly string[]) = export const meetingSettings = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectMeetingSettings().where('id', 'in', ids).execute() }) + +export const _pgagendaItems = primaryKeyLoaderMaker((ids: readonly string[]) => { + return selectAgendaItems().where('id', 'in', ids).execute() +}) diff --git a/packages/server/graphql/mutations/addAgendaItem.ts b/packages/server/graphql/mutations/addAgendaItem.ts index c9200d89215..c6fc4425fc0 100644 --- a/packages/server/graphql/mutations/addAgendaItem.ts +++ b/packages/server/graphql/mutations/addAgendaItem.ts @@ -1,9 +1,11 @@ import {GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import makeAgendaItemSchema from 'parabol-client/validation/makeAgendaItemSchema' +import {positionAfter} from '../../../client/shared/sortOrder' import getRethink from '../../database/rethinkDriver' import AgendaItem, {AgendaItemInput} from '../../database/types/AgendaItem' import generateUID from '../../generateUID' +import getKysely from '../../postgres/getKysely' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' @@ -45,18 +47,33 @@ export default { } // RESOLUTION + const teamAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const lastAgendaItem = teamAgendaItems.at(-1) + const lastSortOrder = lastAgendaItem?.sortOrder ? String(lastAgendaItem.sortOrder) : '' + // this is just during the migration of AgendaItem table + const sortOrder = positionAfter(lastSortOrder) const agendaItemId = `${teamId}::${generateUID()}` await r .table('AgendaItem') .insert( new AgendaItem({ ...validNewAgendaItem, - id: agendaItemId, teamId } as AgendaItemInput) ) .run() - + await getKysely() + .insertInto('AgendaItem') + .values({ + id: agendaItemId, + content: newAgendaItem.content, + meetingId: newAgendaItem.meetingId, + pinned: newAgendaItem.pinned, + sortOrder, + teamId, + teamMemberId: newAgendaItem.teamMemberId + }) + .execute() const meetingId = await addAgendaItemToActiveActionMeeting(agendaItemId, teamId, dataLoader) analytics.addedAgendaItem(viewer, teamId, meetingId) const data = {agendaItemId, meetingId} diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index f009f8347d3..c5bfbca7a28 100644 --- a/packages/server/graphql/mutations/endCheckIn.ts +++ b/packages/server/graphql/mutations/endCheckIn.ts @@ -3,6 +3,7 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' import {AGENDA_ITEMS, DONE, LAST_CALL} from 'parabol-client/utils/constants' import getMeetingPhase from 'parabol-client/utils/getMeetingPhase' import findStageById from 'parabol-client/utils/meetings/findStageById' +import {positionAfter} from '../../../client/shared/sortOrder' import {checkTeamsLimit} from '../../billing/helpers/teamLimitsCheck' import getRethink from '../../database/rethinkDriver' import {RDatum} from '../../database/stricterR' @@ -10,6 +11,7 @@ import AgendaItem from '../../database/types/AgendaItem' 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 archiveTasksForDB from '../../safeMutations/archiveTasksForDB' @@ -61,8 +63,14 @@ const updateTaskSortOrders = async (userIds: string[], tasks: SortOrderTask[]) = return tasks } -const clearAgendaItems = async (teamId: string) => { +const clearAgendaItems = async (teamId: string, dataLoader: DataLoaderInstance) => { + await getKysely() + .updateTable('AgendaItem') + .set({isActive: false}) + .where('teamId', '=', teamId) + .execute() const r = await getRethink() + dataLoader.clearAll('agendaItems') return r .table('AgendaItem') .getAll(teamId, {index: 'teamId'}) @@ -72,16 +80,15 @@ const clearAgendaItems = async (teamId: string) => { .run() } -const getPinnedAgendaItems = async (teamId: string) => { - const r = await getRethink() - return r - .table('AgendaItem') - .getAll(teamId, {index: 'teamId'}) - .filter({isActive: true, pinned: true}) - .run() +const getPinnedAgendaItems = async (teamId: string, dataLoader: DataLoaderInstance) => { + const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + return agendaItems.filter((agendaItem) => agendaItem.pinned) } -const clonePinnedAgendaItems = async (pinnedAgendaItems: AgendaItem[]) => { +const clonePinnedAgendaItems = async ( + pinnedAgendaItems: AgendaItem[], + dataLoader: DataLoaderInstance +) => { const r = await getRethink() const clonedPins = pinnedAgendaItems.map((agendaItem) => { const agendaItemId = `${agendaItem.teamId}::${generateUID()}` @@ -96,6 +103,17 @@ const clonePinnedAgendaItems = async (pinnedAgendaItems: AgendaItem[]) => { }) }) await r.table('AgendaItem').insert(clonedPins).run() + let curSortOrder = '' + const pgClonedPins = clonedPins.map((agendaItems) => { + const sortOrder = positionAfter(curSortOrder) + curSortOrder = sortOrder + return { + ...agendaItems, + sortOrder + } + }) + await getKysely().insertInto('AgendaItem').values(pgClonedPins).execute() + dataLoader.clearAll('agendaItems') } const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataLoaderWorker) => { @@ -120,7 +138,7 @@ const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataL .filter({status: DONE}) .filter((task: RDatum) => task('tags').contains('archived').not()) .run(), - r.table('AgendaItem').getAll(teamId, {index: 'teamId'}).filter({isActive: true}).run() + dataLoader.get('agendaItemsByTeamId').load(teamId) ]) const agendaItemPhase = getPhase(phases, 'agendaitems') @@ -128,12 +146,12 @@ const summarizeCheckInMeeting = async (meeting: MeetingAction, dataLoader: DataL const discussionIds = stages.map((stage) => stage.discussionId) const userIds = meetingMembers.map(({userId}) => userId) const meetingPhase = getMeetingPhase(phases) - const pinnedAgendaItems = await getPinnedAgendaItems(teamId) + const pinnedAgendaItems = await getPinnedAgendaItems(teamId, dataLoader) const isKill = !!(meetingPhase && ![AGENDA_ITEMS, LAST_CALL].includes(meetingPhase.phaseType)) - if (!isKill) await clearAgendaItems(teamId) + if (!isKill) await clearAgendaItems(teamId, dataLoader) await Promise.all([ isKill ? undefined : archiveTasksForDB(doneTasks, meetingId), - isKill ? undefined : clonePinnedAgendaItems(pinnedAgendaItems), + isKill ? undefined : clonePinnedAgendaItems(pinnedAgendaItems, dataLoader), updateTaskSortOrders(userIds, tasks), r .table('NewMeeting') diff --git a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index dbd1859dd78..db01b387b0d 100644 --- a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts +++ b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts @@ -1,6 +1,7 @@ import getRethink from '../../../database/rethinkDriver' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' import MeetingAction from '../../../database/types/MeetingAction' +import getKysely from '../../../postgres/getKysely' import insertDiscussions from '../../../postgres/queries/insertDiscussions' import getPhase from '../../../utils/getPhase' import {DataLoaderWorker} from '../../graphql' @@ -47,6 +48,7 @@ const addAgendaItemToActiveActionMeeting = async ( }) .run(), r.table('AgendaItem').get(agendaItemId).update({meetingId: meetingId}).run(), + getKysely().updateTable('AgendaItem').set({meetingId}).where('id', '=', agendaItemId).execute(), insertDiscussions([ { id: discussionId, diff --git a/packages/server/graphql/mutations/helpers/removeTeamMember.ts b/packages/server/graphql/mutations/helpers/removeTeamMember.ts index d9b92314e63..e1e7c1d4fee 100644 --- a/packages/server/graphql/mutations/helpers/removeTeamMember.ts +++ b/packages/server/graphql/mutations/helpers/removeTeamMember.ts @@ -105,13 +105,11 @@ const removeTeamMember = async ( const archivedTasks = await archiveTasksForDB(integratedTasksToArchive) const archivedTaskIds = archivedTasks.map(({id}) => id) - const agendaItemIds = await r - .table('AgendaItem') - .getAll(teamId, {index: 'teamId'}) - .filter((row: RDatum) => row('teamMemberId').eq(teamMemberId)) - .getField('id') - .run() - + const teamAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const agendaItemIds = teamAgendaItems + .filter((agendaItem) => agendaItem.teamMemberId === teamMemberId) + .map(({id}) => id) + dataLoader.clearAll('agendaItems') // if a new meeting was currently running, remove them from it const filterFn = (stage: CheckInStage | UpdatesStage | EstimateStage | AgendaItemsStage) => (stage as CheckInStage | UpdatesStage).teamMemberId === teamMemberId || diff --git a/packages/server/graphql/mutations/removeAgendaItem.ts b/packages/server/graphql/mutations/removeAgendaItem.ts index 4cf87cb57bd..941c0f9dc30 100644 --- a/packages/server/graphql/mutations/removeAgendaItem.ts +++ b/packages/server/graphql/mutations/removeAgendaItem.ts @@ -2,6 +2,7 @@ import {GraphQLID, GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import getRethink from '../../database/rethinkDriver' import AgendaItemsStage from '../../database/types/AgendaItemsStage' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import publish from '../../utils/publish' import standardError from '../../utils/standardError' @@ -42,6 +43,12 @@ export default { .update({isActive: false}, {returnChanges: true})('changes')(0)('old_val') .default(null) .run() + await getKysely() + .updateTable('AgendaItem') + .set({isActive: false}) + .where('id', '=', agendaItemId) + .returning('id') + .execute() if (!agendaItem) { return standardError(new Error('Agenda item not found'), {userId: viewerId}) } diff --git a/packages/server/graphql/mutations/updateAgendaItem.ts b/packages/server/graphql/mutations/updateAgendaItem.ts index 6a9c688ce5d..46d08998dbc 100644 --- a/packages/server/graphql/mutations/updateAgendaItem.ts +++ b/packages/server/graphql/mutations/updateAgendaItem.ts @@ -1,8 +1,10 @@ import {GraphQLNonNull} from 'graphql' import {SubscriptionChannel} from 'parabol-client/types/constEnums' import makeUpdateAgendaItemSchema from 'parabol-client/validation/makeUpdateAgendaItemSchema' +import {getSortOrder} from '../../../client/shared/sortOrder' import getRethink from '../../database/rethinkDriver' import AgendaItemsStage from '../../database/types/AgendaItemsStage' +import getKysely from '../../postgres/getKysely' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' import publish from '../../utils/publish' @@ -27,6 +29,7 @@ export default { ) { const now = new Date() const r = await getRethink() + const pg = getKysely() const operationId = dataLoader.share() const subOptions = {mutatorId, operationId} const viewerId = getUserId(authToken) @@ -49,6 +52,8 @@ export default { } // RESOLUTION + const oldAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const fromIdx = oldAgendaItems.findIndex((agendaItem) => agendaItem.id === id) await r .table('AgendaItem') .get(id) @@ -57,6 +62,25 @@ export default { updatedAt: now }) .run() + dataLoader.clearAll('agendaItems') + if (doc.sortOrder !== null && doc.sortOrder !== undefined) { + const nextAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const pgagendaItems = await dataLoader.get('_pgagendaItemsByTeamId').load(teamId) + const toIdx = nextAgendaItems.findIndex((agendaItem) => agendaItem.id === id) + const pgSortOrder = getSortOrder(pgagendaItems, fromIdx, toIdx) + await pg + .updateTable('AgendaItem') + .set({sortOrder: pgSortOrder}) + .where('id', '=', id) + .execute() + } else { + await pg + .updateTable('AgendaItem') + .set({pinned: doc.pinned, content: doc.content}) + .where('id', '=', id) + .execute() + } + const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) const actionMeeting = activeMeetings.find( (activeMeeting) => activeMeeting.meetingType === 'action' diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index c411c3fcd9d..a832930b505 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -3,6 +3,7 @@ import getRethink from '../../../database/rethinkDriver' import ActionMeetingMember from '../../../database/types/ActionMeetingMember' import MeetingAction from '../../../database/types/MeetingAction' import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' import updateTeamByTeamId from '../../../postgres/queries/updateTeamByTeamId' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' import {analytics} from '../../../utils/analytics/analytics' @@ -89,7 +90,12 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( .insert(new ActionMeetingMember({meetingId, userId: viewerId, teamId})) .run(), updateTeamByTeamId(updates, teamId), - r.table('AgendaItem').getAll(r.args(agendaItemIds)).update({meetingId}).run() + r.table('AgendaItem').getAll(r.args(agendaItemIds)).update({meetingId}).run(), + getKysely() + .updateTable('AgendaItem') + .set({meetingId}) + .where('id', 'in', agendaItemIds) + .execute() ]) IntegrationNotifier.startMeeting(dataLoader, meetingId, teamId) analytics.meetingStarted(viewer, meeting) diff --git a/packages/server/postgres/select.ts b/packages/server/postgres/select.ts index 77559cab3ce..9a6df8e0d30 100644 --- a/packages/server/postgres/select.ts +++ b/packages/server/postgres/select.ts @@ -203,3 +203,5 @@ export const selectMeetingSettings = () => fn('to_json', ['jiraSearchQueries']).as('jiraSearchQueries'), fn('to_json', ['phaseTypes']).as('phaseTypes') ]) + +export const selectAgendaItems = () => getKysely().selectFrom('AgendaItem').selectAll() From 3b0423dab43dd97359fe27c0f4afa72697031c7a Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 14 Aug 2024 16:25:51 -0700 Subject: [PATCH 7/9] chore: migrate existing rows to PG Signed-off-by: Matt Krick --- .../1723672980596_AgendaItem-phase2.ts | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 packages/server/postgres/migrations/1723672980596_AgendaItem-phase2.ts diff --git a/packages/server/postgres/migrations/1723672980596_AgendaItem-phase2.ts b/packages/server/postgres/migrations/1723672980596_AgendaItem-phase2.ts new file mode 100644 index 00000000000..42a199bba9f --- /dev/null +++ b/packages/server/postgres/migrations/1723672980596_AgendaItem-phase2.ts @@ -0,0 +1,184 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +const START_CHAR_CODE = 32 +const END_CHAR_CODE = 126 + +export function positionAfter(pos: string) { + for (let i = pos.length - 1; i >= 0; i--) { + const curCharCode = pos.charCodeAt(i) + if (curCharCode < END_CHAR_CODE) { + return pos.substr(0, i) + String.fromCharCode(curCharCode + 1) + } + } + return pos + String.fromCharCode(START_CHAR_CODE + 1) +} + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + // add a dummy date for nulls + const parabolEpoch = new Date('2016-06-01') + await r + .table('AgendaItem') + .update((row) => ({ + updatedAt: row('updatedAt').default(parabolEpoch), + createdAt: row('createdAt').default(parabolEpoch) + })) + .run() + const strDates = await r + .table('AgendaItem') + .filter((row) => row('updatedAt').typeOf().eq('STRING')) + .pluck('updatedAt', 'id', 'createdAt') + .run() + const dateDates = strDates.map((d) => ({ + id: d.id, + updatedAt: new Date(d.updatedAt), + createdAt: new Date(d.createdAt) + })) + // some dates are + await r(dateDates) + .forEach((row: any) => { + return r + .table('AgendaItem') + .get(row('id')) + .update({updatedAt: row('updatedAt')}) + }) + .run() + + try { + console.log('Adding index') + await r + .table('AgendaItem') + .indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')]) + .run() + await r.table('AgendaItem').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'content', + 'createdAt', + 'isActive', + 'isComplete', + 'sortOrder', + 'teamId', + 'teamMemberId', + 'updatedAt', + 'meetingId', + 'pinned', + 'pinnedParentId' + ] as const + type AgendaItem = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + let curUpdatedAt = r.minval + let curId = r.minval + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('AgendaItem') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as AgendaItem[] + + const rowsToInsert = rawRowsToInsert.map((row) => { + const {sortOrder, ...rest} = row as any + return { + ...rest, + sortOrder: String(sortOrder) + } + }) + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.updatedAt + curId = lastRow.id + try { + await pg + .insertInto('AgendaItem') + .values(rowsToInsert) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + await Promise.all( + rowsToInsert.map(async (row) => { + try { + await pg + .insertInto('AgendaItem') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_teamMemberId' || e.constraint === 'fk_teamId') { + console.log(`Skipping ${row.id} because it has no user/team`) + return + } + console.log(e, row) + } + }) + ) + } + } + + // remap the sortOrder in PG because rethinkdb is too slow to group + const pgRows = await sql<{items: {sortOrder: string; id: string}[]}>` + select jsonb_agg(jsonb_build_object('sortOrder', "sortOrder", 'id', "id", 'meetingId', "meetingId", 'teamId', "teamId") ORDER BY "sortOrder") items from "AgendaItem" +group by "teamId", "meetingId";`.execute(pg) + + const groups = pgRows.rows.map((row) => { + const {items} = row + let curSortOrder = '' + for (let i = 0; i < items.length; i++) { + const item = items[i] + curSortOrder = positionAfter(curSortOrder) + item.sortOrder = curSortOrder + } + return row + }) + for (let i = 0; i < groups.length; i++) { + const group = groups[i] + await Promise.all( + group.items.map((item) => { + return pg + .updateTable('AgendaItem') + .set({sortOrder: item.sortOrder}) + .where('id', '=', item.id) + .execute() + }) + ) + } +} + +export async function down() { + // await connectRethinkDB() + // try { + // await r.table('AgendaItem').indexDrop('updatedAtId').run() + // } catch { + // // index already dropped + // } + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "AgendaItem" CASCADE`.execute(pg) +} From 5dadc1244f51f6fe841c20a9ed5c9442e01df298 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Wed, 14 Aug 2024 17:52:35 -0700 Subject: [PATCH 8/9] remove AgendaItem in R Signed-off-by: Matt Krick --- codegen.json | 5 +- .../components/AgendaInput/AgendaInput.tsx | 4 +- .../components/AgendaList/AgendaList.tsx | 16 +-- packages/server/database/rethinkDriver.ts | 5 - packages/server/database/types/AgendaItem.ts | 61 ---------- .../dataloader/foreignKeyLoaderMakers.ts | 8 +- .../dataloader/primaryKeyLoaderMakers.ts | 2 +- .../rethinkForeignKeyLoaderMakers.ts | 27 ----- .../rethinkPrimaryKeyLoaderMakers.ts | 1 - .../server/graphql/mutations/addAgendaItem.ts | 83 ------------- .../server/graphql/mutations/endCheckIn.ts | 30 ++--- .../addAgendaItemToActiveActionMeeting.ts | 1 - .../graphql/mutations/removeAgendaItem.ts | 61 ---------- .../graphql/mutations/updateAgendaItem.ts | 111 ------------------ .../private/mutations/hardDeleteUser.ts | 6 - .../graphql/public/mutations/addAgendaItem.ts | 57 +++++++++ .../public/mutations/removeAgendaItem.ts | 43 +++++++ .../graphql/public/mutations/startCheckIn.ts | 1 - .../public/mutations/updateAgendaItem.ts | 75 ++++++++++++ .../public/typeDefs/AgendaItem.graphql | 2 +- .../typeDefs/CreateAgendaItemInput.graphql | 2 +- .../typeDefs/UpdateAgendaItemInput.graphql | 2 +- .../graphql/public/types/ActionMeeting.ts | 2 +- .../public/types/AddAgendaItemPayload.ts | 24 ++++ .../graphql/public/types/AgendaItemsStage.ts | 2 +- .../public/types/RemoveAgendaItemPayload.ts | 24 ++++ .../public/types/UpdateAgendaItemPayload.ts | 24 ++++ packages/server/graphql/rootMutation.ts | 6 - .../graphql/types/AddAgendaItemPayload.ts | 32 ----- .../graphql/types/CreateAgendaItemInput.ts | 48 -------- .../graphql/types/RemoveAgendaItemPayload.ts | 29 ----- .../graphql/types/UpdateAgendaItemInput.ts | 44 ------- .../graphql/types/UpdateAgendaItemPayload.ts | 32 ----- packages/server/postgres/types/index.d.ts | 3 + 34 files changed, 276 insertions(+), 597 deletions(-) delete mode 100644 packages/server/database/types/AgendaItem.ts delete mode 100644 packages/server/graphql/mutations/addAgendaItem.ts delete mode 100644 packages/server/graphql/mutations/removeAgendaItem.ts delete mode 100644 packages/server/graphql/mutations/updateAgendaItem.ts create mode 100644 packages/server/graphql/public/mutations/addAgendaItem.ts create mode 100644 packages/server/graphql/public/mutations/removeAgendaItem.ts create mode 100644 packages/server/graphql/public/mutations/updateAgendaItem.ts create mode 100644 packages/server/graphql/public/types/AddAgendaItemPayload.ts create mode 100644 packages/server/graphql/public/types/RemoveAgendaItemPayload.ts create mode 100644 packages/server/graphql/public/types/UpdateAgendaItemPayload.ts delete mode 100644 packages/server/graphql/types/AddAgendaItemPayload.ts delete mode 100644 packages/server/graphql/types/CreateAgendaItemInput.ts delete mode 100644 packages/server/graphql/types/RemoveAgendaItemPayload.ts delete mode 100644 packages/server/graphql/types/UpdateAgendaItemInput.ts delete mode 100644 packages/server/graphql/types/UpdateAgendaItemPayload.ts diff --git a/codegen.json b/codegen.json index 83eaf7bf530..b584d84cc68 100644 --- a/codegen.json +++ b/codegen.json @@ -46,6 +46,9 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { + "RemoveAgendaItemPayload": "./types/RemoveAgendaItemPayload#RemoveAgendaItemPayloadSource", + "AddAgendaItemPayload": "./types/AddAgendaItemPayload#AddAgendaItemPayloadSource", + "UpdateAgendaItemPayload": "./types/UpdateAgendaItemPayload#UpdateAgendaItemPayloadSource", "TeamMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB", "TeamPromptMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB", "PokerMeetingSettings": "../../postgres/types/index#PokerMeetingSettings as PokerMeetingSettingsDB", @@ -66,7 +69,7 @@ "AddTeamMemberIntegrationAuthSuccess": "./types/AddTeamMemberIntegrationAuthPayload#AddTeamMemberIntegrationAuthSuccessSource", "AddTranscriptionBotSuccess": "./types/AddTranscriptionBotSuccess#AddTranscriptionBotSuccessSource", "AddedNotification": "./types/AddedNotification#AddedNotificationSource", - "AgendaItem": "../../database/types/AgendaItem#default as AgendaItemDB", + "AgendaItem": "../../postgres/types/index#AgendaItem as AgendaItemDB", "ArchiveTeamPayload": "./types/ArchiveTeamPayload#ArchiveTeamPayloadSource", "AtlassianIntegration": "../../postgres/queries/getAtlassianAuthByUserIdTeamId#AtlassianAuth as AtlassianAuthDB", "AuthTokenPayload": "./types/AuthTokenPayload#AuthTokenPayloadSource", diff --git a/packages/client/modules/teamDashboard/components/AgendaInput/AgendaInput.tsx b/packages/client/modules/teamDashboard/components/AgendaInput/AgendaInput.tsx index 774fe017c27..25f37cfee19 100644 --- a/packages/client/modules/teamDashboard/components/AgendaInput/AgendaInput.tsx +++ b/packages/client/modules/teamDashboard/components/AgendaInput/AgendaInput.tsx @@ -12,11 +12,11 @@ import useHotkey from '../../../../hooks/useHotkey' import useMutationProps from '../../../../hooks/useMutationProps' import useTooltip from '../../../../hooks/useTooltip' import AddAgendaItemMutation from '../../../../mutations/AddAgendaItemMutation' +import {positionAfter} from '../../../../shared/sortOrder' import makeFieldColorPalette from '../../../../styles/helpers/makeFieldColorPalette' import makePlaceholderStyles from '../../../../styles/helpers/makePlaceholderStyles' import {PALETTE} from '../../../../styles/paletteV3' import ui from '../../../../styles/ui' -import getNextSortOrder from '../../../../utils/getNextSortOrder' import toTeamMemberId from '../../../../utils/relay/toTeamMemberId' const AgendaInputBlock = styled('div')({ @@ -123,7 +123,7 @@ const AgendaInput = (props: Props) => { const newAgendaItem = { content, pinned: false, - sortOrder: getNextSortOrder(agendaItems), + sortOrder: positionAfter(agendaItems.at(-1)?.sortOrder ?? ''), teamId, teamMemberId: toTeamMemberId(teamId, atmosphere.viewerId) } diff --git a/packages/client/modules/teamDashboard/components/AgendaList/AgendaList.tsx b/packages/client/modules/teamDashboard/components/AgendaList/AgendaList.tsx index 2ff4a1a27ea..b17b5291aea 100644 --- a/packages/client/modules/teamDashboard/components/AgendaList/AgendaList.tsx +++ b/packages/client/modules/teamDashboard/components/AgendaList/AgendaList.tsx @@ -9,9 +9,9 @@ import useAtmosphere from '../../../../hooks/useAtmosphere' import useEventCallback from '../../../../hooks/useEventCallback' import useGotoStageId from '../../../../hooks/useGotoStageId' import UpdateAgendaItemMutation from '../../../../mutations/UpdateAgendaItemMutation' +import {getSortOrder} from '../../../../shared/sortOrder' import {navItemRaised} from '../../../../styles/elevation' -import {AGENDA_ITEM, SORT_STEP} from '../../../../utils/constants' -import dndNoise from '../../../../utils/dndNoise' +import {AGENDA_ITEM} from '../../../../utils/constants' import AgendaItem from '../AgendaItem/AgendaItem' import AgendaListEmptyState from './AgendaListEmptyState' @@ -83,17 +83,7 @@ const AgendaList = (props: Props) => { return } - let sortOrder - if (destination.index === 0) { - sortOrder = destinationItem.sortOrder - SORT_STEP + dndNoise() - } else if (destination.index === agendaItems.length - 1) { - sortOrder = destinationItem.sortOrder + SORT_STEP + dndNoise() - } else { - const offset = source.index > destination.index ? -1 : 1 - sortOrder = - (agendaItems[destination.index + offset]!.sortOrder + destinationItem.sortOrder) / 2 + - dndNoise() - } + const sortOrder = getSortOrder(agendaItems, source.index, destination.index) UpdateAgendaItemMutation( atmosphere, {updatedAgendaItem: {id: sourceItem.id, sortOrder}}, diff --git a/packages/server/database/rethinkDriver.ts b/packages/server/database/rethinkDriver.ts index f36a3a73eef..18de76dcfcd 100644 --- a/packages/server/database/rethinkDriver.ts +++ b/packages/server/database/rethinkDriver.ts @@ -5,7 +5,6 @@ import TeamInvitation from '../database/types/TeamInvitation' import {AnyMeeting, AnyMeetingTeamMember} from '../postgres/types/Meeting' import getRethinkConfig from './getRethinkConfig' import {R} from './stricterR' -import AgendaItem from './types/AgendaItem' import Comment from './types/Comment' import MassInvitation from './types/MassInvitation' import NotificationKickedOut from './types/NotificationKickedOut' @@ -24,10 +23,6 @@ import RetrospectivePrompt from './types/RetrospectivePrompt' import Task from './types/Task' export type RethinkSchema = { - AgendaItem: { - type: AgendaItem - index: 'teamId' | 'meetingId' - } Comment: { type: Comment index: 'discussionId' diff --git a/packages/server/database/types/AgendaItem.ts b/packages/server/database/types/AgendaItem.ts deleted file mode 100644 index e88304fe36c..00000000000 --- a/packages/server/database/types/AgendaItem.ts +++ /dev/null @@ -1,61 +0,0 @@ -import generateUID from '../../generateUID' - -export interface AgendaItemInput { - id?: string - createdAt?: Date - isActive?: boolean - isComplete?: boolean - sortOrder?: number - teamId: string - teamMemberId: string - updatedAt?: Date - content: string - meetingId?: string - pinned?: boolean - pinnedParentId?: string -} - -export default class AgendaItem { - id: string - content: string - createdAt: Date - isActive: boolean - isComplete: boolean - sortOrder: number - teamId: string - teamMemberId: string - updatedAt: Date - meetingId?: string - pinned?: boolean - pinnedParentId?: string - - constructor(input: AgendaItemInput) { - const { - id, - createdAt, - isActive, - isComplete, - sortOrder, - teamId, - teamMemberId, - updatedAt, - content, - meetingId, - pinned, - pinnedParentId - } = input - const now = new Date() - this.id = id || generateUID() - this.createdAt = createdAt || now - this.isActive = isActive ?? true - this.isComplete = isComplete ?? false - this.sortOrder = sortOrder || 0 - this.teamId = teamId - this.teamMemberId = teamMemberId - this.updatedAt = updatedAt || now - this.content = content || '' - this.meetingId = meetingId - this.pinned = pinned - this.pinnedParentId = pinnedParentId - } -} diff --git a/packages/server/dataloader/foreignKeyLoaderMakers.ts b/packages/server/dataloader/foreignKeyLoaderMakers.ts index 51e21b4ca0b..d5855cf4e3c 100644 --- a/packages/server/dataloader/foreignKeyLoaderMakers.ts +++ b/packages/server/dataloader/foreignKeyLoaderMakers.ts @@ -172,8 +172,8 @@ export const teamPromptResponsesByMeetingId = foreignKeyLoaderMaker( getTeamPromptResponsesByMeetingIds ) -export const _pgagendaItemsByTeamId = foreignKeyLoaderMaker( - '_pgagendaItems', +export const agendaItemsByTeamId = foreignKeyLoaderMaker( + 'agendaItems', 'teamId', async (teamIds) => { return selectAgendaItems() @@ -184,8 +184,8 @@ export const _pgagendaItemsByTeamId = foreignKeyLoaderMaker( } ) -export const _pgagendaItemsByMeetingId = foreignKeyLoaderMaker( - '_pgagendaItems', +export const agendaItemsByMeetingId = foreignKeyLoaderMaker( + 'agendaItems', 'meetingId', async (meetingIds) => { return selectAgendaItems().where('meetingId', 'in', meetingIds).orderBy('sortOrder').execute() diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index 0849187482d..7ac2ed6389d 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -92,6 +92,6 @@ export const meetingSettings = primaryKeyLoaderMaker((ids: readonly string[]) => return selectMeetingSettings().where('id', 'in', ids).execute() }) -export const _pgagendaItems = primaryKeyLoaderMaker((ids: readonly string[]) => { +export const agendaItems = primaryKeyLoaderMaker((ids: readonly string[]) => { return selectAgendaItems().where('id', 'in', ids).execute() }) diff --git a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts index 8cde8dc0bb6..24afe08e897 100644 --- a/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkForeignKeyLoaderMakers.ts @@ -16,33 +16,6 @@ export const activeMeetingsByTeamId = new RethinkForeignKeyLoaderMaker( } ) -export const agendaItemsByTeamId = new RethinkForeignKeyLoaderMaker( - 'agendaItems', - 'teamId', - async (teamIds) => { - const r = await getRethink() - return r - .table('AgendaItem') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter({isActive: true}) - .orderBy('sortOrder') - .run() - } -) - -export const agendaItemsByMeetingId = new RethinkForeignKeyLoaderMaker( - 'agendaItems', - 'meetingId', - async (meetingIds) => { - const r = await getRethink() - return r - .table('AgendaItem') - .getAll(r.args(meetingIds), {index: 'meetingId'}) - .orderBy('sortOrder') - .run() - } -) - export const commentsByDiscussionId = new RethinkForeignKeyLoaderMaker( 'comments', 'discussionId', diff --git a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts index b16d76e3adf..e2840c3ed0a 100644 --- a/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/rethinkPrimaryKeyLoaderMakers.ts @@ -3,7 +3,6 @@ import RethinkPrimaryKeyLoaderMaker from './RethinkPrimaryKeyLoaderMaker' /** * all rethink dataloader types which also must exist in {@link rethinkDriver/RethinkSchema} */ -export const agendaItems = new RethinkPrimaryKeyLoaderMaker('AgendaItem') 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/addAgendaItem.ts b/packages/server/graphql/mutations/addAgendaItem.ts deleted file mode 100644 index c6fc4425fc0..00000000000 --- a/packages/server/graphql/mutations/addAgendaItem.ts +++ /dev/null @@ -1,83 +0,0 @@ -import {GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import makeAgendaItemSchema from 'parabol-client/validation/makeAgendaItemSchema' -import {positionAfter} from '../../../client/shared/sortOrder' -import getRethink from '../../database/rethinkDriver' -import AgendaItem, {AgendaItemInput} from '../../database/types/AgendaItem' -import generateUID from '../../generateUID' -import getKysely from '../../postgres/getKysely' -import {analytics} from '../../utils/analytics/analytics' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import AddAgendaItemPayload from '../types/AddAgendaItemPayload' -import CreateAgendaItemInput, {CreateAgendaItemInputType} from '../types/CreateAgendaItemInput' -import {GQLContext} from './../graphql' -import addAgendaItemToActiveActionMeeting from './helpers/addAgendaItemToActiveActionMeeting' - -export default { - type: AddAgendaItemPayload, - description: 'Create a new agenda item', - args: { - newAgendaItem: { - type: new GraphQLNonNull(CreateAgendaItemInput), - description: 'The new task including an id, teamMemberId, and content' - } - }, - async resolve( - _source: unknown, - {newAgendaItem}: {newAgendaItem: CreateAgendaItemInputType}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - const viewerId = getUserId(authToken) - // AUTH - const {teamId} = newAgendaItem - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - const viewer = await dataLoader.get('users').loadNonNull(viewerId) - // VALIDATION - const schema = makeAgendaItemSchema() - const {errors, data: validNewAgendaItem} = schema(newAgendaItem) - if (Object.keys(errors).length) { - return standardError(new Error('Failed input validation'), {userId: viewerId}) - } - - // RESOLUTION - const teamAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) - const lastAgendaItem = teamAgendaItems.at(-1) - const lastSortOrder = lastAgendaItem?.sortOrder ? String(lastAgendaItem.sortOrder) : '' - // this is just during the migration of AgendaItem table - const sortOrder = positionAfter(lastSortOrder) - const agendaItemId = `${teamId}::${generateUID()}` - await r - .table('AgendaItem') - .insert( - new AgendaItem({ - ...validNewAgendaItem, - teamId - } as AgendaItemInput) - ) - .run() - await getKysely() - .insertInto('AgendaItem') - .values({ - id: agendaItemId, - content: newAgendaItem.content, - meetingId: newAgendaItem.meetingId, - pinned: newAgendaItem.pinned, - sortOrder, - teamId, - teamMemberId: newAgendaItem.teamMemberId - }) - .execute() - const meetingId = await addAgendaItemToActiveActionMeeting(agendaItemId, teamId, dataLoader) - analytics.addedAgendaItem(viewer, teamId, meetingId) - const data = {agendaItemId, meetingId} - publish(SubscriptionChannel.TEAM, teamId, 'AddAgendaItemPayload', data, subOptions) - return data - } -} diff --git a/packages/server/graphql/mutations/endCheckIn.ts b/packages/server/graphql/mutations/endCheckIn.ts index c5bfbca7a28..a3e0c2573da 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 AgendaItem from '../../database/types/AgendaItem' 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 archiveTasksForDB from '../../safeMutations/archiveTasksForDB' import removeSuggestedAction from '../../safeMutations/removeSuggestedAction' import {Logger} from '../../utils/Logger' @@ -69,15 +69,7 @@ const clearAgendaItems = async (teamId: string, dataLoader: DataLoaderInstance) .set({isActive: false}) .where('teamId', '=', teamId) .execute() - const r = await getRethink() dataLoader.clearAll('agendaItems') - return r - .table('AgendaItem') - .getAll(teamId, {index: 'teamId'}) - .update({ - isActive: false - }) - .run() } const getPinnedAgendaItems = async (teamId: string, dataLoader: DataLoaderInstance) => { @@ -89,30 +81,22 @@ const clonePinnedAgendaItems = async ( pinnedAgendaItems: AgendaItem[], dataLoader: DataLoaderInstance ) => { - const r = await getRethink() + let curSortOrder = '' const clonedPins = pinnedAgendaItems.map((agendaItem) => { const agendaItemId = `${agendaItem.teamId}::${generateUID()}` - return new AgendaItem({ + const sortOrder = positionAfter(curSortOrder) + curSortOrder = sortOrder + return { id: agendaItemId, content: agendaItem.content, pinned: agendaItem.pinned, pinnedParentId: agendaItem.pinnedParentId ? agendaItem.pinnedParentId : agendaItemId, - sortOrder: agendaItem.sortOrder, + sortOrder, teamId: agendaItem.teamId, teamMemberId: agendaItem.teamMemberId - }) - }) - await r.table('AgendaItem').insert(clonedPins).run() - let curSortOrder = '' - const pgClonedPins = clonedPins.map((agendaItems) => { - const sortOrder = positionAfter(curSortOrder) - curSortOrder = sortOrder - return { - ...agendaItems, - sortOrder } }) - await getKysely().insertInto('AgendaItem').values(pgClonedPins).execute() + await getKysely().insertInto('AgendaItem').values(clonedPins).execute() dataLoader.clearAll('agendaItems') } diff --git a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index db01b387b0d..4ceb9ab23c9 100644 --- a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts +++ b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts @@ -47,7 +47,6 @@ const addAgendaItemToActiveActionMeeting = async ( updatedAt: now }) .run(), - r.table('AgendaItem').get(agendaItemId).update({meetingId: meetingId}).run(), getKysely().updateTable('AgendaItem').set({meetingId}).where('id', '=', agendaItemId).execute(), insertDiscussions([ { diff --git a/packages/server/graphql/mutations/removeAgendaItem.ts b/packages/server/graphql/mutations/removeAgendaItem.ts deleted file mode 100644 index 941c0f9dc30..00000000000 --- a/packages/server/graphql/mutations/removeAgendaItem.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {GraphQLID, GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getRethink from '../../database/rethinkDriver' -import AgendaItemsStage from '../../database/types/AgendaItemsStage' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import RemoveAgendaItemPayload from '../types/RemoveAgendaItemPayload' -import removeStagesFromMeetings from './helpers/removeStagesFromMeetings' - -export default { - type: RemoveAgendaItemPayload, - description: 'Remove an agenda item', - args: { - agendaItemId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The agenda item unique id' - } - }, - async resolve( - _source: unknown, - {agendaItemId}: {agendaItemId: string}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const r = await getRethink() - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - const viewerId = getUserId(authToken) - - // AUTH - // id is of format 'teamId::randomId' - const [teamId] = agendaItemId.split('::') as [string] - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // RESOLUTION - const agendaItem = await r - .table('AgendaItem') - .get(agendaItemId) - .update({isActive: false}, {returnChanges: true})('changes')(0)('old_val') - .default(null) - .run() - await getKysely() - .updateTable('AgendaItem') - .set({isActive: false}) - .where('id', '=', agendaItemId) - .returning('id') - .execute() - if (!agendaItem) { - return standardError(new Error('Agenda item not found'), {userId: viewerId}) - } - const filterFn = (stage: AgendaItemsStage) => stage.agendaItemId === agendaItemId - await removeStagesFromMeetings(filterFn, teamId, dataLoader) - const data = {agendaItem, meetingId: agendaItem.meetingId} - publish(SubscriptionChannel.TEAM, teamId, 'RemoveAgendaItemPayload', data, subOptions) - return data - } -} diff --git a/packages/server/graphql/mutations/updateAgendaItem.ts b/packages/server/graphql/mutations/updateAgendaItem.ts deleted file mode 100644 index 46d08998dbc..00000000000 --- a/packages/server/graphql/mutations/updateAgendaItem.ts +++ /dev/null @@ -1,111 +0,0 @@ -import {GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import makeUpdateAgendaItemSchema from 'parabol-client/validation/makeUpdateAgendaItemSchema' -import {getSortOrder} from '../../../client/shared/sortOrder' -import getRethink from '../../database/rethinkDriver' -import AgendaItemsStage from '../../database/types/AgendaItemsStage' -import getKysely from '../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../utils/authorization' -import getPhase from '../../utils/getPhase' -import publish from '../../utils/publish' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import UpdateAgendaItemInput, {UpdateAgendaItemInputType} from '../types/UpdateAgendaItemInput' -import UpdateAgendaItemPayload from '../types/UpdateAgendaItemPayload' - -export default { - type: UpdateAgendaItemPayload, - description: 'Update an agenda item', - args: { - updatedAgendaItem: { - type: new GraphQLNonNull(UpdateAgendaItemInput), - description: 'The updated item including an id, content, status, sortOrder' - } - }, - async resolve( - _source: unknown, - {updatedAgendaItem}: {updatedAgendaItem: UpdateAgendaItemInputType}, - {authToken, dataLoader, socketId: mutatorId}: GQLContext - ) { - const now = new Date() - const r = await getRethink() - const pg = getKysely() - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - const viewerId = getUserId(authToken) - - // AUTH - const {id: agendaItemId} = updatedAgendaItem - const [teamId] = agendaItemId.split('::') as [string] - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Team not found'), {userId: viewerId}) - } - - // VALIDATION - const schema = makeUpdateAgendaItemSchema() - const { - errors, - data: {id, ...doc} - } = schema(updatedAgendaItem) as any - if (Object.keys(errors).length) { - return standardError(new Error('Failed input validation'), {userId: viewerId}) - } - - // RESOLUTION - const oldAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) - const fromIdx = oldAgendaItems.findIndex((agendaItem) => agendaItem.id === id) - await r - .table('AgendaItem') - .get(id) - .update({ - ...doc, - updatedAt: now - }) - .run() - dataLoader.clearAll('agendaItems') - if (doc.sortOrder !== null && doc.sortOrder !== undefined) { - const nextAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) - const pgagendaItems = await dataLoader.get('_pgagendaItemsByTeamId').load(teamId) - const toIdx = nextAgendaItems.findIndex((agendaItem) => agendaItem.id === id) - const pgSortOrder = getSortOrder(pgagendaItems, fromIdx, toIdx) - await pg - .updateTable('AgendaItem') - .set({sortOrder: pgSortOrder}) - .where('id', '=', id) - .execute() - } else { - await pg - .updateTable('AgendaItem') - .set({pinned: doc.pinned, content: doc.content}) - .where('id', '=', id) - .execute() - } - - const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) - const actionMeeting = activeMeetings.find( - (activeMeeting) => activeMeeting.meetingType === 'action' - ) - const meetingId = actionMeeting?.id ?? null - if (actionMeeting) { - const {id: meetingId, phases} = actionMeeting - const agendaItemPhase = getPhase(phases, 'agendaitems') - const {stages} = agendaItemPhase - const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) - const getSortOrder = (stage: AgendaItemsStage) => { - const agendaItem = agendaItems.find((item) => item.id === stage.agendaItemId) - return (agendaItem && agendaItem.sortOrder) || 0 - } - stages.sort((a, b) => (getSortOrder(a) > getSortOrder(b) ? 1 : -1)) - await r - .table('NewMeeting') - .get(meetingId) - .update({ - phases - }) - .run() - } - const data = {agendaItemId, meetingId} - publish(SubscriptionChannel.TEAM, teamId, 'UpdateAgendaItemPayload', data, subOptions) - return data - } -} diff --git a/packages/server/graphql/private/mutations/hardDeleteUser.ts b/packages/server/graphql/private/mutations/hardDeleteUser.ts index 7bf8932ca67..cb7a05dc94b 100644 --- a/packages/server/graphql/private/mutations/hardDeleteUser.ts +++ b/packages/server/graphql/private/mutations/hardDeleteUser.ts @@ -67,7 +67,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( dataLoader.get('meetingMembersByUserId').load(userIdToDelete) ]) const teamIds = teamMembers.map(({teamId}) => teamId) - const teamMemberIds = teamMembers.map(({id}) => id) const meetingIds = meetingMembers.map(({meetingId}) => meetingId) const discussions = await pg.query(`SELECT "id" FROM "Discussion" WHERE "teamId" = ANY ($1);`, [ @@ -94,11 +93,6 @@ const hardDeleteUser: MutationResolvers['hardDeleteUser'] = async ( .getAll(r.args(teamIds), {index: 'teamId'}) .filter((row: RValue) => row('createdBy').eq(userIdToDelete)) .delete(), - agendaItem: r - .table('AgendaItem') - .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: RValue) => r(teamMemberIds).contains(row('teamMemberId'))) - .delete(), pushInvitation: r.table('PushInvitation').getAll(userIdToDelete, {index: 'userId'}).delete(), slackNotification: r .table('SlackNotification') diff --git a/packages/server/graphql/public/mutations/addAgendaItem.ts b/packages/server/graphql/public/mutations/addAgendaItem.ts new file mode 100644 index 00000000000..ece26b8bf00 --- /dev/null +++ b/packages/server/graphql/public/mutations/addAgendaItem.ts @@ -0,0 +1,57 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import makeAgendaItemSchema from 'parabol-client/validation/makeAgendaItemSchema' +import {positionAfter} from '../../../../client/shared/sortOrder' +import generateUID from '../../../generateUID' +import getKysely from '../../../postgres/getKysely' +import {analytics} from '../../../utils/analytics/analytics' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import addAgendaItemToActiveActionMeeting from '../../mutations/helpers/addAgendaItemToActiveActionMeeting' +import {MutationResolvers} from '../resolverTypes' + +const addAgendaItem: MutationResolvers['addAgendaItem'] = async ( + _source, + {newAgendaItem}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const viewerId = getUserId(authToken) + // AUTH + const {teamId} = newAgendaItem + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + const viewer = await dataLoader.get('users').loadNonNull(viewerId) + // VALIDATION + const schema = makeAgendaItemSchema() + const {errors} = schema(newAgendaItem) + if (Object.keys(errors).length) { + return standardError(new Error('Failed input validation'), {userId: viewerId}) + } + + // RESOLUTION + const teamAgendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const lastAgendaItem = teamAgendaItems.at(-1) + const agendaItemId = `${teamId}::${generateUID()}` + await getKysely() + .insertInto('AgendaItem') + .values({ + id: agendaItemId, + content: newAgendaItem.content, + meetingId: newAgendaItem.meetingId, + pinned: newAgendaItem.pinned, + sortOrder: positionAfter(lastAgendaItem?.sortOrder ?? ''), + teamId, + teamMemberId: newAgendaItem.teamMemberId + }) + .execute() + const meetingId = await addAgendaItemToActiveActionMeeting(agendaItemId, teamId, dataLoader) + analytics.addedAgendaItem(viewer, teamId, meetingId) + const data = {agendaItemId, meetingId} + publish(SubscriptionChannel.TEAM, teamId, 'AddAgendaItemPayload', data, subOptions) + return data +} + +export default addAgendaItem diff --git a/packages/server/graphql/public/mutations/removeAgendaItem.ts b/packages/server/graphql/public/mutations/removeAgendaItem.ts new file mode 100644 index 00000000000..58b964ae9f8 --- /dev/null +++ b/packages/server/graphql/public/mutations/removeAgendaItem.ts @@ -0,0 +1,43 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import AgendaItemsStage from '../../../database/types/AgendaItemsStage' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import removeStagesFromMeetings from '../../mutations/helpers/removeStagesFromMeetings' +import {MutationResolvers} from '../resolverTypes' + +const removeAgendaItem: MutationResolvers['removeAgendaItem'] = async ( + _source, + {agendaItemId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const viewerId = getUserId(authToken) + + // AUTH + // id is of format 'teamId::randomId' + const [teamId] = agendaItemId.split('::') as [string] + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // RESOLUTION + const agendaItem = await getKysely() + .updateTable('AgendaItem') + .set({isActive: false}) + .where('id', '=', agendaItemId) + .returning(['id', 'meetingId']) + .executeTakeFirst() + if (!agendaItem) { + return standardError(new Error('Agenda item not found'), {userId: viewerId}) + } + const filterFn = (stage: AgendaItemsStage) => stage.agendaItemId === agendaItemId + await removeStagesFromMeetings(filterFn, teamId, dataLoader) + const data = {agendaItemId, meetingId: agendaItem.meetingId} + publish(SubscriptionChannel.TEAM, teamId, 'RemoveAgendaItemPayload', data, subOptions) + return data +} + +export default removeAgendaItem diff --git a/packages/server/graphql/public/mutations/startCheckIn.ts b/packages/server/graphql/public/mutations/startCheckIn.ts index a832930b505..4b22fb54069 100644 --- a/packages/server/graphql/public/mutations/startCheckIn.ts +++ b/packages/server/graphql/public/mutations/startCheckIn.ts @@ -90,7 +90,6 @@ const startCheckIn: MutationResolvers['startCheckIn'] = async ( .insert(new ActionMeetingMember({meetingId, userId: viewerId, teamId})) .run(), updateTeamByTeamId(updates, teamId), - r.table('AgendaItem').getAll(r.args(agendaItemIds)).update({meetingId}).run(), getKysely() .updateTable('AgendaItem') .set({meetingId}) diff --git a/packages/server/graphql/public/mutations/updateAgendaItem.ts b/packages/server/graphql/public/mutations/updateAgendaItem.ts new file mode 100644 index 00000000000..e9fff80393e --- /dev/null +++ b/packages/server/graphql/public/mutations/updateAgendaItem.ts @@ -0,0 +1,75 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import makeUpdateAgendaItemSchema from 'parabol-client/validation/makeUpdateAgendaItemSchema' +import getRethink from '../../../database/rethinkDriver' +import AgendaItemsStage from '../../../database/types/AgendaItemsStage' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import getPhase from '../../../utils/getPhase' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const updateAgendaItem: MutationResolvers['updateAgendaItem'] = async ( + _source, + {updatedAgendaItem}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const pg = getKysely() + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const viewerId = getUserId(authToken) + + // AUTH + const {id: agendaItemId} = updatedAgendaItem + const [teamId] = agendaItemId.split('::') as [string] + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + // VALIDATION + const schema = makeUpdateAgendaItemSchema() + const { + errors, + data: {id, ...doc} + } = schema(updatedAgendaItem) as any + if (Object.keys(errors).length) { + return standardError(new Error('Failed input validation'), {userId: viewerId}) + } + + // RESOLUTION + await pg + .updateTable('AgendaItem') + .set({pinned: doc.pinned, content: doc.content, sortOrder: doc.sortOrder}) + .where('id', '=', id) + .execute() + + const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId) + const actionMeeting = activeMeetings.find( + (activeMeeting) => activeMeeting.meetingType === 'action' + ) + const meetingId = actionMeeting?.id ?? null + if (actionMeeting) { + const {id: meetingId, phases} = actionMeeting + const agendaItemPhase = getPhase(phases, 'agendaitems') + const {stages} = agendaItemPhase + const agendaItems = await dataLoader.get('agendaItemsByTeamId').load(teamId) + const getSortOrder = (stage: AgendaItemsStage) => { + const agendaItem = agendaItems.find((item) => item.id === stage.agendaItemId) + return (agendaItem && agendaItem.sortOrder) || 0 + } + stages.sort((a, b) => (getSortOrder(a) > getSortOrder(b) ? 1 : -1)) + await r + .table('NewMeeting') + .get(meetingId) + .update({ + phases + }) + .run() + } + const data = {agendaItemId, meetingId} + publish(SubscriptionChannel.TEAM, teamId, 'UpdateAgendaItemPayload', data, subOptions) + return data +} + +export default updateAgendaItem diff --git a/packages/server/graphql/public/typeDefs/AgendaItem.graphql b/packages/server/graphql/public/typeDefs/AgendaItem.graphql index a2871a78b03..c985ed2bf38 100644 --- a/packages/server/graphql/public/typeDefs/AgendaItem.graphql +++ b/packages/server/graphql/public/typeDefs/AgendaItem.graphql @@ -40,7 +40,7 @@ type AgendaItem { """ The sort order of the agenda item in the list """ - sortOrder: Float! + sortOrder: String! """ *The team for this agenda item diff --git a/packages/server/graphql/public/typeDefs/CreateAgendaItemInput.graphql b/packages/server/graphql/public/typeDefs/CreateAgendaItemInput.graphql index d04e556d7de..17981db5e0a 100644 --- a/packages/server/graphql/public/typeDefs/CreateAgendaItemInput.graphql +++ b/packages/server/graphql/public/typeDefs/CreateAgendaItemInput.graphql @@ -18,7 +18,7 @@ input CreateAgendaItemInput { """ The sort order of the agenda item in the list """ - sortOrder: Float + sortOrder: String """ The meeting ID of the agenda item diff --git a/packages/server/graphql/public/typeDefs/UpdateAgendaItemInput.graphql b/packages/server/graphql/public/typeDefs/UpdateAgendaItemInput.graphql index d0110ae03ca..ce4bea7817b 100644 --- a/packages/server/graphql/public/typeDefs/UpdateAgendaItemInput.graphql +++ b/packages/server/graphql/public/typeDefs/UpdateAgendaItemInput.graphql @@ -22,5 +22,5 @@ input UpdateAgendaItemInput { """ The sort order of the agenda item in the list """ - sortOrder: Float + sortOrder: String } diff --git a/packages/server/graphql/public/types/ActionMeeting.ts b/packages/server/graphql/public/types/ActionMeeting.ts index daed42c627f..f6a7e04939f 100644 --- a/packages/server/graphql/public/types/ActionMeeting.ts +++ b/packages/server/graphql/public/types/ActionMeeting.ts @@ -5,7 +5,7 @@ import {ActionMeetingResolvers} from '../resolverTypes' const ActionMeeting: ActionMeetingResolvers = { agendaItem: async ({id: meetingId}, {agendaItemId}, {dataLoader}) => { - const agendaItem = await dataLoader.get('agendaItems').load(agendaItemId) + const agendaItem = await dataLoader.get('agendaItems').loadNonNull(agendaItemId) if (agendaItem.meetingId !== meetingId) return null return agendaItem }, diff --git a/packages/server/graphql/public/types/AddAgendaItemPayload.ts b/packages/server/graphql/public/types/AddAgendaItemPayload.ts new file mode 100644 index 00000000000..f82f7966890 --- /dev/null +++ b/packages/server/graphql/public/types/AddAgendaItemPayload.ts @@ -0,0 +1,24 @@ +import {AddAgendaItemPayloadResolvers} from '../resolverTypes' + +export type AddAgendaItemPayloadSource = + | { + agendaItemId: string + meetingId?: string + } + | {error: {message: string}} + +const AddAgendaItemPayload: AddAgendaItemPayloadResolvers = { + agendaItem: (source, _args, {dataLoader}) => { + return 'agendaItemId' in source + ? dataLoader.get('agendaItems').loadNonNull(source.agendaItemId) + : null + }, + + meeting: (source, _args, {dataLoader}) => { + return 'meetingId' in source && source.meetingId + ? dataLoader.get('newMeetings').load(source.meetingId) + : null + } +} + +export default AddAgendaItemPayload diff --git a/packages/server/graphql/public/types/AgendaItemsStage.ts b/packages/server/graphql/public/types/AgendaItemsStage.ts index d260932a88f..1dcfed67720 100644 --- a/packages/server/graphql/public/types/AgendaItemsStage.ts +++ b/packages/server/graphql/public/types/AgendaItemsStage.ts @@ -3,7 +3,7 @@ import {AgendaItemsStageResolvers} from '../resolverTypes' const AgendaItemsStage: AgendaItemsStageResolvers = { __isTypeOf: ({phaseType}) => phaseType === 'agendaitems', agendaItem: ({agendaItemId}, _args, {dataLoader}) => { - return dataLoader.get('agendaItems').load(agendaItemId) + return dataLoader.get('agendaItems').loadNonNull(agendaItemId) } } diff --git a/packages/server/graphql/public/types/RemoveAgendaItemPayload.ts b/packages/server/graphql/public/types/RemoveAgendaItemPayload.ts new file mode 100644 index 00000000000..3527f938e25 --- /dev/null +++ b/packages/server/graphql/public/types/RemoveAgendaItemPayload.ts @@ -0,0 +1,24 @@ +import {RemoveAgendaItemPayloadResolvers} from '../resolverTypes' + +export type RemoveAgendaItemPayloadSource = + | { + agendaItemId: string + meetingId?: string | null + } + | {error: {message: string}} + +const RemoveAgendaItemPayload: RemoveAgendaItemPayloadResolvers = { + agendaItem: (source, _args, {dataLoader}) => { + return 'agendaItemId' in source + ? dataLoader.get('agendaItems').loadNonNull(source.agendaItemId) + : null + }, + + meeting: (source, _args, {dataLoader}) => { + return 'meetingId' in source && source.meetingId + ? dataLoader.get('newMeetings').load(source.meetingId) + : null + } +} + +export default RemoveAgendaItemPayload diff --git a/packages/server/graphql/public/types/UpdateAgendaItemPayload.ts b/packages/server/graphql/public/types/UpdateAgendaItemPayload.ts new file mode 100644 index 00000000000..f267c8be980 --- /dev/null +++ b/packages/server/graphql/public/types/UpdateAgendaItemPayload.ts @@ -0,0 +1,24 @@ +import {UpdateAgendaItemPayloadResolvers} from '../resolverTypes' + +export type UpdateAgendaItemPayloadSource = + | { + agendaItemId: string + meetingId?: string | null + } + | {error: {message: string}} + +const UpdateAgendaItemPayload: UpdateAgendaItemPayloadResolvers = { + agendaItem: (source, _args, {dataLoader}) => { + return 'agendaItemId' in source + ? dataLoader.get('agendaItems').loadNonNull(source.agendaItemId) + : null + }, + + meeting: (source, _args, {dataLoader}) => { + return 'meetingId' in source && source.meetingId + ? dataLoader.get('newMeetings').load(source.meetingId) + : null + } +} + +export default UpdateAgendaItemPayload diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 91654c128b6..9493f6e092c 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -1,6 +1,5 @@ import {GraphQLObjectType} from 'graphql' import {GQLContext} from './graphql' -import addAgendaItem from './mutations/addAgendaItem' import addAtlassianAuth from './mutations/addAtlassianAuth' import addComment from './mutations/addComment' import addGitHubAuth from './mutations/addGitHubAuth' @@ -63,7 +62,6 @@ import promoteToTeamLead from './mutations/promoteToTeamLead' import pushInvitation from './mutations/pushInvitation' import reflectTemplatePromptUpdateDescription from './mutations/reflectTemplatePromptUpdateDescription' import reflectTemplatePromptUpdateGroupColor from './mutations/reflectTemplatePromptUpdateGroupColor' -import removeAgendaItem from './mutations/removeAgendaItem' import removeAtlassianAuth from './mutations/removeAtlassianAuth' import removeGitHubAuth from './mutations/removeGitHubAuth' import removeIntegrationProvider from './mutations/removeIntegrationProvider' @@ -96,7 +94,6 @@ import setTaskHighlight from './mutations/setTaskHighlight' import startDraggingReflection from './mutations/startDraggingReflection' import startSprintPoker from './mutations/startSprintPoker' import toggleTeamDrawer from './mutations/toggleTeamDrawer' -import updateAgendaItem from './mutations/updateAgendaItem' import updateAzureDevOpsDimensionField from './mutations/updateAzureDevOpsDimensionField' import updateCommentContent from './mutations/updateCommentContent' import updateDragLocation from './mutations/updateDragLocation' @@ -119,7 +116,6 @@ export default new GraphQLObjectType({ name: 'Mutation', fields: () => ({ - addAgendaItem, addAtlassianAuth, addComment, addPokerTemplateDimension, @@ -173,7 +169,6 @@ export default new GraphQLObjectType({ reflectTemplatePromptUpdateDescription, pokerTemplateDimensionUpdateDescription, reflectTemplatePromptUpdateGroupColor, - removeAgendaItem, removeAtlassianAuth, removeGitHubAuth, removeOrgUser, @@ -201,7 +196,6 @@ export default new GraphQLObjectType({ startDraggingReflection, startSprintPoker, setTaskHighlight, - updateAgendaItem, updateCommentContent, oldUpdateCreditCard, updatePokerTemplateDimensionScale, diff --git a/packages/server/graphql/types/AddAgendaItemPayload.ts b/packages/server/graphql/types/AddAgendaItemPayload.ts deleted file mode 100644 index b2960e97088..00000000000 --- a/packages/server/graphql/types/AddAgendaItemPayload.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {GraphQLID, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import AgendaItem from './AgendaItem' -import NewMeeting from './NewMeeting' -import StandardMutationError from './StandardMutationError' - -const AddAgendaItemPayload = new GraphQLObjectType({ - name: 'AddAgendaItemPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - agendaItem: { - type: AgendaItem, - resolve: ({agendaItemId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('agendaItems').load(agendaItemId) - } - }, - meetingId: { - type: GraphQLID - }, - meeting: { - type: NewMeeting, - description: 'The meeting with the updated agenda item, if any', - resolve: ({meetingId}, _args: unknown, {dataLoader}) => { - return meetingId ? dataLoader.get('newMeetings').load(meetingId) : null - } - } - }) -}) - -export default AddAgendaItemPayload diff --git a/packages/server/graphql/types/CreateAgendaItemInput.ts b/packages/server/graphql/types/CreateAgendaItemInput.ts deleted file mode 100644 index 35a9e121f2a..00000000000 --- a/packages/server/graphql/types/CreateAgendaItemInput.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLInputObjectType, - GraphQLNonNull, - GraphQLString -} from 'graphql' - -export type CreateAgendaItemInputType = { - content: string - pinned: boolean - teamId: string - teamMemberId: string - sortOrder?: number - meetingId?: string -} - -const CreateAgendaItemInput = new GraphQLInputObjectType({ - name: 'CreateAgendaItemInput', - fields: () => ({ - content: { - type: new GraphQLNonNull(GraphQLString), - description: 'The content of the agenda item' - }, - pinned: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'True if the agenda item has been pinned' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID) - }, - teamMemberId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The team member ID of the person creating the agenda item' - }, - sortOrder: { - type: GraphQLFloat, - description: 'The sort order of the agenda item in the list' - }, - meetingId: { - type: GraphQLString, - description: 'The meeting ID of the agenda item' - } - }) -}) - -export default CreateAgendaItemInput diff --git a/packages/server/graphql/types/RemoveAgendaItemPayload.ts b/packages/server/graphql/types/RemoveAgendaItemPayload.ts deleted file mode 100644 index 3e41c11a090..00000000000 --- a/packages/server/graphql/types/RemoveAgendaItemPayload.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {GraphQLID, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import AgendaItem from './AgendaItem' -import NewMeeting from './NewMeeting' -import StandardMutationError from './StandardMutationError' - -const RemoveAgendaItemPayload = new GraphQLObjectType({ - name: 'RemoveAgendaItemPayload', - fields: () => ({ - error: { - type: StandardMutationError - }, - agendaItem: { - type: AgendaItem - }, - meetingId: { - type: GraphQLID - }, - meeting: { - type: NewMeeting, - description: 'The meeting with the updated agenda item, if any', - resolve: ({meetingId}, _args: unknown, {dataLoader}) => { - return meetingId ? dataLoader.get('newMeetings').load(meetingId) : null - } - } - }) -}) - -export default RemoveAgendaItemPayload diff --git a/packages/server/graphql/types/UpdateAgendaItemInput.ts b/packages/server/graphql/types/UpdateAgendaItemInput.ts deleted file mode 100644 index 06f581c1407..00000000000 --- a/packages/server/graphql/types/UpdateAgendaItemInput.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - GraphQLBoolean, - GraphQLFloat, - GraphQLID, - GraphQLInputObjectType, - GraphQLNonNull, - GraphQLString -} from 'graphql' - -const UpdateAgendaItemInput = new GraphQLInputObjectType({ - name: 'UpdateAgendaItemInput', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'The unique agenda item ID, composed of a teamId::shortid' - }, - content: { - type: GraphQLString, - description: 'The content of the agenda item' - }, - pinned: { - type: GraphQLBoolean, - description: 'True if agenda item has been pinned' - }, - isActive: { - type: GraphQLBoolean, - description: 'True if not processed or deleted' - }, - sortOrder: { - type: GraphQLFloat, - description: 'The sort order of the agenda item in the list' - } - }) -}) - -export type UpdateAgendaItemInputType = { - id: string - content?: string | null - pinned?: boolean - isActive?: boolean | null - sortOrder?: number | null -} - -export default UpdateAgendaItemInput diff --git a/packages/server/graphql/types/UpdateAgendaItemPayload.ts b/packages/server/graphql/types/UpdateAgendaItemPayload.ts deleted file mode 100644 index d10a6e6155c..00000000000 --- a/packages/server/graphql/types/UpdateAgendaItemPayload.ts +++ /dev/null @@ -1,32 +0,0 @@ -import {GraphQLID, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import AgendaItem from './AgendaItem' -import NewMeeting from './NewMeeting' -import StandardMutationError from './StandardMutationError' - -const UpdateAgendaItemPayload = new GraphQLObjectType({ - name: 'UpdateAgendaItemPayload', - fields: () => ({ - agendaItem: { - type: AgendaItem, - resolve: ({agendaItemId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('agendaItems').load(agendaItemId) - } - }, - meetingId: { - type: GraphQLID - }, - meeting: { - type: NewMeeting, - description: 'The meeting with the updated agenda item, if any', - resolve: ({meetingId}, _args: unknown, {dataLoader}) => { - return meetingId ? dataLoader.get('newMeetings').load(meetingId) : null - } - }, - error: { - type: StandardMutationError - } - }) -}) - -export default UpdateAgendaItemPayload diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index f6706233236..ca262a72b5e 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 { + selectAgendaItems, selectMeetingSettings, selectOrganizations, selectRetroReflections, @@ -44,3 +45,5 @@ export type TemplateScaleRef = ExtractTypeFromQueryBuilderSelect export type PokerMeetingSettings = MeetingSettings & {meetingType: 'poker'} export type RetrospectiveMeetingSettings = MeetingSettings & {meetingType: 'retrospective'} + +export type AgendaItem = ExtractTypeFromQueryBuilderSelect From e669f24ea15f44e9facac9526d61348572936499 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 15 Aug 2024 13:52:19 -0700 Subject: [PATCH 9/9] fix: remove old schema for agenda item validation Signed-off-by: Matt Krick --- .../client/validation/makeAgendaItemSchema.ts | 14 ------------- .../validation/makeUpdateAgendaItemSchema.ts | 15 -------------- .../graphql/public/mutations/addAgendaItem.ts | 7 ++----- .../public/mutations/updateAgendaItem.ts | 20 +++++++++---------- 4 files changed, 11 insertions(+), 45 deletions(-) delete mode 100644 packages/client/validation/makeAgendaItemSchema.ts delete mode 100644 packages/client/validation/makeUpdateAgendaItemSchema.ts diff --git a/packages/client/validation/makeAgendaItemSchema.ts b/packages/client/validation/makeAgendaItemSchema.ts deleted file mode 100644 index 3c3cc027008..00000000000 --- a/packages/client/validation/makeAgendaItemSchema.ts +++ /dev/null @@ -1,14 +0,0 @@ -import legitify from './legitify' -import Legitity from './Legitity' -import {compositeId, id} from './templates' - -export default function makeAgendaItemSchema() { - return legitify({ - content: (value: Legitity) => value.trim().max(63, 'Try something a little shorter'), - isActive: (value: Legitity) => value.boolean(), - pinned: (value: Legitity) => value.boolean(), - sortOrder: (value: Legitity) => value.float(), - teamId: id, - teamMemberId: compositeId - }) -} diff --git a/packages/client/validation/makeUpdateAgendaItemSchema.ts b/packages/client/validation/makeUpdateAgendaItemSchema.ts deleted file mode 100644 index 4967d9ed0bb..00000000000 --- a/packages/client/validation/makeUpdateAgendaItemSchema.ts +++ /dev/null @@ -1,15 +0,0 @@ -import legitify from './legitify' -import Legitity from './Legitity' -import {compositeId, id} from './templates' - -export default function makeUpdateAgendaItemSchema() { - return legitify({ - id: compositeId, - content: (value: Legitity) => value.trim().max(63, 'Try something a little shorter'), - isActive: (value: Legitity) => value.boolean(), - pinned: (value: Legitity) => value.boolean(), - sortOrder: (value: Legitity) => value.float(), - teamId: id, - teamMemberId: compositeId - }) -} diff --git a/packages/server/graphql/public/mutations/addAgendaItem.ts b/packages/server/graphql/public/mutations/addAgendaItem.ts index ece26b8bf00..823be535cc0 100644 --- a/packages/server/graphql/public/mutations/addAgendaItem.ts +++ b/packages/server/graphql/public/mutations/addAgendaItem.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import makeAgendaItemSchema from 'parabol-client/validation/makeAgendaItemSchema' import {positionAfter} from '../../../../client/shared/sortOrder' import generateUID from '../../../generateUID' import getKysely from '../../../postgres/getKysely' @@ -25,10 +24,8 @@ const addAgendaItem: MutationResolvers['addAgendaItem'] = async ( } const viewer = await dataLoader.get('users').loadNonNull(viewerId) // VALIDATION - const schema = makeAgendaItemSchema() - const {errors} = schema(newAgendaItem) - if (Object.keys(errors).length) { - return standardError(new Error('Failed input validation'), {userId: viewerId}) + if (newAgendaItem.content.length > 64) { + return {error: {message: 'Agenda item must be shorter'}} } // RESOLUTION diff --git a/packages/server/graphql/public/mutations/updateAgendaItem.ts b/packages/server/graphql/public/mutations/updateAgendaItem.ts index e9fff80393e..51c506e8fdb 100644 --- a/packages/server/graphql/public/mutations/updateAgendaItem.ts +++ b/packages/server/graphql/public/mutations/updateAgendaItem.ts @@ -1,5 +1,4 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import makeUpdateAgendaItemSchema from 'parabol-client/validation/makeUpdateAgendaItemSchema' import getRethink from '../../../database/rethinkDriver' import AgendaItemsStage from '../../../database/types/AgendaItemsStage' import getKysely from '../../../postgres/getKysely' @@ -21,27 +20,26 @@ const updateAgendaItem: MutationResolvers['updateAgendaItem'] = async ( const viewerId = getUserId(authToken) // AUTH - const {id: agendaItemId} = updatedAgendaItem + const {id: agendaItemId, content, pinned, sortOrder} = updatedAgendaItem const [teamId] = agendaItemId.split('::') as [string] if (!isTeamMember(authToken, teamId)) { return standardError(new Error('Team not found'), {userId: viewerId}) } // VALIDATION - const schema = makeUpdateAgendaItemSchema() - const { - errors, - data: {id, ...doc} - } = schema(updatedAgendaItem) as any - if (Object.keys(errors).length) { - return standardError(new Error('Failed input validation'), {userId: viewerId}) + if (content && content.length > 64) { + return {error: {message: 'Agenda item must be shorter'}} } // RESOLUTION await pg .updateTable('AgendaItem') - .set({pinned: doc.pinned, content: doc.content, sortOrder: doc.sortOrder}) - .where('id', '=', id) + .set({ + pinned: pinned ?? undefined, + content: content ?? undefined, + sortOrder: sortOrder ?? undefined + }) + .where('id', '=', agendaItemId) .execute() const activeMeetings = await dataLoader.get('activeMeetingsByTeamId').load(teamId)