From 6d01097ef68bbbe33eea1faf1367d7ad6120f1db Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Thu, 18 Jul 2024 14:51:14 -0700 Subject: [PATCH] chore: move some integrations to SDL pattern (#10000) Signed-off-by: Matt Krick --- codegen.json | 20 ++ .../server/dataloader/azureDevOpsLoaders.ts | 4 +- .../server/dataloader/jiraServerLoaders.ts | 6 +- .../graphql/private/typeDefs/_legacy.graphql | 198 ------------- packages/server/graphql/public/rootSchema.ts | 48 +-- .../typeDefs/AtlassianIntegration.graphql | 60 +++- .../typeDefs/AzureDevOpsIntegration.graphql | 91 ++++++ .../typeDefs/AzureDevOpsRemoteProject.graphql | 29 ++ .../typeDefs/AzureDevOpsSearchQuery.graphql | 29 ++ .../typeDefs/AzureDevOpsWorkItem.graphql | 80 +++++ .../public/typeDefs/GitHubIntegration.graphql | 51 ++++ .../public/typeDefs/GitHubSearchQuery.graphql | 19 ++ .../public/typeDefs/GitLabIntegration.graphql | 84 ++++++ .../public/typeDefs/GitLabSearchQuery.graphql | 24 ++ .../public/typeDefs/JiraSearchQuery.graphql | 29 ++ .../typeDefs/JiraServerIntegration.graphql | 49 ++++ .../public/typeDefs/JiraServerIssue.graphql | 102 +++++++ .../typeDefs/MSTeamsIntegration.graphql | 14 + .../typeDefs/MattermostIntegration.graphql | 14 + .../public/typeDefs/Organization.graphql | 58 +++- .../graphql/public/typeDefs/PageInfo.graphql | 24 ++ .../typeDefs/PageInfoDateCursor.graphql | 24 ++ .../public/typeDefs/SlackIntegration.graphql | 74 +++++ .../typeDefs/TeamMemberIntegrations.graphql | 10 +- .../graphql/public/typeDefs/_legacy.graphql | 277 ------------------ .../public/types/AtlassianIntegration.ts | 34 ++- .../public/types/AzureDevOpsIntegration.ts | 77 +++++ .../public/types/AzureDevOpsRemoteProject.ts | 14 + .../public/types/AzureDevOpsSearchQuery.ts | 7 + .../public/types/AzureDevOpsWorkItem.ts | 25 ++ .../graphql/public/types/GitHubIntegration.ts | 30 ++ .../graphql/public/types/GitLabIntegration.ts | 155 ++++++++++ .../graphql/public/types/JiraSearchQuery.ts | 8 + .../public/types/JiraServerIntegration.ts | 208 +++++++++++++ .../graphql/public/types/JiraServerIssue.ts | 46 +++ .../public/types/JiraServerRemoteProject.ts | 25 ++ .../public/types/MSTeamsIntegration.ts | 24 ++ .../public/types/MattermostIntegration.ts | 27 ++ .../graphql/public/types/Organization.ts | 101 ++++++- .../graphql/public/types/SlackIntegration.ts | 18 ++ .../public/types/TeamMemberIntegrations.ts | 31 +- .../graphql/public/types/_xGitLabProject.ts | 9 + packages/server/graphql/rootTypes.ts | 2 - .../graphql/types/AtlassianIntegration.ts | 90 +----- .../graphql/types/AzureDevOpsIntegration.ts | 170 ----------- .../graphql/types/AzureDevOpsRemoteProject.ts | 59 ---- .../graphql/types/AzureDevOpsSearchQuery.ts | 41 --- .../graphql/types/AzureDevOpsWorkItem.ts | 102 ------- .../server/graphql/types/GitHubIntegration.ts | 81 +---- .../server/graphql/types/GitHubSearchQuery.ts | 25 -- .../server/graphql/types/GitLabIntegration.ts | 242 --------------- .../server/graphql/types/GitLabSearchQuery.ts | 29 -- .../server/graphql/types/JiraSearchQuery.ts | 40 --- .../graphql/types/JiraServerIntegration.ts | 265 ----------------- .../server/graphql/types/JiraServerIssue.ts | 117 -------- .../graphql/types/JiraServerRemoteProject.ts | 61 ---- .../graphql/types/MSTeamsIntegration.ts | 35 --- .../graphql/types/MattermostIntegration.ts | 43 --- packages/server/graphql/types/Organization.ts | 238 +-------------- .../server/graphql/types/SlackIntegration.ts | 84 +----- .../graphql/types/TeamMemberIntegrations.ts | 71 +---- 61 files changed, 1745 insertions(+), 2307 deletions(-) create mode 100644 packages/server/graphql/public/typeDefs/AzureDevOpsIntegration.graphql create mode 100644 packages/server/graphql/public/typeDefs/AzureDevOpsRemoteProject.graphql create mode 100644 packages/server/graphql/public/typeDefs/AzureDevOpsSearchQuery.graphql create mode 100644 packages/server/graphql/public/typeDefs/AzureDevOpsWorkItem.graphql create mode 100644 packages/server/graphql/public/typeDefs/GitHubIntegration.graphql create mode 100644 packages/server/graphql/public/typeDefs/GitHubSearchQuery.graphql create mode 100644 packages/server/graphql/public/typeDefs/GitLabIntegration.graphql create mode 100644 packages/server/graphql/public/typeDefs/GitLabSearchQuery.graphql create mode 100644 packages/server/graphql/public/typeDefs/JiraSearchQuery.graphql create mode 100644 packages/server/graphql/public/typeDefs/JiraServerIntegration.graphql create mode 100644 packages/server/graphql/public/typeDefs/JiraServerIssue.graphql create mode 100644 packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql create mode 100644 packages/server/graphql/public/typeDefs/MattermostIntegration.graphql create mode 100644 packages/server/graphql/public/typeDefs/PageInfo.graphql create mode 100644 packages/server/graphql/public/typeDefs/PageInfoDateCursor.graphql create mode 100644 packages/server/graphql/public/typeDefs/SlackIntegration.graphql create mode 100644 packages/server/graphql/public/types/AzureDevOpsIntegration.ts create mode 100644 packages/server/graphql/public/types/AzureDevOpsRemoteProject.ts create mode 100644 packages/server/graphql/public/types/AzureDevOpsSearchQuery.ts create mode 100644 packages/server/graphql/public/types/AzureDevOpsWorkItem.ts create mode 100644 packages/server/graphql/public/types/GitHubIntegration.ts create mode 100644 packages/server/graphql/public/types/GitLabIntegration.ts create mode 100644 packages/server/graphql/public/types/JiraSearchQuery.ts create mode 100644 packages/server/graphql/public/types/JiraServerIntegration.ts create mode 100644 packages/server/graphql/public/types/JiraServerIssue.ts create mode 100644 packages/server/graphql/public/types/JiraServerRemoteProject.ts create mode 100644 packages/server/graphql/public/types/MSTeamsIntegration.ts create mode 100644 packages/server/graphql/public/types/MattermostIntegration.ts create mode 100644 packages/server/graphql/public/types/SlackIntegration.ts delete mode 100644 packages/server/graphql/types/AzureDevOpsIntegration.ts delete mode 100644 packages/server/graphql/types/AzureDevOpsRemoteProject.ts delete mode 100644 packages/server/graphql/types/AzureDevOpsSearchQuery.ts delete mode 100644 packages/server/graphql/types/AzureDevOpsWorkItem.ts delete mode 100644 packages/server/graphql/types/GitHubSearchQuery.ts delete mode 100644 packages/server/graphql/types/GitLabIntegration.ts delete mode 100644 packages/server/graphql/types/GitLabSearchQuery.ts delete mode 100644 packages/server/graphql/types/JiraSearchQuery.ts delete mode 100644 packages/server/graphql/types/JiraServerIntegration.ts delete mode 100644 packages/server/graphql/types/JiraServerIssue.ts delete mode 100644 packages/server/graphql/types/JiraServerRemoteProject.ts delete mode 100644 packages/server/graphql/types/MSTeamsIntegration.ts delete mode 100644 packages/server/graphql/types/MattermostIntegration.ts diff --git a/codegen.json b/codegen.json index b4f07f4e750..8b189ff16ad 100644 --- a/codegen.json +++ b/codegen.json @@ -43,7 +43,19 @@ "packages/server/graphql/public/resolverTypes.ts": { "config": { "contextType": "../graphql#GQLContext", + "showUnusedMappers": false, "mappers": { + "_xGitLabProject": "./types/_xGitLabProject#_xGitLabProjectSource as _xGitLabProject", + "JiraServerIntegration": "./types/JiraServerIntegration#JiraServerIntegrationSource", + "GitHubIntegration": "../../postgres/queries/getGitHubAuthByUserIdTeamId#GitHubAuth", + "GitLabIntegration": "./types/GitLabIntegration#GitLabIntegrationSource", + "MattermostIntegration": "./types/MattermostIntegration#MattermostIntegrationSource", + "MSTeamsIntegration": "./types/MSTeamsIntegration#MSTeamsIntegrationSource", + "SlackIntegration": "../../database/types/SlackAuth#default as SlackAuthDB", + "SlackNotification": "../../database/types/SlackNotification#default as SlackNotificationDB", + "AzureDevOpsIntegration": ".types/AzureDevOpsIntegration#AzureDevOpsIntegrationSource", + "AzureDevOpsWorkItem": "../../dataloader/azureDevOpsLoaders#AzureDevOpsWorkItem", + "AzureDevOpsRemoteProject": "./types/AzureDevOpsRemoteProject#AzureDevOpsRemoteProjectSource", "AcceptRequestToJoinDomainSuccess": "./types/AcceptRequestToJoinDomainSuccess#AcceptRequestToJoinDomainSuccessSource", "AcceptTeamInvitationPayload": "./types/AcceptTeamInvitationPayload#AcceptTeamInvitationPayloadSource", "ActionMeeting": "../../database/types/MeetingAction#default", @@ -56,6 +68,8 @@ "AddedNotification": "./types/AddedNotification#AddedNotificationSource", "AgendaItem": "../../database/types/AgendaItem#default as AgendaItemDB", "ArchiveTeamPayload": "./types/ArchiveTeamPayload#ArchiveTeamPayloadSource", + "AtlassianIntegration": "../../postgres/queries/getAtlassianAuthByUserIdTeamId#AtlassianAuth as AtlassianAuthDB", + "JiraSearchQuery": "../../database/types/JiraSearchQuery#default as JiraSearchQueryDB", "AuthTokenPayload": "./types/AuthTokenPayload#AuthTokenPayloadSource", "AutogroupSuccess": "./types/AutogroupSuccess#AutogroupSuccessSource", "BatchArchiveTasksSuccess": "./types/BatchArchiveTasksSuccess#BatchArchiveTasksSuccessSource", @@ -71,8 +85,12 @@ "GcalIntegration": "./types/GcalIntegration#GcalIntegrationSource", "GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource", "GetTemplateSuggestionSuccess": "./types/GetTemplateSuggestionSuccess#GetTemplateSuggestionSuccessSource", + "IntegrationProviderWebhook": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", + "IntegrationProviderOAuth1": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", "IntegrationProviderOAuth2": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", "InviteToTeamPayload": "./types/InviteToTeamPayload#InviteToTeamPayloadSource", + "JiraServerIssue": "./types/JiraServerIssue#JiraServerIssueSource", + "JiraServerRemoteProject": "../../dataloader/jiraServerLoaders#JiraServerProject", "JiraIssue": "./types/JiraIssue#JiraIssueSource", "JiraRemoteProject": "../types/JiraRemoteProject#JiraRemoteProjectSource", "MeetingSeries": "../../postgres/types/MeetingSeries#MeetingSeries", @@ -126,6 +144,8 @@ "TeamHealthStage": "./types/TeamHealthStage#TeamHealthStageSource", "TeamInvitation": "../../database/types/TeamInvitation#default", "TeamMember": "../../database/types/TeamMember#default as TeamMemberDB", + "TeamMemberIntegrationAuthWebhook": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", + "TeamMemberIntegrationAuthOAuth1": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrations": "./types/TeamMemberIntegrations#TeamMemberIntegrationsSource", "TeamPromptMeeting": "../../database/types/MeetingTeamPrompt#default as MeetingTeamPromptDB", diff --git a/packages/server/dataloader/azureDevOpsLoaders.ts b/packages/server/dataloader/azureDevOpsLoaders.ts index 87d3c968903..e9fb91d6df6 100644 --- a/packages/server/dataloader/azureDevOpsLoaders.ts +++ b/packages/server/dataloader/azureDevOpsLoaders.ts @@ -103,6 +103,8 @@ export interface AzureDevOpsWorkItem { type: string descriptionHTML: string service: 'azureDevOps' + teamId: string + userId: string } export interface AzureUserInfo { @@ -123,7 +125,7 @@ export interface AzureAccountProject extends TeamProjectReference { service: 'azureDevOps' } -interface AzureProject extends ProjectRes { +export interface AzureProject extends ProjectRes { userId: string teamId: string service: 'azureDevOps' diff --git a/packages/server/dataloader/jiraServerLoaders.ts b/packages/server/dataloader/jiraServerLoaders.ts index 7f7d200b979..0cd151234fe 100644 --- a/packages/server/dataloader/jiraServerLoaders.ts +++ b/packages/server/dataloader/jiraServerLoaders.ts @@ -55,6 +55,8 @@ type TeamUserKey = { export type JiraServerProject = JiraServerRestProject & { service: 'jiraServer' providerId: number + userId: string + teamId: string } export const jiraServerIssue = (parent: RootDataLoader) => { @@ -123,7 +125,9 @@ export const allJiraServerProjects = (parent: RootDataLoader) => { .map((project) => ({ ...project, service: 'jiraServer' as const, - providerId: provider.id + providerId: provider.id, + userId, + teamId })) }) ) diff --git a/packages/server/graphql/private/typeDefs/_legacy.graphql b/packages/server/graphql/private/typeDefs/_legacy.graphql index 7530f49fefe..a8291647a0b 100644 --- a/packages/server/graphql/private/typeDefs/_legacy.graphql +++ b/packages/server/graphql/private/typeDefs/_legacy.graphql @@ -982,35 +982,6 @@ type StandardMutationError { message: String! } -""" -A jira search query including all filters selected when the query was executed -""" -type JiraSearchQuery { - """ - shortid - """ - id: ID! - - """ - The query string, either simple or JQL depending on the isJQL flag - """ - queryString: String! - - """ - true if the queryString is JQL, else false - """ - isJQL: Boolean! - - """ - The list of project keys selected as a filter. null if not set - """ - projectKeyFilters: [ID!]! - - """ - the time the search query was last used. Used for sorting - """ - lastUsedAt: DateTime! -} """ The auth credentials for a token, specific to a team member @@ -1057,97 +1028,8 @@ interface TeamMemberIntegrationAuth { provider: IntegrationProvider! } -""" -A connection to a list of items. -""" -type JiraServerIssueConnection { - """ - Page info with cursors coerced to ISO8601 dates - """ - pageInfo: PageInfo - - """ - A list of edges. - """ - edges: [JiraServerIssueEdge!]! - - """ - An error with the connection, if any - """ - error: StandardMutationError -} - -""" -An edge in a connection. -""" -type JiraServerIssueEdge { - """ - The item at the end of the edge - """ - node: JiraServerIssue! - cursor: String -} - -""" -The Jira Issue that comes direct from Jira Server -""" -type JiraServerIssue implements TaskIntegration { - id: ID! - issueKey: ID! - projectKey: ID! - projectName: String! - - """ - The parabol teamId this issue was fetched for - """ - teamId: ID! - - """ - The parabol userId this issue was fetched for - """ - userId: ID! - - """ - The url to access the issue - """ - url: String! - - """ - The plaintext summary of the jira issue - """ - summary: String! - description: String! - - """ - The description converted into raw HTML - """ - descriptionHTML: String! - - """ - The timestamp the issue was last updated - """ - updatedAt: DateTime! -} - -""" -A GitHub search query including all filters selected when the query was executed -""" -type GitHubSearchQuery { - """ - shortid - """ - id: ID! - """ - The query string in GitHub format, including repository filters. e.g. is:issue is:open - """ - queryString: String! - """ - the time the search query was last used. Used for sorting - """ - lastUsedAt: DateTime! -} """ The event that triggers a slack notification @@ -1174,86 +1056,6 @@ enum SlackNotificationEventTypeEnum { member } -""" -The Azure DevOps auth + integration helpers for a specific team member -""" -type AzureDevOpsIntegration { - """ - The OAuth2 Authorization for this team member - """ - auth: TeamMemberIntegrationAuthOAuth2 - - """ - Composite key in ado:teamId:userId format - """ - id: ID! - - """ - true if the auth is valid, else false - """ - isActive: Boolean! - - """ - The access token to Azure DevOps. null if no access token available or the viewer is not the user - """ - accessToken: ID - - """ - The Azure DevOps account ID - """ - accountId: ID! - - """ - The Azure DevOps instance IDs that the user has granted - """ - instanceIds: [ID!]! - - """ - The timestamp the provider was created - """ - createdAt: DateTime! - - """ - The team that the token is linked to - """ - teamId: ID! - - """ - The timestamp the token was updated at - """ - updatedAt: DateTime! - - """ - The user that the access token is attached to - """ - userId: ID! - - """ - The cloud provider the team member may choose to integrate with. Nullable based on env vars - """ - cloudProvider: IntegrationProviderOAuth2 - - """ - The non-global providers shared with the team or organization - """ - sharedProviders: [IntegrationProviderOAuth2!]! -} - -""" -The Azure DevOps Issue that comes direct from Azure DevOps -""" -type AzureDevOpsWorkItem { - """ - GUID instanceId:issueKey - """ - id: ID! - - """ - URL to the issue - """ - url: String! -} - """ All the user details for a specific meeting """ diff --git a/packages/server/graphql/public/rootSchema.ts b/packages/server/graphql/public/rootSchema.ts index e65b1459189..9ae8dce9f77 100644 --- a/packages/server/graphql/public/rootSchema.ts +++ b/packages/server/graphql/public/rootSchema.ts @@ -21,8 +21,8 @@ import permissions from './permissions' // Resolvers from SDL first definitions import resolvers from './resolvers' -// Schema from legacy TypeScript first definitions -const legacyParabolSchema = new GraphQLSchema({ +// Schema from legacy TypeScript first definitions instead of SDL pattern +const legacyTypeDefs = new GraphQLSchema({ query, mutation, // defining a placeholder subscription because there's a bug in nest-graphql-schema that prefixes to _xGitHubSubscription if missing @@ -30,8 +30,18 @@ const legacyParabolSchema = new GraphQLSchema({ types: rootTypes }) -const {schema: legacyParabolWithGitHubSchema, githubRequest} = nestGitHubEndpoint({ - parentSchema: legacyParabolSchema, +const importAllStrings = (context: __WebpackModuleApi.RequireContext) => { + return context.keys().map((id) => context(id).default) +} + +// Merge old POJO definitions with SDL definitions +const parabolTypeDefs = mergeSchemas({ + schemas: [legacyTypeDefs], + typeDefs: importAllStrings(require.context('./typeDefs', false, /.graphql$/)) +}) + +const {schema: typeDefsWithGitHub, githubRequest} = nestGitHubEndpoint({ + parentSchema: parabolTypeDefs, parentType: 'GitHubIntegration', fieldName: 'api', resolveEndpointContext: ({accessToken}) => ({ @@ -42,8 +52,8 @@ const {schema: legacyParabolWithGitHubSchema, githubRequest} = nestGitHubEndpoin schemaIDL: githubSchema }) -const {schema: legacyParabolWithGitLabSchema, gitlabRequest} = nestGitLabEndpoint({ - parentSchema: legacyParabolSchema, +const {schema: typeDefsWithGitHubGitLab, gitlabRequest} = nestGitLabEndpoint({ + parentSchema: typeDefsWithGitHub, parentType: 'GitLabIntegration', fieldName: 'api', resolveEndpointContext: async ( @@ -67,25 +77,15 @@ const {schema: legacyParabolWithGitLabSchema, gitlabRequest} = nestGitLabEndpoin schemaIDL: gitlabSchema }) -const importAllStrings = (context: __WebpackModuleApi.RequireContext) => { - return context.keys().map((id) => context(id).default) -} - -// Types from SDL first -const typeDefs = importAllStrings(require.context('./typeDefs', false, /.graphql$/)) - -const legacyParabolWithNestedSchema = mergeSchemas({ - schemas: [legacyParabolWithGitHubSchema, legacyParabolWithGitLabSchema], - typeDefs -}) - // IMPORTANT! mergeSchemas has a bug where resolvers will be overwritten by the default resolvers // See https://github.com/ardatan/graphql-tools/issues/4367 -const parabolWithNestedResolversSchema = addResolversToSchema({ - schema: legacyParabolWithNestedSchema, - resolvers: composeResolvers(resolvers, permissions), - inheritResolversFromInterfaces: true -}) +const publicSchema = resolveTypesForMutationPayloads( + addResolversToSchema({ + schema: typeDefsWithGitHubGitLab, + resolvers: composeResolvers(resolvers, permissions), + inheritResolversFromInterfaces: true + }) +) const addRequestors = (schema: GraphQLSchema) => { const finalSchema = schema as any @@ -97,6 +97,6 @@ const addRequestors = (schema: GraphQLSchema) => { } } -const rootSchema = addRequestors(resolveTypesForMutationPayloads(parabolWithNestedResolversSchema)) +const rootSchema = addRequestors(publicSchema) export default rootSchema diff --git a/packages/server/graphql/public/typeDefs/AtlassianIntegration.graphql b/packages/server/graphql/public/typeDefs/AtlassianIntegration.graphql index c40ed3b2f59..d8cdf020b6d 100644 --- a/packages/server/graphql/public/typeDefs/AtlassianIntegration.graphql +++ b/packages/server/graphql/public/typeDefs/AtlassianIntegration.graphql @@ -1,4 +1,62 @@ -extend type AtlassianIntegration { +""" +The atlassian auth + integration helpers for a specific team member +""" +type AtlassianIntegration { + """ + Composite key in atlassiani:teamId:userId format + """ + id: ID! + + """ + true if the auth is valid, else false + """ + isActive: Boolean! + + """ + The access token to atlassian, useful for 1 hour. null if no access token available or the viewer is not the user + """ + accessToken: ID + + """ + *The atlassian account ID + """ + accountId: ID! + + """ + The atlassian cloud IDs that the user has granted + """ + cloudIds: [ID!]! + + """ + The timestamp the provider was created + """ + createdAt: DateTime! + + """ + *The team that the token is linked to + """ + teamId: ID! + + """ + The timestamp the token was updated at + """ + updatedAt: DateTime! + + """ + The user that the access token is attached to + """ + userId: ID! + + """ + A list of projects accessible by this team member. empty if viewer is not the user + """ + projects: [JiraRemoteProject!]! + + """ + the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old + """ + jiraSearchQueries: [JiraSearchQuery!]! + """ A list of issues coming straight from the jira integration for a specific team member """ diff --git a/packages/server/graphql/public/typeDefs/AzureDevOpsIntegration.graphql b/packages/server/graphql/public/typeDefs/AzureDevOpsIntegration.graphql new file mode 100644 index 00000000000..b181d25642e --- /dev/null +++ b/packages/server/graphql/public/typeDefs/AzureDevOpsIntegration.graphql @@ -0,0 +1,91 @@ +""" +The Azure DevOps auth + integration helpers for a specific team member +""" +type AzureDevOpsIntegration { + """ + The OAuth2 Authorization for this team member + """ + auth: TeamMemberIntegrationAuthOAuth2 + + """ + Composite key in ado:teamId:userId format + """ + id: ID! + + """ + The Azure DevOps account ID + """ + accountId: ID! + + """ + The Azure DevOps instance IDs that the user has granted + """ + instanceIds: [ID!]! + + """ + The timestamp the provider was created + """ + createdAt: DateTime! + + """ + The team that the token is linked to + """ + teamId: ID! + + """ + The timestamp the token was updated at + """ + updatedAt: DateTime! + + """ + The user that the access token is attached to + """ + userId: ID! + + """ + A list of work items coming straight from the azure dev ops integration for a specific team member + """ + workItems( + first: Int = 100 + + """ + the datetime cursor + """ + after: DateTime + + """ + A string of text to search for, or WIQL if isWIQL is true + """ + queryString: String + + """ + A list of projects to restrict the search to, if null will search all + """ + projectKeyFilters: [String!]! + + """ + true if the queryString is WIQL, else false + """ + isWIQL: Boolean! + ): AzureDevOpsWorkItemConnection! + + """ + A list of projects coming straight from the azure dev ops integration for a specific team member + """ + projects: [AzureDevOpsRemoteProject!]! + + """ + The cloud provider the team member may choose to integrate with. Nullable based on env vars + """ + cloudProvider: IntegrationProviderOAuth2 + + """ + The non-global providers shared with the team or organization + """ + sharedProviders: [IntegrationProviderOAuth2!]! + + """ + the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old + """ + azureDevOpsSearchQueries: [AzureDevOpsSearchQuery!]! +} diff --git a/packages/server/graphql/public/typeDefs/AzureDevOpsRemoteProject.graphql b/packages/server/graphql/public/typeDefs/AzureDevOpsRemoteProject.graphql new file mode 100644 index 00000000000..facf1700511 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/AzureDevOpsRemoteProject.graphql @@ -0,0 +1,29 @@ +""" +A project fetched from Azure DevOps in real time +""" +type AzureDevOpsRemoteProject implements RepoIntegration { + id: ID! + service: IntegrationProviderServiceEnum! + + """ + The parabol teamId this issue was fetched for + """ + teamId: ID! + + """ + The parabol userId this issue was fetched for + """ + userId: ID! + lastUpdateTime: DateTime! + self: ID! + + """ + The instance ID that the project lives on + """ + instanceId: ID! + name: String! + revision: Int! + state: String! + url: String! + visibility: String! +} diff --git a/packages/server/graphql/public/typeDefs/AzureDevOpsSearchQuery.graphql b/packages/server/graphql/public/typeDefs/AzureDevOpsSearchQuery.graphql new file mode 100644 index 00000000000..dddd73fa718 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/AzureDevOpsSearchQuery.graphql @@ -0,0 +1,29 @@ +""" +An Azure DevOps search query including all filters selected when the query was executed +""" +type AzureDevOpsSearchQuery { + """ + shortid + """ + id: ID! + + """ + The query string, either simple or WIQL depending on the isWIQL flag + """ + queryString: String! + + """ + A list of projects to restrict the search to + """ + projectKeyFilters: [String!]! + + """ + true if the queryString is WIQL, else false + """ + isWIQL: Boolean! + + """ + the time the search query was last used. Used for sorting + """ + lastUsedAt: DateTime! +} diff --git a/packages/server/graphql/public/typeDefs/AzureDevOpsWorkItem.graphql b/packages/server/graphql/public/typeDefs/AzureDevOpsWorkItem.graphql new file mode 100644 index 00000000000..9a95773dec4 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/AzureDevOpsWorkItem.graphql @@ -0,0 +1,80 @@ +""" +The Azure DevOps Work Item that comes direct from Azure DevOps +""" +type AzureDevOpsWorkItem implements TaskIntegration { + """ + GUID instanceId:projectKey:issueKey + """ + id: ID! + + """ + URL to the issue + """ + url: String! + + """ + The id of the issue from Azure, e.g. 7 + """ + issueKey: String! + + """ + Title of the work item + """ + title: String! + + """ + Name or id of the Team Project the work item belongs to + """ + teamProject: String! + + """ + The Azure DevOps Remote Project the work item belongs to + """ + project: AzureDevOpsRemoteProject! + + """ + The Current State of the Work item + """ + state: String! + + """ + The Type of the Work item + """ + type: String! + + """ + The description converted into raw HTML + """ + descriptionHTML: String! +} + +""" +A connection to a list of items. +""" +type AzureDevOpsWorkItemConnection { + """ + Page info with cursors coerced to ISO8601 dates + """ + pageInfo: PageInfoDateCursor + + """ + A list of edges. + """ + edges: [AzureDevOpsWorkItemEdge!]! + + """ + An error with the connection, if any + """ + error: StandardMutationError +} + +""" +An edge in a connection. +""" +type AzureDevOpsWorkItemEdge { + """ + The item at the end of the edge + """ + node: AzureDevOpsWorkItem! + cursor: DateTime +} diff --git a/packages/server/graphql/public/typeDefs/GitHubIntegration.graphql b/packages/server/graphql/public/typeDefs/GitHubIntegration.graphql new file mode 100644 index 00000000000..e84cd02dd81 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/GitHubIntegration.graphql @@ -0,0 +1,51 @@ +type GitHubIntegration { + """ + composite key + """ + id: ID! + + """ + The access token to github. good forever + """ + accessToken: ID + + """ + The timestamp the provider was created + """ + createdAt: DateTime! + + """ + true if an access token exists, else false + """ + isActive: Boolean! + + """ + the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old + """ + githubSearchQueries: [GitHubSearchQuery!]! + + """ + *The GitHub login used for queries + """ + login: ID! + + """ + The comma-separated list of scopes requested from GitHub + """ + scope: String! + + """ + *The team that the token is linked to + """ + teamId: ID! + + """ + The timestamp the token was updated at + """ + updatedAt: DateTime! + + """ + The user that the access token is attached to + """ + userId: ID! +} diff --git a/packages/server/graphql/public/typeDefs/GitHubSearchQuery.graphql b/packages/server/graphql/public/typeDefs/GitHubSearchQuery.graphql new file mode 100644 index 00000000000..c8ed9ca3a71 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/GitHubSearchQuery.graphql @@ -0,0 +1,19 @@ +""" +A GitHub search query including all filters selected when the query was executed +""" +type GitHubSearchQuery { + """ + shortid + """ + id: ID! + + """ + The query string in GitHub format, including repository filters. e.g. is:issue is:open + """ + queryString: String! + + """ + the time the search query was last used. Used for sorting + """ + lastUsedAt: DateTime! +} diff --git a/packages/server/graphql/public/typeDefs/GitLabIntegration.graphql b/packages/server/graphql/public/typeDefs/GitLabIntegration.graphql new file mode 100644 index 00000000000..6308f2a967a --- /dev/null +++ b/packages/server/graphql/public/typeDefs/GitLabIntegration.graphql @@ -0,0 +1,84 @@ +""" +Gitlab integration data for a given team member +""" +type GitLabIntegration { + """ + The OAuth2 Authorization for this team member + """ + auth: TeamMemberIntegrationAuthOAuth2 + + """ + The cloud provider the team member may choose to integrate with. Nullable based on env vars + """ + cloudProvider: IntegrationProviderOAuth2 + + """ + The non-global providers shared with the team or organization + """ + sharedProviders: [IntegrationProviderOAuth2!]! + gitlabSearchQueries: [GitLabSearchQuery!]! + + """ + A list of projects accessible by this team member + """ + projects: [RepoIntegration!]! + projectsIssues( + first: Int! + + """ + the stringified cursors for pagination + """ + after: String + + """ + the ids of the projects selected as filters + """ + projectsIds: [String] + + """ + the search query that the user enters to filter issues + """ + searchQuery: String! + + """ + the sort string that defines the order of the returned issues + """ + sort: String! + + """ + the state of issues, e.g. opened or closed + """ + state: String! + ): GitLabIntegrationConnection! +} + +""" +A connection to a list of items. +""" +type GitLabIntegrationConnection { + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + A list of edges. + """ + edges: [GitLabIntegrationEdge!]! + + """ + An error with the connection, if any + """ + error: StandardMutationError +} + +""" +An edge in a connection. +""" +type GitLabIntegrationEdge { + """ + The item at the end of the edge + """ + node: TaskIntegration! + cursor: String +} diff --git a/packages/server/graphql/public/typeDefs/GitLabSearchQuery.graphql b/packages/server/graphql/public/typeDefs/GitLabSearchQuery.graphql new file mode 100644 index 00000000000..f3e99b1a20e --- /dev/null +++ b/packages/server/graphql/public/typeDefs/GitLabSearchQuery.graphql @@ -0,0 +1,24 @@ +""" +A GitLab search query including the search query and the project filters +""" +type GitLabSearchQuery { + """ + shortid + """ + id: ID! + + """ + The query string used to search GitLab issue titles and descriptions + """ + queryString: String! + + """ + The list of ids of projects that have been selected as a filter. Null if none have been selected + """ + selectedProjectsIds: [ID!] + + """ + the time the search query was last used. Used for sorting + """ + lastUsedAt: DateTime! +} diff --git a/packages/server/graphql/public/typeDefs/JiraSearchQuery.graphql b/packages/server/graphql/public/typeDefs/JiraSearchQuery.graphql new file mode 100644 index 00000000000..3f084e94a55 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/JiraSearchQuery.graphql @@ -0,0 +1,29 @@ +""" +A jira search query including all filters selected when the query was executed +""" +type JiraSearchQuery { + """ + shortid + """ + id: ID! + + """ + The query string, either simple or JQL depending on the isJQL flag + """ + queryString: String! + + """ + true if the queryString is JQL, else false + """ + isJQL: Boolean! + + """ + The list of project keys selected as a filter. null if not set + """ + projectKeyFilters: [ID!]! + + """ + the time the search query was last used. Used for sorting + """ + lastUsedAt: DateTime! +} diff --git a/packages/server/graphql/public/typeDefs/JiraServerIntegration.graphql b/packages/server/graphql/public/typeDefs/JiraServerIntegration.graphql new file mode 100644 index 00000000000..81127b8cb96 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/JiraServerIntegration.graphql @@ -0,0 +1,49 @@ +""" +Jira Server integration data for a given team member +""" +type JiraServerIntegration { + """ + Composite key in jiraServer:providerId format + """ + id: ID + + """ + The OAuth1 Authorization for this team member + """ + auth: TeamMemberIntegrationAuthOAuth1 + + """ + The non-global providers shared with the team or organization + """ + sharedProviders: [IntegrationProviderOAuth1!]! + + """ + A list of issues coming straight from the jira integration for a specific team member + """ + issues( + first: Int = 25 + after: String = "-1" + + """ + A string of text to search for, or JQL if isJQL is true + """ + queryString: String + + """ + true if the queryString is JQL, else false + """ + isJQL: Boolean! + projectKeyFilters: [ID!] + ): JiraServerIssueConnection! + + """ + A list of projects accessible by this team member. empty if viewer is not the user + """ + projects: [JiraServerRemoteProject!]! + providerId: ID + + """ + the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old + """ + searchQueries: [JiraSearchQuery!]! +} diff --git a/packages/server/graphql/public/typeDefs/JiraServerIssue.graphql b/packages/server/graphql/public/typeDefs/JiraServerIssue.graphql new file mode 100644 index 00000000000..38ca5e9c7a1 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/JiraServerIssue.graphql @@ -0,0 +1,102 @@ +""" +A connection to a list of items. +""" +type JiraServerIssueConnection { + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + + """ + A list of edges. + """ + edges: [JiraServerIssueEdge!]! + + """ + An error with the connection, if any + """ + error: StandardMutationError +} + +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String + + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String +} + +""" +An edge in a connection. +""" +type JiraServerIssueEdge { + """ + The item at the end of the edge + """ + node: JiraServerIssue! + cursor: String +} + +""" +The Jira Issue that comes direct from Jira Server +""" +type JiraServerIssue implements TaskIntegration { + """ + GUID providerId:repositoryId:issueId + """ + id: ID! + issueKey: ID! + issueType: ID! + projectId: ID! + projectKey: ID! + projectName: String! + + """ + The parabol teamId this issue was fetched for + """ + teamId: ID! + + """ + The parabol userId this issue was fetched for + """ + userId: ID! + + """ + The url to access the issue + """ + url: String! + + """ + The plaintext summary of the jira issue + """ + summary: String! + description: String! + + """ + The description converted into raw HTML + """ + descriptionHTML: String! + possibleEstimationFieldNames: [String!]! + + """ + The timestamp the issue was last updated + """ + updatedAt: DateTime! +} diff --git a/packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql b/packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql new file mode 100644 index 00000000000..51ef034678a --- /dev/null +++ b/packages/server/graphql/public/typeDefs/MSTeamsIntegration.graphql @@ -0,0 +1,14 @@ +""" +Integration Auth and shared providers available to the team member +""" +type MSTeamsIntegration { + """ + The OAuth2 Authorization for this team member + """ + auth: TeamMemberIntegrationAuthWebhook + + """ + The non-global providers shared with the team or organization + """ + sharedProviders: [IntegrationProviderWebhook!]! +} diff --git a/packages/server/graphql/public/typeDefs/MattermostIntegration.graphql b/packages/server/graphql/public/typeDefs/MattermostIntegration.graphql new file mode 100644 index 00000000000..e072cb55448 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/MattermostIntegration.graphql @@ -0,0 +1,14 @@ +""" +Integration Auth and shared providers available to the team member +""" +type MattermostIntegration { + """ + The OAuth2 Authorization for this team member + """ + auth: TeamMemberIntegrationAuthWebhook + + """ + The non-global providers shared with the team or organization + """ + sharedProviders: [IntegrationProviderWebhook!]! +} diff --git a/packages/server/graphql/public/typeDefs/Organization.graphql b/packages/server/graphql/public/typeDefs/Organization.graphql index 139dc3118e0..49201cbd7ea 100644 --- a/packages/server/graphql/public/typeDefs/Organization.graphql +++ b/packages/server/graphql/public/typeDefs/Organization.graphql @@ -27,39 +27,40 @@ type Organization { """ creditCard: CreditCard - """ - The assumed company this organizaiton belongs to - """ - company: Company - """ true if the viewer is the billing leader for the org """ isBillingLeader: Boolean! """ - Basic meeting metadata for aggregated stats across the entire organization. - Includes metadata on teams the viewer is not apart of + true if the viewer holds the the org admin role on the org """ - meetingStats: [MeetingStat!]! + isOrgAdmin: Boolean! + """ The name of the organization """ name: String! """ - The org avatar + Number of teams with 3+ meetings (>1 attendee) that met within last 30 days """ - picture: URL + activeTeamCount: Int! - tier: TierEnum! + """ + All the teams in the organization. If the viewer is not a billing lead, org admin, super user, or they do not have the publicTeams flag, return the teams they are a member of. + """ + allTeams: [Team!]! - billingTier: TierEnum! + """ + all the teams the viewer is on in the organization + """ + viewerTeams: [Team!]! """ - When the trial started, iff there is a trial active + all the teams that the viewer does not belong to that are in the organization. Only visible if the org has the publicTeams flag set to true. """ - trialStartDate: DateTime + publicTeams: [Team!]! """ THe datetime the current billing cycle ends @@ -141,6 +142,29 @@ type Organization { """ billingLeaders: [OrganizationUser!]! + """ + The assumed company this organizaiton belongs to + """ + company: Company + + """ + Basic meeting metadata for aggregated stats across the entire organization. + Includes metadata on teams the viewer is not apart of + """ + meetingStats: [MeetingStat!]! + + """ + The org avatar + """ + picture: URL + tier: TierEnum! + billingTier: TierEnum! + + """ + When the trial started, iff there is a trial active + """ + trialStartDate: DateTime + """ Minimal details about all teams in the organization """ @@ -155,6 +179,12 @@ type Organization { The SAML record attached to the Organization, if any """ saml: SAML + + """ + A list of domains approved by the organization to join. + Empty if all domains are allowed + """ + approvedDomains: [String!]! } type MeetingStat { diff --git a/packages/server/graphql/public/typeDefs/PageInfo.graphql b/packages/server/graphql/public/typeDefs/PageInfo.graphql new file mode 100644 index 00000000000..766d3468bfb --- /dev/null +++ b/packages/server/graphql/public/typeDefs/PageInfo.graphql @@ -0,0 +1,24 @@ +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String + + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String +} diff --git a/packages/server/graphql/public/typeDefs/PageInfoDateCursor.graphql b/packages/server/graphql/public/typeDefs/PageInfoDateCursor.graphql new file mode 100644 index 00000000000..c8e358eefee --- /dev/null +++ b/packages/server/graphql/public/typeDefs/PageInfoDateCursor.graphql @@ -0,0 +1,24 @@ +""" +Information about pagination in a connection. +""" +type PageInfoDateCursor { + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: DateTime + + """ + When paginating forwards, the cursor to continue. + """ + endCursor: DateTime +} diff --git a/packages/server/graphql/public/typeDefs/SlackIntegration.graphql b/packages/server/graphql/public/typeDefs/SlackIntegration.graphql new file mode 100644 index 00000000000..057e5660ca2 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/SlackIntegration.graphql @@ -0,0 +1,74 @@ +""" +OAuth token for a team member +""" +type SlackIntegration { + """ + shortid + """ + id: ID! + + """ + true if the auth is updated & ready to use for all features, else false + """ + isActive: Boolean! + + """ + the parabol bot user id + """ + botUserId: ID + + """ + the parabol bot access token, used as primary communication + """ + botAccessToken: ID + + """ + The timestamp the provider was created + """ + createdAt: DateTime! + + """ + The default channel to assign to new team notifications + """ + defaultTeamChannelId: String! + + """ + The id of the team in slack + """ + slackTeamId: ID + + """ + The name of the team in slack + """ + slackTeamName: String + + """ + The userId in slack + """ + slackUserId: ID! + + """ + The name of the user in slack + """ + slackUserName: String! + + """ + *The team that the token is linked to + """ + teamId: ID! + + """ + The timestamp the token was updated at + """ + updatedAt: DateTime! + + """ + The id of the user that integrated Slack + """ + userId: ID! + + """ + A list of events and the slack channels they get posted to + """ + notifications: [SlackNotification!]! +} diff --git a/packages/server/graphql/public/typeDefs/TeamMemberIntegrations.graphql b/packages/server/graphql/public/typeDefs/TeamMemberIntegrations.graphql index ed1b9ad2792..02b49984d19 100644 --- a/packages/server/graphql/public/typeDefs/TeamMemberIntegrations.graphql +++ b/packages/server/graphql/public/typeDefs/TeamMemberIntegrations.graphql @@ -17,11 +17,6 @@ type TeamMemberIntegrations { """ jiraServer: JiraServerIntegration! - """ - All things associated with a Gcal integration for a team member - """ - gcal: GcalIntegration - """ All things associated with a GitHub integration for a team member """ @@ -51,4 +46,9 @@ type TeamMemberIntegrations { All things associated with a Microsoft Teams integration for a team member """ msTeams: MSTeamsIntegration! + + """ + All things associated with a Gcal integration for a team member + """ + gcal: GcalIntegration } diff --git a/packages/server/graphql/public/typeDefs/_legacy.graphql b/packages/server/graphql/public/typeDefs/_legacy.graphql index d88cc2d86a6..8034caa107f 100644 --- a/packages/server/graphql/public/typeDefs/_legacy.graphql +++ b/packages/server/graphql/public/typeDefs/_legacy.graphql @@ -856,66 +856,6 @@ enum TeamDrawer { scalar Email -""" -The atlassian auth + integration helpers for a specific team member -""" -type AtlassianIntegration { - """ - Composite key in atlassiani:teamId:userId format - """ - id: ID! - - """ - true if the auth is valid, else false - """ - isActive: Boolean! - - """ - The access token to atlassian, useful for 1 hour. null if no access token available or the viewer is not the user - """ - accessToken: ID - - """ - *The atlassian account ID - """ - accountId: ID! - - """ - The atlassian cloud IDs that the user has granted - """ - cloudIds: [ID!]! - - """ - The timestamp the provider was created - """ - createdAt: DateTime! - - """ - *The team that the token is linked to - """ - teamId: ID! - - """ - The timestamp the token was updated at - """ - updatedAt: DateTime! - - """ - The user that the access token is attached to - """ - userId: ID! - - """ - A list of projects accessible by this team member. empty if viewer is not the user - """ - projects: [JiraRemoteProject!]! - - """ - the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old - """ - jiraSearchQueries: [JiraSearchQuery!]! -} - interface TaskIntegration { id: ID! } @@ -1013,26 +953,6 @@ type JiraSearchQuery { lastUsedAt: DateTime! } -""" -Jira Server integration data for a given team member -""" -type JiraServerIntegration { - """ - The OAuth1 Authorization for this team member - """ - auth: TeamMemberIntegrationAuthOAuth1 - - """ - The non-global providers shared with the team or organization - """ - sharedProviders: [IntegrationProviderOAuth1!]! - - """ - A list of projects accessible by this team member. empty if viewer is not the user - """ - projects: [JiraServerRemoteProject!]! -} - """ An integration token that connects via OAuth1 """ @@ -1145,101 +1065,6 @@ type JiraServerRemoteProject implements RepoIntegration { projectCategory: JiraRemoteProjectCategory! } -""" -OAuth token for a team member -""" -type GitHubIntegration { - """ - composite key - """ - id: ID! - - """ - The access token to github. good forever - """ - accessToken: ID - - """ - The timestamp the provider was created - """ - createdAt: DateTime! - - """ - true if an access token exists, else false - """ - isActive: Boolean! - - """ - the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old - """ - githubSearchQueries: [GitHubSearchQuery!]! - - """ - *The GitHub login used for queries - """ - login: ID! - - """ - The comma-separated list of scopes requested from GitHub - """ - scope: String! - - """ - *The team that the token is linked to - """ - teamId: ID! - - """ - The timestamp the token was updated at - """ - updatedAt: DateTime! - - """ - The user that the access token is attached to - """ - userId: ID! -} - -""" -A GitHub search query including all filters selected when the query was executed -""" -type GitHubSearchQuery { - """ - shortid - """ - id: ID! - - """ - The query string in GitHub format, including repository filters. e.g. is:issue is:open - """ - queryString: String! - - """ - the time the search query was last used. Used for sorting - """ - lastUsedAt: DateTime! -} - -""" -Gitlab integration data for a given team member -""" -type GitLabIntegration { - """ - The OAuth2 Authorization for this team member - """ - auth: TeamMemberIntegrationAuthOAuth2 - - """ - The cloud provider the team member may choose to integrate with. Nullable based on env vars - """ - cloudProvider: IntegrationProviderOAuth2 - - """ - The non-global providers shared with the team or organization - """ - sharedProviders: [IntegrationProviderOAuth2!]! -} - """ An integration token that connects via OAuth2 """ @@ -1295,33 +1120,6 @@ type TeamMemberIntegrationAuthOAuth2 implements TeamMemberIntegrationAuth { scopes: String! } -""" -Integration Auth and shared providers available to the team member -""" -type MattermostIntegration { - """ - The OAuth2 Authorization for this team member - """ - auth: TeamMemberIntegrationAuthWebhook - - """ - The non-global providers shared with the team or organization - """ - sharedProviders: [IntegrationProviderWebhook!]! -} - -type MSTeamsIntegration { - """ - The OAuth2 Authorization for this team member - """ - auth: TeamMemberIntegrationAuthWebhook - - """ - The non-global providers shared with the team or organization - """ - sharedProviders: [IntegrationProviderWebhook!]! -} - """ An integration authorization that connects via Webhook auth strategy """ @@ -1367,81 +1165,6 @@ type TeamMemberIntegrationAuthWebhook implements TeamMemberIntegrationAuth { provider: IntegrationProviderWebhook! } -""" -OAuth token for a team member -""" -type SlackIntegration { - """ - shortid - """ - id: ID! - - """ - true if the auth is updated & ready to use for all features, else false - """ - isActive: Boolean! - - """ - the parabol bot user id - """ - botUserId: ID - - """ - the parabol bot access token, used as primary communication - """ - botAccessToken: ID - - """ - The timestamp the provider was created - """ - createdAt: DateTime! - - """ - The default channel to assign to new team notifications - """ - defaultTeamChannelId: String! - - """ - The id of the team in slack - """ - slackTeamId: ID - - """ - The name of the team in slack - """ - slackTeamName: String - - """ - The userId in slack - """ - slackUserId: ID! - - """ - The name of the user in slack - """ - slackUserName: String! - - """ - *The team that the token is linked to - """ - teamId: ID! - - """ - The timestamp the token was updated at - """ - updatedAt: DateTime! - - """ - The id of the user that integrated Slack - """ - userId: ID! - - """ - A list of events and the slack channels they get posted to - """ - notifications: [SlackNotification!]! -} - """ an event trigger and slack channel to receive it """ diff --git a/packages/server/graphql/public/types/AtlassianIntegration.ts b/packages/server/graphql/public/types/AtlassianIntegration.ts index 4a1fadc7b51..4b68534f42c 100644 --- a/packages/server/graphql/public/types/AtlassianIntegration.ts +++ b/packages/server/graphql/public/types/AtlassianIntegration.ts @@ -1,5 +1,8 @@ -import {downloadAndCacheImages, updateJiraImageUrls} from '../../../utils/atlassian/jiraImages' +import ms from 'ms' +import AtlassianIntegrationId from '../../../../client/shared/gqlIds/AtlassianIntegrationId' +import updateJiraSearchQueries from '../../../postgres/queries/updateJiraSearchQueries' import AtlassianServerManager from '../../../utils/AtlassianServerManager' +import {downloadAndCacheImages, updateJiraImageUrls} from '../../../utils/atlassian/jiraImages' import {getUserId} from '../../../utils/authorization' import standardError from '../../../utils/standardError' import {AtlassianIntegrationResolvers} from '../resolverTypes' @@ -80,6 +83,35 @@ const AtlassianIntegration: AtlassianIntegrationResolvers = { hasPreviousPage: false } } + }, + id: ({teamId, userId}) => AtlassianIntegrationId.join(teamId, userId), + + isActive: ({accessToken}) => !!accessToken, + + accessToken: async ({accessToken, userId}, _args, {authToken}) => { + const viewerId = getUserId(authToken) + return viewerId === userId ? accessToken : null + }, + + projects: ({teamId, userId}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + if (viewerId !== userId) return [] + return dataLoader.get('allJiraProjects').load({teamId, userId}) + }, + + jiraSearchQueries: async ({teamId, userId, jiraSearchQueries}) => { + const expirationThresh = ms('60d') + const thresh = new Date(Date.now() - expirationThresh) + const searchQueries = jiraSearchQueries || [] + const unexpiredQueries = searchQueries.filter((query) => query.lastUsedAt > thresh) + if (unexpiredQueries.length < searchQueries.length) { + await updateJiraSearchQueries({ + jiraSearchQueries: searchQueries, + teamId, + userId + }) + } + return unexpiredQueries } } diff --git a/packages/server/graphql/public/types/AzureDevOpsIntegration.ts b/packages/server/graphql/public/types/AzureDevOpsIntegration.ts new file mode 100644 index 00000000000..3df7a8b401a --- /dev/null +++ b/packages/server/graphql/public/types/AzureDevOpsIntegration.ts @@ -0,0 +1,77 @@ +import {getUserId, isTeamMember} from '../../../utils/authorization' +import standardError from '../../../utils/standardError' +import connectionFromTasks from '../../queries/helpers/connectionFromTasks' +import {AzureDevOpsIntegrationResolvers} from '../resolverTypes' + +export type AzureDevOpsIntegrationSource = { + teamId: string + userId: string +} +type WorkItemArgs = { + first: number + after?: string + queryString: string | null + projectKeyFilters: string[] | null + isWIQL: boolean +} + +const AzureDevOpsIntegration: AzureDevOpsIntegrationResolvers = { + auth: async ({teamId, userId}, _args, {dataLoader}) => { + return dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'azureDevOps', teamId, userId}) + }, + + id: ({teamId, userId}) => `ado:${teamId}:${userId}`, + + workItems: async ({teamId, userId}, args, {authToken, dataLoader}) => { + const {first, queryString, projectKeyFilters, isWIQL} = args as WorkItemArgs + const viewerId = getUserId(authToken) + if (!isTeamMember(authToken, teamId)) { + const err = new Error('Cannot access another team members user stories') + standardError(err, {tags: {teamId, userId}, userId: viewerId}) + return connectionFromTasks([], 0, err) + } + const allUserWorkItems = await dataLoader + .get('azureDevOpsAllWorkItems') + .load({teamId, userId, queryString, projectKeyFilters, isWIQL}) + if (!allUserWorkItems) { + return connectionFromTasks([], 0, undefined) + } else { + const workItems = Array.from( + allUserWorkItems.map((userWorkItem) => { + return { + ...userWorkItem, + updatedAt: new Date() + } + }) + ) + return connectionFromTasks(workItems, first, undefined) + } + }, + + projects: ({teamId, userId}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + if (viewerId !== userId) return [] + return dataLoader.get('allAzureDevOpsProjects').load({teamId, userId}) + }, + + cloudProvider: async (_source, _args, {dataLoader}) => { + const [globalProvider] = await dataLoader + .get('sharedIntegrationProviders') + .load({service: 'azureDevOps', orgTeamIds: ['aGhostTeam'], teamIds: []}) + return globalProvider! + }, + + sharedProviders: async ({teamId}, _args, {dataLoader}) => { + const team = await dataLoader.get('teams').loadNonNull(teamId) + const {orgId} = team + const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) + const orgTeamIds = orgTeams.map(({id}) => id) + return dataLoader + .get('sharedIntegrationProviders') + .load({service: 'azureDevOps', orgTeamIds, teamIds: [teamId]}) + } +} + +export default AzureDevOpsIntegration diff --git a/packages/server/graphql/public/types/AzureDevOpsRemoteProject.ts b/packages/server/graphql/public/types/AzureDevOpsRemoteProject.ts new file mode 100644 index 00000000000..99b9bf2abbc --- /dev/null +++ b/packages/server/graphql/public/types/AzureDevOpsRemoteProject.ts @@ -0,0 +1,14 @@ +import {AzureAccountProject, AzureProject} from '../../../dataloader/azureDevOpsLoaders' +import {getInstanceId} from '../../../utils/azureDevOps/azureDevOpsFieldTypeToId' +import {AzureDevOpsRemoteProjectResolvers} from '../resolverTypes' + +// This type is almost certainly wrong, but during the refactor to SDL I didn't want to mess with runtime logic +export type AzureDevOpsRemoteProjectSource = AzureProject | AzureAccountProject + +const AzureDevOpsRemoteProject: AzureDevOpsRemoteProjectResolvers = { + __isTypeOf: ({service}) => service === 'azureDevOps', + service: () => 'azureDevOps', + instanceId: ({url}) => getInstanceId(new URL(url)) +} + +export default AzureDevOpsRemoteProject diff --git a/packages/server/graphql/public/types/AzureDevOpsSearchQuery.ts b/packages/server/graphql/public/types/AzureDevOpsSearchQuery.ts new file mode 100644 index 00000000000..cd5b02bef7b --- /dev/null +++ b/packages/server/graphql/public/types/AzureDevOpsSearchQuery.ts @@ -0,0 +1,7 @@ +import {AzureDevOpsSearchQueryResolvers} from '../resolverTypes' + +const AzureDevOpsSearchQuery: AzureDevOpsSearchQueryResolvers = { + isWIQL: ({isWIQL}) => !!isWIQL +} + +export default AzureDevOpsSearchQuery diff --git a/packages/server/graphql/public/types/AzureDevOpsWorkItem.ts b/packages/server/graphql/public/types/AzureDevOpsWorkItem.ts new file mode 100644 index 00000000000..1d626072e09 --- /dev/null +++ b/packages/server/graphql/public/types/AzureDevOpsWorkItem.ts @@ -0,0 +1,25 @@ +import AzureDevOpsIssueId from 'parabol-client/shared/gqlIds/AzureDevOpsIssueId' +import {getInstanceId} from '../../../utils/azureDevOps/azureDevOpsFieldTypeToId' +import {AzureDevOpsWorkItemResolvers} from '../resolverTypes' + +const AzureDevOpsWorkItem: AzureDevOpsWorkItemResolvers = { + __isTypeOf: ({service}) => service === 'azureDevOps', + id: ({id, teamProject, url}) => { + const instanceId = getInstanceId(url) + return AzureDevOpsIssueId.join(instanceId, teamProject, id) + }, + + issueKey: async ({id}) => { + return id + }, + + project: async ({teamId, userId, teamProject, url}, _args, {dataLoader}) => { + const instanceId = getInstanceId(url) + const res = await dataLoader + .get('azureDevOpsProject') + .load({instanceId, projectId: teamProject, userId, teamId}) + return res! + } +} + +export default AzureDevOpsWorkItem diff --git a/packages/server/graphql/public/types/GitHubIntegration.ts b/packages/server/graphql/public/types/GitHubIntegration.ts new file mode 100644 index 00000000000..417f9655383 --- /dev/null +++ b/packages/server/graphql/public/types/GitHubIntegration.ts @@ -0,0 +1,30 @@ +import ms from 'ms' +import GitHubIntegrationId from '../../../../client/shared/gqlIds/GitHubIntegrationId' +import updateGitHubSearchQueries from '../../../postgres/queries/updateGitHubSearchQueries' +import {getUserId} from '../../../utils/authorization' +import {GitHubIntegrationResolvers} from '../resolverTypes' + +const GitHubIntegration: GitHubIntegrationResolvers = { + id: ({teamId, userId}) => GitHubIntegrationId.join(teamId, userId), + + accessToken: async ({accessToken, userId}, _args, {authToken}) => { + const viewerId = getUserId(authToken) + return viewerId === userId ? accessToken : null + }, + + isActive: ({accessToken}) => !!accessToken, + + githubSearchQueries: async ({githubSearchQueries, teamId, userId}) => { + const expirationThresh = ms('60d') + const thresh = new Date(Date.now() - expirationThresh) + const unexpiredQueries = githubSearchQueries.filter( + (query) => new Date(query.lastUsedAt) > thresh + ) + if (unexpiredQueries.length < githubSearchQueries.length) { + await updateGitHubSearchQueries({teamId, userId, githubSearchQueries: unexpiredQueries}) + } + return unexpiredQueries + } +} + +export default GitHubIntegration diff --git a/packages/server/graphql/public/types/GitLabIntegration.ts b/packages/server/graphql/public/types/GitLabIntegration.ts new file mode 100644 index 00000000000..ecc26f9905c --- /dev/null +++ b/packages/server/graphql/public/types/GitLabIntegration.ts @@ -0,0 +1,155 @@ +import GitLabServerManager from '../../../integrations/gitlab/GitLabServerManager' +import {GetProjectIssuesQuery, IssuableState, IssueSort} from '../../../types/gitlabTypes' +import sendToSentry from '../../../utils/sendToSentry' +import fetchGitLabProjects from '../../queries/helpers/fetchGitLabProjects' +import {GitLabIntegrationResolvers} from '../resolverTypes' + +export type GitLabIntegrationSource = { + teamId: string + userId: string +} + +type ProjectIssuesRes = NonNullable['issues']> +type ProjectIssueEdgeNullable = Pick< + NonNullable[number]>, + 'cursor' | 'node' +> +type ProjectIssueNode = NonNullable +type ProjectIssueEdge = ProjectIssueEdgeNullable & {node: ProjectIssueNode} + +type CursorDetails = { + fullPath: string + cursor: string +} +const GitLabIntegration: GitLabIntegrationResolvers = { + auth: async ({teamId, userId}, _args, {dataLoader}) => { + return dataLoader.get('freshGitlabAuth').load({teamId, userId}) + }, + + cloudProvider: async (_source, _args, {dataLoader}) => { + const [globalProvider] = await dataLoader + .get('sharedIntegrationProviders') + .load({service: 'gitlab', orgTeamIds: ['aGhostTeam'], teamIds: []}) + return globalProvider! + }, + + sharedProviders: async ({teamId}, _args, {dataLoader}) => { + const team = await dataLoader.get('teams').loadNonNull(teamId) + const {orgId} = team + const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) + const orgTeamIds = orgTeams.map(({id}) => id) + return dataLoader + .get('sharedIntegrationProviders') + .load({service: 'gitlab', orgTeamIds, teamIds: [teamId]}) + }, + + gitlabSearchQueries: async () => [], + + projects: async ({teamId, userId}, _args, context, info) => { + return fetchGitLabProjects(teamId, userId, context, info) + }, + + projectsIssues: async ({teamId, userId}, args, context, info) => { + const {projectsIds} = args + const after = args?.after ?? '' + const {dataLoader} = context + const emptyConnection = {edges: [], pageInfo: {hasNextPage: false, hasPreviousPage: false}} + const auth = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'gitlab', teamId, userId}) + if (!auth?.accessToken) return emptyConnection + const {providerId} = auth + const provider = await dataLoader.get('integrationProviders').load(providerId) + if (!provider?.serverBaseUrl) return emptyConnection + const manager = new GitLabServerManager(auth, context, info, provider.serverBaseUrl) + const [projectsData, projectsErr] = await manager.getProjects({ + ids: projectsIds as string[], + first: 50 // if no project filters have been selected, get the 50 most recently used projects + }) + if (projectsErr) { + sendToSentry(new Error('Unable to get GitLab projects in projectsIssues query'), {userId}) + return emptyConnection + } + const projectsFullPaths = new Set() + projectsData.projects?.edges?.forEach((edge) => { + if (edge?.node?.fullPath) { + projectsFullPaths.add(edge?.node?.fullPath) + } + }) + let parsedAfter: CursorDetails[] | null + try { + parsedAfter = after.length ? JSON.parse(after) : null + } catch (e) { + sendToSentry(new Error('Error parsing after'), {userId, tags: {after}}) + return emptyConnection + } + const isValidJSON = parsedAfter?.every( + (cursorsDetails) => + typeof cursorsDetails.cursor === 'string' && typeof cursorsDetails.fullPath === 'string' + ) + if (isValidJSON === false) { + sendToSentry(new Error('after arg has an invalid JSON structure'), { + userId, + tags: {after} + }) + return emptyConnection + } + + const projectsIssuesPromises = Array.from(projectsFullPaths).map((fullPath) => { + const after = parsedAfter?.find((cursor) => cursor.fullPath === fullPath)?.cursor ?? '' + return manager.getProjectIssues({ + ...args, + fullPath, + after, + sort: args.sort as IssueSort, + state: args.state as IssuableState + }) + }) + const projectsIssues = [] as ProjectIssueEdge[] + const errors = [] as Error[] + let hasNextPage = false + const endCursor = [] as CursorDetails[] + const projectsIssuesResponses = await Promise.all(projectsIssuesPromises) + for (const res of projectsIssuesResponses) { + const [projectIssuesData, err] = res + if (err) { + errors.push(err) + sendToSentry(err, {userId}) + continue + } + const {project} = projectIssuesData + if (!project?.issues) continue + const {fullPath, issues} = project + const {edges, pageInfo} = issues + if (pageInfo.hasNextPage) { + hasNextPage = true + const currentCursorDetails = endCursor.find( + (cursorDetails) => cursorDetails.fullPath === fullPath + ) + const newCursor = pageInfo.endCursor ?? '' + if (currentCursorDetails) currentCursorDetails.cursor = newCursor + else endCursor.push({fullPath, cursor: newCursor}) + } + edges?.forEach((edge) => { + if (!edge?.node) return + const {node, cursor} = edge + projectsIssues.push({cursor, node}) + }) + } + + const firstEdge = projectsIssues[0] + const stringifiedEndCursor = JSON.stringify(endCursor) + return { + error: errors[0], + edges: projectsIssues, + pageInfo: { + startCursor: firstEdge && firstEdge.cursor, + endCursor: stringifiedEndCursor, + hasNextPage, + hasPreviousPage: false + } + } + } +} + +export default GitLabIntegration diff --git a/packages/server/graphql/public/types/JiraSearchQuery.ts b/packages/server/graphql/public/types/JiraSearchQuery.ts new file mode 100644 index 00000000000..7d6beca33c3 --- /dev/null +++ b/packages/server/graphql/public/types/JiraSearchQuery.ts @@ -0,0 +1,8 @@ +import {JiraSearchQueryResolvers} from '../resolverTypes' + +const JiraSearchQuery: JiraSearchQueryResolvers = { + id: ({id}) => `JiraSearchQuery:${id}`, + projectKeyFilters: ({projectKeyFilters}) => projectKeyFilters || [] +} + +export default JiraSearchQuery diff --git a/packages/server/graphql/public/types/JiraServerIntegration.ts b/packages/server/graphql/public/types/JiraServerIntegration.ts new file mode 100644 index 00000000000..029a57cbcfb --- /dev/null +++ b/packages/server/graphql/public/types/JiraServerIntegration.ts @@ -0,0 +1,208 @@ +import IntegrationProviderId from '~/shared/gqlIds/IntegrationProviderId' +import IntegrationRepoId from '~/shared/gqlIds/IntegrationRepoId' +import TeamMember from '../../../database/types/TeamMember' +import JiraServerRestManager from '../../../integrations/jiraServer/JiraServerRestManager' +import {IntegrationProviderJiraServer} from '../../../postgres/queries/getIntegrationProvidersByIds' +import getLatestIntegrationSearchQueries from '../../../postgres/queries/getLatestIntegrationSearchQueries' +import {getUserId} from '../../../utils/authorization' +import standardError from '../../../utils/standardError' +import {JiraServerIntegrationResolvers} from '../resolverTypes' + +export type JiraServerIntegrationSource = { + teamId: string + userId: string +} +type IssueArgs = { + first: number + after: string + queryString: string | null + isJQL: boolean + projectKeyFilters: string[] | null +} + +const JiraServerIntegration: JiraServerIntegrationResolvers = { + id: async ({teamId, userId}, _args, {dataLoader}) => { + const auth = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'jiraServer', teamId, userId}) + + if (!auth) { + return null + } + + return `jiraServer:${teamId}:${auth.providerId}` + }, + + auth: async ({teamId, userId}, _args, {dataLoader}) => { + const auth = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'jiraServer', teamId, userId}) + return auth! + }, + + sharedProviders: async ({teamId, userId}, _args, {dataLoader}) => { + const teamMembers = await dataLoader.get('teamMembersByUserId').load(userId) + const teamMember = teamMembers.find((teamMember: TeamMember) => teamMember.teamId === teamId) + if (!teamMember) return [] + + const team = await dataLoader.get('teams').loadNonNull(teamMember.teamId) + const {orgId} = team + const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) + const orgTeamIds = orgTeams.map(({id}) => id) + + const providers = await dataLoader.get('sharedIntegrationProviders').load({ + service: 'jiraServer', + orgTeamIds: [...orgTeamIds, 'aGhostTeam'], + teamIds: [teamId] + }) + + return providers + }, + + issues: async ({teamId, userId}, args, {authToken, dataLoader}) => { + const {first, after, queryString, isJQL, projectKeyFilters} = args as IssueArgs + const viewerId = getUserId(authToken) + const emptyConnection = {edges: [], pageInfo: {hasNextPage: false, hasPreviousPage: false}} + if (viewerId !== userId) { + const err = new Error('Cannot access another team members issues') + standardError(err, {tags: {teamId, userId}, userId: viewerId}) + return emptyConnection + } + + const auth = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'jiraServer', teamId, userId}) + + if (!auth) { + return emptyConnection + } + + const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId) + + const integrationManager = new JiraServerRestManager( + auth, + provider as IntegrationProviderJiraServer + ) + + if (!integrationManager) { + return emptyConnection + } + + const projectKeys = (projectKeyFilters ?? []).map( + (projectKeyFilter) => IntegrationRepoId.split(projectKeyFilter).projectKey! + ) + + // Request one extra item to see if there are more results + const maxResults = first + 1 + // Relay requires the cursor to be a string + const afterInt = parseInt(after, 10) + const startAt = afterInt + 1 + const issueRes = await integrationManager.getIssues( + queryString, + isJQL, + projectKeys, + maxResults, + startAt + ) + + if (issueRes instanceof Error) { + return { + ...emptyConnection, + error: { + message: issueRes.message + } + } + } + + const {issues} = issueRes + + const mappedIssues = issues.map((issue) => { + const {project, issuetype, summary, description, updated} = issue.fields + return { + ...issue, + userId, + teamId, + providerId: provider.id, + issueKey: issue.key, + description: description ?? '', + descriptionHTML: issue.renderedFields.description, + projectId: project.id, + projectKey: project.key, + projectName: project.name, + issueType: issuetype.id, + summary, + service: 'jiraServer' as const, + updatedAt: new Date(updated) + } + }) + + const nodes = mappedIssues.slice(0, first) + const edges = mappedIssues.map((node, index) => ({ + cursor: `${index + afterInt}`, + node + })) + + const firstEdge = edges[0] + + return { + edges, + pageInfo: { + startCursor: firstEdge && firstEdge.cursor, + endCursor: firstEdge ? edges[edges.length - 1]!.cursor : null, + hasNextPage: mappedIssues.length > nodes.length, + hasPreviousPage: false + } + } + }, + + projects: async ({teamId, userId}, _args, {dataLoader}) => { + return dataLoader.get('allJiraServerProjects').load({teamId, userId}) + }, + + providerId: async ({teamId, userId}, _args, {dataLoader}) => { + const auth = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'jiraServer', teamId, userId}) + + if (!auth) { + return null + } + + return IntegrationProviderId.join(auth.providerId) + }, + + searchQueries: async ({teamId, userId}, _args, {dataLoader}) => { + const auth = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'jiraServer', teamId, userId}) + + if (!auth) { + return [] + } + + const searchQueries = await getLatestIntegrationSearchQueries({ + teamId, + userId, + service: 'jiraServer', + providerId: auth.providerId + }) + + return searchQueries.map((searchQuery) => { + const query = searchQuery.query as { + queryString: string | null + isJQL: boolean + projectKeyFilters: string[] | null + } + + return { + id: String(searchQuery.id), + queryString: query.queryString || '', + isJQL: query.isJQL, + projectKeyFilters: query.projectKeyFilters || [], + lastUsedAt: searchQuery.lastUsedAt + } + }) + } +} + +export default JiraServerIntegration diff --git a/packages/server/graphql/public/types/JiraServerIssue.ts b/packages/server/graphql/public/types/JiraServerIssue.ts new file mode 100644 index 00000000000..c6269b5ebac --- /dev/null +++ b/packages/server/graphql/public/types/JiraServerIssue.ts @@ -0,0 +1,46 @@ +import JiraServerIssueId from '~/shared/gqlIds/JiraServerIssueId' +import {JiraServerIssue as JiraServerRestIssue} from '../../../dataloader/jiraServerLoaders' +import {JiraServerIssueResolvers} from '../resolverTypes' + +export type JiraServerIssueSource = JiraServerRestIssue & { + userId: string + teamId: string + providerId: number +} + +const VOTE_FIELD_ID_BLACKLIST = ['description', 'summary'] +const VOTE_FIELD_ALLOWED_TYPES = ['string', 'number'] + +const JiraServerIssue: JiraServerIssueResolvers = { + __isTypeOf: ({service}) => service === 'jiraServer', + id: ({id, projectId, providerId}) => { + return JiraServerIssueId.join(providerId, projectId, id) + }, + + url: ({issueKey, self}) => { + const {origin} = new URL(self) + return `${origin}/browse/${issueKey}` + }, + + possibleEstimationFieldNames: async ( + {teamId, userId, providerId, issueType, projectId}, + _args, + {dataLoader} + ) => { + const issueMeta = await dataLoader + .get('jiraServerFieldTypes') + .load({teamId, userId, projectId, issueType, providerId}) + if (!issueMeta) return [] + const fieldNames = issueMeta + .filter( + ({fieldId, operations, schema}) => + !VOTE_FIELD_ID_BLACKLIST.includes(fieldId) && + operations.includes('set') && + VOTE_FIELD_ALLOWED_TYPES.includes(schema.type) + ) + .map(({name}) => name) + return fieldNames + } +} + +export default JiraServerIssue diff --git a/packages/server/graphql/public/types/JiraServerRemoteProject.ts b/packages/server/graphql/public/types/JiraServerRemoteProject.ts new file mode 100644 index 00000000000..14ad9a4f6f8 --- /dev/null +++ b/packages/server/graphql/public/types/JiraServerRemoteProject.ts @@ -0,0 +1,25 @@ +import IntegrationRepoId from 'parabol-client/shared/gqlIds/IntegrationRepoId' +import JiraServerRestManager from '../../../integrations/jiraServer/JiraServerRestManager' +import {IntegrationProviderJiraServer} from '../../../postgres/queries/getIntegrationProvidersByIds' +import defaultJiraProjectAvatar from '../../../utils/defaultJiraProjectAvatar' +import {JiraServerRemoteProjectResolvers} from '../resolverTypes' + +const JiraServerRemoteProject: JiraServerRemoteProjectResolvers = { + __isTypeOf: ({service}) => service === 'jiraServer', + id: (item) => IntegrationRepoId.join(item), + service: () => 'jiraServer', + + avatar: async ({avatarUrls, teamId, userId}, _args, {dataLoader}) => { + const url = avatarUrls['48x48'] + const auth = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'jiraServer', teamId, userId}) + if (!auth) return defaultJiraProjectAvatar + const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId) + const manager = new JiraServerRestManager(auth, provider as IntegrationProviderJiraServer) + const avatar = await manager.getProjectAvatar(url) + return avatar || defaultJiraProjectAvatar + } +} + +export default JiraServerRemoteProject diff --git a/packages/server/graphql/public/types/MSTeamsIntegration.ts b/packages/server/graphql/public/types/MSTeamsIntegration.ts new file mode 100644 index 00000000000..21e7089142a --- /dev/null +++ b/packages/server/graphql/public/types/MSTeamsIntegration.ts @@ -0,0 +1,24 @@ +import {MsTeamsIntegrationResolvers} from '../resolverTypes' + +export type MSTeamsIntegrationSource = { + teamId: string + userId: string +} + +const MSTeamsIntegration: MsTeamsIntegrationResolvers = { + auth: async ({teamId, userId}, _args, {dataLoader}) => { + return dataLoader.get('teamMemberIntegrationAuths').load({service: 'msTeams', teamId, userId}) + }, + + sharedProviders: async ({teamId}, _args, {dataLoader}) => { + const team = await dataLoader.get('teams').loadNonNull(teamId) + const {orgId} = team + const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) + const orgTeamIds = orgTeams.map(({id}) => id) + return dataLoader + .get('sharedIntegrationProviders') + .load({service: 'msTeams', orgTeamIds, teamIds: [teamId]}) + } +} + +export default MSTeamsIntegration diff --git a/packages/server/graphql/public/types/MattermostIntegration.ts b/packages/server/graphql/public/types/MattermostIntegration.ts new file mode 100644 index 00000000000..994f81cc89b --- /dev/null +++ b/packages/server/graphql/public/types/MattermostIntegration.ts @@ -0,0 +1,27 @@ +import {MattermostIntegrationResolvers} from '../resolverTypes' + +export type MattermostIntegrationSource = { + teamId: string + userId: string +} + +const MattermostIntegration: MattermostIntegrationResolvers = { + auth: async ({teamId, userId}, _args, {dataLoader}) => { + const res = await dataLoader + .get('teamMemberIntegrationAuths') + .load({service: 'mattermost', teamId, userId}) + return res! + }, + + sharedProviders: async ({teamId}, _args, {dataLoader}) => { + const team = await dataLoader.get('teams').loadNonNull(teamId) + const {orgId} = team + const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) + const orgTeamIds = orgTeams.map(({id}) => id) + return dataLoader + .get('sharedIntegrationProviders') + .load({service: 'mattermost', orgTeamIds, teamIds: [teamId]}) + } +} + +export default MattermostIntegration diff --git a/packages/server/graphql/public/types/Organization.ts b/packages/server/graphql/public/types/Organization.ts index 15b84bd3907..5610172e74b 100644 --- a/packages/server/graphql/public/types/Organization.ts +++ b/packages/server/graphql/public/types/Organization.ts @@ -1,8 +1,15 @@ import {ExtractTypeFromQueryBuilderSelect} from '../../../../client/types/generics' import {selectOrganizations} from '../../../dataloader/primaryKeyLoaderMakers' -import {isSuperUser} from '../../../utils/authorization' +import { + getUserId, + isSuperUser, + isTeamMember, + isUserBillingLeader, + isUserOrgAdmin +} from '../../../utils/authorization' import {getFeatureTier} from '../../types/helpers/getFeatureTier' import {OrganizationResolvers} from '../resolverTypes' +import getActiveTeamCountByOrgIds from './helpers/getActiveTeamCountByOrgIds' export interface OrganizationSource extends ExtractTypeFromQueryBuilderSelect {} @@ -36,6 +43,98 @@ const Organization: OrganizationResolvers = { saml: async ({id: orgId}, _args, {dataLoader}) => { const saml = await dataLoader.get('samlByOrgId').load(orgId) return saml || null + }, + + isBillingLeader: async ({id: orgId}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + return isUserBillingLeader(viewerId, orgId, dataLoader) + }, + + isOrgAdmin: async ({id: orgId}, _args, {authToken, dataLoader}) => { + const viewerId = getUserId(authToken) + return isUserOrgAdmin(viewerId, orgId, dataLoader) + }, + + activeTeamCount: async ({id: orgId}) => { + return getActiveTeamCountByOrgIds(orgId) + }, + + allTeams: async ({id: orgId}, _args, {dataLoader, authToken}) => { + const viewerId = getUserId(authToken) + const [allTeamsOnOrg, organization, isOrgAdmin, isBillingLeader] = await Promise.all([ + dataLoader.get('teamsByOrgIds').load(orgId), + dataLoader.get('organizations').loadNonNull(orgId), + isUserOrgAdmin(viewerId, orgId, dataLoader), + isUserBillingLeader(viewerId, orgId, dataLoader) + ]) + const sortedTeamsOnOrg = allTeamsOnOrg.sort((a, b) => a.name.localeCompare(b.name)) + const hasPublicTeamsFlag = !!organization.featureFlags?.includes('publicTeams') + if (isBillingLeader || isOrgAdmin || isSuperUser(authToken) || hasPublicTeamsFlag) { + const viewerTeams = sortedTeamsOnOrg.filter((team) => authToken.tms.includes(team.id)) + const otherTeams = sortedTeamsOnOrg.filter((team) => !authToken.tms.includes(team.id)) + return [...viewerTeams, ...otherTeams] + } else { + return sortedTeamsOnOrg.filter((team) => authToken.tms.includes(team.id)) + } + }, + + viewerTeams: async ({id: orgId}, _args, {dataLoader, authToken}) => { + const allTeamsOnOrg = await dataLoader.get('teamsByOrgIds').load(orgId) + return allTeamsOnOrg + .filter((team) => authToken.tms.includes(team.id)) + .sort((a, b) => a.name.localeCompare(b.name)) + }, + + publicTeams: async ({id: orgId}, _args, {dataLoader, authToken}) => { + const [allTeamsOnOrg, organization] = await Promise.all([ + dataLoader.get('teamsByOrgIds').load(orgId), + dataLoader.get('organizations').loadNonNull(orgId) + ]) + const hasPublicTeamsFlag = !!organization.featureFlags?.includes('publicTeams') + if (!isSuperUser(authToken) || !hasPublicTeamsFlag) return [] + const publicTeams = allTeamsOnOrg.filter((team) => !isTeamMember(authToken, team.id)) + return publicTeams + }, + + viewerOrganizationUser: async ({id: orgId}, _args, {dataLoader, authToken}) => { + const viewerId = getUserId(authToken) + return dataLoader.get('organizationUsersByUserIdOrgId').load({userId: viewerId, orgId}) + }, + + organizationUsers: async ({id: orgId}, _args, {dataLoader}) => { + const organizationUsers = await dataLoader.get('organizationUsersByOrgId').load(orgId) + organizationUsers.sort((a, b) => (a.orgId > b.orgId ? 1 : -1)) + const edges = organizationUsers.map((node) => ({ + cursor: node.id, + node + })) + // TODO implement pagination + const firstEdge = edges[0] + return { + edges, + pageInfo: { + endCursor: firstEdge ? edges[edges.length - 1]!.cursor : null, + hasNextPage: false, + hasPreviousPage: false + } + } + }, + + orgUserCount: async ({id: orgId}, _args, {dataLoader}) => { + const organizationUsers = await dataLoader.get('organizationUsersByOrgId').load(orgId) + const inactiveUserCount = organizationUsers.filter(({inactive}) => inactive).length + return { + inactiveUserCount, + activeUserCount: organizationUsers.length - inactiveUserCount + } + }, + + billingLeaders: async ({id: orgId}, _args, {dataLoader}) => { + const organizationUsers = await dataLoader.get('organizationUsersByOrgId').load(orgId) + return organizationUsers.filter( + (organizationUser) => + organizationUser.role === 'BILLING_LEADER' || organizationUser.role === 'ORG_ADMIN' + ) } } diff --git a/packages/server/graphql/public/types/SlackIntegration.ts b/packages/server/graphql/public/types/SlackIntegration.ts new file mode 100644 index 00000000000..7f9d623fd48 --- /dev/null +++ b/packages/server/graphql/public/types/SlackIntegration.ts @@ -0,0 +1,18 @@ +import {getUserId} from '../../../utils/authorization' +import {SlackIntegrationResolvers} from '../resolverTypes' + +const SlackIntegration: SlackIntegrationResolvers = { + isActive: ({isActive, botAccessToken}) => !!(isActive && botAccessToken), + + botAccessToken: async ({botAccessToken, userId}, _args, {authToken}) => { + const viewerId = getUserId(authToken) + return viewerId === userId ? botAccessToken : null + }, + + notifications: async ({userId, teamId}, _args, {dataLoader}) => { + const slackNotifications = await dataLoader.get('slackNotificationsByTeamId').load(teamId) + return slackNotifications.filter((notification) => notification.userId === userId) + } +} + +export default SlackIntegration diff --git a/packages/server/graphql/public/types/TeamMemberIntegrations.ts b/packages/server/graphql/public/types/TeamMemberIntegrations.ts index 9c2c2e63924..a30c2ff8957 100644 --- a/packages/server/graphql/public/types/TeamMemberIntegrations.ts +++ b/packages/server/graphql/public/types/TeamMemberIntegrations.ts @@ -1,14 +1,41 @@ +import TeamMemberIntegrationsId from '../../../../client/shared/gqlIds/TeamMemberIntegrationsId' +import {isTeamMember} from '../../../utils/authorization' import {TeamMemberIntegrationsResolvers} from '../resolverTypes' export type TeamMemberIntegrationsSource = { teamId: string userId: string } - const TeamMemberIntegrations: TeamMemberIntegrationsResolvers = { gcal: async ({teamId, userId}) => { return {teamId, userId} - } + }, + + id: ({teamId, userId}) => TeamMemberIntegrationsId.join(teamId, userId), + + atlassian: async ({teamId, userId}, _args, {authToken, dataLoader}) => { + if (!isTeamMember(authToken, teamId)) return null + return dataLoader.get('freshAtlassianAuth').load({teamId, userId}) + }, + + jiraServer: (source) => source, + + github: async ({teamId, userId}, _args, {authToken, dataLoader}) => { + if (!isTeamMember(authToken, teamId)) return null + return dataLoader.get('githubAuth').load({teamId, userId}) + }, + + gitlab: (source) => source, + mattermost: (source) => source, + + slack: async ({teamId, userId}, _args, {authToken, dataLoader}) => { + if (!isTeamMember(authToken, teamId)) return null + const auths = await dataLoader.get('slackAuthByUserId').load(userId) + return auths.find((auth) => auth.teamId === teamId)! + }, + + azureDevOps: (source) => source, + msTeams: (source) => source } export default TeamMemberIntegrations diff --git a/packages/server/graphql/public/types/_xGitLabProject.ts b/packages/server/graphql/public/types/_xGitLabProject.ts index 45b7a6b5b52..30a1c23b2b1 100644 --- a/packages/server/graphql/public/types/_xGitLabProject.ts +++ b/packages/server/graphql/public/types/_xGitLabProject.ts @@ -1,5 +1,14 @@ import {_XGitLabProjectResolvers} from '../resolverTypes' +// There's a bug in GraphQL Codegen that allow mappers to start with `_` +// So, we overwrite the GitLab native object with this type, too +export type _xGitLabProjectSource = { + __typename: 'Project' + service: 'gitlab' + id: string + fullPath: string +} + const _xGitLabProject: _XGitLabProjectResolvers = { __isTypeOf: ({id}) => id.startsWith('gid://'), service: () => 'gitlab' diff --git a/packages/server/graphql/rootTypes.ts b/packages/server/graphql/rootTypes.ts index b676deb4ce1..4a83c7b4672 100644 --- a/packages/server/graphql/rootTypes.ts +++ b/packages/server/graphql/rootTypes.ts @@ -4,7 +4,6 @@ import AgendaItemsPhase from './types/AgendaItemsPhase' import AuthIdentityGoogle from './types/AuthIdentityGoogle' import AuthIdentityLocal from './types/AuthIdentityLocal' import AuthIdentityMicrosoft from './types/AuthIdentityMicrosoft' -import AzureDevOpsWorkItem from './types/AzureDevOpsWorkItem' import CheckInPhase from './types/CheckInPhase' import Comment from './types/Comment' import DiscussPhase from './types/DiscussPhase' @@ -71,7 +70,6 @@ const rootTypes = [ TeamPromptMeetingMember, TeamPromptMeetingSettings, Comment, - AzureDevOpsWorkItem, JiraDimensionField, RenamePokerTemplatePayload, UserTiersCount diff --git a/packages/server/graphql/types/AtlassianIntegration.ts b/packages/server/graphql/types/AtlassianIntegration.ts index 5b7c9f1d516..0827a077c3d 100644 --- a/packages/server/graphql/types/AtlassianIntegration.ts +++ b/packages/server/graphql/types/AtlassianIntegration.ts @@ -1,92 +1,8 @@ -import {GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import ms from 'ms' -import AtlassianIntegrationId from '../../../client/shared/gqlIds/AtlassianIntegrationId' -import {AtlassianAuth} from '../../postgres/queries/getAtlassianAuthByUserIdTeamId' -import updateJiraSearchQueries from '../../postgres/queries/updateJiraSearchQueries' -import {getUserId} from '../../utils/authorization' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import JiraRemoteProject from './JiraRemoteProject' -import JiraSearchQuery from './JiraSearchQuery' +import {GraphQLObjectType} from 'graphql' -const AtlassianIntegration = new GraphQLObjectType({ +const AtlassianIntegration = new GraphQLObjectType({ name: 'AtlassianIntegration', - description: 'The atlassian auth + integration helpers for a specific team member', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'Composite key in atlassiani:teamId:userId format', - resolve: ({teamId, userId}: {teamId: string; userId: string}) => - AtlassianIntegrationId.join(teamId, userId) - }, - isActive: { - description: 'true if the auth is valid, else false', - type: new GraphQLNonNull(GraphQLBoolean), - resolve: ({accessToken}) => !!accessToken - }, - accessToken: { - description: - 'The access token to atlassian, useful for 1 hour. null if no access token available or the viewer is not the user', - type: GraphQLID, - resolve: async ({accessToken, userId}, _args: unknown, {authToken}) => { - const viewerId = getUserId(authToken) - return viewerId === userId ? accessToken : null - } - }, - accountId: { - type: new GraphQLNonNull(GraphQLID), - description: '*The atlassian account ID' - }, - cloudIds: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))), - description: 'The atlassian cloud IDs that the user has granted' - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the provider was created' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: '*The team that the token is linked to' - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the token was updated at' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The user that the access token is attached to' - }, - projects: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(JiraRemoteProject))), - description: - 'A list of projects accessible by this team member. empty if viewer is not the user', - resolve: ({teamId, userId}: AtlassianAuth, _args: unknown, {authToken, dataLoader}) => { - const viewerId = getUserId(authToken) - if (viewerId !== userId) return [] - return dataLoader.get('allJiraProjects').load({teamId, userId}) - } - }, - jiraSearchQueries: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(JiraSearchQuery))), - description: - 'the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old', - resolve: async ({teamId, userId, jiraSearchQueries}) => { - const expirationThresh = ms('60d') - const thresh = new Date(Date.now() - expirationThresh) - const searchQueries = jiraSearchQueries || [] - const unexpiredQueries = searchQueries.filter((query) => query.lastUsedAt > thresh) - if (unexpiredQueries.length < searchQueries.length) { - await updateJiraSearchQueries({ - jiraSearchQueries: searchQueries, - teamId, - userId - }) - } - return unexpiredQueries - } - } - }) + fields: {} }) export default AtlassianIntegration diff --git a/packages/server/graphql/types/AzureDevOpsIntegration.ts b/packages/server/graphql/types/AzureDevOpsIntegration.ts deleted file mode 100644 index ceb7e2f68bc..00000000000 --- a/packages/server/graphql/types/AzureDevOpsIntegration.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { - GraphQLBoolean, - GraphQLID, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import {getUserId, isTeamMember} from '../../utils/authorization' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import connectionFromTasks from '../queries/helpers/connectionFromTasks' -import AzureDevOpsRemoteProject from './AzureDevOpsRemoteProject' -import AzureDevOpsSearchQuery from './AzureDevOpsSearchQuery' -import {AzureDevOpsWorkItemConnection} from './AzureDevOpsWorkItem' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import IntegrationProviderOAuth2 from './IntegrationProviderOAuth2' -import TeamMemberIntegrationAuthOAuth2 from './TeamMemberIntegrationAuthOAuth2' - -type WorkItemArgs = { - first: number - after?: string - queryString: string | null - projectKeyFilters: string[] | null - isWIQL: boolean -} - -const AzureDevOpsIntegration = new GraphQLObjectType({ - name: 'AzureDevOpsIntegration', - description: 'The Azure DevOps auth + integration helpers for a specific team member', - fields: () => ({ - auth: { - description: 'The OAuth2 Authorization for this team member', - type: TeamMemberIntegrationAuthOAuth2, - resolve: async ( - {teamId, userId}: {teamId: string; userId: string}, - _args: unknown, - {dataLoader} - ) => { - return dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'azureDevOps', teamId, userId}) - } - }, - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'Composite key in ado:teamId:userId format', - resolve: ({teamId, userId}: {teamId: string; userId: string}) => `ado:${teamId}:${userId}` - }, - accountId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The Azure DevOps account ID' - }, - instanceIds: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))), - description: 'The Azure DevOps instance IDs that the user has granted' - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the provider was created' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The team that the token is linked to' - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the token was updated at' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The user that the access token is attached to' - }, - workItems: { - type: new GraphQLNonNull(AzureDevOpsWorkItemConnection), - description: - 'A list of work items coming straight from the azure dev ops integration for a specific team member', - args: { - first: { - type: GraphQLInt, - defaultValue: 100 - }, - after: { - type: GraphQLISO8601Type, - description: 'the datetime cursor' - }, - queryString: { - type: GraphQLString, - description: 'A string of text to search for, or WIQL if isWIQL is true' - }, - projectKeyFilters: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))), - description: 'A list of projects to restrict the search to, if null will search all' - }, - isWIQL: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the queryString is WIQL, else false' - } - }, - resolve: async ({teamId, userId}, args: any, {authToken, dataLoader}: GQLContext) => { - const {first, queryString, projectKeyFilters, isWIQL} = args as WorkItemArgs - const viewerId = getUserId(authToken) - if (!isTeamMember(authToken, teamId)) { - const err = new Error('Cannot access another team members user stories') - standardError(err, {tags: {teamId, userId}, userId: viewerId}) - return connectionFromTasks([], 0, err) - } - const allUserWorkItems = await dataLoader - .get('azureDevOpsAllWorkItems') - .load({teamId, userId, queryString, projectKeyFilters, isWIQL}) - if (!allUserWorkItems) { - return connectionFromTasks([], 0, undefined) - } else { - const workItems = Array.from( - allUserWorkItems.map((userWorkItem) => { - return { - ...userWorkItem, - updatedAt: new Date() - } - }) - ) - return connectionFromTasks(workItems, first, undefined) - } - } - }, - projects: { - // Create a new object for ADO Projects (new schema object) - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AzureDevOpsRemoteProject))), - description: - 'A list of projects coming straight from the azure dev ops integration for a specific team member', - resolve: ({teamId, userId}, _args: unknown, {authToken, dataLoader}) => { - const viewerId = getUserId(authToken) - if (viewerId !== userId) return [] - return dataLoader.get('allAzureDevOpsProjects').load({teamId, userId}) - } - }, - cloudProvider: { - description: - 'The cloud provider the team member may choose to integrate with. Nullable based on env vars', - type: IntegrationProviderOAuth2, - resolve: async (_source: unknown, _args: unknown, {dataLoader}) => { - const [globalProvider] = await dataLoader - .get('sharedIntegrationProviders') - .load({service: 'azureDevOps', orgTeamIds: ['aGhostTeam'], teamIds: []}) - return globalProvider - } - }, - sharedProviders: { - description: 'The non-global providers shared with the team or organization', - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(IntegrationProviderOAuth2))), - resolve: async ({teamId}: {teamId: string}, _args: unknown, {dataLoader}) => { - const team = await dataLoader.get('teams').loadNonNull(teamId) - const {orgId} = team - const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) - const orgTeamIds = orgTeams.map(({id}) => id) - return dataLoader - .get('sharedIntegrationProviders') - .load({service: 'azureDevOps', orgTeamIds, teamIds: [teamId]}) - } - }, - azureDevOpsSearchQueries: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(AzureDevOpsSearchQuery))), - description: - 'the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old' - } - }) -}) - -export default AzureDevOpsIntegration diff --git a/packages/server/graphql/types/AzureDevOpsRemoteProject.ts b/packages/server/graphql/types/AzureDevOpsRemoteProject.ts deleted file mode 100644 index c020477530c..00000000000 --- a/packages/server/graphql/types/AzureDevOpsRemoteProject.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {GraphQLID, GraphQLInt, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {getInstanceId} from '../../utils/azureDevOps/azureDevOpsFieldTypeToId' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import IntegrationProviderServiceEnum from './IntegrationProviderServiceEnum' -import RepoIntegration, {repoIntegrationFields} from './RepoIntegration' - -const AzureDevOpsRemoteProject = new GraphQLObjectType({ - name: 'AzureDevOpsRemoteProject', - description: 'A project fetched from Azure DevOps in real time', - interfaces: () => [RepoIntegration], - isTypeOf: ({service}) => service === 'azureDevOps', - fields: () => ({ - ...repoIntegrationFields(), - id: { - type: new GraphQLNonNull(GraphQLID) - }, - service: { - type: new GraphQLNonNull(IntegrationProviderServiceEnum), - resolve: () => 'azureDevOps' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The parabol teamId this issue was fetched for' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The parabol userId this issue was fetched for' - }, - lastUpdateTime: { - type: new GraphQLNonNull(GraphQLISO8601Type) - }, - self: { - type: new GraphQLNonNull(GraphQLID) - }, - instanceId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The instance ID that the project lives on', - resolve: ({url}: {url: string}) => getInstanceId(new URL(url)) - }, - name: { - type: new GraphQLNonNull(GraphQLString) - }, - revision: { - type: new GraphQLNonNull(GraphQLInt) - }, - state: { - type: new GraphQLNonNull(GraphQLString) - }, - url: { - type: new GraphQLNonNull(GraphQLString) - }, - visibility: { - type: new GraphQLNonNull(GraphQLString) - } - }) -}) - -export default AzureDevOpsRemoteProject diff --git a/packages/server/graphql/types/AzureDevOpsSearchQuery.ts b/packages/server/graphql/types/AzureDevOpsSearchQuery.ts deleted file mode 100644 index 58a42a2bb8f..00000000000 --- a/packages/server/graphql/types/AzureDevOpsSearchQuery.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - GraphQLBoolean, - GraphQLID, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' - -const AzureDevOpsSearchQuery = new GraphQLObjectType({ - name: 'AzureDevOpsSearchQuery', - description: - 'An Azure DevOps search query including all filters selected when the query was executed', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'shortid' - }, - queryString: { - type: new GraphQLNonNull(GraphQLString), - description: 'The query string, either simple or WIQL depending on the isWIQL flag' - }, - projectKeyFilters: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))), - description: 'A list of projects to restrict the search to' - }, - isWIQL: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the queryString is WIQL, else false', - resolve: ({isWIQL}) => !!isWIQL - }, - lastUsedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'the time the search query was last used. Used for sorting' - } - }) -}) - -export default AzureDevOpsSearchQuery diff --git a/packages/server/graphql/types/AzureDevOpsWorkItem.ts b/packages/server/graphql/types/AzureDevOpsWorkItem.ts deleted file mode 100644 index b0ebf944690..00000000000 --- a/packages/server/graphql/types/AzureDevOpsWorkItem.ts +++ /dev/null @@ -1,102 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import AzureDevOpsIssueId from 'parabol-client/shared/gqlIds/AzureDevOpsIssueId' -import {getInstanceId} from '../../utils/azureDevOps/azureDevOpsFieldTypeToId' -import connectionDefinitions from '../connectionDefinitions' -import {GQLContext} from '../graphql' -import AzureDevOpsRemoteProject from './AzureDevOpsRemoteProject' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import PageInfoDateCursor from './PageInfoDateCursor' -import StandardMutationError from './StandardMutationError' -import TaskIntegration from './TaskIntegration' - -const AzureDevOpsWorkItem = new GraphQLObjectType({ - name: 'AzureDevOpsWorkItem', - description: 'The Azure DevOps Work Item that comes direct from Azure DevOps', - interfaces: () => [TaskIntegration], - isTypeOf: ({service}) => service === 'azureDevOps', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'GUID instanceId:projectKey:issueKey', - resolve: ({id, teamProject, url}: {id: string; teamProject: string; url: string}) => { - const instanceId = getInstanceId(url) - return AzureDevOpsIssueId.join(instanceId, teamProject, id) - } - }, - issueKey: { - type: new GraphQLNonNull(GraphQLString), - description: 'The id of the issue from Azure, e.g. 7', - resolve: async ({id}: {id: string}) => { - return id - } - }, - title: { - type: new GraphQLNonNull(GraphQLString), - description: 'Title of the work item' - }, - // TODO: change teamProject name: https://github.com/ParabolInc/parabol/issues/7073 - teamProject: { - type: new GraphQLNonNull(GraphQLString), - description: 'Name or id of the Team Project the work item belongs to' - }, - project: { - type: new GraphQLNonNull(AzureDevOpsRemoteProject), - description: 'The Azure DevOps Remote Project the work item belongs to', - resolve: async ( - { - teamId, - userId, - teamProject, - url - }: {teamId: string; userId: string; teamProject: string; url: string}, - _args: unknown, - {dataLoader}: GQLContext - ) => { - const instanceId = getInstanceId(url) - return dataLoader - .get('azureDevOpsProject') - .load({instanceId, projectId: teamProject, userId, teamId}) - } - }, - url: { - type: new GraphQLNonNull(GraphQLString), - description: 'URL to the issue' - }, - state: { - type: new GraphQLNonNull(GraphQLString), - description: 'The Current State of the Work item' - }, - type: { - type: new GraphQLNonNull(GraphQLString), - description: 'The Type of the Work item' - }, - descriptionHTML: { - type: new GraphQLNonNull(GraphQLString), - description: 'The description converted into raw HTML' - } - }) -}) - -const {connectionType, edgeType} = connectionDefinitions({ - name: AzureDevOpsWorkItem.name, - nodeType: AzureDevOpsWorkItem, - edgeFields: () => ({ - cursor: { - type: GraphQLISO8601Type - } - }), - connectionFields: () => ({ - pageInfo: { - type: PageInfoDateCursor, - description: 'Page info with cursors coerced to ISO8601 dates' - }, - error: { - type: StandardMutationError, - description: 'An error with the connection, if any' - } - }) -}) - -export const AzureDevOpsWorkItemConnection = connectionType -export const AzureDevOpsWorkItemEdge = edgeType -export default AzureDevOpsWorkItem diff --git a/packages/server/graphql/types/GitHubIntegration.ts b/packages/server/graphql/types/GitHubIntegration.ts index d6d76319f14..42a7bdf1924 100644 --- a/packages/server/graphql/types/GitHubIntegration.ts +++ b/packages/server/graphql/types/GitHubIntegration.ts @@ -1,83 +1,8 @@ -import { - GraphQLBoolean, - GraphQLID, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import ms from 'ms' -import GitHubIntegrationId from '../../../client/shared/gqlIds/GitHubIntegrationId' -import {GitHubAuth} from '../../postgres/queries/getGitHubAuthByUserIdTeamId' -import updateGitHubSearchQueries from '../../postgres/queries/updateGitHubSearchQueries' -import {getUserId} from '../../utils/authorization' -import {GQLContext} from '../graphql' -import GitHubSearchQuery from './GitHubSearchQuery' -import GraphQLISO8601Type from './GraphQLISO8601Type' +import {GraphQLObjectType} from 'graphql' -const GitHubIntegration = new GraphQLObjectType({ +const GitHubIntegration = new GraphQLObjectType({ name: 'GitHubIntegration', - description: 'OAuth token for a team member', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'composite key', - resolve: ({teamId, userId}) => GitHubIntegrationId.join(teamId, userId) - }, - accessToken: { - description: 'The access token to github. good forever', - type: GraphQLID, - resolve: async ({accessToken, userId}, _args: unknown, {authToken}) => { - const viewerId = getUserId(authToken) - return viewerId === userId ? accessToken : null - } - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the provider was created' - }, - isActive: { - description: 'true if an access token exists, else false', - type: new GraphQLNonNull(GraphQLBoolean), - resolve: ({accessToken}) => !!accessToken - }, - githubSearchQueries: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GitHubSearchQuery))), - description: - 'the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old', - resolve: async ({githubSearchQueries, teamId, userId}: GitHubAuth) => { - const expirationThresh = ms('60d') - const thresh = new Date(Date.now() - expirationThresh) - const unexpiredQueries = githubSearchQueries.filter( - (query) => new Date(query.lastUsedAt) > thresh - ) - if (unexpiredQueries.length < githubSearchQueries.length) { - await updateGitHubSearchQueries({teamId, userId, githubSearchQueries: unexpiredQueries}) - } - return unexpiredQueries - } - }, - login: { - type: new GraphQLNonNull(GraphQLID), - description: '*The GitHub login used for queries' - }, - scope: { - type: new GraphQLNonNull(GraphQLString), - description: 'The comma-separated list of scopes requested from GitHub' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: '*The team that the token is linked to' - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the token was updated at' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The user that the access token is attached to' - } - }) + fields: {} }) export default GitHubIntegration diff --git a/packages/server/graphql/types/GitHubSearchQuery.ts b/packages/server/graphql/types/GitHubSearchQuery.ts deleted file mode 100644 index 283c34e15f0..00000000000 --- a/packages/server/graphql/types/GitHubSearchQuery.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' - -const GitHubSearchQuery = new GraphQLObjectType({ - name: 'GitHubSearchQuery', - description: 'A GitHub search query including all filters selected when the query was executed', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'shortid' - }, - queryString: { - type: new GraphQLNonNull(GraphQLString), - description: - 'The query string in GitHub format, including repository filters. e.g. is:issue is:open' - }, - lastUsedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'the time the search query was last used. Used for sorting' - } - }) -}) - -export default GitHubSearchQuery diff --git a/packages/server/graphql/types/GitLabIntegration.ts b/packages/server/graphql/types/GitLabIntegration.ts deleted file mode 100644 index 7093bf686f5..00000000000 --- a/packages/server/graphql/types/GitLabIntegration.ts +++ /dev/null @@ -1,242 +0,0 @@ -import {GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import GitLabServerManager from '../../integrations/gitlab/GitLabServerManager' -import {GetProjectIssuesQuery} from '../../types/gitlabTypes' -import sendToSentry from '../../utils/sendToSentry' -import connectionDefinitions from '../connectionDefinitions' -import {GQLContext} from '../graphql' -import connectionFromTasks from '../queries/helpers/connectionFromTasks' -import fetchGitLabProjects from '../queries/helpers/fetchGitLabProjects' -import GitLabSearchQuery from './GitLabSearchQuery' -import IntegrationProviderOAuth2 from './IntegrationProviderOAuth2' -import RepoIntegration from './RepoIntegration' -import StandardMutationError from './StandardMutationError' -import TaskIntegration from './TaskIntegration' -import TeamMemberIntegrationAuthOAuth2 from './TeamMemberIntegrationAuthOAuth2' - -type ProjectIssuesRes = NonNullable['issues']> -type ProjectIssue = NonNullable[0]>['node']> -type ProjectIssueEdge = { - node: ProjectIssue - cursor: string | Date -} -export type ProjectsIssuesArgs = { - first: number - after?: string - projectsIds: string[] | null - searchQuery: string - sort: string - state: string - fullPath: string -} -type CursorDetails = { - fullPath: string - cursor: string -} - -const GitLabIntegration = new GraphQLObjectType({ - name: 'GitLabIntegration', - description: 'Gitlab integration data for a given team member', - fields: () => ({ - auth: { - description: 'The OAuth2 Authorization for this team member', - type: TeamMemberIntegrationAuthOAuth2, - resolve: async ( - {teamId, userId}: {teamId: string; userId: string}, - _args: unknown, - {dataLoader} - ) => { - return dataLoader.get('freshGitlabAuth').load({teamId, userId}) - } - }, - cloudProvider: { - description: - 'The cloud provider the team member may choose to integrate with. Nullable based on env vars', - type: IntegrationProviderOAuth2, - resolve: async (_source: unknown, _args: unknown, {dataLoader}) => { - const [globalProvider] = await dataLoader - .get('sharedIntegrationProviders') - .load({service: 'gitlab', orgTeamIds: ['aGhostTeam'], teamIds: []}) - return globalProvider - } - }, - sharedProviders: { - description: 'The non-global providers shared with the team or organization', - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(IntegrationProviderOAuth2))), - resolve: async ({teamId}: {teamId: string}, _args: unknown, {dataLoader}) => { - const team = await dataLoader.get('teams').loadNonNull(teamId) - const {orgId} = team - const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) - const orgTeamIds = orgTeams.map(({id}) => id) - return dataLoader - .get('sharedIntegrationProviders') - .load({service: 'gitlab', orgTeamIds, teamIds: [teamId]}) - } - }, - gitlabSearchQueries: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GitLabSearchQuery))), - resolve: async () => [] - }, - projects: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(RepoIntegration))), - description: 'A list of projects accessible by this team member', - resolve: async ( - {teamId, userId}: {teamId: string; userId: string}, - _args: unknown, - context, - info - ) => { - return fetchGitLabProjects(teamId, userId, context, info) - } - }, - projectsIssues: { - type: new GraphQLNonNull(GitLabProjectIssuesConnection), - args: { - first: { - type: GraphQLNonNull(GraphQLInt) - }, - after: { - type: GraphQLString, - description: 'the stringified cursors for pagination' - }, - projectsIds: { - type: GraphQLList(GraphQLString), - description: 'the ids of the projects selected as filters' - }, - searchQuery: { - type: GraphQLNonNull(GraphQLString), - description: 'the search query that the user enters to filter issues' - }, - sort: { - type: GraphQLNonNull(GraphQLString), - description: 'the sort string that defines the order of the returned issues' - }, - state: { - type: GraphQLNonNull(GraphQLString), - description: 'the state of issues, e.g. opened or closed' - } - }, - resolve: async ( - {teamId, userId}: {teamId: string; userId: string}, - args: any, - context, - info - ) => { - const {projectsIds, after = ''} = args as ProjectsIssuesArgs - const {dataLoader} = context - const auth = await dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'gitlab', teamId, userId}) - if (!auth?.accessToken) return [] - const {providerId} = auth - const provider = await dataLoader.get('integrationProviders').load(providerId) - if (!provider?.serverBaseUrl) return [] - const manager = new GitLabServerManager(auth, context, info, provider.serverBaseUrl) - const [projectsData, projectsErr] = await manager.getProjects({ - ids: projectsIds, - first: 50 // if no project filters have been selected, get the 50 most recently used projects - }) - if (projectsErr) { - sendToSentry(new Error('Unable to get GitLab projects in projectsIssues query'), {userId}) - return connectionFromTasks([], 0) - } - const projectsFullPaths = new Set() - projectsData.projects?.edges?.forEach((edge) => { - if (edge?.node?.fullPath) { - projectsFullPaths.add(edge?.node?.fullPath) - } - }) - let parsedAfter: CursorDetails[] | null - try { - parsedAfter = after.length ? JSON.parse(after) : null - } catch (e) { - sendToSentry(new Error('Error parsing after'), {userId, tags: {after}}) - return connectionFromTasks([], 0) - } - const isValidJSON = parsedAfter?.every( - (cursorsDetails) => - typeof cursorsDetails.cursor === 'string' && typeof cursorsDetails.fullPath === 'string' - ) - if (isValidJSON === false) { - sendToSentry(new Error('after arg has an invalid JSON structure'), { - userId, - tags: {after} - }) - return connectionFromTasks([], 0) - } - - const projectsIssuesPromises = Array.from(projectsFullPaths).map((fullPath) => { - const after = parsedAfter?.find((cursor) => cursor.fullPath === fullPath)?.cursor ?? '' - return manager.getProjectIssues({ - ...args, - fullPath, - after - }) - }) - const projectsIssues = [] as ProjectIssueEdge[] - const errors = [] as Error[] - let hasNextPage = false - const endCursor = [] as CursorDetails[] - const projectsIssuesResponses = await Promise.all(projectsIssuesPromises) - for (const res of projectsIssuesResponses) { - const [projectIssuesData, err] = res - if (err) { - errors.push(err) - sendToSentry(err, {userId}) - return - } - const {project} = projectIssuesData - if (!project?.issues) continue - const {fullPath, issues} = project - const {edges, pageInfo} = issues - if (pageInfo.hasNextPage) { - hasNextPage = true - const currentCursorDetails = endCursor.find( - (cursorDetails) => cursorDetails.fullPath === fullPath - ) - const newCursor = pageInfo.endCursor ?? '' - if (currentCursorDetails) currentCursorDetails.cursor = newCursor - else endCursor.push({fullPath, cursor: newCursor}) - } - edges?.forEach((edge) => { - if (!edge?.node) return - const {node, cursor} = edge - projectsIssues.push({cursor, node}) - }) - } - - const firstEdge = projectsIssues[0] - const stringifiedEndCursor = JSON.stringify(endCursor) - return { - error: errors[0], - edges: projectsIssues, - pageInfo: { - startCursor: firstEdge && firstEdge.cursor, - endCursor: stringifiedEndCursor, - hasNextPage - } - } - } - } - // The GitLab schema get injected here as 'api' - }) -}) - -const {connectionType, edgeType} = connectionDefinitions({ - name: GitLabIntegration.name, - nodeType: TaskIntegration, - edgeFields: () => ({ - cursor: { - type: GraphQLString - } - }), - connectionFields: () => ({ - error: { - type: StandardMutationError, - description: 'An error with the connection, if any' - } - }) -}) - -export const GitLabProjectIssuesConnection = connectionType -export const GitLabProjectIssuesEdge = edgeType -export default GitLabIntegration diff --git a/packages/server/graphql/types/GitLabSearchQuery.ts b/packages/server/graphql/types/GitLabSearchQuery.ts deleted file mode 100644 index 6004ee349c5..00000000000 --- a/packages/server/graphql/types/GitLabSearchQuery.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {GraphQLID, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' - -const GitLabSearchQuery = new GraphQLObjectType({ - name: 'GitLabSearchQuery', - description: 'A GitLab search query including the search query and the project filters', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'shortid' - }, - queryString: { - type: new GraphQLNonNull(GraphQLString), - description: 'The query string used to search GitLab issue titles and descriptions' - }, - selectedProjectsIds: { - type: new GraphQLList(new GraphQLNonNull(GraphQLID)), - description: - 'The list of ids of projects that have been selected as a filter. Null if none have been selected' - }, - lastUsedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'the time the search query was last used. Used for sorting' - } - }) -}) - -export default GitLabSearchQuery diff --git a/packages/server/graphql/types/JiraSearchQuery.ts b/packages/server/graphql/types/JiraSearchQuery.ts deleted file mode 100644 index 1c8c86d0803..00000000000 --- a/packages/server/graphql/types/JiraSearchQuery.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - GraphQLBoolean, - GraphQLID, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import {globalIdField} from 'graphql-relay' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' - -// Used for both Jira and Jira Server -const JiraSearchQuery = new GraphQLObjectType({ - name: 'JiraSearchQuery', - description: 'A jira search query including all filters selected when the query was executed', - fields: () => ({ - id: globalIdField(), - queryString: { - type: new GraphQLNonNull(GraphQLString), - description: 'The query string, either simple or JQL depending on the isJQL flag' - }, - isJQL: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the queryString is JQL, else false', - resolve: ({isJQL}) => !!isJQL - }, - projectKeyFilters: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLID))), - description: 'The list of project keys selected as a filter', - resolve: ({projectKeyFilters}) => projectKeyFilters || [] - }, - lastUsedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'the time the search query was last used. Used for sorting' - } - }) -}) - -export default JiraSearchQuery diff --git a/packages/server/graphql/types/JiraServerIntegration.ts b/packages/server/graphql/types/JiraServerIntegration.ts deleted file mode 100644 index 2f10905f418..00000000000 --- a/packages/server/graphql/types/JiraServerIntegration.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { - GraphQLBoolean, - GraphQLID, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import IntegrationProviderId from '~/shared/gqlIds/IntegrationProviderId' -import IntegrationRepoId from '~/shared/gqlIds/IntegrationRepoId' -import TeamMember from '../../database/types/TeamMember' -import JiraServerRestManager from '../../integrations/jiraServer/JiraServerRestManager' -import {IntegrationProviderJiraServer} from '../../postgres/queries/getIntegrationProvidersByIds' -import getLatestIntegrationSearchQueries from '../../postgres/queries/getLatestIntegrationSearchQueries' -import {getUserId} from '../../utils/authorization' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import connectionFromTasks from '../queries/helpers/connectionFromTasks' -import IntegrationProviderOAuth1 from './IntegrationProviderOAuth1' -import JiraSearchQuery from './JiraSearchQuery' -import {JiraServerIssueConnection} from './JiraServerIssue' -import JiraServerRemoteProject from './JiraServerRemoteProject' -import TeamMemberIntegrationAuthOAuth1 from './TeamMemberIntegrationAuthOAuth1' - -type IssueArgs = { - first: number - after: string - queryString: string | null - isJQL: boolean - projectKeyFilters: string[] | null -} - -const JiraServerIntegration = new GraphQLObjectType<{teamId: string; userId: string}, GQLContext>({ - name: 'JiraServerIntegration', - description: 'Jira Server integration data for a given team member', - fields: () => ({ - id: { - type: GraphQLID, - description: 'Composite key in jiraServer:providerId format', - resolve: async ({teamId, userId}: {teamId: string; userId: string}, _args, {dataLoader}) => { - const auth = await dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'jiraServer', teamId, userId}) - - if (!auth) { - return null - } - - return `jiraServer:${teamId}:${auth.providerId}` - } - }, - auth: { - description: 'The OAuth1 Authorization for this team member', - type: TeamMemberIntegrationAuthOAuth1, - resolve: async ({teamId, userId}, _args, {dataLoader}) => { - const auth = await dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'jiraServer', teamId, userId}) - return auth - } - }, - sharedProviders: { - description: 'The non-global providers shared with the team or organization', - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(IntegrationProviderOAuth1))), - resolve: async ({teamId, userId}, _args, {dataLoader}) => { - const teamMembers = await dataLoader.get('teamMembersByUserId').load(userId) - const teamMember = teamMembers.find( - (teamMember: TeamMember) => teamMember.teamId === teamId - ) - if (!teamMember) return [] - - const team = await dataLoader.get('teams').loadNonNull(teamMember.teamId) - const {orgId} = team - const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) - const orgTeamIds = orgTeams.map(({id}) => id) - - const providers = await dataLoader.get('sharedIntegrationProviders').load({ - service: 'jiraServer', - orgTeamIds: [...orgTeamIds, 'aGhostTeam'], - teamIds: [teamId] - }) - return providers - } - }, - issues: { - type: new GraphQLNonNull(JiraServerIssueConnection), - description: - 'A list of issues coming straight from the jira integration for a specific team member', - args: { - first: { - type: GraphQLInt, - defaultValue: 25 - }, - after: { - type: GraphQLString, - defaultValue: '-1' - }, - queryString: { - type: GraphQLString, - description: 'A string of text to search for, or JQL if isJQL is true' - }, - isJQL: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the queryString is JQL, else false' - }, - projectKeyFilters: { - type: new GraphQLList(new GraphQLNonNull(GraphQLID)), - descrption: - 'A list of projects to restrict the search to. format is cloudId:projectKey. If null, will search all' - } - }, - resolve: async ({teamId, userId}, args: any, {authToken, dataLoader}) => { - const {first, after, queryString, isJQL, projectKeyFilters} = args as IssueArgs - const viewerId = getUserId(authToken) - if (viewerId !== userId) { - const err = new Error('Cannot access another team members issues') - standardError(err, {tags: {teamId, userId}, userId: viewerId}) - return connectionFromTasks([], 0, err) - } - - const auth = await dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'jiraServer', teamId, userId}) - - if (!auth) { - return null - } - - const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId) - - const integrationManager = new JiraServerRestManager( - auth, - provider as IntegrationProviderJiraServer - ) - - if (!integrationManager) { - return null - } - - const projectKeys = (projectKeyFilters ?? []).map( - (projectKeyFilter) => IntegrationRepoId.split(projectKeyFilter).projectKey! - ) - - // Request one extra item to see if there are more results - const maxResults = first + 1 - // Relay requires the cursor to be a string - const afterInt = parseInt(after, 10) - const startAt = afterInt + 1 - const issueRes = await integrationManager.getIssues( - queryString, - isJQL, - projectKeys, - maxResults, - startAt - ) - - if (issueRes instanceof Error) { - return connectionFromTasks([], first, { - message: issueRes.message - }) - } - - const {issues} = issueRes - - const mappedIssues = issues.map((issue) => { - const {project, issuetype, summary, description, updated} = issue.fields - return { - ...issue, - userId, - teamId, - providerId: provider.id, - issueKey: issue.key, - description: description ?? '', - descriptionHTML: issue.renderedFields.description, - projectId: project.id, - projectKey: project.key, - projectName: project.name, - issueType: issuetype.id, - summary, - service: 'jiraServer' as const, - updatedAt: new Date(updated) - } - }) - - const nodes = mappedIssues.slice(0, first) - const edges = mappedIssues.map((node, index) => ({ - cursor: `${index + afterInt}`, - node - })) - - const firstEdge = edges[0] - - return { - edges, - pageInfo: { - startCursor: firstEdge && firstEdge.cursor, - endCursor: firstEdge ? edges[edges.length - 1]!.cursor : null, - hasNextPage: mappedIssues.length > nodes.length - } - } - } - }, - projects: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(JiraServerRemoteProject))), - description: - 'A list of projects accessible by this team member. empty if viewer is not the user', - resolve: async ({teamId, userId}, _args: unknown, {dataLoader}) => { - return dataLoader.get('allJiraServerProjects').load({teamId, userId}) - } - }, - providerId: { - type: GraphQLID, - resolve: async ({teamId, userId}, _args: unknown, {dataLoader}) => { - const auth = await dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'jiraServer', teamId, userId}) - - if (!auth) { - return null - } - - return IntegrationProviderId.join(auth.providerId) - } - }, - searchQueries: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(JiraSearchQuery))), - description: - 'the list of suggested search queries, sorted by most recent. Guaranteed to be < 60 days old', - resolve: async ({teamId, userId}, _args: unknown, {dataLoader}) => { - const auth = await dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'jiraServer', teamId, userId}) - - if (!auth) { - return [] - } - - const searchQueries = await getLatestIntegrationSearchQueries({ - teamId, - userId, - service: 'jiraServer', - providerId: auth.providerId - }) - - return searchQueries.map((searchQuery) => { - const query = searchQuery.query as { - queryString: string | null - isJQL: boolean - projectKeyFilters: string[] | null - } - - return { - id: searchQuery.id, - queryString: query.queryString, - isJQL: query.isJQL, - projectKeyFilters: query.projectKeyFilters, - lastUpdatedAt: searchQuery.lastUsedAt - } - }) - } - } - }) -}) -export default JiraServerIntegration diff --git a/packages/server/graphql/types/JiraServerIssue.ts b/packages/server/graphql/types/JiraServerIssue.ts deleted file mode 100644 index 3047f751c28..00000000000 --- a/packages/server/graphql/types/JiraServerIssue.ts +++ /dev/null @@ -1,117 +0,0 @@ -import {GraphQLID, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import JiraServerIssueId from '~/shared/gqlIds/JiraServerIssueId' -import {JiraServerIssue as JiraServerRestIssue} from '../../dataloader/jiraServerLoaders' -import connectionDefinitions from '../connectionDefinitions' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import StandardMutationError from './StandardMutationError' -import TaskIntegration from './TaskIntegration' - -type JiraServerIssueSource = JiraServerRestIssue & { - userId: string - teamId: string - providerId: number -} - -const VOTE_FIELD_ID_BLACKLIST = ['description', 'summary'] -const VOTE_FIELD_ALLOWED_TYPES = ['string', 'number'] - -const JiraServerIssue = new GraphQLObjectType({ - name: 'JiraServerIssue', - description: 'The Jira Issue that comes direct from Jira Server', - interfaces: () => [TaskIntegration], - isTypeOf: ({service}) => service === 'jiraServer', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'GUID providerId:repositoryId:issueId', - resolve: ({id, projectId, providerId}) => { - return JiraServerIssueId.join(providerId, projectId, id) - } - }, - issueKey: { - type: new GraphQLNonNull(GraphQLID) - }, - issueType: { - type: new GraphQLNonNull(GraphQLID) - }, - projectId: { - type: new GraphQLNonNull(GraphQLID) - }, - projectKey: { - type: new GraphQLNonNull(GraphQLID) - }, - projectName: { - type: new GraphQLNonNull(GraphQLString) - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The parabol teamId this issue was fetched for' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The parabol userId this issue was fetched for' - }, - url: { - type: new GraphQLNonNull(GraphQLString), - description: 'The url to access the issue', - resolve: ({issueKey, self}) => { - const {origin} = new URL(self) - return `${origin}/browse/${issueKey}` - } - }, - summary: { - type: new GraphQLNonNull(GraphQLString), - description: 'The plaintext summary of the jira issue' - }, - description: { - type: new GraphQLNonNull(GraphQLString) - }, - descriptionHTML: { - type: new GraphQLNonNull(GraphQLString), - description: 'The description converted into raw HTML' - }, - possibleEstimationFieldNames: { - type: new GraphQLNonNull(GraphQLList(GraphQLNonNull(GraphQLString))), - resolve: async ({teamId, userId, providerId, issueType, projectId}, _args, {dataLoader}) => { - const issueMeta = await dataLoader - .get('jiraServerFieldTypes') - .load({teamId, userId, projectId, issueType, providerId}) - if (!issueMeta) return [] - const fieldNames = issueMeta - .filter( - ({fieldId, operations, schema}) => - !VOTE_FIELD_ID_BLACKLIST.includes(fieldId) && - operations.includes('set') && - VOTE_FIELD_ALLOWED_TYPES.includes(schema.type) - ) - .map(({name}) => name) - return fieldNames - } - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the issue was last updated' - } - }) -}) - -const {connectionType, edgeType} = connectionDefinitions({ - name: JiraServerIssue.name, - nodeType: JiraServerIssue, - edgeFields: () => ({ - cursor: { - type: GraphQLString - } - }), - connectionFields: () => ({ - error: { - type: StandardMutationError, - description: 'An error with the connection, if any' - } - }) -}) - -export const JiraServerIssueConnection = connectionType -export const JiraServerIssueEdge = edgeType -export default JiraServerIssue diff --git a/packages/server/graphql/types/JiraServerRemoteProject.ts b/packages/server/graphql/types/JiraServerRemoteProject.ts deleted file mode 100644 index e69873dc2dd..00000000000 --- a/packages/server/graphql/types/JiraServerRemoteProject.ts +++ /dev/null @@ -1,61 +0,0 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType, GraphQLString} from 'graphql' -import IntegrationRepoId from 'parabol-client/shared/gqlIds/IntegrationRepoId' -import JiraServerRestManager from '../../integrations/jiraServer/JiraServerRestManager' -import {IntegrationProviderJiraServer} from '../../postgres/queries/getIntegrationProvidersByIds' -import defaultJiraProjectAvatar from '../../utils/defaultJiraProjectAvatar' -import {GQLContext} from '../graphql' -import IntegrationProviderServiceEnum from './IntegrationProviderServiceEnum' -import JiraRemoteAvatarUrls from './JiraRemoteAvatarUrls' -import JiraRemoteProjectCategory from './JiraRemoteProjectCategory' -import RepoIntegration, {repoIntegrationFields} from './RepoIntegration' - -const JiraServerRemoteProject = new GraphQLObjectType({ - name: 'JiraServerRemoteProject', - description: 'A project fetched from Jira in real time', - interfaces: () => [RepoIntegration], - isTypeOf: ({service}) => service === 'jiraServer', - fields: () => ({ - ...repoIntegrationFields(), - id: { - type: new GraphQLNonNull(GraphQLID), - resolve: (item) => IntegrationRepoId.join(item) - }, - service: { - type: new GraphQLNonNull(IntegrationProviderServiceEnum), - resolve: () => 'jiraServer' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The parabol teamId this issue was fetched for' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The parabol userId this issue was fetched for' - }, - name: { - type: new GraphQLNonNull(GraphQLString) - }, - avatar: { - type: new GraphQLNonNull(GraphQLString), - resolve: async ({avatarUrls, teamId, userId}, _args: unknown, {dataLoader}) => { - const url = avatarUrls['48x48'] - const auth = await dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'jiraServer', teamId, userId}) - if (!auth) return defaultJiraProjectAvatar - const provider = await dataLoader.get('integrationProviders').loadNonNull(auth.providerId) - const manager = new JiraServerRestManager(auth, provider as IntegrationProviderJiraServer) - const avatar = await manager.getProjectAvatar(url) - return avatar || defaultJiraProjectAvatar - } - }, - avatarUrls: { - type: new GraphQLNonNull(JiraRemoteAvatarUrls) - }, - projectCategory: { - type: new GraphQLNonNull(JiraRemoteProjectCategory) - } - }) -}) - -export default JiraServerRemoteProject diff --git a/packages/server/graphql/types/MSTeamsIntegration.ts b/packages/server/graphql/types/MSTeamsIntegration.ts deleted file mode 100644 index 3871bafa650..00000000000 --- a/packages/server/graphql/types/MSTeamsIntegration.ts +++ /dev/null @@ -1,35 +0,0 @@ -import {GraphQLList, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {GQLContext} from '../graphql' -import IntegrationProviderWebhook from './IntegrationProviderWebhook' -import TeamMemberIntegrationAuthWebhook from './TeamMemberIntegrationAuthWebhook' - -const MSTeamsIntegration = new GraphQLObjectType({ - name: 'MSTeamsIntegration', - description: 'Integration Auth and shared providers available to the team member', - fields: () => ({ - auth: { - description: 'The Webhook Authorization for this team member', - type: TeamMemberIntegrationAuthWebhook, - resolve: async ({teamId, userId}, _args, {dataLoader}) => { - return dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'msTeams', teamId, userId}) - } - }, - sharedProviders: { - description: 'The non-global providers shared with the team or organization', - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(IntegrationProviderWebhook))), - resolve: async ({teamId}: {teamId: string}, _args: unknown, {dataLoader}: GQLContext) => { - const team = await dataLoader.get('teams').loadNonNull(teamId) - const {orgId} = team - const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) - const orgTeamIds = orgTeams.map(({id}) => id) - return dataLoader - .get('sharedIntegrationProviders') - .load({service: 'msTeams', orgTeamIds, teamIds: [teamId]}) - } - } - }) -}) - -export default MSTeamsIntegration diff --git a/packages/server/graphql/types/MattermostIntegration.ts b/packages/server/graphql/types/MattermostIntegration.ts deleted file mode 100644 index 3d6a168c34a..00000000000 --- a/packages/server/graphql/types/MattermostIntegration.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {GraphQLList, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import {getUserId} from '../../utils/authorization' -import standardError from '../../utils/standardError' -import {GQLContext} from '../graphql' -import IntegrationProviderWebhook from './IntegrationProviderWebhook' -import TeamMemberIntegrationAuthWebhook from './TeamMemberIntegrationAuthWebhook' - -const MattermostIntegration = new GraphQLObjectType({ - name: 'MattermostIntegration', - description: 'Integration Auth and shared providers available to the team member', - fields: () => ({ - auth: { - description: 'The OAuth2 Authorization for this team member', - type: TeamMemberIntegrationAuthWebhook, - resolve: async ({teamId, userId}, _args, {dataLoader}) => { - return dataLoader - .get('teamMemberIntegrationAuths') - .load({service: 'mattermost', teamId, userId}) - } - }, - sharedProviders: { - description: 'The non-global providers shared with the team or organization', - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(IntegrationProviderWebhook))), - resolve: async ( - {teamId}: {teamId: string}, - _args: unknown, - {authToken, dataLoader}: GQLContext - ) => { - const viewerId = getUserId(authToken) - const team = await dataLoader.get('teams').load(teamId) - if (!team) return standardError(new Error('Team not found'), {userId: viewerId}) - const {orgId} = team - const orgTeams = await dataLoader.get('teamsByOrgIds').load(orgId) - const orgTeamIds = orgTeams.map(({id}) => id) - return dataLoader - .get('sharedIntegrationProviders') - .load({service: 'mattermost', orgTeamIds, teamIds: [teamId]}) - } - } - }) -}) - -export default MattermostIntegration diff --git a/packages/server/graphql/types/Organization.ts b/packages/server/graphql/types/Organization.ts index 452c765de79..988cd5c2cc2 100644 --- a/packages/server/graphql/types/Organization.ts +++ b/packages/server/graphql/types/Organization.ts @@ -1,243 +1,9 @@ -import { - GraphQLBoolean, - GraphQLID, - GraphQLInt, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import { - getUserId, - isSuperUser, - isTeamMember, - isUserBillingLeader, - isUserOrgAdmin -} from '../../utils/authorization' +import {GraphQLObjectType} from 'graphql' import {GQLContext} from '../graphql' -import getActiveTeamCountByOrgIds from '../public/types/helpers/getActiveTeamCountByOrgIds' -import {resolveForBillingLeaders} from '../resolvers' -import CreditCard from './CreditCard' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import OrgUserCount from './OrgUserCount' -import OrganizationUser, {OrganizationUserConnection} from './OrganizationUser' -import Team from './Team' const Organization: GraphQLObjectType = new GraphQLObjectType({ name: 'Organization', - description: 'An organization', - fields: () => ({ - id: {type: new GraphQLNonNull(GraphQLID), description: 'The unique organization ID'}, - activeDomain: { - type: GraphQLString, - description: - 'The top level domain this organization is linked to, null if only generic emails used' - }, - isActiveDomainTouched: { - type: new GraphQLNonNull(GraphQLBoolean), - description: - 'false if the activeDomain is null or was set automatically via a heuristic, true if set manually', - resolve: ({isActiveDomainTouched}) => !!isActiveDomainTouched - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The datetime the organization was created' - }, - creditCard: { - type: CreditCard, - description: 'The safe credit card details', - resolve: resolveForBillingLeaders('creditCard') - }, - isBillingLeader: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the viewer holds the billing leader role on the org', - resolve: async ({id: orgId}, _args: unknown, {authToken, dataLoader}) => { - const viewerId = getUserId(authToken) - return isUserBillingLeader(viewerId, orgId, dataLoader) - } - }, - isOrgAdmin: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if the viewer holds the the org admin role on the org', - resolve: async ({id: orgId}, _args: unknown, {authToken, dataLoader}) => { - const viewerId = getUserId(authToken) - return isUserOrgAdmin(viewerId, orgId, dataLoader) - } - }, - name: { - type: new GraphQLNonNull(GraphQLString), - description: 'The name of the organization' - }, - activeTeamCount: { - type: new GraphQLNonNull(GraphQLInt), - description: 'Number of teams with 3+ meetings (>1 attendee) that met within last 30 days', - resolve: async ({id: orgId}) => { - return getActiveTeamCountByOrgIds(orgId) - } - }, - allTeams: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Team))), - description: - 'All the teams in the organization. If the viewer is not a billing lead, org admin, super user, or they do not have the publicTeams flag, return the teams they are a member of.', - resolve: async ({id: orgId}, _args: unknown, {dataLoader, authToken}) => { - const viewerId = getUserId(authToken) - const [allTeamsOnOrg, organization, isOrgAdmin, isBillingLeader] = await Promise.all([ - dataLoader.get('teamsByOrgIds').load(orgId), - dataLoader.get('organizations').loadNonNull(orgId), - isUserOrgAdmin(viewerId, orgId, dataLoader), - isUserBillingLeader(viewerId, orgId, dataLoader) - ]) - const sortedTeamsOnOrg = allTeamsOnOrg.sort((a, b) => a.name.localeCompare(b.name)) - const hasPublicTeamsFlag = !!organization.featureFlags?.includes('publicTeams') - if (isBillingLeader || isOrgAdmin || isSuperUser(authToken) || hasPublicTeamsFlag) { - const viewerTeams = sortedTeamsOnOrg.filter((team) => authToken.tms.includes(team.id)) - const otherTeams = sortedTeamsOnOrg.filter((team) => !authToken.tms.includes(team.id)) - return [...viewerTeams, ...otherTeams] - } else { - return sortedTeamsOnOrg.filter((team) => authToken.tms.includes(team.id)) - } - } - }, - viewerTeams: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Team))), - description: 'all the teams the viewer is on in the organization', - resolve: async ({id: orgId}, _args: unknown, {dataLoader, authToken}) => { - const allTeamsOnOrg = await dataLoader.get('teamsByOrgIds').load(orgId) - return allTeamsOnOrg - .filter((team) => authToken.tms.includes(team.id)) - .sort((a, b) => a.name.localeCompare(b.name)) - } - }, - publicTeams: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Team))), - description: - 'all the teams that the viewer does not belong to that are in the organization. Only visible if the org has the publicTeams flag set to true.', - resolve: async ({id: orgId}, _args: unknown, {dataLoader, authToken}) => { - const [allTeamsOnOrg, organization] = await Promise.all([ - dataLoader.get('teamsByOrgIds').load(orgId), - dataLoader.get('organizations').loadNonNull(orgId) - ]) - const hasPublicTeamsFlag = !!organization.featureFlags?.includes('publicTeams') - if (!isSuperUser(authToken) || !hasPublicTeamsFlag) return [] - const publicTeams = allTeamsOnOrg.filter((team) => !isTeamMember(authToken, team.id)) - return publicTeams - } - }, - periodEnd: { - type: GraphQLISO8601Type, - description: 'THe datetime the current billing cycle ends', - resolve: resolveForBillingLeaders('periodEnd') - }, - periodStart: { - type: GraphQLISO8601Type, - description: 'The datetime the current billing cycle starts', - resolve: resolveForBillingLeaders('periodStart') - }, - tierLimitExceededAt: { - type: GraphQLISO8601Type, - description: 'Flag the organization as exceeding the tariff limits by setting a datetime' - }, - scheduledLockAt: { - type: GraphQLISO8601Type, - description: 'Schedule the organization to be locked at' - }, - lockedAt: { - type: GraphQLISO8601Type, - description: 'Organization locked at' - }, - retroMeetingsOffered: { - deprecationReason: 'Unlimited retros for all!', - type: new GraphQLNonNull(GraphQLInt), - description: 'The total number of retroMeetings given to the team' - }, - retroMeetingsRemaining: { - deprecationReason: 'Unlimited retros for all!', - type: new GraphQLNonNull(GraphQLInt), - description: 'Number of retro meetings that can be run (if not pro)' - }, - showConversionModal: { - type: new GraphQLNonNull(GraphQLBoolean), - description: 'true if should show the org the conversion modal, else false', - resolve: ({showConversionModal}) => !!showConversionModal - }, - stripeId: { - type: GraphQLID, - description: 'The customerId from stripe', - resolve: resolveForBillingLeaders('stripeId') - }, - stripeSubscriptionId: { - type: GraphQLID, - description: 'The subscriptionId from stripe', - resolve: resolveForBillingLeaders('stripeSubscriptionId') - }, - upcomingInvoiceEmailSentAt: { - type: GraphQLISO8601Type, - description: 'The last upcoming invoice email that was sent, null if never sent' - }, - updatedAt: { - type: GraphQLISO8601Type, - description: 'The datetime the organization was last updated' - }, - viewerOrganizationUser: { - type: OrganizationUser, - description: 'The OrganizationUser of the viewer', - resolve: async ({id: orgId}, _args: unknown, {dataLoader, authToken}: GQLContext) => { - const viewerId = getUserId(authToken) - return dataLoader.get('organizationUsersByUserIdOrgId').load({userId: viewerId, orgId}) - } - }, - organizationUsers: { - args: { - after: { - type: GraphQLString - }, - first: { - type: GraphQLInt - } - }, - type: new GraphQLNonNull(OrganizationUserConnection), - resolve: async ({id: orgId}: {id: string}, _args: unknown, {dataLoader}: GQLContext) => { - const organizationUsers = await dataLoader.get('organizationUsersByOrgId').load(orgId) - organizationUsers.sort((a, b) => (a.orgId > b.orgId ? 1 : -1)) - const edges = organizationUsers.map((node) => ({ - cursor: node.id, - node - })) - // TODO implement pagination - const firstEdge = edges[0] - return { - edges, - pageInfo: { - endCursor: firstEdge ? edges[edges.length - 1]!.cursor : null, - hasNextPage: false - } - } - } - }, - orgUserCount: { - type: new GraphQLNonNull(OrgUserCount), - description: 'The count of active & inactive users', - resolve: async ({id: orgId}: {id: string}, _args: unknown, {dataLoader}: GQLContext) => { - const organizationUsers = await dataLoader.get('organizationUsersByOrgId').load(orgId) - const inactiveUserCount = organizationUsers.filter(({inactive}) => inactive).length - return { - inactiveUserCount, - activeUserCount: organizationUsers.length - inactiveUserCount - } - } - }, - billingLeaders: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(OrganizationUser))), - description: 'The leaders of the org', - resolve: async ({id: orgId}: {id: string}, _args: unknown, {dataLoader}: GQLContext) => { - const organizationUsers = await dataLoader.get('organizationUsersByOrgId').load(orgId) - return organizationUsers.filter( - (organizationUser) => - organizationUser.role === 'BILLING_LEADER' || organizationUser.role === 'ORG_ADMIN' - ) - } - } - }) + fields: {} }) export default Organization diff --git a/packages/server/graphql/types/SlackIntegration.ts b/packages/server/graphql/types/SlackIntegration.ts index f7da0f259a8..ae9c958569d 100644 --- a/packages/server/graphql/types/SlackIntegration.ts +++ b/packages/server/graphql/types/SlackIntegration.ts @@ -1,86 +1,8 @@ -import { - GraphQLBoolean, - GraphQLID, - GraphQLList, - GraphQLNonNull, - GraphQLObjectType, - GraphQLString -} from 'graphql' -import {getUserId} from '../../utils/authorization' -import {GQLContext} from '../graphql' -import GraphQLISO8601Type from './GraphQLISO8601Type' -import SlackNotification from './SlackNotification' +import {GraphQLObjectType} from 'graphql' -const SlackIntegration = new GraphQLObjectType({ +const SlackIntegration = new GraphQLObjectType({ name: 'SlackIntegration', - description: 'OAuth token for a team member', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'shortid' - }, - isActive: { - description: 'true if the auth is updated & ready to use for all features, else false', - type: new GraphQLNonNull(GraphQLBoolean), - resolve: ({isActive, botAccessToken}) => !!(isActive && botAccessToken) - }, - botUserId: { - type: GraphQLID, - description: 'the parabol bot user id' - }, - botAccessToken: { - type: GraphQLID, - description: 'the parabol bot access token, used as primary communication', - resolve: async ({botAccessToken, userId}, _args: unknown, {authToken}) => { - const viewerId = getUserId(authToken) - return viewerId === userId ? botAccessToken : null - } - }, - createdAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the provider was created' - }, - defaultTeamChannelId: { - type: new GraphQLNonNull(GraphQLString), - description: 'The default channel to assign to new team notifications' - }, - slackTeamId: { - type: GraphQLID, - description: 'The id of the team in slack' - }, - slackTeamName: { - type: GraphQLString, - description: 'The name of the team in slack' - }, - slackUserId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The userId in slack' - }, - slackUserName: { - type: new GraphQLNonNull(GraphQLString), - description: 'The name of the user in slack' - }, - teamId: { - type: new GraphQLNonNull(GraphQLID), - description: '*The team that the token is linked to' - }, - updatedAt: { - type: new GraphQLNonNull(GraphQLISO8601Type), - description: 'The timestamp the token was updated at' - }, - userId: { - type: new GraphQLNonNull(GraphQLID), - description: 'The id of the user that integrated Slack' - }, - notifications: { - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(SlackNotification))), - description: 'A list of events and the slack channels they get posted to', - resolve: async ({userId, teamId}, _args: unknown, {dataLoader}) => { - const slackNotifications = await dataLoader.get('slackNotificationsByTeamId').load(teamId) - return slackNotifications.filter((notification) => notification.userId === userId) - } - } - }) + fields: {} }) export default SlackIntegration diff --git a/packages/server/graphql/types/TeamMemberIntegrations.ts b/packages/server/graphql/types/TeamMemberIntegrations.ts index a6ed6a58348..a8be71d8b19 100644 --- a/packages/server/graphql/types/TeamMemberIntegrations.ts +++ b/packages/server/graphql/types/TeamMemberIntegrations.ts @@ -1,76 +1,9 @@ -import {GraphQLID, GraphQLNonNull, GraphQLObjectType} from 'graphql' -import TeamMemberIntegrationsId from '../../../client/shared/gqlIds/TeamMemberIntegrationsId' -import {isTeamMember} from '../../utils/authorization' +import {GraphQLObjectType} from 'graphql' import {GQLContext} from '../graphql' -import AtlassianIntegration from './AtlassianIntegration' -import AzureDevOpsIntegration from './AzureDevOpsIntegration' -import GitHubIntegration from './GitHubIntegration' -import GitLabIntegration from './GitLabIntegration' -import JiraServerIntegration from './JiraServerIntegration' -import MSTeamsIntegration from './MSTeamsIntegration' -import MattermostIntegration from './MattermostIntegration' -import SlackIntegration from './SlackIntegration' const TeamMemberIntegrations = new GraphQLObjectType<{teamId: string; userId: string}, GQLContext>({ name: 'TeamMemberIntegrations', - description: 'All the available integrations available for this team member', - fields: () => ({ - id: { - type: new GraphQLNonNull(GraphQLID), - description: 'composite', - resolve: ({teamId, userId}) => TeamMemberIntegrationsId.join(teamId, userId) - }, - atlassian: { - type: AtlassianIntegration, - description: 'All things associated with an Atlassian integration for a team member', - resolve: async ({teamId, userId}, _args: unknown, {authToken, dataLoader}) => { - if (!isTeamMember(authToken, teamId)) return null - return dataLoader.get('freshAtlassianAuth').load({teamId, userId}) - } - }, - jiraServer: { - type: new GraphQLNonNull(JiraServerIntegration), - description: 'All things associated with a Jira Server integration for a team member', - resolve: (source) => source - }, - github: { - type: GitHubIntegration, - description: 'All things associated with a GitHub integration for a team member', - resolve: async ({teamId, userId}, _args: unknown, {authToken, dataLoader}) => { - if (!isTeamMember(authToken, teamId)) return null - return dataLoader.get('githubAuth').load({teamId, userId}) - } - }, - gitlab: { - type: new GraphQLNonNull(GitLabIntegration), - description: 'All things associated with a GitLab integration for a team member', - resolve: (source) => source - }, - mattermost: { - type: new GraphQLNonNull(MattermostIntegration), - description: 'All things associated with a Mattermost integration for a team member', - resolve: (source) => source - }, - slack: { - type: SlackIntegration, - description: 'All things associated with a slack integration for a team member', - resolve: async ({teamId, userId}, _args: unknown, {authToken, dataLoader}) => { - if (!isTeamMember(authToken, teamId)) return null - const auths = await dataLoader.get('slackAuthByUserId').load(userId) - return auths.find((auth) => auth.teamId === teamId) - } - }, - azureDevOps: { - type: new GraphQLNonNull(AzureDevOpsIntegration), - description: 'All things associated with a A integration for a team member', - resolve: (source) => source - }, - msTeams: { - type: new GraphQLNonNull(MSTeamsIntegration), - description: 'All things associated with a Microsoft Teams integration for a team member', - resolve: (source) => source - } - }) + fields: {} }) export default TeamMemberIntegrations