diff --git a/src/renderer/__mocks__/user-mocks.ts b/src/renderer/__mocks__/user-mocks.ts index fe3c6af51..d1435222c 100644 --- a/src/renderer/__mocks__/user-mocks.ts +++ b/src/renderer/__mocks__/user-mocks.ts @@ -1,4 +1,5 @@ import type { GitifyNotificationUser, GitifyUser, Link } from '../types'; +import type { AuthorFieldsFragment } from '../utils/api/graphql/generated/graphql'; import type { RawUser } from '../utils/api/types'; export const mockGitifyUser: GitifyUser = { @@ -19,7 +20,7 @@ export function createPartialMockUser(login: string): RawUser { return mockUser as RawUser; } -export function createMockNotificationUser( +export function createMockGitifyNotificationUser( login: string, ): GitifyNotificationUser { return { @@ -29,3 +30,13 @@ export function createMockNotificationUser( type: 'User', }; } + +/** + * Creates a mock author for use in GraphQL response mocks. + */ +export function createMockGraphQLAuthor(login: string): AuthorFieldsFragment { + return { + ...createMockGitifyNotificationUser(login), + __typename: 'User', + } as AuthorFieldsFragment; +} diff --git a/src/renderer/hooks/useNotifications.test.ts b/src/renderer/hooks/useNotifications.test.ts index e99f84bdc..bb3932843 100644 --- a/src/renderer/hooks/useNotifications.test.ts +++ b/src/renderer/hooks/useNotifications.test.ts @@ -3,12 +3,8 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios, { AxiosError } from 'axios'; import nock from 'nock'; -import { mockGitHubCloudAccount } from '../__mocks__/account-mocks'; import { mockAuth, mockSettings, mockState } from '../__mocks__/state-mocks'; -import { - mockNotificationUser, - mockSingleNotification, -} from '../utils/api/__mocks__/response-mocks'; +import { mockSingleNotification } from '../utils/api/__mocks__/response-mocks'; import { Errors } from '../utils/errors'; import * as logger from '../utils/logger'; import { useNotifications } from './useNotifications'; @@ -118,204 +114,6 @@ describe('renderer/hooks/useNotifications.ts', () => { expect(result.current.notifications[1].notifications.length).toBe(2); }); - it('should fetch detailed notifications with success', async () => { - const mockRepository = { - name: 'notifications-test', - full_name: 'gitify-app/notifications-test', - html_url: 'https://github.com/gitify-app/notifications-test', - owner: { - login: 'gitify-app', - avatar_url: 'https://avatar.url', - type: 'Organization', - }, - }; - - const mockNotifications = [ - { - id: '1', - unread: true, - updated_at: '2024-01-01T00:00:00Z', - reason: 'subscribed', - subject: { - title: 'This is a check suite workflow.', - type: 'CheckSuite', - url: null, - latest_comment_url: null, - }, - repository: mockRepository, - }, - { - id: '2', - unread: true, - updated_at: '2024-02-26T00:00:00Z', - reason: 'subscribed', - subject: { - title: 'This is a Discussion.', - type: 'Discussion', - url: null, - latest_comment_url: null, - }, - repository: mockRepository, - }, - { - id: '3', - unread: true, - updated_at: '2024-01-01T00:00:00Z', - reason: 'subscribed', - subject: { - title: 'This is an Issue.', - type: 'Issue', - url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/3', - latest_comment_url: - 'https://api.github.com/repos/gitify-app/notifications-test/issues/3/comments', - }, - repository: mockRepository, - }, - { - id: '4', - unread: true, - updated_at: '2024-01-01T00:00:00Z', - reason: 'subscribed', - subject: { - title: 'This is a Pull Request.', - type: 'PullRequest', - url: 'https://api.github.com/repos/gitify-app/notifications-test/pulls/4', - latest_comment_url: - 'https://api.github.com/repos/gitify-app/notifications-test/issues/4/comments', - }, - repository: mockRepository, - }, - { - id: '5', - unread: true, - updated_at: '2024-01-01T00:00:00Z', - reason: 'subscribed', - subject: { - title: 'This is an invitation.', - type: 'RepositoryInvitation', - url: null, - latest_comment_url: null, - }, - repository: mockRepository, - }, - { - id: '6', - unread: true, - updated_at: '2024-01-01T00:00:00Z', - reason: 'subscribed', - subject: { - title: 'This is a workflow run.', - type: 'WorkflowRun', - url: null, - latest_comment_url: null, - }, - repository: mockRepository, - }, - ]; - - nock('https://api.github.com') - .get('/notifications?participating=false') - .reply(200, mockNotifications); - - nock('https://api.github.com') - .post('/graphql') - .reply(200, { - data: { - search: { - nodes: [ - { - title: 'This is a Discussion.', - stateReason: null, - isAnswered: true, - url: 'https://github.com/gitify-app/notifications-test/discussions/612', - author: { - login: 'discussion-creator', - url: 'https://github.com/discussion-creator', - avatar_url: - 'https://avatars.githubusercontent.com/u/133795385?s=200&v=4', - type: 'User', - }, - comments: { - nodes: [ - { - databaseId: 2297637, - createdAt: '2022-03-04T20:39:44Z', - author: { - login: 'comment-user', - url: 'https://github.com/comment-user', - avatar_url: - 'https://avatars.githubusercontent.com/u/1?v=4', - type: 'User', - }, - replies: { - nodes: [], - }, - }, - ], - }, - labels: null, - }, - ], - }, - }, - }); - - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/3') - .reply(200, { - state: 'closed', - merged: true, - user: mockNotificationUser, - labels: [], - }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/3/comments') - .reply(200, { - user: mockNotificationUser, - }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/4') - .reply(200, { - state: 'closed', - merged: false, - user: mockNotificationUser, - labels: [], - }); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/pulls/4/reviews') - .reply(200, {}); - nock('https://api.github.com') - .get('/repos/gitify-app/notifications-test/issues/4/comments') - .reply(200, { - user: mockNotificationUser, - }); - - const { result } = renderHook(() => useNotifications()); - - act(() => { - result.current.fetchNotifications({ - auth: { - accounts: [mockGitHubCloudAccount], - }, - settings: { - ...mockSettings, - detailedNotifications: true, - }, - }); - }); - - expect(result.current.status).toBe('loading'); - - await waitFor(() => { - expect(result.current.status).toBe('success'); - }); - - expect(result.current.notifications[0].account.hostname).toBe( - 'github.com', - ); - expect(result.current.notifications[0].notifications.length).toBe(6); - }); - it('should fetch notifications with same failures', async () => { const code = AxiosError.ERR_BAD_REQUEST; const status = 401; diff --git a/src/renderer/utils/api/client.ts b/src/renderer/utils/api/client.ts index 7806dc75b..2b19c21aa 100644 --- a/src/renderer/utils/api/client.ts +++ b/src/renderer/utils/api/client.ts @@ -25,6 +25,7 @@ import { apiRequestAuth, type ExecutionResultWithHeaders, performGraphQLRequest, + performGraphQLRequestString, } from './request'; import type { NotificationThreadSubscription, @@ -194,6 +195,33 @@ export async function fetchAuthenticatedUserDetails( ); } +/** + * Fetch GitHub Discussion by Discussion Number. + */ +export async function fetchDiscussionByNumber( + notification: GitifyNotification, +): Promise> { + const url = getGitHubGraphQLUrl(notification.account.hostname); + const number = getNumberFromUrl(notification.subject.url); + + return performGraphQLRequest( + url.toString() as Link, + notification.account.token, + FetchDiscussionByNumberDocument, + { + owner: notification.repository.owner.login, + name: notification.repository.name, + number: number, + firstLabels: 100, + lastThreadedComments: 10, + lastReplies: 10, + includeIsAnswered: isAnsweredDiscussionFeatureSupported( + notification.account, + ), + }, + ); +} + /** * Fetch GitHub Issue by Issue Number. */ @@ -234,8 +262,8 @@ export async function fetchPullByNumber( owner: notification.repository.owner.login, name: notification.repository.name, number: number, - firstLabels: 100, firstClosingIssues: 100, + firstLabels: 100, lastComments: 1, lastReviews: 100, }, @@ -243,28 +271,19 @@ export async function fetchPullByNumber( } /** - * Fetch GitHub Discussion by Discussion Number. + * Fetch Batched Details for Discussions, Issues and Pull Requests. */ -export async function fetchDiscussionByNumber( +export async function fetchMergedQueryDetails( notification: GitifyNotification, -): Promise> { + mergedQuery: string, + mergedVariables: Record, +): Promise>> { const url = getGitHubGraphQLUrl(notification.account.hostname); - const number = getNumberFromUrl(notification.subject.url); - return performGraphQLRequest( + return performGraphQLRequestString( url.toString() as Link, notification.account.token, - FetchDiscussionByNumberDocument, - { - owner: notification.repository.owner.login, - name: notification.repository.name, - number: number, - lastComments: 10, - lastReplies: 10, - firstLabels: 100, - includeIsAnswered: isAnsweredDiscussionFeatureSupported( - notification.account, - ), - }, + mergedQuery, + mergedVariables, ); } diff --git a/src/renderer/utils/api/graphql/MergeQueryBuilder.ts b/src/renderer/utils/api/graphql/MergeQueryBuilder.ts new file mode 100644 index 000000000..3a7b5039a --- /dev/null +++ b/src/renderer/utils/api/graphql/MergeQueryBuilder.ts @@ -0,0 +1,46 @@ +import type { FragmentInfo } from './utils'; + +type VarValue = string | number | boolean; + +export class MergeQueryBuilder { + private selections: string[] = []; + private variableDefinitions: string[] = []; + private variableValues: Record = {}; + private fragments: FragmentInfo[] = []; + + addSelection(selection: string): this { + if (selection) { + this.selections.push(selection); + } + return this; + } + + addVariableDefs(defs: string): this { + if (defs) { + this.variableDefinitions.push(defs); + } + return this; + } + + setVar(name: string, value: VarValue): this { + this.variableValues[name] = value; + return this; + } + + addFragments(fragments: FragmentInfo[] | undefined): this { + if (fragments?.length) { + this.fragments.push(...fragments); + } + return this; + } + + buildQuery(docName = 'FetchMergedNotifications'): string { + const vars = this.variableDefinitions.join(', '); + const frags = this.fragments.map((f) => f.printed).join('\n'); + return `query ${docName}(${vars}) {\n${this.selections.join('\n')}\n}\n\n${frags}\n`; + } + + getVariables(): Record { + return this.variableValues; + } +} diff --git a/src/renderer/utils/api/graphql/common.graphql b/src/renderer/utils/api/graphql/common.graphql index 6acf59c00..b1e5a8aef 100644 --- a/src/renderer/utils/api/graphql/common.graphql +++ b/src/renderer/utils/api/graphql/common.graphql @@ -1,7 +1,7 @@ fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } diff --git a/src/renderer/utils/api/graphql/discussion.graphql b/src/renderer/utils/api/graphql/discussion.graphql index 63a70304d..e17d20e52 100644 --- a/src/renderer/utils/api/graphql/discussion.graphql +++ b/src/renderer/utils/api/graphql/discussion.graphql @@ -1,8 +1,10 @@ +#import './common.graphql' + query FetchDiscussionByNumber( $owner: String! $name: String! $number: Int! - $lastComments: Int + $lastThreadedComments: Int $lastReplies: Int $firstLabels: Int $includeIsAnswered: Boolean! @@ -24,7 +26,7 @@ fragment DiscussionDetails on Discussion { author { ...AuthorFields } - comments(last: $lastComments) { + comments(last: $lastThreadedComments) { totalCount nodes { ...DiscussionCommentFields diff --git a/src/renderer/utils/api/graphql/generated/gql.ts b/src/renderer/utils/api/graphql/generated/gql.ts index 806d82aeb..cd48ef095 100644 --- a/src/renderer/utils/api/graphql/generated/gql.ts +++ b/src/renderer/utils/api/graphql/generated/gql.ts @@ -15,16 +15,18 @@ import * as types from './graphql'; * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size */ type Documents = { - "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.AuthorFieldsFragmentDoc, - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": typeof types.FetchDiscussionByNumberDocument, + "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": typeof types.AuthorFieldsFragmentDoc, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastThreadedComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastThreadedComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": typeof types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": typeof types.FetchIssueByNumberDocument, + "query FetchBatch($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) {\n ...BatchMergedDetailsQueryTemplate\n}\n\nfragment BatchMergedDetailsQueryTemplate on Query {\n repository(owner: $ownerINDEX, name: $nameINDEX) {\n discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) {\n ...DiscussionDetails\n }\n issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) {\n ...IssueDetails\n }\n pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) {\n ...PullRequestDetails\n }\n }\n}": typeof types.FetchBatchDocument, "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": typeof types.FetchPullRequestByNumberDocument, "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": typeof types.FetchAuthenticatedUserDetailsDocument, }; const documents: Documents = { - "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.AuthorFieldsFragmentDoc, - "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": types.FetchDiscussionByNumberDocument, + "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}": types.AuthorFieldsFragmentDoc, + "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastThreadedComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastThreadedComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}": types.FetchDiscussionByNumberDocument, "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}": types.FetchIssueByNumberDocument, + "query FetchBatch($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) {\n ...BatchMergedDetailsQueryTemplate\n}\n\nfragment BatchMergedDetailsQueryTemplate on Query {\n repository(owner: $ownerINDEX, name: $nameINDEX) {\n discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) {\n ...DiscussionDetails\n }\n issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) {\n ...IssueDetails\n }\n pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) {\n ...PullRequestDetails\n }\n }\n}": types.FetchBatchDocument, "query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) {\n repository(owner: $owner, name: $name) {\n pullRequest(number: $number) {\n ...PullRequestDetails\n }\n }\n}\n\nfragment PullRequestDetails on PullRequest {\n __typename\n number\n title\n url\n state\n merged\n isDraft\n isInMergeQueue\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n reviews(last: $lastReviews) {\n totalCount\n nodes {\n ...PullRequestReviewFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n closingIssuesReferences(first: $firstClosingIssues) {\n nodes {\n number\n }\n }\n}\n\nfragment PullRequestReviewFields on PullRequestReview {\n state\n author {\n login\n }\n}": types.FetchPullRequestByNumberDocument, "query FetchAuthenticatedUserDetails {\n viewer {\n id\n name\n login\n avatarUrl\n }\n}": types.FetchAuthenticatedUserDetailsDocument, }; @@ -32,15 +34,19 @@ const documents: Documents = { /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').AuthorFieldsFragmentDoc; +export function graphql(source: "fragment AuthorFields on Actor {\n login\n htmlUrl: url\n avatarUrl: avatarUrl\n type: __typename\n}\n\nfragment MilestoneFields on Milestone {\n state\n title\n}"): typeof import('./graphql').AuthorFieldsFragmentDoc; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; +export function graphql(source: "query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastThreadedComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) {\n repository(owner: $owner, name: $name) {\n discussion(number: $number) {\n ...DiscussionDetails\n }\n }\n}\n\nfragment DiscussionDetails on Discussion {\n __typename\n number\n title\n stateReason\n isAnswered @include(if: $includeIsAnswered)\n url\n author {\n ...AuthorFields\n }\n comments(last: $lastThreadedComments) {\n totalCount\n nodes {\n ...DiscussionCommentFields\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}\n\nfragment CommentFields on DiscussionComment {\n databaseId\n createdAt\n author {\n ...AuthorFields\n }\n url\n}\n\nfragment DiscussionCommentFields on DiscussionComment {\n ...CommentFields\n replies(last: $lastReplies) {\n totalCount\n nodes {\n ...CommentFields\n }\n }\n}"): typeof import('./graphql').FetchDiscussionByNumberDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "query FetchIssueByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $firstLabels: Int) {\n repository(owner: $owner, name: $name) {\n issue(number: $number) {\n ...IssueDetails\n }\n }\n}\n\nfragment IssueDetails on Issue {\n __typename\n number\n title\n url\n state\n stateReason\n milestone {\n ...MilestoneFields\n }\n author {\n ...AuthorFields\n }\n comments(last: $lastComments) {\n totalCount\n nodes {\n url\n author {\n ...AuthorFields\n }\n }\n }\n labels(first: $firstLabels) {\n nodes {\n name\n }\n }\n}"): typeof import('./graphql').FetchIssueByNumberDocument; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query FetchBatch($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) {\n ...BatchMergedDetailsQueryTemplate\n}\n\nfragment BatchMergedDetailsQueryTemplate on Query {\n repository(owner: $ownerINDEX, name: $nameINDEX) {\n discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) {\n ...DiscussionDetails\n }\n issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) {\n ...IssueDetails\n }\n pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) {\n ...PullRequestDetails\n }\n }\n}"): typeof import('./graphql').FetchBatchDocument; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/src/renderer/utils/api/graphql/generated/graphql.ts b/src/renderer/utils/api/graphql/generated/graphql.ts index 82d3cf2ae..d9350cf82 100644 --- a/src/renderer/utils/api/graphql/generated/graphql.ts +++ b/src/renderer/utils/api/graphql/generated/graphql.ts @@ -35894,15 +35894,15 @@ export type WorkflowsParametersInput = { export type _Entity = Issue; -type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' }; +type AuthorFields_Bot_Fragment = { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' }; -type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' }; +type AuthorFields_EnterpriseUserAccount_Fragment = { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' }; -type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' }; +type AuthorFields_Mannequin_Fragment = { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' }; -type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' }; +type AuthorFields_Organization_Fragment = { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' }; -type AuthorFields_User_Fragment = { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' }; +type AuthorFields_User_Fragment = { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' }; export type AuthorFieldsFragment = | AuthorFields_Bot_Fragment @@ -35918,7 +35918,7 @@ export type FetchDiscussionByNumberQueryVariables = Exact<{ owner: Scalars['String']['input']; name: Scalars['String']['input']; number: Scalars['Int']['input']; - lastComments?: InputMaybe; + lastThreadedComments?: InputMaybe; lastReplies?: InputMaybe; firstLabels?: InputMaybe; includeIsAnswered: Scalars['Boolean']['input']; @@ -35926,65 +35926,65 @@ export type FetchDiscussionByNumberQueryVariables = Exact<{ export type FetchDiscussionByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', discussion?: { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; export type DiscussionDetailsFragment = { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null }; export type CommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null }; export type DiscussionCommentFieldsFragment = { __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null }; export type FetchIssueByNumberQueryVariables = Exact<{ @@ -35997,33 +35997,150 @@ export type FetchIssueByNumberQueryVariables = Exact<{ export type FetchIssueByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', issue?: { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null } | null }; export type IssueDetailsFragment = { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null }; +export type FetchBatchQueryVariables = Exact<{ + ownerINDEX: Scalars['String']['input']; + nameINDEX: Scalars['String']['input']; + numberINDEX: Scalars['Int']['input']; + isDiscussionNotificationINDEX: Scalars['Boolean']['input']; + isIssueNotificationINDEX: Scalars['Boolean']['input']; + isPullRequestNotificationINDEX: Scalars['Boolean']['input']; + lastComments?: InputMaybe; + lastThreadedComments?: InputMaybe; + lastReplies?: InputMaybe; + lastReviews?: InputMaybe; + firstLabels?: InputMaybe; + firstClosingIssues?: InputMaybe; + includeIsAnswered: Scalars['Boolean']['input']; +}>; + + +export type FetchBatchQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', discussion?: { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null, issue?: { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null, pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', state: PullRequestReviewState, author?: + | { __typename?: 'Bot', login: string } + | { __typename?: 'EnterpriseUserAccount', login: string } + | { __typename?: 'Mannequin', login: string } + | { __typename?: 'Organization', login: string } + | { __typename?: 'User', login: string } + | null } | null> | null } | null, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null, closingIssuesReferences?: { __typename?: 'IssueConnection', nodes?: Array<{ __typename?: 'Issue', number: number } | null> | null } | null } | null } | null }; + +export type BatchMergedDetailsQueryTemplateFragment = { __typename?: 'Query', repository?: { __typename?: 'Repository', discussion?: { __typename: 'Discussion', number: number, title: string, stateReason?: DiscussionStateReason | null, isAnswered?: boolean | null, url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null, comments: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, replies: { __typename?: 'DiscussionCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'DiscussionComment', databaseId?: number | null, createdAt: any, url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null, issue?: { __typename: 'Issue', number: number, title: string, url: any, state: IssueState, stateReason?: IssueStateReason | null, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null } | null, pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } + | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', state: PullRequestReviewState, author?: + | { __typename?: 'Bot', login: string } + | { __typename?: 'EnterpriseUserAccount', login: string } + | { __typename?: 'Mannequin', login: string } + | { __typename?: 'Organization', login: string } + | { __typename?: 'User', login: string } + | null } | null> | null } | null, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null, closingIssuesReferences?: { __typename?: 'IssueConnection', nodes?: Array<{ __typename?: 'Issue', number: number } | null> | null } | null } | null } | null }; + export type FetchPullRequestByNumberQueryVariables = Exact<{ owner: Scalars['String']['input']; name: Scalars['String']['input']; @@ -36036,17 +36153,17 @@ export type FetchPullRequestByNumberQueryVariables = Exact<{ export type FetchPullRequestByNumberQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', pullRequest?: { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', state: PullRequestReviewState, author?: | { __typename?: 'Bot', login: string } | { __typename?: 'EnterpriseUserAccount', login: string } @@ -36056,17 +36173,17 @@ export type FetchPullRequestByNumberQuery = { __typename?: 'Query', repository?: | null } | null> | null } | null, labels?: { __typename?: 'LabelConnection', nodes?: Array<{ __typename?: 'Label', name: string } | null> | null } | null, closingIssuesReferences?: { __typename?: 'IssueConnection', nodes?: Array<{ __typename?: 'Issue', number: number } | null> | null } | null } | null } | null }; export type PullRequestDetailsFragment = { __typename: 'PullRequest', number: number, title: string, url: any, state: PullRequestState, merged: boolean, isDraft: boolean, isInMergeQueue: boolean, milestone?: { __typename?: 'Milestone', state: MilestoneState, title: string } | null, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null, comments: { __typename?: 'IssueCommentConnection', totalCount: number, nodes?: Array<{ __typename?: 'IssueComment', url: any, author?: - | { __typename?: 'Bot', login: string, avatarUrl: any, htmlUrl: any, type: 'Bot' } - | { __typename?: 'EnterpriseUserAccount', login: string, avatarUrl: any, htmlUrl: any, type: 'EnterpriseUserAccount' } - | { __typename?: 'Mannequin', login: string, avatarUrl: any, htmlUrl: any, type: 'Mannequin' } - | { __typename?: 'Organization', login: string, avatarUrl: any, htmlUrl: any, type: 'Organization' } - | { __typename?: 'User', login: string, avatarUrl: any, htmlUrl: any, type: 'User' } + | { __typename?: 'Bot', login: string, htmlUrl: any, avatarUrl: any, type: 'Bot' } + | { __typename?: 'EnterpriseUserAccount', login: string, htmlUrl: any, avatarUrl: any, type: 'EnterpriseUserAccount' } + | { __typename?: 'Mannequin', login: string, htmlUrl: any, avatarUrl: any, type: 'Mannequin' } + | { __typename?: 'Organization', login: string, htmlUrl: any, avatarUrl: any, type: 'Organization' } + | { __typename?: 'User', login: string, htmlUrl: any, avatarUrl: any, type: 'User' } | null } | null> | null }, reviews?: { __typename?: 'PullRequestReviewConnection', totalCount: number, nodes?: Array<{ __typename?: 'PullRequestReview', state: PullRequestReviewState, author?: | { __typename?: 'Bot', login: string } | { __typename?: 'EnterpriseUserAccount', login: string } @@ -36110,7 +36227,7 @@ export const AuthorFieldsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } `, {"fragmentName":"AuthorFields"}) as unknown as TypedDocumentString; @@ -36126,7 +36243,7 @@ export const CommentFieldsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename }`, {"fragmentName":"CommentFields"}) as unknown as TypedDocumentString; export const DiscussionCommentFieldsFragmentDoc = new TypedDocumentString(` @@ -36142,7 +36259,7 @@ export const DiscussionCommentFieldsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } fragment CommentFields on DiscussionComment { @@ -36164,7 +36281,7 @@ export const DiscussionDetailsFragmentDoc = new TypedDocumentString(` author { ...AuthorFields } - comments(last: $lastComments) { + comments(last: $lastThreadedComments) { totalCount nodes { ...DiscussionCommentFields @@ -36179,7 +36296,7 @@ export const DiscussionDetailsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } fragment CommentFields on DiscussionComment { @@ -36237,7 +36354,7 @@ export const IssueDetailsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } fragment MilestoneFields on Milestone { @@ -36297,7 +36414,7 @@ export const PullRequestDetailsFragmentDoc = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } fragment MilestoneFields on Milestone { @@ -36310,8 +36427,146 @@ fragment PullRequestReviewFields on PullRequestReview { login } }`, {"fragmentName":"PullRequestDetails"}) as unknown as TypedDocumentString; +export const BatchMergedDetailsQueryTemplateFragmentDoc = new TypedDocumentString(` + fragment BatchMergedDetailsQueryTemplate on Query { + repository(owner: $ownerINDEX, name: $nameINDEX) { + discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) { + ...DiscussionDetails + } + issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) { + ...IssueDetails + } + pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) { + ...PullRequestDetails + } + } +} + fragment AuthorFields on Actor { + login + htmlUrl: url + avatarUrl: avatarUrl + type: __typename +} +fragment MilestoneFields on Milestone { + state + title +} +fragment DiscussionDetails on Discussion { + __typename + number + title + stateReason + isAnswered @include(if: $includeIsAnswered) + url + author { + ...AuthorFields + } + comments(last: $lastThreadedComments) { + totalCount + nodes { + ...DiscussionCommentFields + } + } + labels(first: $firstLabels) { + nodes { + name + } + } +} +fragment CommentFields on DiscussionComment { + databaseId + createdAt + author { + ...AuthorFields + } + url +} +fragment DiscussionCommentFields on DiscussionComment { + ...CommentFields + replies(last: $lastReplies) { + totalCount + nodes { + ...CommentFields + } + } +} +fragment IssueDetails on Issue { + __typename + number + title + url + state + stateReason + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: $lastComments) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } +} +fragment PullRequestDetails on PullRequest { + __typename + number + title + url + state + merged + isDraft + isInMergeQueue + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: $lastComments) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + reviews(last: $lastReviews) { + totalCount + nodes { + ...PullRequestReviewFields + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + closingIssuesReferences(first: $firstClosingIssues) { + nodes { + number + } + } +} +fragment PullRequestReviewFields on PullRequestReview { + state + author { + login + } +}`, {"fragmentName":"BatchMergedDetailsQueryTemplate"}) as unknown as TypedDocumentString; export const FetchDiscussionByNumberDocument = new TypedDocumentString(` - query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) { + query FetchDiscussionByNumber($owner: String!, $name: String!, $number: Int!, $lastThreadedComments: Int, $lastReplies: Int, $firstLabels: Int, $includeIsAnswered: Boolean!) { repository(owner: $owner, name: $name) { discussion(number: $number) { ...DiscussionDetails @@ -36321,7 +36576,7 @@ export const FetchDiscussionByNumberDocument = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } fragment DiscussionDetails on Discussion { @@ -36334,7 +36589,7 @@ fragment DiscussionDetails on Discussion { author { ...AuthorFields } - comments(last: $lastComments) { + comments(last: $lastThreadedComments) { totalCount nodes { ...DiscussionCommentFields @@ -36374,7 +36629,7 @@ export const FetchIssueByNumberDocument = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } fragment MilestoneFields on Milestone { @@ -36409,6 +36664,147 @@ fragment IssueDetails on Issue { } } }`) as unknown as TypedDocumentString; +export const FetchBatchDocument = new TypedDocumentString(` + query FetchBatch($ownerINDEX: String!, $nameINDEX: String!, $numberINDEX: Int!, $isDiscussionNotificationINDEX: Boolean!, $isIssueNotificationINDEX: Boolean!, $isPullRequestNotificationINDEX: Boolean!, $lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!) { + ...BatchMergedDetailsQueryTemplate +} + fragment AuthorFields on Actor { + login + htmlUrl: url + avatarUrl: avatarUrl + type: __typename +} +fragment MilestoneFields on Milestone { + state + title +} +fragment DiscussionDetails on Discussion { + __typename + number + title + stateReason + isAnswered @include(if: $includeIsAnswered) + url + author { + ...AuthorFields + } + comments(last: $lastThreadedComments) { + totalCount + nodes { + ...DiscussionCommentFields + } + } + labels(first: $firstLabels) { + nodes { + name + } + } +} +fragment CommentFields on DiscussionComment { + databaseId + createdAt + author { + ...AuthorFields + } + url +} +fragment DiscussionCommentFields on DiscussionComment { + ...CommentFields + replies(last: $lastReplies) { + totalCount + nodes { + ...CommentFields + } + } +} +fragment IssueDetails on Issue { + __typename + number + title + url + state + stateReason + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: $lastComments) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + labels(first: $firstLabels) { + nodes { + name + } + } +} +fragment BatchMergedDetailsQueryTemplate on Query { + repository(owner: $ownerINDEX, name: $nameINDEX) { + discussion(number: $numberINDEX) @include(if: $isDiscussionNotificationINDEX) { + ...DiscussionDetails + } + issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) { + ...IssueDetails + } + pullRequest(number: $numberINDEX) @include(if: $isPullRequestNotificationINDEX) { + ...PullRequestDetails + } + } +} +fragment PullRequestDetails on PullRequest { + __typename + number + title + url + state + merged + isDraft + isInMergeQueue + milestone { + ...MilestoneFields + } + author { + ...AuthorFields + } + comments(last: $lastComments) { + totalCount + nodes { + url + author { + ...AuthorFields + } + } + } + reviews(last: $lastReviews) { + totalCount + nodes { + ...PullRequestReviewFields + } + } + labels(first: $firstLabels) { + nodes { + name + } + } + closingIssuesReferences(first: $firstClosingIssues) { + nodes { + number + } + } +} +fragment PullRequestReviewFields on PullRequestReview { + state + author { + login + } +}`) as unknown as TypedDocumentString; export const FetchPullRequestByNumberDocument = new TypedDocumentString(` query FetchPullRequestByNumber($owner: String!, $name: String!, $number: Int!, $firstLabels: Int, $lastComments: Int, $lastReviews: Int, $firstClosingIssues: Int) { repository(owner: $owner, name: $name) { @@ -36420,7 +36816,7 @@ export const FetchPullRequestByNumberDocument = new TypedDocumentString(` fragment AuthorFields on Actor { login htmlUrl: url - avatarUrl + avatarUrl: avatarUrl type: __typename } fragment MilestoneFields on Milestone { diff --git a/src/renderer/utils/api/graphql/issue.graphql b/src/renderer/utils/api/graphql/issue.graphql index 091a6543e..ceb1b77e0 100644 --- a/src/renderer/utils/api/graphql/issue.graphql +++ b/src/renderer/utils/api/graphql/issue.graphql @@ -1,3 +1,5 @@ +#import './common.graphql' + query FetchIssueByNumber( $owner: String! $name: String! diff --git a/src/renderer/utils/api/graphql/merged.graphql b/src/renderer/utils/api/graphql/merged.graphql new file mode 100644 index 000000000..f1adb75ec --- /dev/null +++ b/src/renderer/utils/api/graphql/merged.graphql @@ -0,0 +1,39 @@ +query FetchBatch( + # Arguments that will be per notification item + $ownerINDEX: String! + $nameINDEX: String! + $numberINDEX: Int! + $isDiscussionNotificationINDEX: Boolean! + $isIssueNotificationINDEX: Boolean! + $isPullRequestNotificationINDEX: Boolean! + # Stable arguments for the merged query as a whole + $lastComments: Int + $lastThreadedComments: Int + $lastReplies: Int + $lastReviews: Int + $firstLabels: Int + $firstClosingIssues: Int + $includeIsAnswered: Boolean! +) { + ...BatchMergedDetailsQueryTemplate +} + +fragment BatchMergedDetailsQueryTemplate on Query { + repository(owner: $ownerINDEX, name: $nameINDEX) { + discussion(number: $numberINDEX) + @include(if: $isDiscussionNotificationINDEX) + { + ...DiscussionDetails + } + + issue(number: $numberINDEX) @include(if: $isIssueNotificationINDEX) { + ...IssueDetails + } + + pullRequest(number: $numberINDEX) + @include(if: $isPullRequestNotificationINDEX) + { + ...PullRequestDetails + } + } +} diff --git a/src/renderer/utils/api/graphql/pull.graphql b/src/renderer/utils/api/graphql/pull.graphql index 9d5862cb6..857580b39 100644 --- a/src/renderer/utils/api/graphql/pull.graphql +++ b/src/renderer/utils/api/graphql/pull.graphql @@ -1,3 +1,5 @@ +#import './common.graphql' + query FetchPullRequestByNumber( $owner: String! $name: String! diff --git a/src/renderer/utils/api/graphql/utils.test.ts b/src/renderer/utils/api/graphql/utils.test.ts new file mode 100644 index 000000000..09d91dc59 --- /dev/null +++ b/src/renderer/utils/api/graphql/utils.test.ts @@ -0,0 +1,159 @@ +describe('renderer/utils/api/graphql/utils.ts', () => { + // describe('getQueryFragmentBody', () => { + // it('should extract query fragment body from BatchMergedDetailsQueryFragmentDoc', () => { + // const body = getQueryFragmentBody(BatchMergedDetailsQueryFragmentDoc); + + // expect(body).not.toBeNull(); + // expect(body).toContain('repository'); + // expect(body).toContain('$ownerINDEX'); + // expect(body).toContain('$nameINDEX'); + // }); + + // it('should return null for non-Query fragments', () => { + // // IssueDetailsFragmentDoc is a fragment on Issue, not Query + // const body = getQueryFragmentBody(IssueDetailsFragmentDoc); + + // expect(body).toBeNull(); + // }); + // }); + + // describe('extractFragments', () => { + // it('should extract fragment definitions from IssueDetailsFragmentDoc', () => { + // const fragments = extractFragments(IssueDetailsFragmentDoc); + + // expect(fragments.size).toBeGreaterThan(0); + // expect(fragments.has('IssueDetails')).toBe(true); + // // IssueDetails uses AuthorFields and MilestoneFields + // expect(fragments.has('AuthorFields')).toBe(true); + // expect(fragments.has('MilestoneFields')).toBe(true); + // }); + + // it('should extract fragment definitions from PullRequestDetailsFragmentDoc', () => { + // const fragments = extractFragments(PullRequestDetailsFragmentDoc); + + // expect(fragments.size).toBeGreaterThan(0); + // expect(fragments.has('PullRequestDetails')).toBe(true); + // expect(fragments.has('PullRequestReviewFields')).toBe(true); + // }); + // }); + + // describe('extractFragmentsAll', () => { + // it('should merge fragments from multiple documents without duplicates', () => { + // const fragments = extractFragmentsAll([ + // IssueDetailsFragmentDoc, + // PullRequestDetailsFragmentDoc, + // ]); + + // expect(fragments.has('IssueDetails')).toBe(true); + // expect(fragments.has('PullRequestDetails')).toBe(true); + // // Shared fragments should only appear once + // expect(fragments.has('AuthorFields')).toBe(true); + // expect(fragments.has('MilestoneFields')).toBe(true); + // }); + + // it('should handle empty array', () => { + // const fragments = extractFragmentsAll([]); + + // expect(fragments.size).toBe(0); + // }); + // }); + + describe('composeMergedQuery', () => { + // it('should compose a valid merged query string', () => { + // const selections = [ + // 'node0: repository(owner: $owner0, name: $name0) { issue(number: $number0) { title } }', + // 'node1: repository(owner: $owner1, name: $name1) { pullRequest(number: $number1) { title } }', + // ]; + // const fragmentMap = new Map(); + // fragmentMap.set('TestFragment', 'fragment TestFragment on Issue { id }'); + // const variableDefinitions = [ + // '$owner0: String!', + // '$name0: String!', + // '$number0: Int!', + // '$owner1: String!', + // '$name1: String!', + // '$number1: Int!', + // ]; + // const query = composeMergedQuery( + // selections, + // fragmentMap, + // variableDefinitions, + // ); + // expect(query).toContain('query FetchMergedNotifications'); + // expect(query).toContain('$owner0: String!'); + // expect(query).toContain('node0: repository'); + // expect(query).toContain('node1: repository'); + // expect(query).toContain('fragment TestFragment on Issue'); + // }); + // it('should handle empty fragments map', () => { + // const selections = ['node0: repository { id }']; + // const fragmentMap = new Map(); + // const variableDefinitions = ['$id: ID!']; + // const query = composeMergedQuery( + // selections, + // fragmentMap, + // variableDefinitions, + // ); + // expect(query).toContain('query FetchMergedNotifications($id: ID!)'); + // expect(query).toContain('node0: repository { id }'); + // }); + }); + + // describe('aliasRootAndKeyVariables', () => { + // it('should add alias and index suffix to variables', () => { + // const input = + // 'repository(owner: $owner, name: $name) { issue(number: $number) { title } }'; + + // const result = aliasRootAndKeyVariables(input, 0); + + // expect(result).toContain('node0: repository'); + // expect(result).toContain('$owner0'); + // expect(result).toContain('$name0'); + // expect(result).toContain('$number0'); + // }); + + // it('should handle boolean condition variables', () => { + // const input = + // 'repository(owner: $owner, name: $name) { issue(number: $number) @include(if: $isIssueNotification) { title } }'; + + // const result = aliasRootAndKeyVariables(input, 1); + + // expect(result).toContain('node1: repository'); + // expect(result).toContain('$owner1'); + // expect(result).toContain('$isIssueNotification1'); + // }); + + // it('should handle all notification type condition variables', () => { + // const input = + // 'repository(owner: $owner, name: $name) { discussion @include(if: $isDiscussionNotification) { id } issue @include(if: $isIssueNotification) { id } pullRequest @include(if: $isPullRequestNotification) { id } }'; + + // const result = aliasRootAndKeyVariables(input, 2); + + // expect(result).toContain('$isDiscussionNotification2'); + // expect(result).toContain('$isIssueNotification2'); + // expect(result).toContain('$isPullRequestNotification2'); + // }); + + // it('should work with string index', () => { + // const input = 'repository(owner: $owner, name: $name) { id }'; + + // const result = aliasRootAndKeyVariables(input, '5'); + + // expect(result).toContain('node5: repository'); + // expect(result).toContain('$owner5'); + // expect(result).toContain('$name5'); + // }); + + // it('should not modify non-key variables', () => { + // const input = + // 'repository(owner: $owner) { issues(first: $firstLabels) { nodes { title } } }'; + + // const result = aliasRootAndKeyVariables(input, 0); + + // expect(result).toContain('$owner0'); + // // $firstLabels should remain unchanged (not a key variable) + // expect(result).toContain('$firstLabels'); + // expect(result).not.toContain('$firstLabels0'); + // }); + // }); +}); diff --git a/src/renderer/utils/api/graphql/utils.ts b/src/renderer/utils/api/graphql/utils.ts new file mode 100644 index 000000000..9b0bcf7b2 --- /dev/null +++ b/src/renderer/utils/api/graphql/utils.ts @@ -0,0 +1,136 @@ +import { type DocumentNode, parse, print } from 'graphql'; + +import type { TypedDocumentString } from './generated/graphql'; + +// AST-based helpers for robust fragment parsing and deduping + +function toDocumentNode( + doc: TypedDocumentString, +): DocumentNode { + return parse(doc.toString()); +} + +export type FragmentInfo = { + name: string; + typeCondition: string; + printed: string; + inner: string; +}; + +/** + * Extract all fragments from a GraphQL document with metadata. + */ +export function extractAllFragments( + doc: TypedDocumentString, +): FragmentInfo[] { + const ast: DocumentNode = toDocumentNode(doc); + const fragments: FragmentInfo[] = []; + + for (const def of ast.definitions) { + if (def.kind === 'FragmentDefinition') { + const printed = print(def); + const open = printed.indexOf('{'); + const close = printed.lastIndexOf('}'); + + fragments.push({ + name: def.name.value, + typeCondition: def.typeCondition.name.value, + printed: printed, + inner: printed.slice(open + 1, close).trim(), + }); + } + } + + return fragments; +} + +/** + * Return only `Query` fragments from a GraphQL document. + */ +export function extractQueryFragments( + doc: TypedDocumentString, +): FragmentInfo[] { + return extractAllFragments(doc).filter((f) => f.typeCondition === 'Query'); +} + +/** + * Return all non-`Query` fragments from a GraphQL document. + */ +export function extractNonQueryFragments( + doc: TypedDocumentString, +): FragmentInfo[] { + return extractAllFragments(doc).filter((f) => f.typeCondition !== 'Query'); +} + +// Helper to compose a merged query given selections, fragments and variable defs +export function composeMergedQuery( + selections: string[], + fragments: FragmentInfo[], + variableDefinitions: string[], +): string { + const vars = variableDefinitions.join(', '); + const selects = selections.join('\n'); + const frags = fragments.map((f) => f.printed).join('\n'); + return `query FetchMergedNotifications(${vars}) {\n${selects}\n}\n\n${frags}\n`; +} + +/** + * Alias the root field and suffix key variables with the provided index. + * + * Example: + * repository(owner: $owner, name: $name) { issue(number: $number) { ...IssueDetails } } + * becomes: + * nodeINDEX: repository(owner: $ownerINDEX, name: $nameINDEX) { issue(number: $numberINDEX) { ...IssueDetails } } + */ +export function aliasRootAndKeyVariables( + rootAlias: string, + index: number, + selectionBody: string, +): string { + const idx = String(index); + + // Add alias to the first root field name + const withAlias = selectionBody.replace( + /^\s*([_A-Za-z][_A-Za-z0-9]*)/, + (_m, name: string) => `${rootAlias}: ${name}`, + ); + + // Only alias variables that explicitly end with `INDEX`. + // Example: $ownerINDEX -> $owner0, $nameINDEX -> $name0 + const withIndexedVars = withAlias.replace( + /\$([_A-Za-z][_A-Za-z0-9]*)INDEX\b/g, + (_m, v: string) => `$${v}${idx}`, + ); + + return withIndexedVars; +} + +export function extractArgumentNames(selectionBody: string): Set { + const names = new Set(); + const regex = /\$([_A-Za-z][_A-Za-z0-9]*)\b/g; + let match: RegExpExecArray | null = regex.exec(selectionBody); + + while (match !== null) { + names.add(match[1]); + match = regex.exec(selectionBody); + } + + return names; +} + +export function filterArgumentsByIndexSuffix( + args: Iterable, + indexed: boolean, +): string[] { + return Array.from(args).filter((name) => name.endsWith('INDEX') === indexed); +} + +export function extractIndexedArguments(selectionBody: string): string[] { + const all = extractArgumentNames(selectionBody); + return filterArgumentsByIndexSuffix(all, true); +} + +export function extractNonIndexedArguments(selectionBody: string): string[] { + const all = extractArgumentNames(selectionBody); + return filterArgumentsByIndexSuffix(all, false); +} diff --git a/src/renderer/utils/api/request.ts b/src/renderer/utils/api/request.ts index 035e2e396..201f4fe2a 100644 --- a/src/renderer/utils/api/request.ts +++ b/src/renderer/utils/api/request.ts @@ -121,6 +121,35 @@ export async function performGraphQLRequest( }) as Promise>; } +/** + * Perform a GraphQL API request using a raw query string instead of a TypedDocumentString. + * + * Useful for dynamically composed queries (e.g., merged queries built at runtime). + */ +export async function performGraphQLRequestString( + url: Link, + token: Token, + query: string, + variables?: Record, +): Promise> { + const headers = await getHeaders(url, token); + + return axios({ + method: 'POST', + url, + data: { + query, + variables, + }, + headers: headers, + }).then((response) => { + return { + ...response.data, + headers: response.headers, + } as ExecutionResultWithHeaders; + }); +} + /** * Return true if the request should be made with no-cache * diff --git a/src/renderer/utils/links.test.ts b/src/renderer/utils/links.test.ts index fba2a9d60..71b3957dc 100644 --- a/src/renderer/utils/links.test.ts +++ b/src/renderer/utils/links.test.ts @@ -1,12 +1,7 @@ import { mockGitHubCloudAccount } from '../__mocks__/account-mocks'; -import { createMockNotificationUser } from '../__mocks__/user-mocks'; +import { createMockGitifyNotificationUser } from '../__mocks__/user-mocks'; import { Constants } from '../constants'; -import type { - GitifyNotificationUser, - GitifyRepository, - Hostname, - Link, -} from '../types'; +import type { GitifyRepository, Hostname, Link } from '../types'; import { mockSingleNotification } from './api/__mocks__/response-mocks'; import * as authUtils from './auth/utils'; import * as comms from './comms'; @@ -75,9 +70,7 @@ describe('renderer/utils/links.ts', () => { }); it('openUserProfile', () => { - const mockUser = createMockNotificationUser( - 'mock-user', - ) as GitifyNotificationUser; + const mockUser = createMockGitifyNotificationUser('mock-user'); openUserProfile(mockUser); diff --git a/src/renderer/utils/notifications/filters/search.test.ts b/src/renderer/utils/notifications/filters/search.test.ts index 5bbfe941c..8e70afb6e 100644 --- a/src/renderer/utils/notifications/filters/search.test.ts +++ b/src/renderer/utils/notifications/filters/search.test.ts @@ -21,7 +21,7 @@ describe('renderer/utils/notifications/filters/search.ts', () => { it('matches each known qualifier by its exact prefix and additional value', () => { for (const q of ALL_SEARCH_QUALIFIERS) { - const token = q.prefix + 'someValue'; + const token = `${q.prefix}someValue`; const parsed = parseSearchInput(token); expect(parsed).not.toBeNull(); expect(parsed?.qualifier).toBe(q); diff --git a/src/renderer/utils/notifications/handlers/default.test.ts b/src/renderer/utils/notifications/handlers/default.test.ts index 346f63199..1ed591a9d 100644 --- a/src/renderer/utils/notifications/handlers/default.test.ts +++ b/src/renderer/utils/notifications/handlers/default.test.ts @@ -12,6 +12,13 @@ import { import { defaultHandler } from './default'; describe('renderer/utils/notifications/handlers/default.ts', () => { + describe('mergeQueryConfig', () => { + it('should return undefined (no merge query support)', () => { + const mergeType = defaultHandler.mergeQueryNodeResponseType; + expect(mergeType).toBeUndefined(); + }); + }); + describe('enrich', () => { it('unhandled subject details', async () => { const mockNotification = createPartialMockNotification({ diff --git a/src/renderer/utils/notifications/handlers/default.ts b/src/renderer/utils/notifications/handlers/default.ts index df542ffd8..1a0e9b085 100644 --- a/src/renderer/utils/notifications/handlers/default.ts +++ b/src/renderer/utils/notifications/handlers/default.ts @@ -11,12 +11,15 @@ import { type SettingsState, type SubjectType, } from '../../../types'; +import type { TypedDocumentString } from '../../api/graphql/generated/graphql'; import type { NotificationTypeHandler } from './types'; import { formatForDisplay } from './utils'; export class DefaultHandler implements NotificationTypeHandler { type?: SubjectType; + mergeQueryNodeResponseType?: TypedDocumentString; + async enrich( _notification: GitifyNotification, _settings: SettingsState, diff --git a/src/renderer/utils/notifications/handlers/discussion.test.ts b/src/renderer/utils/notifications/handlers/discussion.test.ts index e220612b6..296d31f7d 100644 --- a/src/renderer/utils/notifications/handlers/discussion.test.ts +++ b/src/renderer/utils/notifications/handlers/discussion.test.ts @@ -6,7 +6,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; +import { createMockGraphQLAuthor } from '../../../__mocks__/user-mocks'; import type { GitifyNotification } from '../../../types'; import { type GitifyDiscussionState, @@ -14,17 +14,27 @@ import { IconColor, type Link, } from '../../../types'; -import type { - DiscussionDetailsFragment, - DiscussionStateReason, +import { + type DiscussionDetailsFragment, + DiscussionDetailsFragmentDoc, + type DiscussionStateReason, } from '../../api/graphql/generated/graphql'; import { discussionHandler } from './discussion'; -const mockAuthor = createMockNotificationUser('discussion-author'); -const mockCommenter = createMockNotificationUser('discussion-commenter'); -const mockReplier = createMockNotificationUser('discussion-replier'); +const mockAuthor = createMockGraphQLAuthor('discussion-author'); +const mockCommenter = createMockGraphQLAuthor('discussion-commenter'); +const mockReplier = createMockGraphQLAuthor('discussion-replier'); describe('renderer/utils/notifications/handlers/discussion.ts', () => { + describe('mergeQueryConfig', () => { + it('should return the correct query merge type response fragments', () => { + const mergeType = discussionHandler.mergeQueryNodeResponseType; + + expect(mergeType).toBeDefined(); + expect(mergeType).toBe(DiscussionDetailsFragmentDoc); + }); + }); + describe('enrich', () => { const mockNotification = createPartialMockNotification({ title: 'This is a mock discussion', @@ -63,8 +73,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, comments: 0, @@ -97,8 +107,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, comments: 0, @@ -134,8 +144,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'DUPLICATE', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, comments: 0, @@ -175,8 +185,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, comments: 0, @@ -223,8 +233,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockCommenter.login, - htmlUrl: mockCommenter.htmlUrl, avatarUrl: mockCommenter.avatarUrl, + htmlUrl: mockCommenter.htmlUrl, type: mockCommenter.type, }, comments: 1, @@ -277,8 +287,8 @@ describe('renderer/utils/notifications/handlers/discussion.ts', () => { state: 'ANSWERED', user: { login: mockReplier.login, - htmlUrl: mockReplier.htmlUrl, avatarUrl: mockReplier.avatarUrl, + htmlUrl: mockReplier.htmlUrl, type: mockReplier.type, }, comments: 1, diff --git a/src/renderer/utils/notifications/handlers/discussion.ts b/src/renderer/utils/notifications/handlers/discussion.ts index f7290dc56..ef0849c7e 100644 --- a/src/renderer/utils/notifications/handlers/discussion.ts +++ b/src/renderer/utils/notifications/handlers/discussion.ts @@ -19,9 +19,11 @@ import { type SettingsState, } from '../../../types'; import { fetchDiscussionByNumber } from '../../api/client'; -import type { - CommentFieldsFragment, - DiscussionCommentFieldsFragment, +import { + type CommentFieldsFragment, + type DiscussionCommentFieldsFragment, + type DiscussionDetailsFragment, + DiscussionDetailsFragmentDoc, } from '../../api/graphql/generated/graphql'; import { DefaultHandler, defaultHandler } from './default'; import { getNotificationAuthor } from './utils'; @@ -29,12 +31,21 @@ import { getNotificationAuthor } from './utils'; class DiscussionHandler extends DefaultHandler { readonly type = 'Discussion'; + readonly mergeQueryNodeResponseType = DiscussionDetailsFragmentDoc; + async enrich( notification: GitifyNotification, _settings: SettingsState, + fetchedData?: DiscussionDetailsFragment, ): Promise> { - const response = await fetchDiscussionByNumber(notification); - const discussion = response.data.repository?.discussion; + // If no fetched data and no URL, we can't enrich - return empty + if (!fetchedData && !notification.subject.url) { + return {}; + } + + const discussion = + fetchedData ?? + (await fetchDiscussionByNumber(notification)).data.repository?.discussion; let discussionState: GitifyDiscussionState = 'OPEN'; @@ -59,7 +70,10 @@ class DiscussionHandler extends DefaultHandler { discussion.author, ]), comments: discussion.comments.totalCount, - labels: discussion.labels?.nodes.map((label) => label.name) ?? [], + labels: + discussion.labels?.nodes?.flatMap((label) => + label ? [label.name] : [], + ) ?? [], htmlUrl: latestDiscussionComment?.url ?? discussion.url, }; } diff --git a/src/renderer/utils/notifications/handlers/issue.test.ts b/src/renderer/utils/notifications/handlers/issue.test.ts index 24c5d7883..205e1b09a 100644 --- a/src/renderer/utils/notifications/handlers/issue.test.ts +++ b/src/renderer/utils/notifications/handlers/issue.test.ts @@ -6,7 +6,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; +import { createMockGraphQLAuthor } from '../../../__mocks__/user-mocks'; import type { GitifyNotification } from '../../../types'; import { type GitifyIssueState, @@ -14,17 +14,27 @@ import { IconColor, type Link, } from '../../../types'; -import type { - IssueDetailsFragment, - IssueState, - IssueStateReason, +import { + type IssueDetailsFragment, + IssueDetailsFragmentDoc, + type IssueState, + type IssueStateReason, } from '../../api/graphql/generated/graphql'; import { issueHandler } from './issue'; -const mockAuthor = createMockNotificationUser('issue-author'); -const mockCommenter = createMockNotificationUser('issue-commenter'); +const mockAuthor = createMockGraphQLAuthor('issue-author'); +const mockCommenter = createMockGraphQLAuthor('issue-commenter'); describe('renderer/utils/notifications/handlers/issue.ts', () => { + describe('mergeQueryConfig', () => { + it('should return the correct query and response fragments', () => { + const mergeType = issueHandler.mergeQueryNodeResponseType; + + expect(mergeType).toBeDefined(); + expect(mergeType).toBe(IssueDetailsFragmentDoc); + }); + }); + describe('enrich', () => { let mockNotification: GitifyNotification; @@ -64,14 +74,14 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, comments: 0, htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], - milestone: null, + milestone: undefined, } as Partial); }); @@ -98,14 +108,14 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'COMPLETED', user: { login: mockAuthor.login, + avatarUrl: mockCommenter.avatarUrl, htmlUrl: mockAuthor.htmlUrl, - avatarUrl: mockAuthor.avatarUrl, type: mockAuthor.type, }, comments: 0, htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: [], - milestone: null, + milestone: undefined, } as Partial); }); @@ -140,15 +150,15 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockCommenter.login, - htmlUrl: mockCommenter.htmlUrl, avatarUrl: mockCommenter.avatarUrl, + htmlUrl: mockCommenter.htmlUrl, type: mockCommenter.type, }, comments: 1, htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123#issuecomment-1234', labels: [], - milestone: null, + milestone: undefined, } as Partial); }); @@ -177,14 +187,14 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, comments: 0, htmlUrl: 'https://github.com/gitify-app/notifications-test/issues/123', labels: ['enhancement'], - milestone: null, + milestone: undefined, } as Partial); }); @@ -214,8 +224,8 @@ describe('renderer/utils/notifications/handlers/issue.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, comments: 0, diff --git a/src/renderer/utils/notifications/handlers/issue.ts b/src/renderer/utils/notifications/handlers/issue.ts index 13de90dd2..1f97f86ef 100644 --- a/src/renderer/utils/notifications/handlers/issue.ts +++ b/src/renderer/utils/notifications/handlers/issue.ts @@ -17,22 +17,30 @@ import type { } from '../../../types'; import { IconColor } from '../../../types'; import { fetchIssueByNumber } from '../../api/client'; +import { + type IssueDetailsFragment, + IssueDetailsFragmentDoc, +} from '../../api/graphql/generated/graphql'; import { DefaultHandler, defaultHandler } from './default'; import { getNotificationAuthor } from './utils'; class IssueHandler extends DefaultHandler { readonly type = 'Issue'; + readonly mergeQueryNodeResponseType = IssueDetailsFragmentDoc; + async enrich( notification: GitifyNotification, _settings: SettingsState, + fetchedData?: IssueDetailsFragment, ): Promise> { - const response = await fetchIssueByNumber(notification); - const issue = response.data.repository?.issue; + const issue = + fetchedData ?? + (await fetchIssueByNumber(notification)).data.repository?.issue; const issueState = issue.stateReason ?? issue.state; - const issueComment = issue.comments.nodes[0]; + const issueComment = issue.comments?.nodes?.[0]; const issueUser = getNotificationAuthor([ issueComment?.author, @@ -43,9 +51,11 @@ class IssueHandler extends DefaultHandler { number: issue.number, state: issueState, user: issueUser, - comments: issue.comments.totalCount, - labels: issue.labels?.nodes.map((label) => label.name), - milestone: issue.milestone, + comments: issue.comments?.totalCount ?? 0, + labels: + issue.labels?.nodes?.flatMap((label) => (label ? [label.name] : [])) ?? + undefined, + milestone: issue.milestone ?? undefined, htmlUrl: issueComment?.url ?? issue.url, }; } diff --git a/src/renderer/utils/notifications/handlers/pullRequest.test.ts b/src/renderer/utils/notifications/handlers/pullRequest.test.ts index d34d1740e..09093360c 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.test.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.test.ts @@ -6,7 +6,7 @@ import { createPartialMockNotification, } from '../../../__mocks__/notifications-mocks'; import { mockSettings } from '../../../__mocks__/state-mocks'; -import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; +import { createMockGraphQLAuthor } from '../../../__mocks__/user-mocks'; import type { GitifyNotification } from '../../../types'; import { type GitifyPullRequestState, @@ -14,15 +14,16 @@ import { IconColor, type Link, } from '../../../types'; -import type { - PullRequestDetailsFragment, - PullRequestReviewState, - PullRequestState, +import { + type PullRequestDetailsFragment, + PullRequestDetailsFragmentDoc, + type PullRequestReviewState, + type PullRequestState, } from '../../api/graphql/generated/graphql'; import { getLatestReviewForReviewers, pullRequestHandler } from './pullRequest'; -const mockAuthor = createMockNotificationUser('some-author'); -const mockCommenter = createMockNotificationUser('some-commenter'); +const mockAuthor = createMockGraphQLAuthor('some-author'); +const mockCommenter = createMockGraphQLAuthor('some-commenter'); describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { let mockNotification: GitifyNotification; @@ -37,6 +38,15 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { }); }); + describe('mergeQueryConfig', () => { + it('should return the correct query and response fragments', () => { + const mergeType = pullRequestHandler.mergeQueryNodeResponseType; + + expect(mergeType).toBeDefined(); + expect(mergeType).toBe(PullRequestDetailsFragmentDoc); + }); + }); + describe('enrich', () => { beforeEach(() => { // axios will default to using the XHR adapter which can't be intercepted @@ -67,8 +77,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'CLOSED', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, reviews: null, @@ -106,8 +116,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'DRAFT', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, reviews: null, @@ -145,8 +155,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'MERGE_QUEUE', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, reviews: null, @@ -184,8 +194,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'MERGED', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, reviews: null, @@ -231,8 +241,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockCommenter.login, - htmlUrl: mockCommenter.htmlUrl, avatarUrl: mockCommenter.avatarUrl, + htmlUrl: mockCommenter.htmlUrl, type: mockCommenter.type, }, reviews: null, @@ -277,8 +287,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, reviews: null, @@ -322,8 +332,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, reviews: null, @@ -364,8 +374,8 @@ describe('renderer/utils/notifications/handlers/pullRequest.ts', () => { state: 'OPEN', user: { login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }, reviews: null, diff --git a/src/renderer/utils/notifications/handlers/pullRequest.ts b/src/renderer/utils/notifications/handlers/pullRequest.ts index 5fc237f16..25538f250 100644 --- a/src/renderer/utils/notifications/handlers/pullRequest.ts +++ b/src/renderer/utils/notifications/handlers/pullRequest.ts @@ -19,19 +19,27 @@ import { type SettingsState, } from '../../../types'; import { fetchPullByNumber } from '../../api/client'; -import type { PullRequestReviewFieldsFragment } from '../../api/graphql/generated/graphql'; +import { + type PullRequestDetailsFragment, + PullRequestDetailsFragmentDoc, + type PullRequestReviewFieldsFragment, +} from '../../api/graphql/generated/graphql'; import { DefaultHandler, defaultHandler } from './default'; import { getNotificationAuthor } from './utils'; class PullRequestHandler extends DefaultHandler { readonly type = 'PullRequest' as const; + readonly mergeQueryNodeResponseType = PullRequestDetailsFragmentDoc; + async enrich( notification: GitifyNotification, _settings: SettingsState, + fetchedData?: PullRequestDetailsFragment, ): Promise> { - const response = await fetchPullByNumber(notification); - const pr = response.data.repository.pullRequest; + const pr = + fetchedData ?? + (await fetchPullByNumber(notification)).data.repository?.pullRequest; let prState: GitifyPullRequestState = pr.state; if (pr.isDraft) { @@ -108,7 +116,7 @@ export function getLatestReviewForReviewers( } // Find the most recent review for each reviewer - const latestReviews = []; + const latestReviews: PullRequestReviewFieldsFragment[] = []; const sortedReviews = reviews.toReversed(); for (const prReview of sortedReviews) { const reviewerFound = latestReviews.find( diff --git a/src/renderer/utils/notifications/handlers/types.ts b/src/renderer/utils/notifications/handlers/types.ts index dd8477085..2386f7cd0 100644 --- a/src/renderer/utils/notifications/handlers/types.ts +++ b/src/renderer/utils/notifications/handlers/types.ts @@ -9,16 +9,27 @@ import type { SettingsState, SubjectType, } from '../../../types'; +import type { TypedDocumentString } from '../../api/graphql/generated/graphql'; export interface NotificationTypeHandler { readonly type?: SubjectType; /** - * Enrich a notification. Settings may be unused for some handlers. + * The merge query response type to expect. + */ + readonly mergeQueryNodeResponseType?: TypedDocumentString; + + /** + * Enriches a base notification with additional information (state, author, metrics, etc). + * + * @param notification The base notification being enriched + * @param settings The app settings, which for some handlers may not be used during enrichment. + * @param fetchedData Previously fetched enrichment data (upstream). If present, then enrich will skip fetching detailed data inline. */ enrich( notification: GitifyNotification, settings: SettingsState, + fetchedData?: unknown, ): Promise>; /** diff --git a/src/renderer/utils/notifications/handlers/utils.test.ts b/src/renderer/utils/notifications/handlers/utils.test.ts index 478d0cba0..3d4e98d66 100644 --- a/src/renderer/utils/notifications/handlers/utils.test.ts +++ b/src/renderer/utils/notifications/handlers/utils.test.ts @@ -1,9 +1,9 @@ -import { createMockNotificationUser } from '../../../__mocks__/user-mocks'; +import { createMockGraphQLAuthor } from '../../../__mocks__/user-mocks'; import { formatForDisplay, getNotificationAuthor } from './utils'; describe('renderer/utils/notifications/handlers/utils.ts', () => { describe('getNotificationAuthor', () => { - const mockAuthor = createMockNotificationUser('some-author'); + const mockAuthor = createMockGraphQLAuthor('some-author'); it('returns null when all users are null', () => { const result = getNotificationAuthor([null, null]); @@ -16,8 +16,8 @@ describe('renderer/utils/notifications/handlers/utils.ts', () => { expect(result).toEqual({ login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }); }); @@ -27,8 +27,8 @@ describe('renderer/utils/notifications/handlers/utils.ts', () => { expect(result).toEqual({ login: mockAuthor.login, - htmlUrl: mockAuthor.htmlUrl, avatarUrl: mockAuthor.avatarUrl, + htmlUrl: mockAuthor.htmlUrl, type: mockAuthor.type, }); }); diff --git a/src/renderer/utils/notifications/handlers/utils.ts b/src/renderer/utils/notifications/handlers/utils.ts index e46da63e2..cc295db07 100644 --- a/src/renderer/utils/notifications/handlers/utils.ts +++ b/src/renderer/utils/notifications/handlers/utils.ts @@ -1,4 +1,12 @@ -import type { GitifyNotificationUser } from '../../../types'; +import type { GitifyNotificationUser, UserType } from '../../../types'; +import type { AuthorFieldsFragment } from '../../api/graphql/generated/graphql'; + +// Author type from GraphQL or manually constructed +type AuthorInput = + | AuthorFieldsFragment + | GitifyNotificationUser + | null + | undefined; /** * Construct the notification subject user based on an order prioritized list of users @@ -6,7 +14,7 @@ import type { GitifyNotificationUser } from '../../../types'; * @returns the subject user */ export function getNotificationAuthor( - users: GitifyNotificationUser[], + users: AuthorInput[], ): GitifyNotificationUser { let subjectUser: GitifyNotificationUser = null; @@ -14,9 +22,9 @@ export function getNotificationAuthor( if (user) { subjectUser = { login: user.login, - htmlUrl: user.htmlUrl, avatarUrl: user.avatarUrl, - type: user.type, + htmlUrl: user.htmlUrl, + type: user.type as UserType, }; return subjectUser; diff --git a/src/renderer/utils/notifications/notifications.test.ts b/src/renderer/utils/notifications/notifications.test.ts index 6fd71e952..548b448b0 100644 --- a/src/renderer/utils/notifications/notifications.test.ts +++ b/src/renderer/utils/notifications/notifications.test.ts @@ -13,6 +13,7 @@ import { import { mockSettings } from '../../__mocks__/state-mocks'; import { type AccountNotifications, + type GitifyNotification, type GitifyRepository, GroupBy, type Link, @@ -21,6 +22,7 @@ import { import * as logger from '../../utils/logger'; import { enrichNotification, + enrichNotifications, getNotificationCount, getUnreadNotificationCount, stabilizeNotificationsOrder, @@ -141,4 +143,51 @@ describe('renderer/utils/notifications/notifications.ts', () => { ).toEqual([0, 2, 1, 3, 5, 4]); }); }); + + describe('enrichNotifications', () => { + it('should skip enrichment when detailedNotifications is false', async () => { + const notification = createPartialMockNotification({ + title: 'Issue #1', + type: 'Issue', + url: 'https://api.github.com/repos/gitify-app/notifications-test/issues/1' as Link, + }) as GitifyNotification; + const settings: SettingsState = { + ...mockSettings, + detailedNotifications: false, + }; + + const result = await enrichNotifications([notification], settings); + + expect(result).toEqual([notification]); + }); + + it('should return notifications when all types do not support merge query', async () => { + // CheckSuite types don't support merge query and have no URL + const notification = createPartialMockNotification({ + title: 'CI workflow run', + type: 'CheckSuite', + url: null, + }) as GitifyNotification; + const settings: SettingsState = { + ...mockSettings, + detailedNotifications: true, + }; + + const result = await enrichNotifications([notification], settings); + + expect(result).toHaveLength(1); + expect(result[0].subject.title).toBe('CI workflow run'); + }); + + it('should handle empty notifications array', async () => { + const settings: SettingsState = { + ...mockSettings, + detailedNotifications: true, + }; + + const result = await enrichNotifications([], settings); + + expect(result).toEqual([]); + }); + }); }); diff --git a/src/renderer/utils/notifications/notifications.ts b/src/renderer/utils/notifications/notifications.ts index e242157cd..b1d80e607 100644 --- a/src/renderer/utils/notifications/notifications.ts +++ b/src/renderer/utils/notifications/notifications.ts @@ -5,9 +5,21 @@ import type { GitifySubject, SettingsState, } from '../../types'; -import { listNotificationsForAuthenticatedUser } from '../api/client'; +import { + fetchMergedQueryDetails, + listNotificationsForAuthenticatedUser, +} from '../api/client'; import { determineFailureType } from '../api/errors'; +import { BatchMergedDetailsQueryTemplateFragmentDoc } from '../api/graphql/generated/graphql'; +import { MergeQueryBuilder } from '../api/graphql/MergeQueryBuilder'; +import { + aliasRootAndKeyVariables, + extractNonQueryFragments, + extractQueryFragments, +} from '../api/graphql/utils'; import { transformNotification } from '../api/transform'; +import { getNumberFromUrl } from '../api/utils'; +import { isAnsweredDiscussionFeatureSupported } from '../features'; import { rendererLogError, rendererLogWarn } from '../logger'; import { filterBaseNotifications, @@ -129,12 +141,139 @@ export async function enrichNotifications( return notifications; } + const builder = new MergeQueryBuilder(); + const targets: Array<{ + rootAlias: string; + notification: GitifyNotification; + handler: ReturnType; + }> = []; + + let index = 0; + for (const notification of notifications) { + const handler = createNotificationHandler(notification); + const mergeType = handler.mergeQueryNodeResponseType; + + // Skip notification types that aren't suitable for batch merged enrichment + if (!mergeType) { + continue; + } + + /** + * To construct the graphql query, we need to + * 1 - extract the indexed arguments and rename them + * 2 - initialize the indexed argument values + * 3 - extract the global arguments + * 4 - initialize the global argument values + * 5 - construct the merged query using the utility helper + * 6 - map the response to the correct handler mergeType before parsing into handler enrich + **/ + + const org = notification.repository.owner.login; + const repo = notification.repository.name; + const number = getNumberFromUrl(notification.subject.url); + const isNotificationDiscussion = notification.subject.type === 'Discussion'; + const isNotificationIssue = notification.subject.type === 'Issue'; + const isNotificationPullRequest = + notification.subject.type === 'PullRequest'; + + const rootAlias = `node${index}`; + + const queryFragmentBody = extractQueryFragments( + BatchMergedDetailsQueryTemplateFragmentDoc, + )[0].inner; + + const queryFragment = aliasRootAndKeyVariables( + rootAlias, + index, + queryFragmentBody, + ); + builder.addSelection(queryFragment); + + // TODO - Extract this from the BatchMergedDetailsQueryTemplateFragmentDoc + builder.addVariableDefs( + `$owner${index}: String!, $name${index}: String!, $number${index}: Int!, $isDiscussionNotification${index}: Boolean!, $isIssueNotification${index}: Boolean!, $isPullRequestNotification${index}: Boolean!`, + ); + builder + .setVar(`owner${index}`, org) + .setVar(`name${index}`, repo) + .setVar(`number${index}`, number) + .setVar(`isDiscussionNotification${index}`, isNotificationDiscussion) + .setVar(`isIssueNotification${index}`, isNotificationIssue) + .setVar(`isPullRequestNotification${index}`, isNotificationPullRequest); + + targets.push({ rootAlias, notification, handler }); + + index += 1; + } + + const nonQueryFragments = extractNonQueryFragments( + BatchMergedDetailsQueryTemplateFragmentDoc, + ); + builder.addFragments(nonQueryFragments); + + // TODO - Extract this from the BatchMergedDetailsQueryTemplateFragmentDoc + builder.addVariableDefs( + '$lastComments: Int, $lastThreadedComments: Int, $lastReplies: Int, $lastReviews: Int, $firstLabels: Int, $firstClosingIssues: Int, $includeIsAnswered: Boolean!', + ); + + const mergedQuery = builder.buildQuery(); + + // TODO - consolidate static args into constants, refactor below and other graphql query variables in api/clients to be consistent + builder + .setVar('firstLabels', 100) + .setVar('lastComments', 1) + .setVar('lastThreadedComments', 10) + .setVar('lastReplies', 10) + .setVar( + 'includeIsAnswered', + isAnsweredDiscussionFeatureSupported(notifications[0].account), + ) + .setVar('firstClosingIssues', 100) + .setVar('lastReviews', 100); + const queryVariables = builder.getVariables(); + + let mergedData: Record | null = null; + + try { + const response = await fetchMergedQueryDetails( + notifications[0], + mergedQuery, + queryVariables, + ); + + mergedData = response.data; + } catch (err) { + rendererLogError( + 'enrichNotifications', + 'Failed to fetch merged notification details', + err, + ); + } + const enrichedNotifications = await Promise.all( notifications.map(async (notification: GitifyNotification) => { - return enrichNotification(notification, settings); + const target = targets.find((item) => item.notification === notification); + + // TODO - simplify the below where possible + let fragment: unknown; + if (mergedData && target) { + const repoData = mergedData[target.rootAlias] as Record< + string, + unknown + >; + if (repoData) { + for (const value of Object.values(repoData)) { + if (value !== undefined) { + fragment = value; + break; + } + } + } + } + + return enrichNotification(notification, settings, fragment); }), ); - return enrichedNotifications; } @@ -148,12 +287,17 @@ export async function enrichNotifications( export async function enrichNotification( notification: GitifyNotification, settings: SettingsState, + fetchedData?: unknown, ): Promise { let additionalSubjectDetails: Partial = {}; try { const handler = createNotificationHandler(notification); - additionalSubjectDetails = await handler.enrich(notification, settings); + additionalSubjectDetails = await handler.enrich( + notification, + settings, + fetchedData, + ); } catch (err) { rendererLogError( 'enrichNotification',