Commit 354fa99

Anton Medvedev <anton@medv.io>
2024-07-10 22:59:27
Use graphql-megaera
1 parent 2d74c06
badges/most-reactions/most-reactions.ts
@@ -34,10 +34,10 @@ export default define({
     }
 
     for (const discussion of data.discussionComments) {
-      if (discussion.reactions.totalCount > 0) {
+      if (discussion.reactions.totalCount > 0 && discussion.discussion) {
         reactions[discussion.discussion.repository.nameWithOwner] = {
           count: discussion.reactions.totalCount,
-          repository: discussion.discussion.repository.nameWithOwner,
+          repository: discussion.discussion?.repository.nameWithOwner,
         }
       }
     }
badges/my-badges-contributor/my-badges-contributor.ts
@@ -1,10 +1,10 @@
-import { define, Pull } from '#src'
+import { define, PullRequest } from '#src'
 
 export default define({
   url: import.meta.url,
   badges: ['my-badges-contributor'] as const,
   present(data, grant) {
-    const pulls: Pull[] = []
+    const pulls: PullRequest[] = []
     for (const pull of data.pulls) {
       if (
         pull.repository.name === 'my-badges' &&
badges/old-issue/old-issue.ts
@@ -21,7 +21,7 @@ export default define({
     for (const issue of data.issues.sort(age)) {
       if (!issue.closed) continue
       const createdAt = new Date(issue.createdAt)
-      const closedAt = new Date(issue.closedAt)
+      const closedAt = new Date(issue.closedAt!)
       let years = Math.floor(
         (closedAt.getTime() - createdAt.getTime()) / 1000 / 60 / 60 / 24 / 365,
       )
@@ -47,5 +47,5 @@ export default define({
 })
 
 function age(a: Issue, b: Issue) {
-  return new Date(a.closedAt).getTime() - new Date(b.closedAt).getTime()
+  return new Date(a.closedAt!).getTime() - new Date(b.closedAt!).getTime()
 }
badges/pr-collaboration/pr-collaboration.ts
@@ -1,4 +1,4 @@
-import { define, Pull } from '#src'
+import { define, PullRequest } from '#src'
 
 export default define({
   url: import.meta.url,
@@ -56,6 +56,6 @@ export default define({
   },
 })
 
-function byParticipantsCount(a: Pull, b: Pull) {
+function byParticipantsCount(a: PullRequest, b: PullRequest) {
   return a.participants.totalCount - b.participants.totalCount
 }
badges/the-ultimate-question/the-ultimate-question.ts
@@ -1,10 +1,10 @@
-import { define, Issue, Pull } from '#src'
+import { define, Issue, PullRequest } from '#src'
 
 export default define({
   url: import.meta.url,
   badges: ['the-ultimate-question'] as const,
   present(data, grant) {
-    const list: (Issue | Pull)[] = []
+    const list: (Issue | PullRequest)[] = []
 
     for (const issue of data.issues) {
       if (issue.number == 42) list.push(issue)
@@ -22,6 +22,6 @@ export default define({
   },
 })
 
-function link(x: Issue | Pull): string {
+function link(x: Issue | PullRequest): string {
   return `<a href="https://github.com/${x.repository.owner.login}/${x.repository.name}/issues/${x.number}">#${x.number}</a>`
 }
badges/this-is-fine/this-is-fine.ts
@@ -1,27 +1,27 @@
-import { define, Pull } from '#src'
+import { define, PullRequest } from '#src'
 
 export default define({
   url: import.meta.url,
   badges: ['this-is-fine'] as const,
   present(data, grant) {
-    const pulls: Pull[] = []
+    const pulls: PullRequest[] = []
 
     for (const pull of data.pulls) {
       if (!pull.merged) continue
       if (pull.mergedBy?.login != data.user.login) continue
 
-      const commit = pull.lastCommit.nodes[0]?.commit
+      const commit = pull.lastCommit.nodes?.[0]?.commit
       if (!commit) continue
 
-      const checkRuns = commit.checkSuites.nodes.flatMap(
-        (x) => x.lastCheckRun.nodes,
+      const checkRuns = commit.checkSuites?.nodes?.flatMap(
+        (x) => x.lastCheckRun?.nodes,
       )
-      if (checkRuns.length == 0) continue
+      if (!checkRuns || checkRuns?.length == 0) continue
       const successCount = checkRuns.filter(
-        (x) => x.conclusion == 'SUCCESS',
+        (x) => x?.conclusion == 'SUCCESS',
       ).length
       const failureCount = checkRuns.filter(
-        (x) => x.conclusion == 'FAILURE',
+        (x) => x?.conclusion == 'FAILURE',
       ).length
 
       if (successCount <= failureCount) {
src/collect/comments.graphql
@@ -0,0 +1,99 @@
+fragment DiscussionComment on DiscussionComment {
+  url
+  author {
+    login
+  }
+  discussion {
+    number
+    repository {
+      nameWithOwner
+    }
+    author {
+      login
+    }
+  }
+  body
+  createdAt
+  updatedAt
+  editor {
+    login
+  }
+  ...Reactions
+}
+
+fragment IssueComment on IssueComment {
+  url
+  author {
+    login
+  }
+  repository {
+    nameWithOwner
+  }
+  issue {
+    number
+    author {
+      login
+    }
+  }
+  body
+  createdAt
+  updatedAt
+  editor {
+    login
+  }
+  ...Reactions
+}
+
+fragment Reactions on Reactable {
+  reactions(first: 100) {
+    totalCount
+    nodes {
+      content
+      user {
+        login
+      }
+    }
+  }
+}
+
+query DiscussionCommentsQuery($login: String!, $num: Int = 100, $cursor: String) {
+  user(login: $login) {
+    repositoryDiscussionComments(first: $num, after: $cursor) {
+      totalCount
+      nodes {
+        ...DiscussionComment
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
+
+query IssueCommentsQuery($login: String!, $num: Int = 100, $cursor: String) {
+  user(login: $login) {
+    issueComments(first: $num, after: $cursor) {
+      totalCount
+      nodes {
+        ...IssueComment
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
src/collect/comments.graphql.ts
@@ -0,0 +1,210 @@
+// DO NOT EDIT. This file was generated by megaera.
+
+const DiscussionComment = `#graphql
+fragment DiscussionComment on DiscussionComment {
+  url
+  author {
+    login
+  }
+  discussion {
+    number
+    repository {
+      nameWithOwner
+    }
+    author {
+      login
+    }
+  }
+  body
+  createdAt
+  updatedAt
+  editor {
+    login
+  }
+  ...Reactions
+}`
+
+export type DiscussionComment = {
+  url: string
+  author: {
+    login: string
+  } | null
+  discussion: {
+    number: number
+    repository: {
+      nameWithOwner: string
+    }
+    author: {
+      login: string
+    } | null
+  } | null
+  body: string
+  createdAt: string
+  updatedAt: string
+  editor: {
+    login: string
+  } | null
+} & Reactions
+
+
+const IssueComment = `#graphql
+fragment IssueComment on IssueComment {
+  url
+  author {
+    login
+  }
+  repository {
+    nameWithOwner
+  }
+  issue {
+    number
+    author {
+      login
+    }
+  }
+  body
+  createdAt
+  updatedAt
+  editor {
+    login
+  }
+  ...Reactions
+}`
+
+export type IssueComment = {
+  url: string
+  author: {
+    login: string
+  } | null
+  repository: {
+    nameWithOwner: string
+  }
+  issue: {
+    number: number
+    author: {
+      login: string
+    } | null
+  }
+  body: string
+  createdAt: string
+  updatedAt: string
+  editor: {
+    login: string
+  } | null
+} & Reactions
+
+
+const Reactions = `#graphql
+fragment Reactions on Reactable {
+  reactions(first: 100) {
+    totalCount
+    nodes {
+      content
+      user {
+        login
+      }
+    }
+  }
+}`
+
+export type Reactions = {
+  reactions: {
+    totalCount: number
+    nodes: Array<{
+      content: 'CONFUSED' | 'EYES' | 'HEART' | 'HOORAY' | 'LAUGH' | 'ROCKET' | 'THUMBS_DOWN' | 'THUMBS_UP'
+      user: {
+        login: string
+      } | null
+    }> | null
+  }
+}
+
+
+export const DiscussionCommentsQuery = `#graphql
+${Reactions}
+${IssueComment}
+${DiscussionComment}
+query DiscussionCommentsQuery($login: String!, $num: Int = 100, $cursor: String) {
+  user(login: $login) {
+    repositoryDiscussionComments(first: $num, after: $cursor) {
+      totalCount
+      nodes {
+        ...DiscussionComment
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}` as string & DiscussionCommentsQuery
+
+export type DiscussionCommentsQuery = (vars: { login: string, num?: number | null, cursor?: string | null }) => {
+  user: {
+    repositoryDiscussionComments: {
+      totalCount: number
+      nodes: Array<{} & DiscussionComment> | null
+      pageInfo: {
+        hasNextPage: boolean
+        endCursor: string | null
+      }
+    }
+  } | null
+  rateLimit: {
+    limit: number
+    cost: number
+    remaining: number
+    resetAt: string
+  } | null
+}
+
+
+export const IssueCommentsQuery = `#graphql
+${Reactions}
+${IssueComment}
+${DiscussionComment}
+query IssueCommentsQuery($login: String!, $num: Int = 100, $cursor: String) {
+  user(login: $login) {
+    issueComments(first: $num, after: $cursor) {
+      totalCount
+      nodes {
+        ...IssueComment
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}` as string & IssueCommentsQuery
+
+export type IssueCommentsQuery = (vars: { login: string, num?: number | null, cursor?: string | null }) => {
+  user: {
+    issueComments: {
+      totalCount: number
+      nodes: Array<{} & IssueComment> | null
+      pageInfo: {
+        hasNextPage: boolean
+        endCursor: string | null
+      }
+    }
+  } | null
+  rateLimit: {
+    limit: number
+    cost: number
+    remaining: number
+    resetAt: string
+  } | null
+}
src/collect/commits.graphql
@@ -0,0 +1,58 @@
+fragment Commit on Commit {
+  sha: oid
+  committedDate
+  message
+  messageBody
+  additions
+  deletions
+  author {
+    user {
+      login
+    }
+  }
+  committer {
+    user {
+      login
+    }
+  }
+  repository {
+    owner {
+      login
+    }
+    name
+  }
+}
+
+query CommitsQuery(
+  $owner: String!
+  $name: String!
+  $author: ID!
+  $num: Int = 100
+  $cursor: String
+) {
+  repository(owner: $owner, name: $name) {
+    defaultBranchRef {
+      target {
+        ... on Commit {
+          history(first: $num, after: $cursor, author: { id: $author }) {
+            totalCount
+            nodes {
+              ...Commit
+            }
+            pageInfo {
+              hasNextPage
+              endCursor
+            }
+          }
+        }
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
+
src/collect/commits.graphql.ts
@@ -0,0 +1,105 @@
+// DO NOT EDIT. This file was generated by megaera.
+
+const Commit = `#graphql
+fragment Commit on Commit {
+  sha: oid
+  committedDate
+  message
+  messageBody
+  additions
+  deletions
+  author {
+    user {
+      login
+    }
+  }
+  committer {
+    user {
+      login
+    }
+  }
+  repository {
+    owner {
+      login
+    }
+    name
+  }
+}`
+
+export type Commit = {
+  sha: string
+  committedDate: string
+  message: string
+  messageBody: string
+  additions: number
+  deletions: number
+  author: {
+    user: {
+      login: string
+    } | null
+  } | null
+  committer: {
+    user: {
+      login: string
+    } | null
+  } | null
+  repository: {
+    owner: {
+      login: string
+    }
+    name: string
+  }
+}
+
+
+export const CommitsQuery = `#graphql
+${Commit}
+query CommitsQuery($owner: String!, $name: String!, $author: ID!, $num: Int = 100, $cursor: String) {
+  repository(owner: $owner, name: $name) {
+    defaultBranchRef {
+      target {
+        ... on Commit {
+          history(first: $num, after: $cursor, author: {id: $author}) {
+            totalCount
+            nodes {
+              ...Commit
+            }
+            pageInfo {
+              hasNextPage
+              endCursor
+            }
+          }
+        }
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}` as string & CommitsQuery
+
+export type CommitsQuery = (vars: { owner: string, name: string, author: string, num?: number | null, cursor?: string | null }) => {
+  repository: {
+    defaultBranchRef: {
+      target: {} & {
+        history: {
+          totalCount: number
+          nodes: Array<{} & Commit> | null
+          pageInfo: {
+            hasNextPage: boolean
+            endCursor: string | null
+          }
+        }
+      } | null | null
+    } | null
+  } | null
+  rateLimit: {
+    limit: number
+    cost: number
+    remaining: number
+    resetAt: string
+  } | null
+}
src/collect/commits.ts
@@ -1,101 +0,0 @@
-export const commitsQuery = `#graphql
-query CommitsQuery(
-  $owner: String!
-  $name: String!
-  $author: ID!
-  $num: Int = 100
-  $cursor: String
-) {
-  repository(owner: $owner, name: $name) {
-    defaultBranchRef {
-      target {
-        ... on Commit {
-          history(first: $num, after: $cursor, author: { id: $author }) {
-            totalCount
-            nodes {
-              sha: oid
-              committedDate
-              message
-              messageBody
-              additions
-              deletions
-              author {
-                user {
-                  login
-                }
-              }
-              committer {
-                user {
-                  login
-                }
-              }
-              repository {
-                owner {
-                  login
-                }
-                name
-              }
-            }
-            pageInfo {
-              hasNextPage
-              endCursor
-            }
-          }
-        }
-      }
-    }
-  }
-  rateLimit {
-    limit
-    cost
-    remaining
-    resetAt
-  }
-}
-`
-
-export type CommitsQuery = {
-  repository: {
-    defaultBranchRef: {
-      target: {
-        history: {
-          totalCount: number
-          nodes: Array<{
-            sha: string
-            committedDate: string
-            message: string
-            messageBody: string
-            additions: number
-            deletions: number
-            author: {
-              user: {
-                login: string
-              }
-            }
-            committer: {
-              user: {
-                login: string
-              }
-            }
-            repository: {
-              owner: {
-                login: string
-              }
-              name: string
-            }
-          }>
-          pageInfo: {
-            hasNextPage: boolean
-            endCursor: string
-          }
-        }
-      }
-    }
-  }
-  rateLimit: {
-    limit: number
-    cost: number
-    remaining: number
-    resetAt: string
-  }
-}
src/collect/discussion-comments.ts
@@ -1,91 +0,0 @@
-import { Reactions } from './types.js'
-
-export const discussionCommentsQuery = `#graphql
-query DiscussionCommentsQuery($login: String!, $num: Int = 100, $cursor: String) {
-  user(login: $login) {
-    repositoryDiscussionComments(first: $num, after: $cursor) {
-      totalCount
-      nodes {
-        url
-        author {
-          login
-        }
-        discussion {
-          number
-          repository {
-            nameWithOwner
-          }
-          author {
-            login
-          }
-        }
-        body
-        createdAt
-        updatedAt
-        editor {
-          login
-        }
-        reactions(first: 100) {
-          totalCount
-          nodes {
-            content
-            user {
-              login
-            }
-          }
-        }
-      }
-      pageInfo {
-        hasNextPage
-        endCursor
-      }
-    }
-  }
-  rateLimit {
-    limit
-    cost
-    remaining
-    resetAt
-  }
-}
-`
-
-export type DiscussionCommentsQuery = {
-  user: {
-    repositoryDiscussionComments: {
-      totalCount: number
-      nodes: Array<{
-        url: string
-        author: {
-          login: string
-        }
-        discussion: {
-          number: number
-          repository: {
-            nameWithOwner: string
-          }
-          author: {
-            login: string
-          }
-        }
-        body: string
-        createdAt: string
-        updatedAt: string
-        editor: {
-          login: string
-        } | null
-        reactions: Reactions
-      }>
-      pageInfo: {
-        hasNextPage: boolean
-        endCursor: string
-      }
-    }
-  }
-  rateLimit: {
-    limit: number
-    cost: number
-    remaining: number
-    resetAt: string
-  }
-}
src/collect/index.ts
@@ -0,0 +1,22 @@
+import { Endpoints } from '@octokit/types'
+import { User } from './user.graphql.js'
+import { PullRequest } from './pulls.graphql.js'
+import { Issue } from './issues.graphql.js'
+import { DiscussionComment, IssueComment } from './comments.graphql.js'
+import { StarredRepo } from './stars.graphql.js'
+import { Commit } from './commits.graphql.js'
+
+export type Data = {
+  user: User
+  starredRepositories: StarredRepo[]
+  repos: Repo[]
+  pulls: PullRequest[]
+  issues: Issue[]
+  issueComments: IssueComment[]
+  discussionComments: DiscussionComment[]
+}
+
+export type Repo =
+  Endpoints['GET /users/{username}/repos']['response']['data'][0] & {
+    commits: Commit[]
+  }
src/collect/issue-comments.ts
@@ -1,91 +0,0 @@
-import { Reactions } from './types.js'
-
-export const issueCommentsQuery = `#graphql
-query IssueCommentsQuery($login: String!, $num: Int = 100, $cursor: String) {
-  user(login: $login) {
-    issueComments(first: $num, after: $cursor) {
-      totalCount
-      nodes {
-        url
-        author {
-          login
-        }
-        repository {
-          nameWithOwner
-        }
-        issue {
-          number
-          author {
-            login
-          }
-        }
-        body
-        createdAt
-        updatedAt
-        editor {
-          login
-        }
-        reactions(first: 100) {
-          totalCount
-          nodes {
-            content
-            user {
-              login
-            }
-          }
-        }
-      }
-      pageInfo {
-        hasNextPage
-        endCursor
-      }
-    }
-  }
-  rateLimit {
-    limit
-    cost
-    remaining
-    resetAt
-  }
-}
-`
-
-export type IssueCommentsQuery = {
-  user: {
-    issueComments: {
-      totalCount: number
-      nodes: Array<{
-        url: string
-        author: {
-          login: string
-        }
-        repository: {
-          nameWithOwner: string
-        }
-        issue: {
-          number: number
-          author: {
-            login: string
-          }
-        }
-        body: string
-        createdAt: string
-        updatedAt: string
-        editor: {
-          login: string
-        } | null
-        reactions: Reactions
-      }>
-      pageInfo: {
-        hasNextPage: boolean
-        endCursor: string
-      }
-    }
-  }
-  rateLimit: {
-    limit: number
-    cost: number
-    remaining: number
-    resetAt: string
-  }
-}
src/collect/issue-timeline.graphql
@@ -0,0 +1,28 @@
+query IssueTimelineQuery($owner: String!, $name: String!, $number: Int!, $num: Int = 100, $cursor: String) {
+  repository(owner: $owner, name: $name) {
+    issue(number: $number) {
+      timelineItems(first: $num, after: $cursor) {
+        totalCount
+        nodes {
+          __typename
+          ... on ClosedEvent {
+            createdAt
+            actor {
+              login
+            }
+          }
+        }
+        pageInfo {
+          hasNextPage
+          endCursor
+        }
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
src/collect/issue-timeline.ts → src/collect/issue-timeline.graphql.ts
@@ -1,4 +1,6 @@
-export const issueTimelineQuery = `#graphql
+// DO NOT EDIT. This file was generated by megaera.
+
+export const IssueTimelineQuery = `#graphql
 query IssueTimelineQuery($owner: String!, $name: String!, $number: Int!, $num: Int = 100, $cursor: String) {
   repository(owner: $owner, name: $name) {
     issue(number: $number) {
@@ -26,32 +28,32 @@ query IssueTimelineQuery($owner: String!, $name: String!, $number: Int!, $num: I
     remaining
     resetAt
   }
-}
-`
+}` as string & IssueTimelineQuery
 
-export type IssueTimelineQuery = {
+export type IssueTimelineQuery = (vars: { owner: string, name: string, number: number, num?: number | null, cursor?: string | null }) => {
   repository: {
     issue: {
       timelineItems: {
         totalCount: number
         nodes: Array<{
           __typename: string
+        } & {
           createdAt: string
           actor: {
             login: string
-          }
-        }>
+          } | null
+        } | null> | null
         pageInfo: {
           hasNextPage: boolean
-          endCursor: string
+          endCursor: string | null
         }
       }
-    }
-  }
+    } | null
+  } | null
   rateLimit: {
     limit: number
     cost: number
     remaining: number
     resetAt: string
-  }
+  } | null
 }
src/collect/issues.graphql
@@ -0,0 +1,61 @@
+fragment Issue on Issue {
+  createdAt
+  closedAt
+  closed
+  author {
+    login
+  }
+  url
+  number
+  title
+  labels(first: 10) {
+    totalCount
+    nodes {
+      name
+    }
+  }
+  body
+  comments(first: 1) {
+    totalCount
+  }
+  reactions(first: 100) {
+    totalCount
+    nodes {
+      content
+      user {
+        login
+      }
+    }
+  }
+  assignees(first: 3) {
+    totalCount
+  }
+  repository {
+    nameWithOwner
+    owner {
+      login
+    }
+    name
+  }
+}
+
+query IssuesQuery($username: String!, $num: Int = 100, $cursor: String) {
+  user(login: $username) {
+    issues(first: $num, after: $cursor, filterBy: { createdBy: $username }) {
+      totalCount
+      nodes {
+        ...Issue
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
src/collect/issues.graphql.ts
@@ -0,0 +1,127 @@
+// DO NOT EDIT. This file was generated by megaera.
+
+const Issue = `#graphql
+fragment Issue on Issue {
+  createdAt
+  closedAt
+  closed
+  author {
+    login
+  }
+  url
+  number
+  title
+  labels(first: 10) {
+    totalCount
+    nodes {
+      name
+    }
+  }
+  body
+  comments(first: 1) {
+    totalCount
+  }
+  reactions(first: 100) {
+    totalCount
+    nodes {
+      content
+      user {
+        login
+      }
+    }
+  }
+  assignees(first: 3) {
+    totalCount
+  }
+  repository {
+    nameWithOwner
+    owner {
+      login
+    }
+    name
+  }
+}`
+
+export type Issue = {
+  createdAt: string
+  closedAt: string | null
+  closed: boolean
+  author: {
+    login: string
+  } | null
+  url: string
+  number: number
+  title: string
+  labels: {
+    totalCount: number
+    nodes: Array<{
+      name: string
+    }> | null
+  } | null
+  body: string
+  comments: {
+    totalCount: number
+  }
+  reactions: {
+    totalCount: number
+    nodes: Array<{
+      content: 'CONFUSED' | 'EYES' | 'HEART' | 'HOORAY' | 'LAUGH' | 'ROCKET' | 'THUMBS_DOWN' | 'THUMBS_UP'
+      user: {
+        login: string
+      } | null
+    }> | null
+  }
+  assignees: {
+    totalCount: number
+  }
+  repository: {
+    nameWithOwner: string
+    owner: {
+      login: string
+    }
+    name: string
+  }
+}
+
+
+export const IssuesQuery = `#graphql
+${Issue}
+query IssuesQuery($username: String!, $num: Int = 100, $cursor: String) {
+  user(login: $username) {
+    issues(first: $num, after: $cursor, filterBy: {createdBy: $username}) {
+      totalCount
+      nodes {
+        ...Issue
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}` as string & IssuesQuery
+
+export type IssuesQuery = (vars: { username: string, num?: number | null, cursor?: string | null }) => {
+  user: {
+    issues: {
+      totalCount: number
+      nodes: Array<{} & Issue> | null
+      pageInfo: {
+        hasNextPage: boolean
+        endCursor: string | null
+      }
+    }
+  } | null
+  rateLimit: {
+    limit: number
+    cost: number
+    remaining: number
+    resetAt: string
+  } | null
+}
src/collect/issues.ts
@@ -1,112 +0,0 @@
-import { Extra, Reactions } from './types.js'
-
-export const issuesQuery = `#graphql
-query IssuesQuery($username: String!, $num: Int = 100, $cursor: String) {
-  user(login: $username) {
-    issues(first: $num, after: $cursor, filterBy: { createdBy: $username }) {
-      totalCount
-      nodes {
-        createdAt
-        closedAt
-        closed
-        author {
-          login
-        }
-        url
-        number
-        title
-        labels(first: 10) {
-          totalCount
-          nodes {
-            name
-          }
-        }
-        body
-        comments(first: 1) {
-          totalCount
-        }
-        reactions(first: 100) {
-          totalCount
-          nodes {
-            content
-            user {
-              login
-            }
-          }
-        }
-        assignees(first: 3) {
-          totalCount
-        }
-        repository {
-          nameWithOwner
-          owner {
-            login
-          }
-          name
-        }
-      }
-      pageInfo {
-        hasNextPage
-        endCursor
-      }
-    }
-  }
-  rateLimit {
-    limit
-    cost
-    remaining
-    resetAt
-  }
-}
-`
-
-export type IssuesQuery = {
-  user: {
-    issues: {
-      totalCount: number
-      nodes: Array<{
-        createdAt: string
-        closedAt: string
-        closed: boolean
-        closedBy: Extra<string>
-        author: {
-          login: string
-        }
-        url: string
-        number: number
-        title: string
-        labels: {
-          totalCount: number
-          nodes: Array<{
-            name: string
-          }>
-        }
-        body: string
-        comments: {
-          totalCount: number
-        }
-        reactions: Reactions
-        assignees: {
-          totalCount: number
-        }
-        repository: {
-          nameWithOwner: string
-          owner: {
-            login: string
-          }
-          name: string
-        }
-      }>
-      pageInfo: {
-        hasNextPage: boolean
-        endCursor: string
-      }
-    }
-  }
-  rateLimit: {
-    limit: number
-    cost: number
-    remaining: number
-    resetAt: string
-  }
-}
src/collect/pulls.graphql
@@ -0,0 +1,91 @@
+fragment PullRequest on PullRequest {
+  createdAt
+  url
+  number
+  title
+  body
+  closed
+  merged
+  mergedAt
+  mergedBy {
+    login
+  }
+  repository {
+    nameWithOwner
+    owner {
+      login
+    }
+    name
+    languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
+      totalCount
+      nodes {
+        name
+      }
+    }
+  }
+  participants(first: 20) {
+    totalCount
+    nodes {
+      login
+    }
+  }
+  lastCommit: commits(last: 1) {
+    nodes {
+      commit {
+        checkSuites(first: 20) {
+          totalCount
+          nodes {
+            app {
+              name
+            }
+            workflowRun {
+              workflow {
+                name
+              }
+            }
+            lastCheckRun: checkRuns(last: 1) {
+              totalCount
+              nodes {
+                name
+                conclusion
+                status
+                startedAt
+                completedAt
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  reactions(first: 100) {
+    totalCount
+    nodes {
+      content
+      user {
+        login
+      }
+    }
+  }
+}
+
+query PullsQuery($username: String!, $num: Int = 100, $cursor: String) {
+  user(login: $username) {
+    pullRequests(first: $num, after: $cursor) {
+      totalCount
+      nodes {
+        ...PullRequest
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
src/collect/pulls.graphql.ts
@@ -0,0 +1,187 @@
+// DO NOT EDIT. This file was generated by megaera.
+
+const PullRequest = `#graphql
+fragment PullRequest on PullRequest {
+  createdAt
+  url
+  number
+  title
+  body
+  closed
+  merged
+  mergedAt
+  mergedBy {
+    login
+  }
+  repository {
+    nameWithOwner
+    owner {
+      login
+    }
+    name
+    languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
+      totalCount
+      nodes {
+        name
+      }
+    }
+  }
+  participants(first: 20) {
+    totalCount
+    nodes {
+      login
+    }
+  }
+  lastCommit: commits(last: 1) {
+    nodes {
+      commit {
+        checkSuites(first: 20) {
+          totalCount
+          nodes {
+            app {
+              name
+            }
+            workflowRun {
+              workflow {
+                name
+              }
+            }
+            lastCheckRun: checkRuns(last: 1) {
+              totalCount
+              nodes {
+                name
+                conclusion
+                status
+                startedAt
+                completedAt
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  reactions(first: 100) {
+    totalCount
+    nodes {
+      content
+      user {
+        login
+      }
+    }
+  }
+}`
+
+export type PullRequest = {
+  createdAt: string
+  url: string
+  number: number
+  title: string
+  body: string
+  closed: boolean
+  merged: boolean
+  mergedAt: string | null
+  mergedBy: {
+    login: string
+  } | null
+  repository: {
+    nameWithOwner: string
+    owner: {
+      login: string
+    }
+    name: string
+    languages: {
+      totalCount: number
+      nodes: Array<{
+        name: string
+      }> | null
+    } | null
+  }
+  participants: {
+    totalCount: number
+    nodes: Array<{
+      login: string
+    }> | null
+  }
+  lastCommit: {
+    nodes: Array<{
+      commit: {
+        checkSuites: {
+          totalCount: number
+          nodes: Array<{
+            app: {
+              name: string
+            } | null
+            workflowRun: {
+              workflow: {
+                name: string
+              }
+            } | null
+            lastCheckRun: {
+              totalCount: number
+              nodes: Array<{
+                name: string
+                conclusion: 'ACTION_REQUIRED' | 'CANCELLED' | 'FAILURE' | 'NEUTRAL' | 'SKIPPED' | 'STALE' | 'STARTUP_FAILURE' | 'SUCCESS' | 'TIMED_OUT'
+                status: 'COMPLETED' | 'IN_PROGRESS' | 'PENDING' | 'QUEUED' | 'REQUESTED' | 'WAITING'
+                startedAt: string | null
+                completedAt: string | null
+              }> | null
+            } | null
+          }> | null
+        } | null
+      }
+    }> | null
+  }
+  reactions: {
+    totalCount: number
+    nodes: Array<{
+      content: 'CONFUSED' | 'EYES' | 'HEART' | 'HOORAY' | 'LAUGH' | 'ROCKET' | 'THUMBS_DOWN' | 'THUMBS_UP'
+      user: {
+        login: string
+      } | null
+    }> | null
+  }
+}
+
+
+export const PullsQuery = `#graphql
+${PullRequest}
+query PullsQuery($username: String!, $num: Int = 100, $cursor: String) {
+  user(login: $username) {
+    pullRequests(first: $num, after: $cursor) {
+      totalCount
+      nodes {
+        ...PullRequest
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}` as string & PullsQuery
+
+export type PullsQuery = (vars: { username: string, num?: number | null, cursor?: string | null }) => {
+  user: {
+    pullRequests: {
+      totalCount: number
+      nodes: Array<{} & PullRequest> | null
+      pageInfo: {
+        hasNextPage: boolean
+        endCursor: string | null
+      }
+    }
+  } | null
+  rateLimit: {
+    limit: number
+    cost: number
+    remaining: number
+    resetAt: string
+  } | null
+}
src/collect/pulls.ts
@@ -1,186 +0,0 @@
-import { Reactions } from './types.js'
-
-export const pullsQuery = `#graphql
-query PullsQuery($username: String!, $num: Int = 100, $cursor: String) {
-  user(login: $username) {
-    pullRequests(first: $num, after: $cursor) {
-      totalCount
-      nodes {
-        createdAt
-        url
-        number
-        title
-        body
-        closed
-        merged
-        mergedAt
-        mergedBy {
-          login
-        }
-        repository {
-          nameWithOwner
-          owner {
-            login
-          }
-          name
-          languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
-            totalCount
-            nodes {
-              name
-            }
-          }
-        }
-        participants(first: 20) {
-          totalCount
-          nodes {
-            login
-          }
-        }
-        lastCommit: commits(last: 1) {
-          nodes {
-            commit {
-              checkSuites(first: 20) {
-                totalCount
-                nodes {
-                  app {
-                    name
-                  }
-                  workflowRun {
-                    workflow {
-                      name
-                    }
-                  }
-                  lastCheckRun: checkRuns(last: 1) {
-                    totalCount
-                    nodes {
-                      name
-                      conclusion
-                      status
-                      startedAt
-                      completedAt
-                    }
-                  }
-                }
-              }
-            }
-          }
-        }
-        reactions(first: 100) {
-          totalCount
-          nodes {
-            content
-            user {
-              login
-            }
-          }
-        }
-      }
-      pageInfo {
-        hasNextPage
-        endCursor
-      }
-    }
-  }
-  rateLimit {
-    limit
-    cost
-    remaining
-    resetAt
-  }
-}
-`
-
-export type PullsQuery = {
-  user: {
-    pullRequests: {
-      totalCount: number
-      nodes: Array<{
-        createdAt: string
-        url: string
-        number: number
-        title: string
-        body: string
-        closed: boolean
-        merged: boolean
-        mergedAt: string
-        mergedBy?: {
-          login: string
-        }
-        repository: {
-          nameWithOwner: string
-          owner: {
-            login: string
-          }
-          name: string
-          languages: {
-            totalCount: number
-            nodes: Array<{
-              name: string
-            }>
-          }
-        }
-        participants: {
-          totalCount: number
-          nodes: Array<{
-            login: string
-          }>
-        }
-        lastCommit: {
-          nodes: Array<{
-            commit: {
-              checkSuites: {
-                totalCount: number
-                nodes: Array<{
-                  app: {
-                    name: string
-                  }
-                  workflowRun: {
-                    workflow: {
-                      name: string
-                    }
-                  }
-                  lastCheckRun: {
-                    totalCount: number
-                    nodes: Array<{
-                      name: string
-                      conclusion:
-                        | 'ACTION_REQUIRED'
-                        | 'TIMED_OUT'
-                        | 'CANCELLED'
-                        | 'FAILURE'
-                        | 'SUCCESS'
-                        | 'NEUTRAL'
-                        | 'SKIPPED'
-                        | 'STARTUP_FAILURE'
-                        | 'STALE'
-                      status:
-                        | 'COMPLETED'
-                        | 'IN_PROGRESS'
-                        | 'PENDING'
-                        | 'QUEUED'
-                        | 'REQUESTED'
-                        | 'WAITING'
-                      startedAt: string
-                      completedAt: string
-                    }>
-                  }
-                }>
-              }
-            }
-          }>
-        }
-        reactions: Reactions
-      }>
-    }
-    pageInfo: {
-      hasNextPage: boolean
-      endCursor: string
-    }
-  }
-  rateLimit: {
-    limit: number
-    cost: number
-    remaining: number
-    resetAt: string
-  }
-}
src/collect/stars.graphql
@@ -0,0 +1,43 @@
+fragment StarredRepo on Repository {
+  nameWithOwner
+  description
+  stargazers {
+    totalCount
+  }
+  languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
+    totalCount
+    edges {
+      size
+      node {
+        name
+      }
+    }
+  }
+  licenseInfo {
+    name
+    nickname
+  }
+}
+
+query StarsQuery($login: String!, $num: Int = 100, $cursor: String) {
+  user(login: $login) {
+    starredRepositories(first: $num, after: $cursor) {
+      totalCount
+      isOverLimit
+      nodes {
+        ...StarredRepo
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
+
src/collect/stars.graphql.ts
@@ -0,0 +1,89 @@
+// DO NOT EDIT. This file was generated by megaera.
+
+const StarredRepo = `#graphql
+fragment StarredRepo on Repository {
+  nameWithOwner
+  description
+  stargazers {
+    totalCount
+  }
+  languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
+    totalCount
+    edges {
+      size
+      node {
+        name
+      }
+    }
+  }
+  licenseInfo {
+    name
+    nickname
+  }
+}`
+
+export type StarredRepo = {
+  nameWithOwner: string
+  description: string | null
+  stargazers: {
+    totalCount: number
+  }
+  languages: {
+    totalCount: number
+    edges: Array<{
+      size: number
+      node: {
+        name: string
+      }
+    }> | null
+  } | null
+  licenseInfo: {
+    name: string
+    nickname: string | null
+  } | null
+}
+
+
+export const StarsQuery = `#graphql
+${StarredRepo}
+query StarsQuery($login: String!, $num: Int = 100, $cursor: String) {
+  user(login: $login) {
+    starredRepositories(first: $num, after: $cursor) {
+      totalCount
+      isOverLimit
+      nodes {
+        ...StarredRepo
+      }
+      pageInfo {
+        hasNextPage
+        endCursor
+      }
+    }
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}` as string & StarsQuery
+
+export type StarsQuery = (vars: { login: string, num?: number | null, cursor?: string | null }) => {
+  user: {
+    starredRepositories: {
+      totalCount: number
+      isOverLimit: boolean
+      nodes: Array<{} & StarredRepo> | null
+      pageInfo: {
+        hasNextPage: boolean
+        endCursor: string | null
+      }
+    }
+  } | null
+  rateLimit: {
+    limit: number
+    cost: number
+    remaining: number
+    resetAt: string
+  } | null
+}
src/collect/stars.ts
@@ -1,79 +0,0 @@
-export const starsQuery = `#graphql
-query StarsQuery($login: String!, $num: Int = 100, $cursor: String) {
-  user(login: $login) {
-    starredRepositories(first: $num, after: $cursor) {
-      totalCount
-      isOverLimit
-      nodes {
-        nameWithOwner
-        description
-        stargazers {
-          totalCount
-        }
-        languages(first: 10, orderBy: {field: SIZE, direction: DESC}) {
-          totalCount
-          edges {
-            size
-            node {
-              name
-            }
-          }
-        }
-        licenseInfo {
-          name
-          nickname
-        }
-      }
-      pageInfo {
-        hasNextPage
-        endCursor
-      }
-    }
-  }
-  rateLimit {
-    limit
-    cost
-    remaining
-    resetAt
-  }
-}
-`
-
-export type StarsQuery = {
-  user: {
-    starredRepositories: {
-      totalCount: number
-      isOverLimit: boolean
-      nodes: Array<{
-        nameWithOwner: string
-        description: string
-        stargazers: {
-          totalCount: number
-        }
-        languages: {
-          totalCount: number
-          edges: Array<{
-            size: number
-            node: {
-              name: string
-            }
-          }>
-        }
-        licenseInfo: {
-          name: string
-          nickname: string
-        }
-      }>
-      pageInfo: {
-        hasNextPage: boolean
-        endCursor: string
-      }
-    }
-  }
-  rateLimit: {
-    limit: number
-    cost: number
-    remaining: number
-    resetAt: string
-  }
-}
src/collect/types.ts
@@ -1,62 +0,0 @@
-import { Endpoints } from '@octokit/types'
-import { CommitsQuery } from './commits.js'
-import { IssuesQuery } from './issues.js'
-import { UserQuery } from './user.js'
-import { PullsQuery } from './pulls.js'
-import { IssueCommentsQuery } from './issue-comments.js'
-import { DiscussionCommentsQuery } from './discussion-comments.js'
-import { StarsQuery } from './stars.js'
-
-// Extra<T> represents additional data that is not returned by the GraphQL API,
-// but enriched by some other means (e.g., separate queries).
-export type Extra<T> = T | undefined
-
-export type Data = {
-  user: User
-  starredRepositories: StarredRepo[]
-  repos: Repo[]
-  pulls: Pull[]
-  issues: Issue[]
-  issueComments: IssueComment[]
-  discussionComments: DiscussionComment[]
-}
-
-export type User = UserQuery['user']
-
-export type Repo =
-  Endpoints['GET /users/{username}/repos']['response']['data'][0] & {
-    commits: Commit[]
-  }
-
-export type Commit =
-  CommitsQuery['repository']['defaultBranchRef']['target']['history']['nodes'][0]
-
-export type Pull = PullsQuery['user']['pullRequests']['nodes'][0]
-
-export type Issue = IssuesQuery['user']['issues']['nodes'][0]
-
-export type IssueComment =
-  IssueCommentsQuery['user']['issueComments']['nodes'][0]
-
-export type DiscussionComment =
-  DiscussionCommentsQuery['user']['repositoryDiscussionComments']['nodes'][0]
-
-export type StarredRepo = StarsQuery['user']['starredRepositories']['nodes'][0]
-
-export type Reactions = {
-  totalCount: number
-  nodes: Array<{
-    content:
-      | 'CONFUSED'
-      | 'EYES'
-      | 'HEART'
-      | 'HOORAY'
-      | 'LAUGH'
-      | 'ROCKET'
-      | 'THUMBS_DOWN'
-      | 'THUMBS_UP'
-    user: {
-      login: string
-    }
-  }>
-}
src/collect/user.graphql
@@ -0,0 +1,64 @@
+fragment User on User {
+  id
+  login
+  name
+  avatarUrl
+  bio
+  company
+  location
+  email
+  twitterUsername
+  websiteUrl
+  status {
+    createdAt
+    emoji
+    message
+  }
+  createdAt
+  followers {
+    totalCount
+  }
+  following {
+    totalCount
+  }
+  anyPinnableItems
+  pinnedItems(first: 6) {
+    totalCount
+    nodes {
+      ... on Gist {
+        name
+      }
+      ... on Repository {
+        name
+      }
+    }
+  }
+  sponsoring {
+    totalCount
+  }
+  sponsors {
+    totalCount
+  }
+  starredRepositories {
+    totalCount
+  }
+  publicKeys(first: 5) {
+    totalCount
+    nodes {
+      key
+    }
+  }
+}
+
+query UserQuery($login: String!) {
+  user(login: $login) {
+    ...User
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}
+
src/collect/user.graphql.ts
@@ -0,0 +1,128 @@
+// DO NOT EDIT. This file was generated by megaera.
+
+const User = `#graphql
+fragment User on User {
+  id
+  login
+  name
+  avatarUrl
+  bio
+  company
+  location
+  email
+  twitterUsername
+  websiteUrl
+  status {
+    createdAt
+    emoji
+    message
+  }
+  createdAt
+  followers {
+    totalCount
+  }
+  following {
+    totalCount
+  }
+  anyPinnableItems
+  pinnedItems(first: 6) {
+    totalCount
+    nodes {
+      ... on Gist {
+        name
+      }
+      ... on Repository {
+        name
+      }
+    }
+  }
+  sponsoring {
+    totalCount
+  }
+  sponsors {
+    totalCount
+  }
+  starredRepositories {
+    totalCount
+  }
+  publicKeys(first: 5) {
+    totalCount
+    nodes {
+      key
+    }
+  }
+}`
+
+export type User = {
+  id: string
+  login: string
+  name: string | null
+  avatarUrl: string
+  bio: string | null
+  company: string | null
+  location: string | null
+  email: string
+  twitterUsername: string | null
+  websiteUrl: string | null
+  status: {
+    createdAt: string
+    emoji: string | null
+    message: string | null
+  } | null
+  createdAt: string
+  followers: {
+    totalCount: number
+  }
+  following: {
+    totalCount: number
+  }
+  anyPinnableItems: boolean
+  pinnedItems: {
+    totalCount: number
+    nodes: Array<{} & {
+      name: string
+    } & {
+      name: string
+    } | null> | null
+  }
+  sponsoring: {
+    totalCount: number
+  }
+  sponsors: {
+    totalCount: number
+  }
+  starredRepositories: {
+    totalCount: number
+  }
+  publicKeys: {
+    totalCount: number
+    nodes: Array<{
+      key: string
+    }> | null
+  }
+}
+
+
+export const UserQuery = `#graphql
+${User}
+query UserQuery($login: String!) {
+  user(login: $login) {
+    ...User
+  }
+  rateLimit {
+    limit
+    cost
+    remaining
+    resetAt
+  }
+}` as string & UserQuery
+
+export type UserQuery = (vars: { login: string }) => {
+  user: {} & User | null
+  rateLimit: {
+    limit: number
+    cost: number
+    remaining: number
+    resetAt: string
+  } | null
+}
src/collect/user.ts
@@ -1,116 +0,0 @@
-export const userQuery = `#graphql
-query UserQuery($login: String!) {
-  user(login: $login) {
-    id
-    login
-    name
-    avatarUrl
-    bio
-    company
-    location
-    email
-    twitterUsername
-    websiteUrl
-    status {
-      createdAt
-      emoji
-      message
-    }
-    createdAt
-    followers {
-      totalCount
-    }
-    following {
-      totalCount
-    }
-    anyPinnableItems
-    pinnedItems(first: 6) {
-      totalCount
-      nodes {
-        ... on Gist {
-          name
-        }
-        ... on Repository {
-          name
-        }
-      }
-    }
-    sponsoring {
-      totalCount
-    }
-    sponsors {
-      totalCount
-    }
-    starredRepositories {
-      totalCount
-    }
-    publicKeys(first: 5) {
-      totalCount
-      nodes {
-        key
-      }
-    }
-  }
-  rateLimit {
-    limit
-    cost
-    remaining
-    resetAt
-  }
-}
-`
-
-export type UserQuery = {
-  user: {
-    id: string
-    login: string
-    name: string
-    avatarUrl: string
-    bio: string | null
-    company: string | null
-    location: string | null
-    email: string
-    twitterUsername: string | null
-    websiteUrl: string | null
-    status: {
-      createdAt: string
-      emoji: string
-      message: string
-    } | null
-    createdAt: string
-    followers: {
-      totalCount: number
-    }
-    following: {
-      totalCount: number
-    }
-    anyPinnableItems: boolean
-    pinnedItems: {
-      totalCount: number
-      nodes: Array<{
-        name: string
-      }>
-    }
-    sponsoring: {
-      totalCount: number
-    }
-    sponsors: {
-      totalCount: number
-    }
-    starredRepositories: {
-      totalCount: number
-    }
-    publicKeys: {
-      totalCount: number
-      nodes: Array<{
-        key: string
-      }>
-    }
-  }
-  rateLimit: {
-    limit: number
-    cost: number
-    remaining: number
-    resetAt: string
-  }
-}
src/badges.ts
@@ -1,6 +1,9 @@
 import allBadges from '#badges'
 import { linkCommit, linkIssue, linkPull } from './utils.js'
-import { Commit, Data, Issue, Pull } from './collect/types.js'
+import { Data } from './collect/index.js'
+import { Commit } from './collect/commits.graphql.js'
+import { PullRequest } from './collect/pulls.graphql.js'
+import { Issue } from './collect/issues.graphql.js'
 
 export type Presenters = (typeof allBadges)[number]['default']
 
@@ -58,7 +61,7 @@ export class Evidence {
     return this
   }
 
-  evidencePRs(...pulls: Pull[]) {
+  evidencePRs(...pulls: PullRequest[]) {
     this.evidence(
       'Pull requests:\n\n' +
         pulls
@@ -69,7 +72,7 @@ export class Evidence {
     return this
   }
 
-  evidencePRsWithTitle(...pulls: Pull[]) {
+  evidencePRsWithTitle(...pulls: PullRequest[]) {
     this.evidence(
       'Pull requests:\n\n' +
         pulls.map((x) => `- ${linkPull(x)}: ${x.title}`).join('\n'),
src/collect/collect.ts → src/collect.ts
@@ -1,24 +1,36 @@
 import { Octokit } from 'octokit'
-import { pullsQuery, PullsQuery } from './pulls.js'
-import { commitsQuery, CommitsQuery } from './commits.js'
-import { issuesQuery, IssuesQuery } from './issues.js'
-import { userQuery, UserQuery } from './user.js'
-import { IssueTimelineQuery, issueTimelineQuery } from './issue-timeline.js'
-import { Data } from './types.js'
-import { issueCommentsQuery, IssueCommentsQuery } from './issue-comments.js'
+import { Data } from './collect/index.js'
+import { Query, Variables } from 'megaera'
+import { UserQuery } from './collect/user.graphql.js'
+import { CommitsQuery } from './collect/commits.graphql.js'
+import { PullsQuery } from './collect/pulls.graphql.js'
+import { IssuesQuery } from './collect/issues.graphql.js'
+import { IssueTimelineQuery } from './collect/issue-timeline.graphql.js'
 import {
-  discussionCommentsQuery,
   DiscussionCommentsQuery,
-} from './discussion-comments.js'
-import { starsQuery, StarsQuery } from './stars.js'
+  IssueCommentsQuery,
+} from './collect/comments.graphql.js'
+import { StarsQuery } from './collect/stars.graphql.js'
 
 export async function collect(
   octokit: Octokit,
   username: string,
 ): Promise<Data> {
-  const { user } = await octokit.graphql<UserQuery>(userQuery, {
+  function query<T extends Query>(query: T, variables: Variables<T>) {
+    return octokit.graphql<ReturnType<T>>(query, variables)
+  }
+
+  function paginate<T extends Query>(query: T, variables: Variables<T>) {
+    return octokit.graphql.paginate.iterator<ReturnType<T>>(query, variables)
+  }
+
+  const { user } = await query(UserQuery, {
     login: username,
-  })
+  })!
+
+  if (!user) {
+    throw new Error('Failed to load user')
+  }
 
   const data: Data = {
     user: user,
@@ -51,18 +63,19 @@ export async function collect(
   for (const repo of data.repos) {
     console.log(`Loading commits for ${repo.owner.login}/${repo.name}`)
     try {
-      const commits = octokit.graphql.paginate.iterator<CommitsQuery>(
-        commitsQuery,
-        {
-          owner: repo.owner.login,
-          name: repo.name,
-          author: user.id,
-        },
-      )
+      const commits = paginate(CommitsQuery, {
+        owner: repo.owner.login,
+        name: repo.name,
+        author: user.id,
+      })
 
       for await (const resp of commits) {
         const { totalCount, nodes } =
-          resp.repository.defaultBranchRef.target.history
+          resp.repository?.defaultBranchRef?.target?.history!
+
+        if (!nodes) {
+          throw new Error('Failed to load commits')
+        }
 
         if (totalCount >= 10_000) {
           console.error(
@@ -75,7 +88,7 @@ export async function collect(
           repo.commits.push(commit)
         }
         console.log(
-          `| commits ${repo.commits.length}/${totalCount} (cost: ${resp.rateLimit.cost}, remaining: ${resp.rateLimit.remaining})`,
+          `| commits ${repo.commits.length}/${totalCount} (cost: ${resp.rateLimit?.cost}, remaining: ${resp.rateLimit?.remaining})`,
         )
       }
     } catch (err) {
@@ -86,17 +99,21 @@ export async function collect(
     }
   }
 
-  const pulls = octokit.graphql.paginate.iterator<PullsQuery>(pullsQuery, {
+  const pulls = paginate(PullsQuery, {
     username,
   })
   try {
     for await (const resp of pulls) {
+      if (!resp.user?.pullRequests.nodes) {
+        throw new Error('Failed to load pull requests')
+      }
+
       console.log(
         `Loading pull requests ${
           data.pulls.length + resp.user.pullRequests.nodes.length
         }/${resp.user.pullRequests.totalCount} (cost: ${
-          resp.rateLimit.cost
-        }, remaining: ${resp.rateLimit.remaining})`,
+          resp.rateLimit?.cost
+        }, remaining: ${resp.rateLimit?.remaining})`,
       )
       for (const pull of resp.user.pullRequests.nodes) {
         data.pulls.push(pull)
@@ -107,16 +124,20 @@ export async function collect(
     console.error(err)
   }
 
-  const issues = octokit.graphql.paginate.iterator<IssuesQuery>(issuesQuery, {
+  const issues = paginate(IssuesQuery, {
     username,
   })
   try {
     for await (const resp of issues) {
+      if (!resp.user?.issues.nodes) {
+        throw new Error('Failed to load issues')
+      }
+
       console.log(
         `Loading issues ${data.issues.length + resp.user.issues.nodes.length}/${
           resp.user.issues.totalCount
-        } (cost: ${resp.rateLimit.cost}, remaining: ${
-          resp.rateLimit.remaining
+        } (cost: ${resp.rateLimit?.cost}, remaining: ${
+          resp.rateLimit?.remaining
         })`,
       )
       for (const issue of resp.user.issues.nodes) {
@@ -133,22 +154,22 @@ export async function collect(
       `Loading issue timeline for ${issue.repository.name}#${issue.number}`,
     )
     try {
-      const timeline = octokit.graphql.paginate.iterator<IssueTimelineQuery>(
-        issueTimelineQuery,
-        {
-          owner: issue.repository.owner.login,
-          name: issue.repository.name,
-          number: issue.number,
-        },
-      )
+      const timeline = paginate(IssueTimelineQuery, {
+        owner: issue.repository.owner.login,
+        name: issue.repository.name,
+        number: issue.number,
+      })
       for await (const resp of timeline) {
+        if (!resp.repository?.issue?.timelineItems.nodes) {
+          throw new Error('Failed to load issue timeline')
+        }
+
         console.log(
-          `| timeline ${resp.repository.issue.timelineItems.nodes.length}/${resp.repository.issue.timelineItems.totalCount} (cost: ${resp.rateLimit.cost}, remaining: ${resp.rateLimit.remaining})`,
+          `| timeline ${resp.repository.issue.timelineItems.nodes.length}/${resp.repository.issue.timelineItems.totalCount} (cost: ${resp.rateLimit?.cost}, remaining: ${resp.rateLimit?.remaining})`,
         )
         for (const event of resp.repository.issue.timelineItems.nodes) {
-          if (event.__typename == 'ClosedEvent') {
+          if (event?.__typename == 'ClosedEvent') {
             issue.closedAt = event.createdAt
-            issue.closedBy = event.actor.login
           }
         }
       }
@@ -160,19 +181,20 @@ export async function collect(
     }
   }
 
-  const issueComments = octokit.graphql.paginate.iterator<IssueCommentsQuery>(
-    issueCommentsQuery,
-    {
-      login: username,
-    },
-  )
+  const issueComments = paginate(IssueCommentsQuery, {
+    login: username,
+  })
   try {
     for await (const resp of issueComments) {
+      if (!resp.user?.issueComments.nodes) {
+        throw new Error('Failed to load issue comments')
+      }
+
       for (const comment of resp.user.issueComments.nodes) {
         data.issueComments.push(comment)
       }
       console.log(
-        `| issue comments ${data.issueComments.length}/${resp.user.issueComments.totalCount} (cost: ${resp.rateLimit.cost}, remaining: ${resp.rateLimit.remaining})`,
+        `| issue comments ${data.issueComments.length}/${resp.user.issueComments.totalCount} (cost: ${resp.rateLimit?.cost}, remaining: ${resp.rateLimit?.remaining})`,
       )
     }
   } catch (err) {
@@ -180,20 +202,20 @@ export async function collect(
     console.error(err)
   }
 
-  const discussionComments =
-    octokit.graphql.paginate.iterator<DiscussionCommentsQuery>(
-      discussionCommentsQuery,
-      {
-        login: username,
-      },
-    )
+  const discussionComments = paginate(DiscussionCommentsQuery, {
+    login: username,
+  })
   try {
     for await (const resp of discussionComments) {
+      if (!resp.user?.repositoryDiscussionComments.nodes) {
+        throw new Error('Failed to load discussion comments')
+      }
+
       for (const comment of resp.user.repositoryDiscussionComments.nodes) {
         data.discussionComments.push(comment)
       }
       console.log(
-        `| discussion comments ${data.discussionComments.length}/${resp.user.repositoryDiscussionComments.totalCount} (cost: ${resp.rateLimit.cost}, remaining: ${resp.rateLimit.remaining})`,
+        `| discussion comments ${data.discussionComments.length}/${resp.user.repositoryDiscussionComments.totalCount} (cost: ${resp.rateLimit?.cost}, remaining: ${resp.rateLimit?.remaining})`,
       )
     }
   } catch (err) {
@@ -201,16 +223,20 @@ export async function collect(
     console.error(err)
   }
 
-  const stars = octokit.graphql.paginate.iterator<StarsQuery>(starsQuery, {
+  const stars = paginate(StarsQuery, {
     login: username,
   })
   try {
     for await (const resp of stars) {
+      if (!resp.user?.starredRepositories.nodes) {
+        throw new Error('Failed to load stars')
+      }
+
       for (const repo of resp.user.starredRepositories.nodes) {
         data.starredRepositories.push(repo)
       }
       console.log(
-        `| stars ${data.starredRepositories.length}/${resp.user.starredRepositories.totalCount} (cost: ${resp.rateLimit.cost}, remaining: ${resp.rateLimit.remaining})`,
+        `| stars ${data.starredRepositories.length}/${resp.user.starredRepositories.totalCount} (cost: ${resp.rateLimit?.cost}, remaining: ${resp.rateLimit?.remaining})`,
       )
     }
   } catch (err) {
src/get-data.ts
@@ -1,7 +1,7 @@
-import { collect } from './collect/collect.js'
+import { collect } from './collect.js'
 import fs from 'node:fs'
 import { Octokit } from 'octokit'
-import { Data } from './collect/types.js'
+import { Data } from './collect/index.js'
 
 export async function getData(
   octokit: Octokit,
src/index.ts
@@ -1,3 +1,8 @@
 export { define } from './badges.js'
-export { Repo, User, Issue, Pull, Commit } from './collect/types.js'
+export { Repo } from './collect/index.js'
+export { User } from './collect/user.graphql.js'
+export { Issue } from './collect/issues.graphql.js'
+export { PullRequest } from './collect/pulls.graphql.js'
+export { Commit } from './collect/commits.graphql.js'
+
 export { linkCommit, linkIssue, linkPull, latest, plural } from './utils.js'
src/present-badges.ts
@@ -2,7 +2,7 @@ import { Badge, Evidence, ID, List, Presenter } from './badges.js'
 import path from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { parseMask } from './utils.js'
-import { Data } from './collect/types.js'
+import { Data } from './collect/index.js'
 
 export const presentBadges = <P extends Presenter<List>>(
   presenters: P[],
src/utils.ts
@@ -1,5 +1,7 @@
 import { spawnSync } from 'node:child_process'
-import { Commit, Issue, Pull } from './collect/types.js'
+import { Commit } from './collect/commits.graphql.js'
+import { PullRequest } from './collect/pulls.graphql.js'
+import { Issue } from './collect/issues.graphql.js'
 
 export function linkCommit(commit: Commit): string {
   return `<a href="https://github.com/${commit.repository.owner.login}/${
@@ -7,7 +9,7 @@ export function linkCommit(commit: Commit): string {
   }/commit/${commit.sha}">${commit.sha.slice(0, 7)}</a>`
 }
 
-export function linkPull(pull: Pull): string {
+export function linkPull(pull: PullRequest): string {
   return `<a href="https://github.com/${pull.repository.owner.login}/${pull.repository.name}/pull/${pull.number}">#${pull.number}</a>`
 }
 
test/present-badges.test.ts
@@ -1,8 +1,8 @@
 import * as assert from 'node:assert'
 import { describe, it } from 'node:test'
 import { presentBadges } from '../src/present-badges.js'
-import { Badge, define, List, Presenter } from '../src/badges.js'
-import { Data } from '../src/collect/types.js'
+import { Badge, define } from '../src/badges.js'
+import { Data } from '../src/collect/index.js'
 
 describe('present-badges', () => {
   const data: Data = {
@@ -31,7 +31,7 @@ describe('present-badges', () => {
         commits: [] as any[],
       },
     ] as Data['repos'],
-  }
+  } as Data
 
   it('presentBadges() applies `pick`', async () => {
     const userBadges = presentBadges(
test/stars.test.ts
@@ -2,7 +2,7 @@ import * as assert from 'node:assert'
 import { describe, it } from 'node:test'
 import starsPresenter from '#badges/stars/stars.js'
 import { badgeCollection } from '../src/present-badges.js'
-import { Data } from '../src/collect/types.js'
+import { Data } from '../src/collect/index.js'
 import { Badge } from '../src/badges.js'
 
 describe('stars', () => {
@@ -31,7 +31,7 @@ describe('stars', () => {
           commits: [] as any[],
         },
       ] as Data['repos'],
-    }
+    } as Data
 
     starsPresenter.present(data, grant)
 
package-lock.json
@@ -11,6 +11,7 @@
       "dependencies": {
         "@octokit/plugin-retry": "^7.1.1",
         "@octokit/plugin-throttling": "^9.3.0",
+        "megaera": "^0.0.2",
         "minimist": "^1.2.8",
         "octokit": "^4.0.2"
       },
@@ -1041,7 +1042,6 @@
       "version": "16.8.1",
       "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz",
       "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==",
-      "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
       }
@@ -1250,6 +1250,18 @@
       "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
       "dev": true
     },
+    "node_modules/megaera": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/megaera/-/megaera-0.0.2.tgz",
+      "integrity": "sha512-VXzm1d9Qoi+jY3H4kyi87YZojQ3fs73T/FUKo37WfJ+74nId0nOM0fREJUc3V7m2pZ3i+w7IVK9XDm2V9bE/iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "graphql": "^16.0.0"
+      },
+      "bin": {
+        "megaera": "dist/cli.js"
+      }
+    },
     "node_modules/merge2": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
package.json
@@ -25,6 +25,7 @@
   "dependencies": {
     "@octokit/plugin-retry": "^7.1.1",
     "@octokit/plugin-throttling": "^9.3.0",
+    "megaera": "^0.0.2",
     "minimist": "^1.2.8",
     "octokit": "^4.0.2"
   },