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