From 49dc95a2403984818b3752dab15e67ec7d2bcea7 Mon Sep 17 00:00:00 2001 From: Georg Bremer Date: Mon, 3 Mar 2025 12:09:57 +0100 Subject: [PATCH] chore(Notifications): Refactor notification settings to be per team (#10899) --- codegen.json | 10 +- package.json | 2 +- .../StageTimerModalEndTimeSlackToggle.tsx | 19 +-- .../components/ProviderRow/MSTeamsPanel.tsx | 8 +- .../ProviderRow/MSTeamsProviderRow.tsx | 4 +- .../ProviderRow/MattermostPanel.tsx | 8 +- .../ProviderRow/MattermostProviderRow.tsx | 4 +- .../ProviderRow/NotificationSettings.tsx | 22 +-- .../SetNotificationSettingMutation.ts | 59 -------- .../SetTeamNotificationSettingMutation.ts | 59 ++++++++ .../gqlIds/TeamNotificationSettingsId.ts | 6 + .../client/subscriptions/TeamSubscription.ts | 5 +- .../dataloader/integrationAuthLoaders.ts | 45 ++++-- .../dataloader/primaryKeyLoaderMakers.ts | 14 ++ .../mutations/addIntegrationProvider.ts | 130 ------------------ .../helpers/notifications/MSTeamsNotifier.ts | 40 ++++-- .../notifications/MattermostNotifier.ts | 37 +++-- .../mutations/addIntegrationProvider.ts | 114 +++++++++++++++ .../mutations/addTeamMemberIntegrationAuth.ts | 21 +-- .../mutations/setNotificationSetting.ts | 54 -------- .../mutations/setTeamNotificationSetting.ts | 58 ++++++++ .../typeDefs/MSTeamsIntegration.graphql | 15 ++ .../typeDefs/MattermostIntegration.graphql | 15 ++ .../graphql/public/typeDefs/Mutation.graphql | 10 +- .../SetNotificationSettingPayload.graphql | 4 - .../SetNotificationSettingSuccess.graphql | 14 -- .../SetTeamNotificationSettingPayload.graphql | 4 + .../SetTeamNotificationSettingSuccess.graphql | 6 + .../TeamMemberIntegrationAuthWebhook.graphql | 5 - .../typeDefs/TeamNotificationSettings.graphql | 19 +++ .../typeDefs/TeamSubscriptionPayload.graphql | 2 +- .../public/types/IntegrationProvider.ts | 11 ++ .../public/types/IntegrationProviderOAuth1.ts | 12 ++ .../public/types/IntegrationProviderOAuth2.ts | 12 ++ .../types/IntegrationProviderWebhook.ts | 12 ++ .../public/types/MSTeamsIntegration.ts | 31 +++++ .../public/types/MattermostIntegration.ts | 31 +++++ .../types/SetNotificationSettingSuccess.ts | 16 --- .../SetTeamNotificationSettingSuccess.ts | 16 +++ .../types/TeamMemberIntegrationAuthWebhook.ts | 3 - .../public/types/TeamNotificationSettings.ts | 12 ++ packages/server/graphql/rootMutation.ts | 2 - packages/server/graphql/rootTypes.ts | 8 -- .../types/AddIntegrationProviderPayload.ts | 24 ---- .../graphql/types/IntegrationProvider.ts | 56 -------- .../types/IntegrationProviderOAuth1.ts | 20 --- .../types/IntegrationProviderOAuth2.ts | 28 ---- .../types/IntegrationProviderSharedSecret.ts | 24 ---- .../types/IntegrationProviderWebhook.ts | 20 --- ...Z_notificationSettingsPerTeamAndChannel.ts | 83 +++++++++++ .../queries/upsertIntegrationProvider.ts | 2 +- yarn.lock | 47 +++++-- 52 files changed, 721 insertions(+), 562 deletions(-) delete mode 100644 packages/client/mutations/SetNotificationSettingMutation.ts create mode 100644 packages/client/mutations/SetTeamNotificationSettingMutation.ts create mode 100644 packages/client/shared/gqlIds/TeamNotificationSettingsId.ts delete mode 100644 packages/server/graphql/mutations/addIntegrationProvider.ts create mode 100644 packages/server/graphql/public/mutations/addIntegrationProvider.ts delete mode 100644 packages/server/graphql/public/mutations/setNotificationSetting.ts create mode 100644 packages/server/graphql/public/mutations/setTeamNotificationSetting.ts delete mode 100644 packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql delete mode 100644 packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql create mode 100644 packages/server/graphql/public/typeDefs/SetTeamNotificationSettingPayload.graphql create mode 100644 packages/server/graphql/public/typeDefs/SetTeamNotificationSettingSuccess.graphql create mode 100644 packages/server/graphql/public/typeDefs/TeamNotificationSettings.graphql create mode 100644 packages/server/graphql/public/types/IntegrationProvider.ts create mode 100644 packages/server/graphql/public/types/IntegrationProviderOAuth1.ts create mode 100644 packages/server/graphql/public/types/IntegrationProviderOAuth2.ts create mode 100644 packages/server/graphql/public/types/IntegrationProviderWebhook.ts delete mode 100644 packages/server/graphql/public/types/SetNotificationSettingSuccess.ts create mode 100644 packages/server/graphql/public/types/SetTeamNotificationSettingSuccess.ts create mode 100644 packages/server/graphql/public/types/TeamNotificationSettings.ts delete mode 100644 packages/server/graphql/types/AddIntegrationProviderPayload.ts delete mode 100644 packages/server/graphql/types/IntegrationProvider.ts delete mode 100644 packages/server/graphql/types/IntegrationProviderOAuth1.ts delete mode 100644 packages/server/graphql/types/IntegrationProviderOAuth2.ts delete mode 100644 packages/server/graphql/types/IntegrationProviderSharedSecret.ts delete mode 100644 packages/server/graphql/types/IntegrationProviderWebhook.ts create mode 100644 packages/server/postgres/migrations/2025-02-28T15:28:11.536Z_notificationSettingsPerTeamAndChannel.ts diff --git a/codegen.json b/codegen.json index 932fcbe384f..4349e98347d 100644 --- a/codegen.json +++ b/codegen.json @@ -92,9 +92,10 @@ "GifResponse": "./types/GifResponse#GifResponseSource", "GitHubIntegration": "../../postgres/queries/getGitHubAuthByUserIdTeamId#GitHubAuth", "GitLabIntegration": "./types/GitLabIntegration#GitLabIntegrationSource", - "IntegrationProviderOAuth1": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", - "IntegrationProviderOAuth2": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", - "IntegrationProviderWebhook": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", + "IntegrationProvider": "./types/IntegrationProvider#IntegrationProviderSource", + "IntegrationProviderOAuth1": "./types/IntegrationProviderOAuth1#IntegrationProviderOAuth1Source", + "IntegrationProviderOAuth2": "./types/IntegrationProviderOAuth2#IntegrationProviderOAuth2Source", + "IntegrationProviderWebhook": "./types/IntegrationProviderWebhook#IntegrationProviderWebhookSource", "InviteToTeamPayload": "./types/InviteToTeamPayload#InviteToTeamPayloadSource", "JiraIssue": "./types/JiraIssue#JiraIssueSource", "JiraRemoteAvatarUrls": "./types/JiraRemoteAvatarUrls#JiraRemoteAvatarUrlsSource", @@ -163,9 +164,9 @@ "SetDefaultSlackChannelSuccess": "./types/SetDefaultSlackChannelSuccess#SetDefaultSlackChannelSuccessSource", "SetMeetingSettingsPayload": "../types/SetMeetingSettingsPayload#SetMeetingSettingsPayloadSource", "SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource", - "SetNotificationSettingSuccess": "./types/SetNotificationSettingSuccess#SetNotificationSettingSuccessSource", "SetOrgUserRoleSuccess": "./types/SetOrgUserRoleSuccess#SetOrgUserRoleSuccessSource", "SetSlackNotificationPayload": "./types/SetSlackNotificationPayload#SetSlackNotificationPayloadSource", + "SetTeamNotificationSettingSuccess": "./types/SetTeamNotificationSettingSuccess#SetTeamNotificationSettingSuccessSource", "ShareTopicSuccess": "./types/ShareTopicSuccess#ShareTopicSuccessSource", "SlackIntegration": "../../postgres/types/index#SlackAuth as SlackAuthDB", "SlackNotification": "../../postgres/types/index#SlackNotification as SlackNotificationDB", @@ -186,6 +187,7 @@ "TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrationAuthWebhook": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrations": "./types/TeamMemberIntegrations#TeamMemberIntegrationsSource", + "TeamNotificationSettings": "./types/TeamNotificationSettings#TeamNotificationSettingsSource", "TeamPromptMeeting": "../../postgres/types/Meeting#TeamPromptMeeting", "TeamPromptMeetingMember": "../../postgres/types/Meeting.d#TeamPromptMeetingMember as TeamPromptMeetingMemberDB", "TeamPromptMeetingSettings": "../../postgres/types/index#MeetingSettings as TeamMeetingSettingsDB", diff --git a/package.json b/package.json index 6de00dd0954..1f61389e923 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "husky": "^7.0.4", "jscodeshift": "^0.14.0", "kysely": "^0.27.5", - "kysely-codegen": "^0.15.0", + "kysely-codegen": "^0.17.0", "kysely-ctl": "^0.11.0", "lerna": "^6.4.1", "mini-css-extract-plugin": "^2.7.2", diff --git a/packages/client/components/StageTimerModalEndTimeSlackToggle.tsx b/packages/client/components/StageTimerModalEndTimeSlackToggle.tsx index dc220441205..f4751608d10 100644 --- a/packages/client/components/StageTimerModalEndTimeSlackToggle.tsx +++ b/packages/client/components/StageTimerModalEndTimeSlackToggle.tsx @@ -61,11 +61,12 @@ const StyledNotificationErrorMessage = styled(NotificationErrorMessage)({ }) const isNotificationActive = (integration: { - auth: null | undefined | {isActive: boolean; events: readonly SlackNotificationEventEnum[]} + isActive: boolean + teamNotificationSettings: {events: readonly SlackNotificationEventEnum[]} | null | undefined }) => { - const {auth} = integration - if (!auth?.isActive) return false - const {events} = auth + const {isActive, teamNotificationSettings} = integration + if (!isActive || !teamNotificationSettings) return false + const {events} = teamNotificationSettings if (!events) return false return ( events.includes('MEETING_STAGE_TIME_LIMIT_START') || @@ -81,14 +82,16 @@ const StageTimerModalEndTimeSlackToggle = (props: Props) => { teamId integrations { mattermost { - auth { - isActive + isActive + teamNotificationSettings { + id events } } msTeams { - auth { - isActive + isActive + teamNotificationSettings { + id events } } diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsPanel.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsPanel.tsx index 51f61b53ddc..bd380e5783e 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsPanel.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsPanel.tsx @@ -73,12 +73,14 @@ const MSTeamsPanel = (props: Props) => { integrations { msTeams { auth { - ...NotificationSettings_auth provider { id webhookUrl } } + teamNotificationSettings { + ...NotificationSettings_settings + } } } } @@ -89,7 +91,7 @@ const MSTeamsPanel = (props: Props) => { const {teamMember} = viewer const {integrations} = teamMember! const {msTeams} = integrations - const {auth} = msTeams + const {teamNotificationSettings, auth} = msTeams const activeProvider = auth?.provider const atmosphere = useAtmosphere() @@ -209,7 +211,7 @@ const MSTeamsPanel = (props: Props) => { {fieldError && {fieldError}} {!fieldError && mutationError && {mutationError.message}} - {auth && } + {teamNotificationSettings && } ) } diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsProviderRow.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsProviderRow.tsx index 36564a80260..a0f4bd69a27 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsProviderRow.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/MSTeamsProviderRow.tsx @@ -21,11 +21,13 @@ graphql` fragment MSTeamsProviderRowTeamMemberIntegrations on TeamMemberIntegrations { msTeams { auth { - ...NotificationSettings_auth provider { id } } + teamNotificationSettings { + ...NotificationSettings_settings + } } } ` diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/MattermostPanel.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/MattermostPanel.tsx index 6695d38f01e..44be7e23f3e 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/MattermostPanel.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/MattermostPanel.tsx @@ -87,12 +87,14 @@ const MattermostPanel = (props: Props) => { integrations { mattermost { auth { - ...NotificationSettings_auth provider { id webhookUrl } } + teamNotificationSettings { + ...NotificationSettings_settings + } } } } @@ -103,7 +105,7 @@ const MattermostPanel = (props: Props) => { const {teamMember} = viewer const {integrations} = teamMember! const {mattermost} = integrations - const {auth} = mattermost + const {teamNotificationSettings, auth} = mattermost const activeProvider = auth?.provider const atmosphere = useAtmosphere() @@ -224,7 +226,7 @@ const MattermostPanel = (props: Props) => { {fieldError && {fieldError}} {!fieldError && mutationError && {mutationError.message}} - {auth && } + {teamNotificationSettings && } ) } diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx index 82799c82bd5..4709edae839 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/MattermostProviderRow.tsx @@ -21,11 +21,13 @@ graphql` fragment MattermostProviderRowTeamMemberIntegrations on TeamMemberIntegrations { mattermost { auth { - ...NotificationSettings_auth provider { id } } + teamNotificationSettings { + ...NotificationSettings_settings + } } } ` diff --git a/packages/client/modules/teamDashboard/components/ProviderRow/NotificationSettings.tsx b/packages/client/modules/teamDashboard/components/ProviderRow/NotificationSettings.tsx index 6672b9d0677..c1cb023a189 100644 --- a/packages/client/modules/teamDashboard/components/ProviderRow/NotificationSettings.tsx +++ b/packages/client/modules/teamDashboard/components/ProviderRow/NotificationSettings.tsx @@ -1,14 +1,14 @@ import graphql from 'babel-plugin-relay/macro' import {useFragment} from 'react-relay' import { - NotificationSettings_auth$key, + NotificationSettings_settings$key, SlackNotificationEventEnum -} from '../../../../__generated__/NotificationSettings_auth.graphql' +} from '../../../../__generated__/NotificationSettings_settings.graphql' import StyledError from '../../../../components/StyledError' import Toggle from '../../../../components/Toggle/Toggle' import useAtmosphere from '../../../../hooks/useAtmosphere' import useMutationProps from '../../../../hooks/useMutationProps' -import SetNotificationSettingMutation from '../../../../mutations/SetNotificationSettingMutation' +import SetTeamNotificationSettingMutation from '../../../../mutations/SetTeamNotificationSettingMutation' import {MeetingLabels} from '../../../../types/constEnums' const EVENTS = [ @@ -31,21 +31,21 @@ const labelLookup = { } as Record interface Props { - auth: NotificationSettings_auth$key + settings: NotificationSettings_settings$key } const NotificationSettings = (props: Props) => { - const {auth: authRef} = props - const auth = useFragment( + const {settings: settingsRef} = props + const settings = useFragment( graphql` - fragment NotificationSettings_auth on TeamMemberIntegrationAuthWebhook { + fragment NotificationSettings_settings on TeamNotificationSettings { id events } `, - authRef + settingsRef ) - const {events} = auth + const {events} = settings const atmosphere = useAtmosphere() const {submitting, onError, onCompleted, submitMutation, error} = useMutationProps() @@ -54,10 +54,10 @@ const NotificationSettings = (props: Props) => { return } submitMutation() - SetNotificationSettingMutation( + SetTeamNotificationSettingMutation( atmosphere, { - authId: auth.id, + id: settings.id, event, isEnabled }, diff --git a/packages/client/mutations/SetNotificationSettingMutation.ts b/packages/client/mutations/SetNotificationSettingMutation.ts deleted file mode 100644 index 3d9ea3a4ac1..00000000000 --- a/packages/client/mutations/SetNotificationSettingMutation.ts +++ /dev/null @@ -1,59 +0,0 @@ -import graphql from 'babel-plugin-relay/macro' -import {commitMutation} from 'react-relay' -import { - SlackNotificationEventEnum, - SetNotificationSettingMutation as TSetSlackNotificationMutation -} from '../__generated__/SetNotificationSettingMutation.graphql' -import {StandardMutation} from '../types/relayMutations' - -graphql` - fragment SetNotificationSettingMutation_auth on SetNotificationSettingSuccess { - auth { - id - events - } - } -` - -const mutation = graphql` - mutation SetNotificationSettingMutation( - $authId: ID! - $event: SlackNotificationEventEnum! - $isEnabled: Boolean! - ) { - setNotificationSetting(authId: $authId, event: $event, isEnabled: $isEnabled) { - ... on ErrorPayload { - error { - message - } - } - ...SetNotificationSettingMutation_auth @relay(mask: false) - } - } -` - -const SetSlackNotificationMutation: StandardMutation = ( - atmosphere, - variables, - {onError, onCompleted} -) => { - return commitMutation(atmosphere, { - mutation, - variables, - optimisticUpdater: (store) => { - const {authId, event, isEnabled} = variables - const auth = store.get(authId) - if (!auth) return - const enabledEvents = auth.getValue('events') as SlackNotificationEventEnum[] - if (!enabledEvents) return - const newEvents = isEnabled - ? [...enabledEvents, event] - : enabledEvents.filter((enabledEvent) => enabledEvent !== event) - auth.setValue(newEvents, 'events') - }, - onCompleted, - onError - }) -} - -export default SetSlackNotificationMutation diff --git a/packages/client/mutations/SetTeamNotificationSettingMutation.ts b/packages/client/mutations/SetTeamNotificationSettingMutation.ts new file mode 100644 index 00000000000..d87c48c9c6a --- /dev/null +++ b/packages/client/mutations/SetTeamNotificationSettingMutation.ts @@ -0,0 +1,59 @@ +import graphql from 'babel-plugin-relay/macro' +import {commitMutation} from 'react-relay' +import { + SlackNotificationEventEnum, + SetTeamNotificationSettingMutation as TSetTeamNotificationSettingMutation +} from '../__generated__/SetTeamNotificationSettingMutation.graphql' +import {StandardMutation} from '../types/relayMutations' + +graphql` + fragment SetTeamNotificationSettingMutation_settings on SetTeamNotificationSettingSuccess { + teamNotificationSettings { + id + events + } + } +` + +const mutation = graphql` + mutation SetTeamNotificationSettingMutation( + $id: ID! + $event: SlackNotificationEventEnum! + $isEnabled: Boolean! + ) { + setTeamNotificationSetting(id: $id, event: $event, isEnabled: $isEnabled) { + ... on ErrorPayload { + error { + message + } + } + ...SetTeamNotificationSettingMutation_settings @relay(mask: false) + } + } +` + +const SetTeamNotificationMutation: StandardMutation = ( + atmosphere, + variables, + {onError, onCompleted} +) => { + return commitMutation(atmosphere, { + mutation, + variables, + optimisticUpdater: (store) => { + const {id, event, isEnabled} = variables + const settings = store.get(id) + if (!settings) return + const enabledEvents = settings.getValue('events') as SlackNotificationEventEnum[] + if (!enabledEvents) return + const newEvents = isEnabled + ? [...enabledEvents, event] + : enabledEvents.filter((enabledEvent) => enabledEvent !== event) + settings.setValue(newEvents, 'events') + }, + onCompleted, + onError + }) +} + +export default SetTeamNotificationMutation diff --git a/packages/client/shared/gqlIds/TeamNotificationSettingsId.ts b/packages/client/shared/gqlIds/TeamNotificationSettingsId.ts new file mode 100644 index 00000000000..686e2788aca --- /dev/null +++ b/packages/client/shared/gqlIds/TeamNotificationSettingsId.ts @@ -0,0 +1,6 @@ +const TeamNotificationSettingsId = { + join: (id: number) => `teamNotificationSettings:${id}`, + split: (id: string) => parseInt(id.split(':')[1]!) +} + +export default TeamNotificationSettingsId diff --git a/packages/client/subscriptions/TeamSubscription.ts b/packages/client/subscriptions/TeamSubscription.ts index 156fcf2041a..eef3097dab9 100644 --- a/packages/client/subscriptions/TeamSubscription.ts +++ b/packages/client/subscriptions/TeamSubscription.ts @@ -152,8 +152,8 @@ const subscription = graphql` SetMeetingSettingsPayload { ...SetMeetingSettingsMutation_team @relay(mask: false) } - SetNotificationSettingSuccess { - ...SetNotificationSettingMutation_auth @relay(mask: false) + SetTeamNotificationSettingSuccess { + ...SetTeamNotificationSettingMutation_settings @relay(mask: false) } StartCheckInSuccess { ...StartCheckInMutation_team @relay(mask: false) @@ -170,7 +170,6 @@ const subscription = graphql` UpdateAgendaItemPayload { ...UpdateAgendaItemMutation_team @relay(mask: false) } - UpdateCreditCardPayload { ...UpdateCreditCardMutation_team @relay(mask: false) } diff --git a/packages/server/dataloader/integrationAuthLoaders.ts b/packages/server/dataloader/integrationAuthLoaders.ts index 9099b920886..3e12ab931a1 100644 --- a/packages/server/dataloader/integrationAuthLoaders.ts +++ b/packages/server/dataloader/integrationAuthLoaders.ts @@ -1,4 +1,5 @@ import DataLoader from 'dataloader' +import {Selectable, sql} from 'kysely' import errorFilter from '../graphql/errorFilter' import isValid from '../graphql/isValid' import getKysely from '../postgres/getKysely' @@ -8,9 +9,9 @@ import getIntegrationProvidersByIds, { } from '../postgres/queries/getIntegrationProvidersByIds' import {selectSlackNotifications, selectTeamMemberIntegrationAuth} from '../postgres/select' import {SlackAuth, SlackNotification, TeamMemberIntegrationAuth} from '../postgres/types' -import {NotificationSettings} from '../postgres/types/pg' +import {TeamNotificationSettings} from '../postgres/types/pg' import NullableDataLoader from './NullableDataLoader' -import RootDataLoader from './RootDataLoader' +import RootDataLoader, {RegisterDependsOn} from './RootDataLoader' interface TeamMemberIntegrationAuthServiceTeamUserKey { service: IntegrationProviderServiceEnum @@ -164,9 +165,9 @@ export const slackNotificationsByTeamIdAndEvent = (parent: RootDataLoader) => { }) } -export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoader) => { +export const teamMemberIntegrationAuthsByTeamIdAndService = (parent: RootDataLoader) => { return new DataLoader< - {teamId: string; service: IntegrationProviderServiceEnum; event: SlackNotification['event']}, + {teamId: string; service: IntegrationProviderServiceEnum}, TeamMemberIntegrationAuth[], string >( @@ -174,13 +175,12 @@ export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoade const pg = getKysely() const res = (await pg .selectFrom('TeamMemberIntegrationAuth') - .innerJoin('NotificationSettings', 'authId', 'TeamMemberIntegrationAuth.id') .selectAll() .where(({eb, refTuple, tuple}) => eb( - refTuple('teamId', 'service', 'event'), + refTuple('teamId', 'service'), 'in', - keys.map(({teamId, service, event}) => tuple(teamId, service, event)) + keys.map(({teamId, service}) => tuple(teamId, service)) ) ) .execute()) as unknown as TeamMemberIntegrationAuth[] @@ -196,20 +196,39 @@ export const teamMemberIntegrationAuthsByTeamIdAndEvent = (parent: RootDataLoade ) } -export const notificationSettingsByAuthId = (parent: RootDataLoader) => { - return new DataLoader( +export const teamNotificationSettingsByProviderIdAndTeamId = ( + parent: RootDataLoader, + dependsOn: RegisterDependsOn +) => { + dependsOn('teamNotificationSettings') + return new DataLoader< + {providerId: number; teamId: string}, + Selectable[], + string + >( async (keys) => { const pg = getKysely() const res = await pg - .selectFrom('NotificationSettings') + .selectFrom('TeamNotificationSettings') .selectAll() - .where(({eb}) => eb('authId', 'in', keys)) + // convert to text[] as kysely would otherwise not parse the array + .select(sql`events::text[]`.as('events')) + .where(({eb, refTuple, tuple}) => + eb( + refTuple('providerId', 'teamId'), + 'in', + keys.map(({providerId, teamId}) => tuple(providerId, teamId)) + ) + ) .execute() - return keys.map((key) => res.filter(({authId}) => authId === key).map(({event}) => event)) + return keys.map((key) => + res.filter(({providerId, teamId}) => providerId === key.providerId && teamId === key.teamId) + ) }, { - ...parent.dataLoaderOptions + ...parent.dataLoaderOptions, + cacheKeyFn: ({providerId, teamId}) => `${providerId}-${teamId}` } ) } diff --git a/packages/server/dataloader/primaryKeyLoaderMakers.ts b/packages/server/dataloader/primaryKeyLoaderMakers.ts index c4680eddfd3..26428d85e66 100644 --- a/packages/server/dataloader/primaryKeyLoaderMakers.ts +++ b/packages/server/dataloader/primaryKeyLoaderMakers.ts @@ -1,3 +1,4 @@ +import {sql} from 'kysely' import getKysely from '../postgres/getKysely' import {getDomainJoinRequestsByIds} from '../postgres/queries/getDomainJoinRequestsByIds' import getMeetingSeriesByIds from '../postgres/queries/getMeetingSeriesByIds' @@ -29,6 +30,7 @@ import { selectTemplateScaleRef, selectTimelineEvent } from '../postgres/select' +import {TeamNotificationSettings} from '../postgres/types/pg' import {primaryKeyLoaderMaker} from './primaryKeyLoaderMaker' export const users = primaryKeyLoaderMaker(getUsersByIds) @@ -164,3 +166,15 @@ export const teamMemberIntegrationAuths = primaryKeyLoaderMaker((ids: readonly n .where('id', 'in', ids) .execute() }) + +export const teamNotificationSettings = primaryKeyLoaderMaker((ids: readonly number[]) => { + return ( + getKysely() + .selectFrom('TeamNotificationSettings') + .selectAll() + // convert to text[] as kysely would otherwise not parse the array + .select(sql`events::text[]`.as('events')) + .where('id', 'in', ids) + .execute() + ) +}) diff --git a/packages/server/graphql/mutations/addIntegrationProvider.ts b/packages/server/graphql/mutations/addIntegrationProvider.ts deleted file mode 100644 index 9679e0c3541..00000000000 --- a/packages/server/graphql/mutations/addIntegrationProvider.ts +++ /dev/null @@ -1,130 +0,0 @@ -import {GraphQLNonNull} from 'graphql' -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import {isNotNull} from 'parabol-client/utils/predicates' -import upsertIntegrationProvider from '../../postgres/queries/upsertIntegrationProvider' -import {getUserId, isSuperUser, isTeamMember, isUserOrgAdmin} from '../../utils/authorization' -import publish from '../../utils/publish' -import {GQLContext} from '../graphql' -import AddIntegrationProviderInput, { - IAddIntegrationProviderInput -} from '../types/AddIntegrationProviderInput' -import AddIntegrationProviderPayload from '../types/AddIntegrationProviderPayload' - -const addIntegrationProvider = { - name: 'AddIntegrationProvider', - type: new GraphQLNonNull(AddIntegrationProviderPayload), - description: 'Adds a new Integration Provider configuration', - args: { - input: { - type: new GraphQLNonNull(AddIntegrationProviderInput), - description: 'The new Integration Provider' - } - }, - resolve: async ( - _source: unknown, - { - input - }: { - input: IAddIntegrationProviderInput - }, - context: GQLContext - ) => { - const {authToken, dataLoader, socketId: mutatorId} = context - const {teamId, orgId, scope} = input - const viewerId = getUserId(authToken) - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - - // INPUT VALIDATION - if (scope === 'global' && (teamId || orgId)) { - return {error: {message: 'Global providers must not have an `orgId` nor `teamId`'}} - } - if (scope === 'org' && (!orgId || teamId)) { - return {error: {message: 'Org providers must have an `orgId` and no `teamId`'}} - } - if (scope === 'team' && (!teamId || orgId)) { - return {error: {message: 'Team providers must have a `teamId` and no `orgId`'}} - } - - // AUTH - if (!isSuperUser(authToken)) { - if (scope === 'global') { - return {error: {message: 'Must be a super user to add a global provider'}} - } - if (scope === 'org' && !(await isUserOrgAdmin(viewerId, orgId!, dataLoader))) { - return { - error: { - message: - 'Must be an organization admin to add an integration provider on organization level' - } - } - } - if (scope === 'team' && !isTeamMember(authToken, teamId!)) { - return {error: {message: 'Must be on the team for the integration provider'}} - } - } - - // VALIDATION - const { - authStrategy, - oAuth1ProviderMetadataInput, - oAuth2ProviderMetadataInput, - webhookProviderMetadataInput, - sharedSecretMetadataInput, - ...rest - } = input - - if (authStrategy === 'oauth1' && !oAuth1ProviderMetadataInput) { - return {error: {message: 'Auth strategy oauth1 requires oAuth1ProviderMetadataInput'}} - } - if (authStrategy === 'oauth2' && !oAuth2ProviderMetadataInput) { - return {error: {message: 'Auth strategy oauth2 requires oAuth2ProviderMetadataInput'}} - } - if (authStrategy === 'webhook' && !webhookProviderMetadataInput) { - return {error: {message: 'Auth strategy webhook requires webhookProviderMetadataInput'}} - } - if ( - [ - oAuth1ProviderMetadataInput, - oAuth2ProviderMetadataInput, - webhookProviderMetadataInput, - sharedSecretMetadataInput - ].filter(isNotNull).length !== 1 - ) { - return {error: {message: 'Exactly 1 metadata provider is expected'}} - } - - // RESOLUTION - const providerId = await upsertIntegrationProvider({ - authStrategy, - ...rest, - ...oAuth1ProviderMetadataInput, - ...oAuth2ProviderMetadataInput, - ...webhookProviderMetadataInput, - ...sharedSecretMetadataInput, - ...(scope === 'global' - ? {orgId: null, teamId: null} - : scope === 'org' - ? {orgId, teamId: null} - : {orgId: null, teamId}) - }) - - const data = { - providerId, - orgId, - teamId - } - if (orgId) { - publish( - SubscriptionChannel.ORGANIZATION, - orgId, - 'AddIntegrationProviderSuccess', - data, - subOptions - ) - } - return data - } -} - -export default addIntegrationProvider diff --git a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts index 3172da054f8..917cd27fa0b 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MSTeamsNotifier.ts @@ -7,11 +7,12 @@ 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' import {DataLoaderWorker} from '../../../graphql' +import isValid from '../../../isValid' +import {SlackNotificationEventEnum} from '../../../public/resolverTypes' import {NotificationIntegrationHelper} from './NotificationIntegrationHelper' import {createNotifier} from './Notifier' import getSummaryText from './getSummaryText' @@ -338,22 +339,39 @@ async function getMSTeams( dataLoader: DataLoaderWorker, teamId: string, userId: string, - event: NotificationSettings['event'] + event: SlackNotificationEventEnum ) { const [auths, user] = await Promise.all([ dataLoader - .get('teamMemberIntegrationAuthsByTeamIdAndEvent') - .load({service: 'msTeams', teamId, event}), + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({service: 'msTeams', teamId}), dataLoader.get('users').loadNonNull(userId) ]) - 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 + + const providers = ( + await Promise.all( + auths.map(async (auth) => { + const {providerId} = auth + const [provider, settings] = await Promise.all([ + dataLoader + .get('integrationProviders') + .loadNonNull(providerId) as Promise, + dataLoader.get('teamNotificationSettingsByProviderIdAndTeamId').load({providerId, teamId}) + ]) + const activeSettings = settings.find(({channelId}) => channelId === null) + if (activeSettings?.events.includes(event)) { + return provider + } + return null }) + ) + ).filter(isValid) + + return providers.map((provider) => + MSTeamsNotificationHelper({ + ...(provider as IntegrationProviderMSTeams), + userId, + email: user.email }) ) } diff --git a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts index 7bbe29a2965..7519494d7fc 100644 --- a/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts +++ b/packages/server/graphql/mutations/helpers/notifications/MattermostNotifier.ts @@ -8,12 +8,13 @@ import {IntegrationProviderMattermost} from '../../../../postgres/queries/getInt 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 MattermostServerManager from '../../../../utils/MattermostServerManager' import {analytics} from '../../../../utils/analytics/analytics' import {toEpochSeconds} from '../../../../utils/epochTime' import sendToSentry from '../../../../utils/sendToSentry' import {DataLoaderWorker} from '../../../graphql' +import isValid from '../../../isValid' +import {SlackNotificationEventEnum} from '../../../public/resolverTypes' import {NotificationIntegrationHelper} from './NotificationIntegrationHelper' import {createNotifier} from './Notifier' import getSummaryText from './getSummaryText' @@ -359,7 +360,7 @@ async function getMattermost( dataLoader: DataLoaderWorker, teamId: string, userId: string, - event: NotificationSettings['event'] + event: SlackNotificationEventEnum ) { if (MATTERMOST_SECRET && MATTERMOST_URL) { return [ @@ -374,17 +375,29 @@ async function getMattermost( } const auths = await dataLoader - .get('teamMemberIntegrationAuthsByTeamIdAndEvent') - .load({service: 'mattermost', teamId, event}) + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({service: 'mattermost', teamId}) - return Promise.all( - auths.map(async (auth) => { - const provider = (await dataLoader - .get('integrationProviders') - .loadNonNull(auth.providerId)) as IntegrationProviderMattermost - return MattermostNotificationHelper({...provider, teamId, userId}) - }) - ) + const providers = ( + await Promise.all( + auths.map(async (auth) => { + const {providerId} = auth + const [provider, settings] = await Promise.all([ + dataLoader + .get('integrationProviders') + .loadNonNull(providerId) as Promise, + dataLoader.get('teamNotificationSettingsByProviderIdAndTeamId').load({providerId, teamId}) + ]) + const activeSettings = settings.find(({channelId}) => channelId === null) + if (activeSettings?.events.includes(event)) { + return provider + } + return null + }) + ) + ).filter(isValid) + + return providers.map((provider) => MattermostNotificationHelper({...provider, teamId, userId})) } export const MattermostNotifier = createNotifier(getMattermost) diff --git a/packages/server/graphql/public/mutations/addIntegrationProvider.ts b/packages/server/graphql/public/mutations/addIntegrationProvider.ts new file mode 100644 index 00000000000..041ca93d0a1 --- /dev/null +++ b/packages/server/graphql/public/mutations/addIntegrationProvider.ts @@ -0,0 +1,114 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import {isNotNull} from 'parabol-client/utils/predicates' +import upsertIntegrationProvider from '../../../postgres/queries/upsertIntegrationProvider' +import {getUserId, isSuperUser, isTeamMember, isUserOrgAdmin} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import {MutationResolvers} from '../resolverTypes' + +const addIntegrationProvider: MutationResolvers['addIntegrationProvider'] = async ( + _source, + {input}, + context +) => { + const {authToken, dataLoader, socketId: mutatorId} = context + const {teamId, orgId, scope, service} = input + const viewerId = getUserId(authToken) + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + + // INPUT VALIDATION + if (service === 'jira' || service === 'github') { + return {error: {message: 'Service is not supported'}} + } + if (scope === 'global' && (teamId || orgId)) { + return {error: {message: 'Global providers must not have an `orgId` nor `teamId`'}} + } + if (scope === 'org' && (!orgId || teamId)) { + return {error: {message: 'Org providers must have an `orgId` and no `teamId`'}} + } + if (scope === 'team' && (!teamId || orgId)) { + return {error: {message: 'Team providers must have a `teamId` and no `orgId`'}} + } + + // AUTH + if (!isSuperUser(authToken)) { + if (scope === 'global') { + return {error: {message: 'Must be a super user to add a global provider'}} + } + if (scope === 'org' && !(await isUserOrgAdmin(viewerId, orgId!, dataLoader))) { + return { + error: { + message: + 'Must be an organization admin to add an integration provider on organization level' + } + } + } + if (scope === 'team' && !isTeamMember(authToken, teamId!)) { + return {error: {message: 'Must be on the team for the integration provider'}} + } + } + + // VALIDATION + const { + authStrategy, + oAuth1ProviderMetadataInput, + oAuth2ProviderMetadataInput, + webhookProviderMetadataInput, + sharedSecretMetadataInput, + ...rest + } = input + + if (authStrategy === 'oauth1' && !oAuth1ProviderMetadataInput) { + return {error: {message: 'Auth strategy oauth1 requires oAuth1ProviderMetadataInput'}} + } + if (authStrategy === 'oauth2' && !oAuth2ProviderMetadataInput) { + return {error: {message: 'Auth strategy oauth2 requires oAuth2ProviderMetadataInput'}} + } + if (authStrategy === 'webhook' && !webhookProviderMetadataInput) { + return {error: {message: 'Auth strategy webhook requires webhookProviderMetadataInput'}} + } + if ( + [ + oAuth1ProviderMetadataInput, + oAuth2ProviderMetadataInput, + webhookProviderMetadataInput, + sharedSecretMetadataInput + ].filter(isNotNull).length !== 1 + ) { + return {error: {message: 'Exactly 1 metadata provider is expected'}} + } + + // RESOLUTION + const providerId = await upsertIntegrationProvider({ + authStrategy, + ...rest, + service, + ...oAuth1ProviderMetadataInput, + ...oAuth2ProviderMetadataInput, + ...webhookProviderMetadataInput, + ...sharedSecretMetadataInput, + ...(scope === 'global' + ? {orgId: null, teamId: null} + : scope === 'org' + ? {orgId: orgId!, teamId: null} + : {orgId: null, teamId: teamId!}) + }) + + const data = { + providerId, + orgId: orgId || undefined, + teamId: teamId || undefined + } + if (orgId) { + publish( + SubscriptionChannel.ORGANIZATION, + orgId, + 'AddIntegrationProviderSuccess', + data, + subOptions + ) + } + return data +} + +export default addIntegrationProvider diff --git a/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts b/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts index bf5fa254250..b4578c2a85e 100644 --- a/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts +++ b/packages/server/graphql/public/mutations/addTeamMemberIntegrationAuth.ts @@ -149,15 +149,18 @@ const addTeamMemberIntegrationAuth: MutationResolvers['addTeamMemberIntegrationA }) } - await pg - .insertInto('NotificationSettings') - .columns(['authId', 'event']) - .values(() => ({ - authId, - event: sql`unnest(enum_range(NULL::"SlackNotificationEventEnum"))` - })) - .onConflict((oc) => oc.doNothing()) - .execute() + if (service === 'msTeams' || service === 'mattermost') { + await pg + .insertInto('TeamNotificationSettings') + .columns(['providerId', 'teamId', 'events']) + .values(() => ({ + providerId: providerDbId, + teamId, + events: sql`enum_range(NULL::"SlackNotificationEventEnum")` + })) + .onConflict((oc) => oc.doNothing()) + .execute() + } updateRepoIntegrationsCacheByPerms(dataLoader, viewerId, teamId, true) diff --git a/packages/server/graphql/public/mutations/setNotificationSetting.ts b/packages/server/graphql/public/mutations/setNotificationSetting.ts deleted file mode 100644 index cd79ed2a4b4..00000000000 --- a/packages/server/graphql/public/mutations/setNotificationSetting.ts +++ /dev/null @@ -1,54 +0,0 @@ -import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import TeamMemberIntegrationAuthId from '../../../../client/shared/gqlIds/TeamMemberIntegrationAuthId' -import getKysely from '../../../postgres/getKysely' -import {getUserId, isTeamMember} from '../../../utils/authorization' -import publish from '../../../utils/publish' -import standardError from '../../../utils/standardError' -import {MutationResolvers} from '../resolverTypes' - -const setNotificationSetting: MutationResolvers['setNotificationSetting'] = async ( - _source, - {authId: gqlAuthId, event, isEnabled}, - {authToken, dataLoader, socketId: mutatorId} -) => { - const viewerId = getUserId(authToken) - const operationId = dataLoader.share() - const subOptions = {mutatorId, operationId} - const pg = getKysely() - - // AUTH - const authId = TeamMemberIntegrationAuthId.split(gqlAuthId) - const auth = await dataLoader.get('teamMemberIntegrationAuths').load(authId) - if (!auth) { - return standardError(new Error('Integration auth not found'), {userId: viewerId}) - } - const {teamId, service} = auth - if (!isTeamMember(authToken, teamId)) { - return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) - } - - // VALIDATION - if (service !== 'mattermost' && service !== 'msTeams') { - return standardError(new Error('Invalid integration provider'), {userId: viewerId}) - } - - // RESOLUTION - if (isEnabled) { - await pg - .insertInto('NotificationSettings') - .values({authId, event}) - .onConflict((oc) => oc.doNothing()) - .execute() - } else { - await pg - .deleteFrom('NotificationSettings') - .where('authId', '=', authId) - .where('event', '=', event) - .execute() - } - const data = {authId} - publish(SubscriptionChannel.TEAM, teamId, 'SetNotificationSettingSuccess', data, subOptions) - return data -} - -export default setNotificationSetting diff --git a/packages/server/graphql/public/mutations/setTeamNotificationSetting.ts b/packages/server/graphql/public/mutations/setTeamNotificationSetting.ts new file mode 100644 index 00000000000..55d82031bd1 --- /dev/null +++ b/packages/server/graphql/public/mutations/setTeamNotificationSetting.ts @@ -0,0 +1,58 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import TeamNotificationSettingsId from '../../../../client/shared/gqlIds/TeamNotificationSettingsId' +import getKysely from '../../../postgres/getKysely' +import {getUserId, isTeamMember} from '../../../utils/authorization' +import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' + +const setTeamNotificationSetting: MutationResolvers['setTeamNotificationSetting'] = async ( + _source, + {id: gqlId, event, isEnabled}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const pg = getKysely() + const id = TeamNotificationSettingsId.split(gqlId) + + // AUTH + const setting = await dataLoader.get('teamNotificationSettings').load(id) + if (!setting) { + return standardError(new Error('TeamNotificationSetting not found'), {userId: viewerId}) + } + const {teamId} = setting + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Attempted teamId spoof'), {userId: viewerId}) + } + + // RESOLUTION + if (isEnabled) { + await pg + .updateTable('TeamNotificationSettings') + .set(({fn, val}) => ({ + events: fn('arr_append_uniq', ['events', val(event)]) + })) + .where('id', '=', id) + .execute() + } else { + await pg + .updateTable('TeamNotificationSettings') + .set(({fn, val}) => ({ + events: fn('array_remove', ['events', val(event)]) + })) + .where('id', '=', id) + .execute() + } + // update dataLoader + setting.events = isEnabled + ? [...setting.events, event] + : setting.events.filter((e: any) => e !== event) + + const data = {id} + publish(SubscriptionChannel.TEAM, teamId, 'SetNotificationSettingSuccess', data, subOptions) + return data +} + +export default setTeamNotificationSetting diff --git a/packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql b/packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql index 51ef034678a..8cf853b2101 100644 --- a/packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql +++ b/packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql @@ -11,4 +11,19 @@ type MSTeamsIntegration { The non-global providers shared with the team or organization """ sharedProviders: [IntegrationProviderWebhook!]! + + """ + An active team member has integrated with a provider for this integration + """ + isActive: Boolean! + + """ + If any team member integrated with mattermost, this will be the active provider for this team + """ + activeProvider: IntegrationProviderWebhook + + """ + Team notification settings for this provider. + """ + teamNotificationSettings(channel: ID): TeamNotificationSettings } diff --git a/packages/server/graphql/public/typeDefs/MattermostIntegration.graphql b/packages/server/graphql/public/typeDefs/MattermostIntegration.graphql index e072cb55448..bdff353f27a 100644 --- a/packages/server/graphql/public/typeDefs/MattermostIntegration.graphql +++ b/packages/server/graphql/public/typeDefs/MattermostIntegration.graphql @@ -11,4 +11,19 @@ type MattermostIntegration { The non-global providers shared with the team or organization """ sharedProviders: [IntegrationProviderWebhook!]! + + """ + An active team member has integrated with a provider for this integration + """ + isActive: Boolean! + + """ + If any team member integrated with mattermost, this will be the active provider for this team + """ + activeProvider: IntegrationProviderWebhook + + """ + Team notification settings for this provider. + """ + teamNotificationSettings(channel: ID): TeamNotificationSettings } diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql index ea0a7b472b5..adbd1df3cd5 100644 --- a/packages/server/graphql/public/typeDefs/Mutation.graphql +++ b/packages/server/graphql/public/typeDefs/Mutation.graphql @@ -802,20 +802,20 @@ type Mutation { ): SetSlackNotificationPayload! """ - Set the notification settings for a provider and team + Update the notification settings for a provider and team """ - setNotificationSetting( + setTeamNotificationSetting( """ - ID of the TeamMemberIntegrationAuth for which to set the notification setting + The unique id for the setting """ - authId: ID! + id: ID! """ Event type to modify """ event: SlackNotificationEventEnum! isEnabled: Boolean! - ): SetNotificationSettingPayload! + ): SetTeamNotificationSettingPayload! """ Broadcast that the viewer started dragging a reflection diff --git a/packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql b/packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql deleted file mode 100644 index 5c691a798c0..00000000000 --- a/packages/server/graphql/public/typeDefs/SetNotificationSettingPayload.graphql +++ /dev/null @@ -1,4 +0,0 @@ -""" -Return for setNotificationSetting mutation -""" -union SetNotificationSettingPayload = ErrorPayload | SetNotificationSettingSuccess diff --git a/packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql b/packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql deleted file mode 100644 index 7399371f82b..00000000000 --- a/packages/server/graphql/public/typeDefs/SetNotificationSettingSuccess.graphql +++ /dev/null @@ -1,14 +0,0 @@ -type SetNotificationSettingSuccess { - authId: ID! - - """ - The updated auth object - For now this is only implemented for webhook - """ - auth: TeamMemberIntegrationAuthWebhook! - - """ - Enabled events for this provider and team - """ - events: [SlackNotificationEventEnum!]! -} diff --git a/packages/server/graphql/public/typeDefs/SetTeamNotificationSettingPayload.graphql b/packages/server/graphql/public/typeDefs/SetTeamNotificationSettingPayload.graphql new file mode 100644 index 00000000000..272579e49fd --- /dev/null +++ b/packages/server/graphql/public/typeDefs/SetTeamNotificationSettingPayload.graphql @@ -0,0 +1,4 @@ +""" +Return for setTeamNotificationSetting mutation +""" +union SetTeamNotificationSettingPayload = ErrorPayload | SetTeamNotificationSettingSuccess diff --git a/packages/server/graphql/public/typeDefs/SetTeamNotificationSettingSuccess.graphql b/packages/server/graphql/public/typeDefs/SetTeamNotificationSettingSuccess.graphql new file mode 100644 index 00000000000..b83378886b2 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/SetTeamNotificationSettingSuccess.graphql @@ -0,0 +1,6 @@ +type SetTeamNotificationSettingSuccess { + """ + The updated settings + """ + teamNotificationSettings: TeamNotificationSettings! +} diff --git a/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql b/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql index 17bac55ca03..aa0bf1c0bad 100644 --- a/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql +++ b/packages/server/graphql/public/typeDefs/TeamMemberIntegrationAuthWebhook.graphql @@ -41,9 +41,4 @@ type TeamMemberIntegrationAuthWebhook implements TeamMemberIntegrationAuth { The provider strategy this token connects to """ provider: IntegrationProviderWebhook! - - """ - The events that trigger a notification - """ - events: [SlackNotificationEventEnum!]! } diff --git a/packages/server/graphql/public/typeDefs/TeamNotificationSettings.graphql b/packages/server/graphql/public/typeDefs/TeamNotificationSettings.graphql new file mode 100644 index 00000000000..cf6be0a5802 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/TeamNotificationSettings.graphql @@ -0,0 +1,19 @@ +""" +Notification settings for a team. All team members +""" +type TeamNotificationSettings { + id: ID! + teamId: ID! + providerId: ID! + + """ + The channel in the integration to wich these settings apply. + Null for the default channel. + """ + channel: ID + + """ + The events that trigger a notification + """ + events: [SlackNotificationEventEnum!]! +} diff --git a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql index 07648a0bb0d..3b2f0f46483 100644 --- a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql +++ b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql @@ -22,7 +22,7 @@ type TeamSubscriptionPayload { RemoveTeamMemberPayload: RemoveTeamMemberPayload RenameMeetingSuccess: RenameMeetingSuccess SelectTemplatePayload: SelectTemplatePayload - SetNotificationSettingSuccess: SetNotificationSettingSuccess + SetTeamNotificationSettingSuccess: SetTeamNotificationSettingSuccess StartCheckInSuccess: StartCheckInSuccess StartRetrospectiveSuccess: StartRetrospectiveSuccess StartSprintPokerSuccess: StartSprintPokerSuccess diff --git a/packages/server/graphql/public/types/IntegrationProvider.ts b/packages/server/graphql/public/types/IntegrationProvider.ts new file mode 100644 index 00000000000..9d229b94205 --- /dev/null +++ b/packages/server/graphql/public/types/IntegrationProvider.ts @@ -0,0 +1,11 @@ +import IntegrationProviderId from '../../../../client/shared/gqlIds/IntegrationProviderId' +import {TIntegrationProvider} from '../../../postgres/queries/getIntegrationProvidersByIds' +import {IntegrationProviderResolvers} from '../resolverTypes' + +export type IntegrationProviderSource = TIntegrationProvider + +const IntegrationProvider: IntegrationProviderResolvers = { + id: ({id}) => IntegrationProviderId.join(id) +} + +export default IntegrationProvider diff --git a/packages/server/graphql/public/types/IntegrationProviderOAuth1.ts b/packages/server/graphql/public/types/IntegrationProviderOAuth1.ts new file mode 100644 index 00000000000..3eda033704a --- /dev/null +++ b/packages/server/graphql/public/types/IntegrationProviderOAuth1.ts @@ -0,0 +1,12 @@ +import {TIntegrationProvider} from '../../../postgres/queries/getIntegrationProvidersByIds' +import {IntegrationProviderOAuth1Resolvers} from '../resolverTypes' +import IntegrationProvider from './IntegrationProvider' + +export type IntegrationProviderOAuth1Source = TIntegrationProvider + +const IntegrationProviderOAuth1: IntegrationProviderOAuth1Resolvers = { + ...IntegrationProvider, + __isTypeOf: ({authStrategy}) => authStrategy === 'oauth1' +} + +export default IntegrationProviderOAuth1 diff --git a/packages/server/graphql/public/types/IntegrationProviderOAuth2.ts b/packages/server/graphql/public/types/IntegrationProviderOAuth2.ts new file mode 100644 index 00000000000..091512b46d6 --- /dev/null +++ b/packages/server/graphql/public/types/IntegrationProviderOAuth2.ts @@ -0,0 +1,12 @@ +import {TIntegrationProvider} from '../../../postgres/queries/getIntegrationProvidersByIds' +import {IntegrationProviderOAuth2Resolvers} from '../resolverTypes' +import IntegrationProvider from './IntegrationProvider' + +export type IntegrationProviderOAuth2Source = TIntegrationProvider + +const IntegrationProviderOAuth2: IntegrationProviderOAuth2Resolvers = { + ...IntegrationProvider, + __isTypeOf: ({authStrategy}) => authStrategy === 'oauth2' +} + +export default IntegrationProviderOAuth2 diff --git a/packages/server/graphql/public/types/IntegrationProviderWebhook.ts b/packages/server/graphql/public/types/IntegrationProviderWebhook.ts new file mode 100644 index 00000000000..c4b3d59674d --- /dev/null +++ b/packages/server/graphql/public/types/IntegrationProviderWebhook.ts @@ -0,0 +1,12 @@ +import {TIntegrationProvider} from '../../../postgres/queries/getIntegrationProvidersByIds' +import {IntegrationProviderWebhookResolvers} from '../resolverTypes' +import IntegrationProvider from './IntegrationProvider' + +export type IntegrationProviderWebhookSource = TIntegrationProvider + +const IntegrationProviderWebhook: IntegrationProviderWebhookResolvers = { + ...IntegrationProvider, + __isTypeOf: ({authStrategy}) => authStrategy === 'webhook' +} + +export default IntegrationProviderWebhook diff --git a/packages/server/graphql/public/types/MSTeamsIntegration.ts b/packages/server/graphql/public/types/MSTeamsIntegration.ts index 9b434ed29fe..78ceae203f2 100644 --- a/packages/server/graphql/public/types/MSTeamsIntegration.ts +++ b/packages/server/graphql/public/types/MSTeamsIntegration.ts @@ -1,3 +1,4 @@ +import {DataLoaderWorker} from '../../graphql' import {MsTeamsIntegrationResolvers} from '../resolverTypes' export type MSTeamsIntegrationSource = { @@ -5,6 +6,15 @@ export type MSTeamsIntegrationSource = { userId: string } +const loadActiveProvider = async (teamId: string, dataLoader: DataLoaderWorker) => { + const auths = await dataLoader + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({teamId, service: 'mattermost'}) + if (!auths || auths.length !== 1) return null + const {providerId} = auths[0]! + return await dataLoader.get('integrationProviders').loadNonNull(providerId) +} + const MSTeamsIntegration: MsTeamsIntegrationResolvers = { auth: async ({teamId, userId}, _args, {dataLoader}) => { return dataLoader @@ -18,6 +28,27 @@ const MSTeamsIntegration: MsTeamsIntegrationResolvers = { return dataLoader .get('sharedIntegrationProviders') .load({service: 'msTeams', orgIds: [orgId], teamIds: [teamId]}) + }, + + isActive: async ({teamId}, _args, {dataLoader}) => { + const auths = await dataLoader + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({teamId, service: 'mattermost'}) + return auths && auths.length > 1 + }, + + activeProvider: async ({teamId}, _args, {dataLoader}) => { + return loadActiveProvider(teamId, dataLoader) + }, + + teamNotificationSettings: async ({teamId}, {channel}, {dataLoader}) => { + const activeProvider = await loadActiveProvider(teamId, dataLoader) + if (!activeProvider) return null + const {id} = activeProvider + const settings = await dataLoader + .get('teamNotificationSettingsByProviderIdAndTeamId') + .load({providerId: id, teamId}) + return settings.find(({channelId}) => (!channelId && !channel) || channelId === channel) || null } } diff --git a/packages/server/graphql/public/types/MattermostIntegration.ts b/packages/server/graphql/public/types/MattermostIntegration.ts index 9bbc3521e9a..a0c2269f471 100644 --- a/packages/server/graphql/public/types/MattermostIntegration.ts +++ b/packages/server/graphql/public/types/MattermostIntegration.ts @@ -1,3 +1,4 @@ +import {DataLoaderWorker} from '../../graphql' import {MattermostIntegrationResolvers} from '../resolverTypes' export type MattermostIntegrationSource = { @@ -5,6 +6,15 @@ export type MattermostIntegrationSource = { userId: string } +const loadActiveProvider = async (teamId: string, dataLoader: DataLoaderWorker) => { + const auths = await dataLoader + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({teamId, service: 'mattermost'}) + if (!auths || auths.length !== 1) return null + const {providerId} = auths[0]! + return await dataLoader.get('integrationProviders').loadNonNull(providerId) +} + const MattermostIntegration: MattermostIntegrationResolvers = { auth: async ({teamId, userId}, _args, {dataLoader}) => { const res = await dataLoader @@ -19,6 +29,27 @@ const MattermostIntegration: MattermostIntegrationResolvers = { return dataLoader .get('sharedIntegrationProviders') .load({service: 'mattermost', orgIds: [orgId], teamIds: [teamId]}) + }, + + isActive: async ({teamId}, _args, {dataLoader}) => { + const auths = await dataLoader + .get('teamMemberIntegrationAuthsByTeamIdAndService') + .load({teamId, service: 'mattermost'}) + return auths && auths.length > 1 + }, + + activeProvider: async ({teamId}, _args, {dataLoader}) => { + return loadActiveProvider(teamId, dataLoader) + }, + + teamNotificationSettings: async ({teamId}, {channel}, {dataLoader}) => { + const activeProvider = await loadActiveProvider(teamId, dataLoader) + if (!activeProvider) return null + const {id} = activeProvider + const settings = await dataLoader + .get('teamNotificationSettingsByProviderIdAndTeamId') + .load({providerId: id, teamId}) + return settings.find(({channelId}) => (!channelId && !channel) || channelId === channel) || null } } diff --git a/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts b/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts deleted file mode 100644 index c799bac1cc0..00000000000 --- a/packages/server/graphql/public/types/SetNotificationSettingSuccess.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {SetNotificationSettingSuccessResolvers} from '../resolverTypes' - -export type SetNotificationSettingSuccessSource = { - authId: number -} - -const SetNotificationSettingSuccess: SetNotificationSettingSuccessResolvers = { - auth: async ({authId}, _args, {dataLoader}) => { - return dataLoader.get('teamMemberIntegrationAuths').loadNonNull(authId) - }, - events: async ({authId}, _args, {dataLoader}) => { - return dataLoader.get('notificationSettingsByAuthId').load(authId) - } -} - -export default SetNotificationSettingSuccess diff --git a/packages/server/graphql/public/types/SetTeamNotificationSettingSuccess.ts b/packages/server/graphql/public/types/SetTeamNotificationSettingSuccess.ts new file mode 100644 index 00000000000..5ba4887b016 --- /dev/null +++ b/packages/server/graphql/public/types/SetTeamNotificationSettingSuccess.ts @@ -0,0 +1,16 @@ +import {SetTeamNotificationSettingSuccessResolvers} from '../resolverTypes' + +export type SetTeamNotificationSettingSuccessSource = { + // db id + id: number +} + +const SetTeamNotificationSettingSuccess: SetTeamNotificationSettingSuccessResolvers = { + teamNotificationSettings: async (source, _args, {dataLoader}) => { + const {id} = source + const settings = await dataLoader.get('teamNotificationSettings').loadNonNull(id) + return settings + } +} + +export default SetTeamNotificationSettingSuccess diff --git a/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts b/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts index fe7b1e72ab5..e0246a384d5 100644 --- a/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts +++ b/packages/server/graphql/public/types/TeamMemberIntegrationAuthWebhook.ts @@ -8,9 +8,6 @@ const TeamMemberIntegrationAuthWebhook: TeamMemberIntegrationAuthWebhookResolver providerId: ({providerId}) => IntegrationProviderId.join(providerId), provider: async ({providerId}, _args, {dataLoader}) => { return dataLoader.get('integrationProviders').loadNonNull(providerId) - }, - events: async ({id}, _args, {dataLoader}) => { - return dataLoader.get('notificationSettingsByAuthId').load(id) } } diff --git a/packages/server/graphql/public/types/TeamNotificationSettings.ts b/packages/server/graphql/public/types/TeamNotificationSettings.ts new file mode 100644 index 00000000000..7b6bb2ea997 --- /dev/null +++ b/packages/server/graphql/public/types/TeamNotificationSettings.ts @@ -0,0 +1,12 @@ +import {Selectable} from 'kysely' +import TeamNotificationSettingsId from '../../../../client/shared/gqlIds/TeamNotificationSettingsId' +import {TeamNotificationSettings as TeamNotificationSettingsDB} from '../../../postgres/types/pg' +import {TeamNotificationSettingsResolvers} from '../resolverTypes' + +export type TeamNotificationSettingsSource = Selectable + +const TeamNotificationSettings: TeamNotificationSettingsResolvers = { + id: ({id}) => TeamNotificationSettingsId.join(id) +} + +export default TeamNotificationSettings diff --git a/packages/server/graphql/rootMutation.ts b/packages/server/graphql/rootMutation.ts index 4a5aa57d749..5b457bf14ee 100644 --- a/packages/server/graphql/rootMutation.ts +++ b/packages/server/graphql/rootMutation.ts @@ -2,7 +2,6 @@ import {GraphQLObjectType} from 'graphql' import {GQLContext} from './graphql' import addAtlassianAuth from './mutations/addAtlassianAuth' import addGitHubAuth from './mutations/addGitHubAuth' -import addIntegrationProvider from './mutations/addIntegrationProvider' import addOrg from './mutations/addOrg' import addPokerTemplateDimension from './mutations/addPokerTemplateDimension' import addPokerTemplateScale from './mutations/addPokerTemplateScale' @@ -192,7 +191,6 @@ export default new GraphQLObjectType({ toggleTeamDrawer, updateGitHubDimensionField, createPoll, - addIntegrationProvider, removeIntegrationProvider, updateAzureDevOpsDimensionField }) as any diff --git a/packages/server/graphql/rootTypes.ts b/packages/server/graphql/rootTypes.ts index e9294b3f48c..b974f7f054d 100644 --- a/packages/server/graphql/rootTypes.ts +++ b/packages/server/graphql/rootTypes.ts @@ -1,7 +1,3 @@ -import IntegrationProviderOAuth1 from './types/IntegrationProviderOAuth1' -import IntegrationProviderOAuth2 from './types/IntegrationProviderOAuth2' -import IntegrationProviderSharedSecret from './types/IntegrationProviderSharedSecret' -import IntegrationProviderWebhook from './types/IntegrationProviderWebhook' import JiraDimensionField from './types/JiraDimensionField' import RenamePokerTemplatePayload from './types/RenamePokerTemplatePayload' import SetMeetingSettingsPayload from './types/SetMeetingSettingsPayload' @@ -13,10 +9,6 @@ import TimelineEventTeamCreated from './types/TimelineEventTeamCreated' import UserTiersCount from './types/UserTiersCount' const rootTypes = [ - IntegrationProviderOAuth1, - IntegrationProviderOAuth2, - IntegrationProviderSharedSecret, - IntegrationProviderWebhook, SetMeetingSettingsPayload, TimelineEventTeamCreated, TimelineEventJoinedParabol, diff --git a/packages/server/graphql/types/AddIntegrationProviderPayload.ts b/packages/server/graphql/types/AddIntegrationProviderPayload.ts deleted file mode 100644 index 04ea60b7f17..00000000000 --- a/packages/server/graphql/types/AddIntegrationProviderPayload.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import IntegrationProvider from './IntegrationProvider' -import makeMutationPayload from './makeMutationPayload' - -export const AddIntegrationProviderSuccess = new GraphQLObjectType({ - name: 'AddIntegrationProviderSuccess', - fields: () => ({ - provider: { - type: new GraphQLNonNull(IntegrationProvider), - description: 'The provider that was added', - resolve: async ({providerId}, _args, {dataLoader}) => { - return dataLoader.get('integrationProviders').load(providerId) - } - } - }) -}) - -const AddIntegrationProviderPayload = makeMutationPayload( - 'AddIntegrationProviderPayload', - AddIntegrationProviderSuccess -) - -export default AddIntegrationProviderPayload diff --git a/packages/server/graphql/types/IntegrationProvider.ts b/packages/server/graphql/types/IntegrationProvider.ts deleted file mode 100644 index e0a0bd6d2c0..00000000000 --- a/packages/server/graphql/types/IntegrationProvider.ts +++ /dev/null @@ -1,56 +0,0 @@ -import {GraphQLBoolean, GraphQLID, GraphQLInterfaceType, GraphQLNonNull} from 'graphql' -import IntegrationProviderId from 'parabol-client/shared/gqlIds/IntegrationProviderId' -import {TIntegrationProvider} from '../../postgres/queries/getIntegrationProvidersByIds' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import IntegrationProviderAuthStrategyEnum from './IntegrationProviderAuthStrategyEnum' -import IntegrationProviderScopeEnum from './IntegrationProviderScopeEnum' -import IntegrationProviderServiceEnum from './IntegrationProviderServiceEnum' - -export const integrationProviderFields = () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: "The provider's unique identifier", - resolve: ({id}: TIntegrationProvider) => IntegrationProviderId.join(id) - }, - teamId: { - type: GraphQLID, - description: 'The team that belongs to the provider if team scoped' - }, - orgId: { - type: GraphQLID, - description: 'The organization that belongs to the provider if org scoped' - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the provider was created' - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the token was updated at' - }, - service: { - description: 'The name of the integration service (GitLab, Mattermost, etc)', - type: new GraphQLNonNull(IntegrationProviderServiceEnum) - }, - authStrategy: { - description: 'The kind of token used by this provider (OAuth2, PAT, Webhook)', - type: new GraphQLNonNull(IntegrationProviderAuthStrategyEnum) - }, - scope: { - description: - 'The scope this provider configuration was created at (globally, org-wide, or by the team)', - type: new GraphQLNonNull(IntegrationProviderScopeEnum) - }, - isActive: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the provider configuration should be used' - } -}) - -const IntegrationProvider = new GraphQLInterfaceType({ - name: 'IntegrationProvider', - description: 'An authentication provider configuration', - fields: integrationProviderFields -}) - -export default IntegrationProvider diff --git a/packages/server/graphql/types/IntegrationProviderOAuth1.ts b/packages/server/graphql/types/IntegrationProviderOAuth1.ts deleted file mode 100644 index ac9631eeef8..00000000000 --- a/packages/server/graphql/types/IntegrationProviderOAuth1.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import GraphQLURLType from './GraphQLURLType' -import IntegrationProvider, {integrationProviderFields} from './IntegrationProvider' - -const IntegrationProviderOAuth1 = new GraphQLObjectType({ - name: 'IntegrationProviderOAuth1', - description: 'An integration provider that connects via OAuth1.0', - interfaces: () => [IntegrationProvider], - isTypeOf: ({authStrategy}) => authStrategy === 'oauth1', - fields: () => ({ - ...integrationProviderFields(), - serverBaseUrl: { - type: new GraphQLNonNull(GraphQLURLType), - description: 'The base URL of the OAuth1 server' - } - }) -}) - -export default IntegrationProviderOAuth1 diff --git a/packages/server/graphql/types/IntegrationProviderOAuth2.ts b/packages/server/graphql/types/IntegrationProviderOAuth2.ts deleted file mode 100644 index 3dbb591c5b1..00000000000 --- a/packages/server/graphql/types/IntegrationProviderOAuth2.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import GraphQLURLType from './GraphQLURLType' -import IntegrationProvider, {integrationProviderFields} from './IntegrationProvider' - -const IntegrationProviderOAuth2 = new GraphQLObjectType({ - name: 'IntegrationProviderOAuth2', - description: 'An integration provider that connects via OAuth2', - interfaces: () => [IntegrationProvider], - isTypeOf: ({authStrategy}) => authStrategy === 'oauth2', - fields: () => ({ - ...integrationProviderFields(), - serverBaseUrl: { - type: new GraphQLNonNull(GraphQLURLType), - description: 'The base URL of the OAuth2 server' - }, - clientId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The OAuth2 client id' - }, - tenantId: { - type: GraphQLID, - description: 'The tenant ID for Azure Active Directory Auth' - } - }) -}) - -export default IntegrationProviderOAuth2 diff --git a/packages/server/graphql/types/IntegrationProviderSharedSecret.ts b/packages/server/graphql/types/IntegrationProviderSharedSecret.ts deleted file mode 100644 index 05b16946dac..00000000000 --- a/packages/server/graphql/types/IntegrationProviderSharedSecret.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' -import GraphQLURLType from './GraphQLURLType' -import IntegrationProvider, {integrationProviderFields} from './IntegrationProvider' - -const IntegrationProviderSharedSecret = new GraphQLObjectType({ - name: 'IntegrationProviderSharedSecret', - description: 'An integration provider that connects via a shared secret', - interfaces: () => [IntegrationProvider], - isTypeOf: ({authStrategy}) => authStrategy === 'sharedSecret', - fields: () => ({ - ...integrationProviderFields(), - serverBaseUrl: { - type: new GraphQLNonNull(GraphQLURLType), - description: 'The base URL of the OAuth1 server' - }, - sharedSecret: { - type: new GraphQLNonNull(GraphQLString), - description: 'The shared secret used to sign requests' - } - }) -}) - -export default IntegrationProviderSharedSecret diff --git a/packages/server/graphql/types/IntegrationProviderWebhook.ts b/packages/server/graphql/types/IntegrationProviderWebhook.ts deleted file mode 100644 index ee5aa8be1f2..00000000000 --- a/packages/server/graphql/types/IntegrationProviderWebhook.ts +++ /dev/null @@ -1,20 +0,0 @@ -import {GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import GraphQLURLType from './GraphQLURLType' -import IntegrationProvider, {integrationProviderFields} from './IntegrationProvider' - -const IntegrationProviderWebhook = new GraphQLObjectType({ - name: 'IntegrationProviderWebhook', - description: 'An integration provider that connects via webhook', - interfaces: () => [IntegrationProvider], - isTypeOf: ({authStrategy}) => authStrategy === 'webhook', - fields: () => ({ - ...integrationProviderFields(), - webhookUrl: { - type: new GraphQLNonNull(GraphQLURLType), - description: 'The webhook URL' - } - }) -}) - -export default IntegrationProviderWebhook diff --git a/packages/server/postgres/migrations/2025-02-28T15:28:11.536Z_notificationSettingsPerTeamAndChannel.ts b/packages/server/postgres/migrations/2025-02-28T15:28:11.536Z_notificationSettingsPerTeamAndChannel.ts new file mode 100644 index 00000000000..c722a9c3574 --- /dev/null +++ b/packages/server/postgres/migrations/2025-02-28T15:28:11.536Z_notificationSettingsPerTeamAndChannel.ts @@ -0,0 +1,83 @@ +import {sql, type Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + // Schema changes significantly, easier to create a new table + await db.schema + .createTable('TeamNotificationSettings') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('providerId', 'integer', (col) => + col.references('IntegrationProvider.id').onDelete('cascade').notNull() + ) + .addColumn('teamId', 'varchar(100)', (col) => + col.references('Team.id').onDelete('cascade').notNull() + ) + .addColumn('channelId', 'varchar(255)') + .addColumn('events', sql`"SlackNotificationEventEnum"[]`, (col) => + col.defaultTo(sql`enum_range(NULL::"SlackNotificationEventEnum")`).notNull() + ) + .addUniqueConstraint( + 'TeamNotificationSettings_providerId_teamId_channelId_key', + ['providerId', 'teamId', 'channelId'], + (uc) => uc.nullsNotDistinct() + ) + .execute() + + await db + .insertInto('TeamNotificationSettings') + .columns(['providerId', 'teamId', 'events']) + .expression((eb) => + eb + .selectFrom('TeamMemberIntegrationAuth as auth') + .leftJoin('NotificationSettings as settings', 'auth.id', 'settings.authId') + .select(['auth.providerId', 'auth.teamId', sql`array_remove(array_agg(event), NULL)`]) + // There was a bug which might have added settings for other providers like gcal + .where((eb) => eb.or([eb('service', '=', 'mattermost'), eb('service', '=', 'msTeams')])) + .groupBy(['auth.providerId', 'auth.teamId']) + ) + .onConflict((oc) => + oc.constraint('TeamNotificationSettings_providerId_teamId_channelId_key').doNothing() + ) + .execute() + + /* dropping the old table will be done in a later change + await db.schema + .dropTable('NotificationSettings') + .execute() + */ +} + +export async function down(db: Kysely): Promise { + /* + await db.schema + .createTable('NotificationSettings') + .addColumn('id', 'serial', (col) => col.primaryKey()) + .addColumn('authId', 'integer', (col) => + col.references('TeamMemberIntegrationAuth.id').onDelete('cascade').notNull() + ) + .addColumn('event', sql`"SlackNotificationEventEnum"`, (col) => col.notNull()) + .addUniqueConstraint('NotificationSettings_authId_event_key', ['authId', 'event']) + .execute() + + await db.schema + .createIndex('NotificationSettings_authId_idx') + .on('NotificationSettings') + .column('authId') + .execute() + + await db + .insertInto('NotificationSettings') + .columns(['authId', 'event']) + .expression((eb) => + eb + .selectFrom('TeamMemberIntegrationAuth as auth') + .innerJoin('TeamNotificationSettings as settings', (join) => join + .onRef('auth.teamId', '=', 'settings.teamId') + .onRef('auth.providerId', '=', 'settings.providerId') + ) + .select(['auth.id', sql`unnest(events)`]) + ) + .execute() + */ + + await db.schema.dropTable('TeamNotificationSettings').execute() +} diff --git a/packages/server/postgres/queries/upsertIntegrationProvider.ts b/packages/server/postgres/queries/upsertIntegrationProvider.ts index a3221ff4766..890cd13f7f8 100644 --- a/packages/server/postgres/queries/upsertIntegrationProvider.ts +++ b/packages/server/postgres/queries/upsertIntegrationProvider.ts @@ -11,7 +11,7 @@ interface IUpsertIntegrationProviderInput { authStrategy: IntegrationProviderAuthStrategyEnum scope?: IntegrationProviderScopeEnum | null | undefined clientId?: string - tenantId?: string + tenantId?: string | null clientSecret?: string serverBaseUrl?: string webhookUrl?: string diff --git a/yarn.lock b/yarn.lock index e564cb9b901..03b89b091bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16705,17 +16705,18 @@ koalas@^1.0.2: resolved "https://registry.yarnpkg.com/koalas/-/koalas-1.0.2.tgz#318433f074235db78fae5661a02a8ca53ee295cd" integrity sha512-RYhBbYaTTTHId3l6fnMZc3eGQNW6FVCqMG6AMwA5I1Mafr6AflaXeoi6x3xQuATRotGYRLk6+1ELZH4dstFNOA== -kysely-codegen@^0.15.0: - version "0.15.0" - resolved "https://registry.yarnpkg.com/kysely-codegen/-/kysely-codegen-0.15.0.tgz#771c0256c24897ea64d5713dc10e40e8a359b96b" - integrity sha512-LPta2nQOyoEPDQ3w/Gsplc+2iyZPAsGvtWoS21VzOB0NDQ0B38Xy1gS8WlbGef542Zdw2eLJHxekud9DzVdNRw== +kysely-codegen@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/kysely-codegen/-/kysely-codegen-0.17.0.tgz#07bb2182ce2f315953c2407a52c99ee1ee942f91" + integrity sha512-C36g6epial8cIOSBEWGI9sRfkKSsEzTcivhjPivtYFQnhMdXnrVFaUe7UMZHeSdXaHiWDqDOkReJgWLD8nPKdg== dependencies: chalk "4.1.2" dotenv "^16.4.5" dotenv-expand "^11.0.6" git-diff "^2.0.6" - micromatch "^4.0.5" + micromatch "^4.0.8" minimist "^1.2.8" + pluralize "^8.0.0" kysely-ctl@^0.11.0: version "0.11.0" @@ -19680,6 +19681,11 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + pm2-axon-rpc@~0.7.0, pm2-axon-rpc@~0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/pm2-axon-rpc/-/pm2-axon-rpc-0.7.1.tgz#2daec5383a63135b3f18babb70266dacdcbc429a" @@ -22521,7 +22527,7 @@ string-similarity@^3.0.0: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-3.0.0.tgz#07b0bc69fae200ad88ceef4983878d03793847c7" integrity sha512-7kS7LyTp56OqOI2BDWQNVnLX/rCxIQn+/5M0op1WV6P8Xx6TZNdajpuqQdiJ7Xx+p1C5CsWMvdiBp9ApMhxzEQ== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -22539,6 +22545,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -22625,7 +22640,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -22639,6 +22654,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -24673,7 +24695,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -24691,6 +24713,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"