Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add GraphQL notification settings for MS Teams and Mattermost #10694

Merged
merged 4 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
"SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource",
"SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource",
"SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource",
"SetNotificationSettingSuccess": "./types/SetNotificationSettingSuccess#SetNotificationSettingSuccessSource",
"SetOrgUserRoleSuccess": "./types/SetOrgUserRoleSuccess#SetOrgUserRoleSuccessSource",
"SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource",
"ShareTopicSuccess": "./types/ShareTopicSuccess#ShareTopicSuccessSource",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const MattermostProviderRow = (props: Props) => {
const {mattermost} = integrations
const {auth} = mattermost

if (window.__ACTION__.mattermostDisabled) return null
if (window.__ACTION__.mattermostGlobal || window.__ACTION__.mattermostDisabled) return null

return (
<>
Expand Down
8 changes: 4 additions & 4 deletions packages/client/shared/gqlIds/TeamMemberIntegrationAuthId.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const TeamMemberIntegrationAuthId = {
join: (service: string, teamId: string, userId: string) => `${service}:${teamId}:${userId}`,
split: (id: string) => {
const [service, teamId, userId] = id.split(':')
return {service, teamId, userId}
join: (dbId: number) => `TeamMemberIntegrationAuth:${dbId}`,
split: (gqlId: string) => {
const [_, id] = gqlId.split(':')
return Number.parseInt(id!)!
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/client/types/modules.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface Window {
google: string
googleAnalytics: string
mattermostDisabled: boolean | undefined
mattermostGlobal: boolean | undefined
msTeamsDisabled: boolean | undefined
publicPath: string
sentry: string
Expand Down
12 changes: 7 additions & 5 deletions packages/server/dataloader/azureDevOpsLoaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,11 +138,13 @@ export const freshAzureDevOpsAuth = (
async (keys) => {
const results = await Promise.allSettled(
keys.map(async ({userId, teamId}) => {
const azureDevOpsAuthToRefresh = await parent.get('teamMemberIntegrationAuths').load({
service: 'azureDevOps',
teamId,
userId
})
const azureDevOpsAuthToRefresh = await parent
.get('teamMemberIntegrationAuthsByServiceTeamAndUserId')
.load({
service: 'azureDevOps',
teamId,
userId
})
if (azureDevOpsAuthToRefresh === null) {
return null
}
Expand Down
12 changes: 7 additions & 5 deletions packages/server/dataloader/gcalLoaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export const freshGcalAuth = (parent: RootDataLoader) => {
async (keys) => {
const results = await Promise.allSettled(
keys.map(async ({teamId, userId}) => {
const gcalAuth = await parent.get('teamMemberIntegrationAuths').load({
service: 'gcal',
teamId,
userId
})
const gcalAuth = await parent
.get('teamMemberIntegrationAuthsByServiceTeamAndUserId')
.load({
service: 'gcal',
teamId,
userId
})
if (!gcalAuth) return null
const {expiresAt} = gcalAuth
const now = new Date()
Expand Down
12 changes: 7 additions & 5 deletions packages/server/dataloader/gitlabLoaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export const freshGitlabAuth = (parent: RootDataLoader) => {
async (keys) => {
const results = await Promise.allSettled(
keys.map(async ({teamId, userId}) => {
const gitlabAuth = await parent.get('teamMemberIntegrationAuths').load({
service: 'gitlab',
teamId,
userId
})
const gitlabAuth = await parent
.get('teamMemberIntegrationAuthsByServiceTeamAndUserId')
.load({
service: 'gitlab',
teamId,
userId
})
if (!gitlabAuth) return null
const {expiresAt} = gitlabAuth
const now = new Date()
Expand Down
100 changes: 33 additions & 67 deletions packages/server/dataloader/integrationAuthLoaders.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import DataLoader from 'dataloader'
import TeamMemberIntegrationAuthId from '../../client/shared/gqlIds/TeamMemberIntegrationAuthId'
import errorFilter from '../graphql/errorFilter'
import isValid from '../graphql/isValid'
import getKysely from '../postgres/getKysely'
import {IGetBestTeamIntegrationAuthQueryResult} from '../postgres/queries/generated/getBestTeamIntegrationAuthQuery'
import {IntegrationProviderServiceEnum} from '../postgres/queries/generated/getIntegrationProvidersByIdsQuery'
import getBestTeamIntegrationAuth from '../postgres/queries/getBestTeamIntegrationAuth'
import getIntegrationProvidersByIds, {
TIntegrationProvider
} from '../postgres/queries/getIntegrationProvidersByIds'
import {selectSlackNotifications, selectTeamMemberIntegrationAuth} from '../postgres/select'
import {SlackAuth, SlackNotification, TeamMemberIntegrationAuth} from '../postgres/types'
import {NotificationSettings} from '../postgres/types/pg'
import NullableDataLoader from './NullableDataLoader'
import RootDataLoader from './RootDataLoader'

interface TeamMemberIntegrationAuthPrimaryKey {
interface TeamMemberIntegrationAuthServiceTeamUserKey {
service: IntegrationProviderServiceEnum
teamId: string
userId: string
Expand All @@ -32,7 +30,7 @@ const teamMemberIntegrationAuthCacheKeyFn = ({
service,
teamId,
userId
}: TeamMemberIntegrationAuthPrimaryKey) => TeamMemberIntegrationAuthId.join(service, teamId, userId)
}: TeamMemberIntegrationAuthServiceTeamUserKey) => `${service}-${teamId}-${userId}`

export const integrationProviders = (parent: RootDataLoader) => {
return new NullableDataLoader<number, TIntegrationProvider, string>(
Expand Down Expand Up @@ -87,40 +85,9 @@ export const sharedIntegrationProviders = (parent: RootDataLoader) => {
)
}

export const bestTeamIntegrationProviders = (parent: RootDataLoader) => {
return new DataLoader<TeamMemberIntegrationAuthPrimaryKey, TIntegrationProvider | null, string>(
async (keys) => {
// given token params, get the best team token
const bestTeamIntegrationAuths = (
await parent.get('bestTeamIntegrationAuths').loadMany(keys)
).filter(isValid)
// dedupe providerIds
const providerIds = Array.from(
new Set(bestTeamIntegrationAuths.map((token) => token.providerId))
)
// get the providers for each token
const integrationProviders = (
await parent.get('integrationProviders').loadMany(providerIds)
).filter(isValid)
return keys.map((key) => {
const token = bestTeamIntegrationAuths.find(
({service, teamId}) => service === key.service && teamId === key.teamId
)
if (!token) return null
const provider = integrationProviders.find(({id}) => id === token.providerId)
return provider ?? null
})
},
{
...parent.dataLoaderOptions,
cacheKeyFn: teamMemberIntegrationAuthCacheKeyFn
}
)
}

export const teamMemberIntegrationAuths = (parent: RootDataLoader) => {
export const teamMemberIntegrationAuthsByServiceTeamAndUserId = (parent: RootDataLoader) => {
return new DataLoader<
TeamMemberIntegrationAuthPrimaryKey,
TeamMemberIntegrationAuthServiceTeamUserKey,
TeamMemberIntegrationAuth | null,
string
>(
Expand Down Expand Up @@ -150,29 +117,6 @@ export const teamMemberIntegrationAuths = (parent: RootDataLoader) => {
)
}

export const bestTeamIntegrationAuths = (parent: RootDataLoader) => {
return new DataLoader<
TeamMemberIntegrationAuthPrimaryKey,
IGetBestTeamIntegrationAuthQueryResult | null,
string
>(
async (keys) => {
// TODO check the teamMemberIntegrationAuths loader first, it probably exists there & then we don't have to hit the DB
const results = await Promise.allSettled(
keys.map(async ({service, teamId, userId}) =>
getBestTeamIntegrationAuth(service, teamId, userId)
)
)
const vals = results.map((result) => (result.status === 'fulfilled' ? result.value : null))
return vals
},
{
...parent.dataLoaderOptions,
cacheKeyFn: teamMemberIntegrationAuthCacheKeyFn
}
)
}

interface TeamNotificationEvent {
teamId: string
event: SlackNotification['event']
Expand Down Expand Up @@ -220,21 +164,25 @@ export const slackNotificationsByTeamIdAndEvent = (parent: RootDataLoader) => {
})
}

export const teamMemberIntegrationAuthsByTeamId = (parent: RootDataLoader) => {
export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoader) => {
return new DataLoader<
{teamId: string; service: IntegrationProviderServiceEnum},
{teamId: string; service: IntegrationProviderServiceEnum; event: SlackNotification['event']},
TeamMemberIntegrationAuth[],
string
>(
async (keys) => {
const pg = getKysely()
const teamIds = keys.map(({teamId}) => teamId)
const services = keys.map(({service}) => service)
const res = (await pg
.selectFrom('TeamMemberIntegrationAuth')
.innerJoin('NotificationSettings', 'authId', 'TeamMemberIntegrationAuth.id')
.selectAll()
.where(({eb}) => eb('teamId', 'in', teamIds))
.where(({eb}) => eb('service', 'in', services))
.where(({eb, refTuple, tuple}) =>
eb(
refTuple('teamId', 'service', 'event'),
'in',
keys.map(({teamId, service, event}) => tuple(teamId, service, event))
)
)
.execute()) as unknown as TeamMemberIntegrationAuth[]

return keys.map((key) =>
Expand All @@ -247,3 +195,21 @@ export const teamMemberIntegrationAuthsByTeamId = (parent: RootDataLoader) => {
}
)
}

export const notificationSettingsByAuthId = (parent: RootDataLoader) => {
return new DataLoader<number, NotificationSettings['event'][], string>(
async (keys) => {
const pg = getKysely()
const res = await pg
.selectFrom('NotificationSettings')
.selectAll()
.where(({eb}) => eb('authId', 'in', keys))
.execute()

return keys.map((key) => res.filter(({authId}) => authId === key).map(({event}) => event))
},
{
...parent.dataLoaderOptions
}
)
}
6 changes: 3 additions & 3 deletions packages/server/dataloader/jiraServerLoaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const jiraServerIssue = (parent: RootDataLoader) => {
const results = await Promise.allSettled(
keys.map(async ({teamId, userId, issueId}) => {
const auth = await parent
.get('teamMemberIntegrationAuths')
.get('teamMemberIntegrationAuthsByServiceTeamAndUserId')
.load({service: 'jiraServer', teamId, userId})

if (!auth) {
Expand Down Expand Up @@ -110,7 +110,7 @@ export const allJiraServerProjects = (parent: RootDataLoader) => {
return Promise.all(
keys.map(async ({userId, teamId}) => {
const auth = await parent
.get('teamMemberIntegrationAuths')
.get('teamMemberIntegrationAuthsByServiceTeamAndUserId')
.load({service: 'jiraServer', teamId, userId})
if (!auth) return []
const provider = await parent.get('integrationProviders').loadNonNull(auth.providerId)
Expand Down Expand Up @@ -140,7 +140,7 @@ export const jiraServerFieldTypes = (parent: RootDataLoader) =>
return Promise.all(
keys.map(async ({teamId, userId, projectId, issueType}) => {
const auth = await parent
.get('teamMemberIntegrationAuths')
.get('teamMemberIntegrationAuthsByServiceTeamAndUserId')
.load({service: 'jiraServer', teamId, userId})

if (!auth) {
Expand Down
8 changes: 8 additions & 0 deletions packages/server/dataloader/primaryKeyLoaderMakers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,11 @@ export const tasks = primaryKeyLoaderMaker((ids: readonly string[]) => {
export const notifications = primaryKeyLoaderMaker((ids: readonly string[]) => {
return selectNotifications().where('id', 'in', ids).execute()
})

export const teamMemberIntegrationAuths = primaryKeyLoaderMaker((ids: readonly number[]) => {
return getKysely()
.selectFrom('TeamMemberIntegrationAuth')
.selectAll()
.where('id', 'in', ids)
.execute()
})
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {IntegrationProviderMSTeams as IIntegrationProviderMSTeams} from '../../.
import {SlackNotification, Team} from '../../../../postgres/types'
import IUser from '../../../../postgres/types/IUser'
import {AnyMeeting, MeetingTypeEnum} from '../../../../postgres/types/Meeting'
import {NotificationSettings} from '../../../../postgres/types/pg'
import MSTeamsServerManager from '../../../../utils/MSTeamsServerManager'
import {analytics} from '../../../../utils/analytics/analytics'
import sendToSentry from '../../../../utils/sendToSentry'
Expand Down Expand Up @@ -333,20 +334,28 @@ export const MSTeamsNotificationHelper: NotificationIntegrationHelper<MSTeamsNot
}
})

async function getMSTeams(dataLoader: DataLoaderWorker, teamId: string, userId: string) {
const [provider, user] = await Promise.all([
dataLoader.get('bestTeamIntegrationProviders').load({service: 'msTeams', teamId, userId}),
async function getMSTeams(
dataLoader: DataLoaderWorker,
teamId: string,
userId: string,
event: NotificationSettings['event']
) {
const [auths, user] = await Promise.all([
dataLoader
.get('teamMemberIntegrationAuthsByTeamIdAndEvent')
.load({service: 'msTeams', teamId, event}),
dataLoader.get('users').loadNonNull(userId)
])
return provider && provider.teamId
? [
MSTeamsNotificationHelper({
...(provider as IntegrationProviderMSTeams),
userId,
email: user.email
})
]
: []
return Promise.all(
auths.map(async (auth) => {
const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId)
return MSTeamsNotificationHelper({
...(provider as IntegrationProviderMSTeams),
userId,
email: user.email
})
})
)
}

export const MSTeamsNotifier = createNotifier(getMSTeams)
Expand Down
Loading
Loading