diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0e19973322f..38db86c7257 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.46.0" + ".": "7.46.2" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe32e47683..bb42db53b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ This project adheres to [Semantic Versioning](http://semver.org/). This CHANGELOG follows conventions [outlined here](http://keepachangelog.com/). +## [7.46.2](https://github.com/ParabolInc/parabol/compare/v7.46.1...v7.46.2) (2024-09-06) + + +### Fixed + +* insert discussion before comment ([#10194](https://github.com/ParabolInc/parabol/issues/10194)) ([724a340](https://github.com/ParabolInc/parabol/commit/724a340e5872fc13963cad278bb13017f0ec1270)) + +## [7.46.1](https://github.com/ParabolInc/parabol/compare/v7.46.0...v7.46.1) (2024-09-06) + + +### Fixed + +* multiple slack notifications ([#10190](https://github.com/ParabolInc/parabol/issues/10190)) ([c4444ef](https://github.com/ParabolInc/parabol/commit/c4444ef9814a30bb2659427d17f639dbf151f46e)) + + +### Changed + +* **rethinkdb:** Comment: Phase 2 ([#10180](https://github.com/ParabolInc/parabol/issues/10180)) ([9148205](https://github.com/ParabolInc/parabol/commit/91482054870809d133fbc70af078b033d55c6ace)) + ## [7.46.0](https://github.com/ParabolInc/parabol/compare/v7.45.2...v7.46.0) (2024-09-04) diff --git a/package.json b/package.json index d3864155421..7178b32572c 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.46.0", + "version": "7.46.2", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/chronos/package.json b/packages/chronos/package.json index 60f90f1391d..459a90d9a9a 100644 --- a/packages/chronos/package.json +++ b/packages/chronos/package.json @@ -1,6 +1,6 @@ { "name": "chronos", - "version": "7.46.0", + "version": "7.46.2", "description": "A cron job scheduler", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/chronos#readme", @@ -25,6 +25,6 @@ }, "dependencies": { "cron": "^2.3.1", - "parabol-server": "7.46.0" + "parabol-server": "7.46.2" } } diff --git a/packages/client/package.json b/packages/client/package.json index 9c7eecb93f3..cf308db6ef6 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.46.0", + "version": "7.46.2", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" diff --git a/packages/embedder/package.json b/packages/embedder/package.json index 833139fa5a0..b86bff53eb1 100644 --- a/packages/embedder/package.json +++ b/packages/embedder/package.json @@ -1,6 +1,6 @@ { "name": "parabol-embedder", - "version": "7.46.0", + "version": "7.46.2", "description": "A service that computes embedding vectors from Parabol objects", "author": "Jordan Husney ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/embedder#readme", diff --git a/packages/gql-executor/package.json b/packages/gql-executor/package.json index 488642e3f6f..87ebf8e5828 100644 --- a/packages/gql-executor/package.json +++ b/packages/gql-executor/package.json @@ -1,6 +1,6 @@ { "name": "gql-executor", - "version": "7.46.0", + "version": "7.46.2", "description": "A Stateless GraphQL Executor", "author": "Matt Krick ", "homepage": "https://github.com/ParabolInc/parabol/tree/master/packages/gqlExecutor#readme", @@ -27,8 +27,8 @@ }, "dependencies": { "dd-trace": "^4.2.0", - "parabol-client": "7.46.0", - "parabol-server": "7.46.0", + "parabol-client": "7.46.2", + "parabol-server": "7.46.2", "undici": "^5.26.2" } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 6c027507821..cb1fc4cc465 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -2,7 +2,7 @@ "name": "integration-tests", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.46.0", + "version": "7.46.2", "description": "", "main": "index.js", "scripts": { diff --git a/packages/server/dataloader/integrationAuthLoaders.ts b/packages/server/dataloader/integrationAuthLoaders.ts index 6fda9b74f56..6ae61e334be 100644 --- a/packages/server/dataloader/integrationAuthLoaders.ts +++ b/packages/server/dataloader/integrationAuthLoaders.ts @@ -187,6 +187,7 @@ export const slackNotificationsByTeamIdAndEvent = (parent: RootDataLoader) => { .flat() return keys.map((key) => { + const usedChannelIds = new Set() return res .filter((doc) => doc.teamId === key.teamId && doc.event === key.event) .map((notification) => { @@ -200,6 +201,11 @@ export const slackNotificationsByTeamIdAndEvent = (parent: RootDataLoader) => { } }) .filter(isValid) + .filter(({channelId}) => { + if (!channelId || usedChannelIds.has(channelId)) return false + usedChannelIds.add(channelId) + return true + }) }) }) } diff --git a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts index 4ceb9ab23c9..735ba4be831 100644 --- a/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts +++ b/packages/server/graphql/mutations/helpers/addAgendaItemToActiveActionMeeting.ts @@ -2,7 +2,6 @@ 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,16 +46,20 @@ const addAgendaItemToActiveActionMeeting = async ( updatedAt: now }) .run(), - getKysely().updateTable('AgendaItem').set({meetingId}).where('id', '=', agendaItemId).execute(), - insertDiscussions([ - { - id: discussionId, - teamId, - meetingId, - discussionTopicType: 'agendaItem' as const, - discussionTopicId: agendaItemId - } - ]) + getKysely() + .with('InsertDiscussion', (qb) => + qb.insertInto('Discussion').values({ + id: discussionId, + teamId, + meetingId, + discussionTopicType: 'agendaItem', + discussionTopicId: agendaItemId + }) + ) + .updateTable('AgendaItem') + .set({meetingId}) + .where('id', '=', agendaItemId) + .execute() ]) return meetingId diff --git a/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts b/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts index 56d4115ef63..098392fc6a6 100644 --- a/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts +++ b/packages/server/graphql/mutations/helpers/createNewMeetingPhases.ts @@ -23,7 +23,7 @@ import TeamHealthPhase from '../../../database/types/TeamHealthPhase' import TeamHealthStage from '../../../database/types/TeamHealthStage' import UpdatesPhase from '../../../database/types/UpdatesPhase' import UpdatesStage from '../../../database/types/UpdatesStage' -import insertDiscussions from '../../../postgres/queries/insertDiscussions' +import getKysely from '../../../postgres/getKysely' import {MeetingTypeEnum} from '../../../postgres/types/Meeting' import isPhaseAvailable from '../../../utils/isPhaseAvailable' import {DataLoaderWorker} from '../../graphql' @@ -77,6 +77,7 @@ const createNewMeetingPhases = async ( meetingType: MeetingTypeEnum, dataLoader: DataLoaderWorker ) => { + const pg = getKysely() const [meetingSettings, stageDurations, team] = await Promise.all([ dataLoader.get('meetingSettingsByType').load({teamId, meetingType}), getPastStageDurations(teamId), @@ -108,17 +109,22 @@ const createNewMeetingPhases = async ( case DISCUSS: const discussPhase = new DiscussPhase(durations) const discussStages = discussPhase.stages.filter((stage) => stage.reflectionGroupId) - asyncSideEffects.push( - insertDiscussions( - discussStages.map((stage) => ({ - id: stage.discussionId, - teamId, - meetingId, - discussionTopicId: stage.reflectionGroupId, - discussionTopicType: 'reflectionGroup' as const - })) + if (discussStages.length > 0) { + asyncSideEffects.push( + pg + .insertInto('Discussion') + .values( + discussStages.map((stage) => ({ + id: stage.discussionId, + teamId, + meetingId, + discussionTopicId: stage.reflectionGroupId, + discussionTopicType: 'reflectionGroup' + })) + ) + .execute() ) - ) + } return discussPhase case UPDATES: return new UpdatesPhase({durations, stages: [new UpdatesStage(facilitatorTeamMemberId)]}) @@ -127,14 +133,22 @@ const createNewMeetingPhases = async ( const agendaItemIds = agendaItems.map(({id}) => id) const agendaItemPhase = new AgendaItemsPhase(agendaItemIds, durations) const {stages} = agendaItemPhase - const discussions = stages.map((stage) => ({ - id: stage.discussionId, - teamId, - meetingId, - discussionTopicId: stage.agendaItemId, - discussionTopicType: 'agendaItem' as const - })) - asyncSideEffects.push(insertDiscussions(discussions)) + if (stages.length > 0) { + asyncSideEffects.push( + pg + .insertInto('Discussion') + .values( + stages.map((stage) => ({ + id: stage.discussionId, + teamId, + meetingId, + discussionTopicId: stage.agendaItemId, + discussionTopicType: 'agendaItem' + })) + ) + .execute() + ) + } return agendaItemPhase case 'ESTIMATE': return new EstimatePhase() diff --git a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts index 7bd65438355..a6aa7267f88 100644 --- a/packages/server/graphql/mutations/helpers/handleCompletedStage.ts +++ b/packages/server/graphql/mutations/helpers/handleCompletedStage.ts @@ -6,7 +6,6 @@ import DiscussStage from '../../../database/types/DiscussStage' import GenericMeetingStage from '../../../database/types/GenericMeetingStage' import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import getKysely from '../../../postgres/getKysely' -import insertDiscussions from '../../../postgres/queries/insertDiscussions' import {AnyMeeting} from '../../../postgres/types/Meeting' import {DataLoaderWorker} from '../../graphql' import addAIGeneratedContentToThreads from './addAIGeneratedContentToThreads' @@ -28,12 +27,11 @@ const handleCompletedRetrospectiveStage = async ( meeting: MeetingRetrospective, dataLoader: DataLoaderWorker ) => { + const pg = getKysely() if (stage.phaseType === REFLECT || stage.phaseType === GROUP) { const data: Record = await removeEmptyReflections(meeting, dataLoader) if (stage.phaseType === REFLECT) { - const pg = getKysely() - const [reflectionGroups, unsortedReflections] = await Promise.all([ dataLoader.get('retroReflectionGroupsByMeetingId').load(meeting.id), dataLoader.get('retroReflectionsByMeetingId').load(meeting.id) @@ -93,7 +91,9 @@ const handleCompletedRetrospectiveStage = async ( discussionTopicId: stage.reflectionGroupId })) // discussions must exist before we can add comments to them! - await insertDiscussions(discussions) + if (discussions.length > 0) { + await pg.insertInto('Discussion').values(discussions).execute() + } await Promise.all([ addAIGeneratedContentToThreads(discussPhaseStages, meetingId, dataLoader), publishToEmbedder({jobType: 'relatedDiscussions:start', data: {meetingId}, priority: 0}) diff --git a/packages/server/graphql/mutations/joinMeeting.ts b/packages/server/graphql/mutations/joinMeeting.ts index 7be33d39e2f..c95582aeea1 100644 --- a/packages/server/graphql/mutations/joinMeeting.ts +++ b/packages/server/graphql/mutations/joinMeeting.ts @@ -13,7 +13,7 @@ import RetroMeetingMember from '../../database/types/RetroMeetingMember' import TeamPromptMeetingMember from '../../database/types/TeamPromptMeetingMember' import TeamPromptResponseStage from '../../database/types/TeamPromptResponseStage' import UpdatesStage from '../../database/types/UpdatesStage' -import insertDiscussions from '../../postgres/queries/insertDiscussions' +import getKysely from '../../postgres/getKysely' import {TeamMember} from '../../postgres/types' import {analytics} from '../../utils/analytics/analytics' import {getUserId, isTeamMember} from '../../utils/authorization' @@ -145,15 +145,16 @@ const joinMeeting = { // only add a new stage for the new users (ie. invited to the team after the meeting was started) if (teamMemberResponseStage) return const responsesStage = new TeamPromptResponseStage({teamMemberId}) - await insertDiscussions([ - { + await getKysely() + .insertInto('Discussion') + .values({ id: responsesStage.discussionId, teamId, meetingId, discussionTopicId: teamMemberId, - discussionTopicType: 'teamPromptResponse' as const - } - ]) + discussionTopicType: 'teamPromptResponse' + }) + .execute() return addStageToPhase(responsesStage, 'RESPONSES') } diff --git a/packages/server/graphql/mutations/updatePokerScope.ts b/packages/server/graphql/mutations/updatePokerScope.ts index 0300d2f9832..823a6381383 100644 --- a/packages/server/graphql/mutations/updatePokerScope.ts +++ b/packages/server/graphql/mutations/updatePokerScope.ts @@ -1,12 +1,13 @@ import {GraphQLID, GraphQLList, GraphQLNonNull} from 'graphql' +import {Insertable} from 'kysely' import {SubscriptionChannel, Threshold} from 'parabol-client/types/constEnums' -import {Writeable} from '../../../client/types/generics' import {ESTIMATE_TASK_SORT_ORDER} from '../../../client/utils/constants' import getRethink from '../../database/rethinkDriver' import EstimateStage from '../../database/types/EstimateStage' import MeetingPoker from '../../database/types/MeetingPoker' import {TaskServiceEnum} from '../../database/types/Task' -import insertDiscussions, {InputDiscussions} from '../../postgres/queries/insertDiscussions' +import getKysely from '../../postgres/getKysely' +import {Discussion} from '../../postgres/pg' import RedisLockQueue from '../../utils/RedisLockQueue' import {getUserId, isTeamMember} from '../../utils/authorization' import getPhase from '../../utils/getPhase' @@ -112,7 +113,7 @@ const updatePokerScope = { // add stages const templateRef = await dataLoader.get('templateRefs').loadNonNull(templateRefId) const {dimensions} = templateRef - const newDiscussions = [] as Writeable + const newDiscussions = [] as Insertable[] const additiveUpdates = updates.filter((update) => { const {action, serviceTaskId} = update return action === 'ADD' && !stages.find((stage) => stage.serviceTaskId === serviceTaskId) @@ -168,7 +169,7 @@ const updatePokerScope = { }) .run() if (newDiscussions.length > 0) { - await insertDiscussions(newDiscussions) + await getKysely().insertInto('Discussion').values(newDiscussions).execute() } const data = {meetingId, newStageIds} publish(SubscriptionChannel.MEETING, meetingId, 'UpdatePokerScopeSuccess', data, subOptions) diff --git a/packages/server/package.json b/packages/server/package.json index b540cb84d97..80a7fcc66b2 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -3,7 +3,7 @@ "description": "An open-source app for building smarter, more agile teams.", "author": "Parabol Inc. (http://github.com/ParabolInc)", "license": "AGPL-3.0", - "version": "7.46.0", + "version": "7.46.2", "repository": { "type": "git", "url": "https://github.com/ParabolInc/parabol" @@ -124,7 +124,7 @@ "openai": "^4.53.0", "openapi-fetch": "^0.9.7", "oy-vey": "^0.12.1", - "parabol-client": "7.46.0", + "parabol-client": "7.46.2", "pg": "^8.5.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/server/postgres/migrations/1724884922936_Comment-phase2.ts b/packages/server/postgres/migrations/1724884922936_Comment-phase2.ts new file mode 100644 index 00000000000..896774f9f81 --- /dev/null +++ b/packages/server/postgres/migrations/1724884922936_Comment-phase2.ts @@ -0,0 +1,140 @@ +import {Kysely, PostgresDialect, sql} from 'kysely' +import {r} from 'rethinkdb-ts' +import connectRethinkDB from '../../database/connectRethinkDB' +import getPg from '../getPg' + +export async function up() { + await connectRethinkDB() + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + + try { + console.log('Adding index') + await r + .table('Comment') + .indexCreate('updatedAtId', (row: any) => [row('updatedAt'), row('id')]) + .run() + await r.table('Comment').indexWait().run() + } catch { + // index already exists + } + + console.log('Adding index complete') + + // must truncate because some rows didn't have a threadParentId + await sql`TRUNCATE TABLE "Comment"`.execute(pg) + + const MAX_PG_PARAMS = 65545 + const PG_COLS = [ + 'id', + 'createdAt', + 'updatedAt', + 'isActive', + 'isAnonymous', + 'threadParentId', + 'reactjis', + 'content', + 'createdBy', + 'plaintextContent', + 'discussionId', + 'threadSortOrder' + ] as const + type Comment = { + [K in (typeof PG_COLS)[number]]: any + } + const BATCH_SIZE = Math.trunc(MAX_PG_PARAMS / PG_COLS.length) + + let curUpdatedAt = r.minval + let curId = r.minval + for (let i = 0; i < 1e6; i++) { + console.log('inserting row', i * BATCH_SIZE, String(curUpdatedAt), String(curId)) + const rawRowsToInsert = (await r + .table('Comment') + .between([curUpdatedAt, curId], [r.maxval, r.maxval], { + index: 'updatedAtId', + leftBound: 'open', + rightBound: 'closed' + }) + .orderBy({index: 'updatedAtId'}) + .limit(BATCH_SIZE) + .pluck(...PG_COLS) + .run()) as Comment[] + + const rowsToInsert = rawRowsToInsert + .map((row) => { + const {plaintextContent, threadSortOrder, reactjis, ...rest} = row as any + return { + ...rest, + plaintextContent: plaintextContent.slice(0, 2000), + threadSortOrder: threadSortOrder ? Math.trunc(threadSortOrder) : 0, + reactjis: reactjis?.map((r: any) => `(${r.id},${r.userId})`) ?? [] + } + }) + .filter((row) => row.discussionId) + + if (rowsToInsert.length === 0) break + const lastRow = rowsToInsert[rowsToInsert.length - 1] + curUpdatedAt = lastRow.updatedAt + curId = lastRow.id + await Promise.all( + rowsToInsert.map(async (row) => { + try { + await pg + .insertInto('Comment') + .values(row) + .onConflict((oc) => oc.doNothing()) + .execute() + } catch (e) { + if (e.constraint === 'fk_createdBy') { + await pg + .insertInto('Comment') + .values({...row, createdBy: null}) + .onConflict((oc) => oc.doNothing()) + .execute() + return + } + if (e.constraint === 'fk_discussionId') { + console.log(`Skipping ${row.id} because it has no discussion`) + return + } + console.log(e, row) + } + }) + ) + } + + // if the threadParentId references an id that does not exist, set it to null + console.log('adding threadParentId constraint') + await pg + .updateTable('Comment') + .set({threadParentId: null}) + .where(({eb, selectFrom}) => + eb( + 'id', + 'in', + selectFrom('Comment as child') + .select('child.id') + .leftJoin('Comment as parent', 'child.threadParentId', 'parent.id') + .where('parent.id', 'is', null) + .where('child.threadParentId', 'is not', null) + ) + ) + .execute() + await pg.schema + .alterTable('Comment') + .addForeignKeyConstraint('fk_threadParentId', ['threadParentId'], 'Comment', ['id']) + .onDelete('set null') + .execute() +} + +export async function down() { + const pg = new Kysely({ + dialect: new PostgresDialect({ + pool: getPg() + }) + }) + await sql`TRUNCATE TABLE "Comment" CASCADE`.execute(pg) +} diff --git a/packages/server/postgres/queries/insertDiscussions.ts b/packages/server/postgres/queries/insertDiscussions.ts deleted file mode 100644 index 6e473a40bb8..00000000000 --- a/packages/server/postgres/queries/insertDiscussions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {DeepNonNullable} from '../../../client/types/generics' -import getPg from '../getPg' -import { - IInsertDiscussionsQueryParams, - insertDiscussionsQuery -} from './generated/insertDiscussionsQuery' - -export type InputDiscussions = DeepNonNullable - -const insertDiscussions = async (discussions: InputDiscussions) => { - if (discussions.length === 0) return - insertDiscussionsQuery.run({discussions} as any, getPg()) -} - -export default insertDiscussions diff --git a/packages/server/postgres/queries/src/insertDiscussionsQuery.sql b/packages/server/postgres/queries/src/insertDiscussionsQuery.sql deleted file mode 100644 index 787f675b0ed..00000000000 --- a/packages/server/postgres/queries/src/insertDiscussionsQuery.sql +++ /dev/null @@ -1,6 +0,0 @@ -/* - @name insertDiscussionsQuery - @param discussions -> ((id, teamId, meetingId, discussionTopicId, discussionTopicType)...) -*/ -INSERT INTO "Discussion" ("id", "teamId", "meetingId", "discussionTopicId", "discussionTopicType") -VALUES :discussions;