shoetBlog
技術や好きなことについて発信しています。
GraphQLでGitHubAPIv4から草を取得する
2024/3/31 17:11:44
GraphQLGoReactGitHub
GraphQLでGitHubAPIv4から草を取得する

GitHubから草(Contribution数)🌿を取得して、本ブログアプリに表示しました!

このブログアプリに装飾を増やしたいと思い、GraphQLの素振りも兼ねてGitHubのAPIからContribution数を取得して表示するようにしました。
以下が実装の手引になります!

①GitHubのAPIの仕様を確認する。
②サーバーサイドでGitHubAPIv4からContirbution数を取得する。
③フロントエンドからサーバーサイドにリクエストし、結果を描画する。

①GitHubのAPI仕様を確認する。

GitHubAPI v4 がGraphQLで公開されているため、サーバーサイドでGolangでContribution数を取得します。

プログラム作成に必要な材料としては以下になります。

  • GitHubのPersonalAccessToken
  • GraphQLクライアント(必要であれば)
  • API仕様

PersonalAccessTokenの取得

GitHubのコンソールから、
自分のプロフィールアイコンクリック > settings > 左メニューの一番下Developer settings > Personal Access Tokenになります。

権限については、デフォルト状態でPublic Repositories (read-only)にチェックがついていればContribution数取得には問題ありません!

GraphQLクライアント

GraphQLの基本としては、POSTリクエストでBodyにQuery(クエリ)とVariables(引数)を指定することなります。
以下はGitHubAPI v4へのリクエストのサンプルです。単純なQueryを渡しています。
TOKENの部分はPersonalAccessTokenになります。

curl -H "Authorization: bearer TOKEN" -X POST -d " \\
 { \\
   \\"query\\": \\"query { viewer { login }}\\" \\
 } \\
" https://api.github.com/graphql

リクエストすると、GraphQLサーバーが以下のように、リクエストしたクエリと同じ構造でJSONでデータを返してきます。

{"data":{"viewer":{"login":"shoet"}}}

このときデータを受け取る側では、JSONをデシリアライズしてアプリケーション上で使用できるようにする必要がありますが、Goでこれを行うには、あらかじめ構造体を定義してUnmarshalする必要があります。

構造がシンプルなデータであれば気にする必要がありませんが、ネストの深いデータでは定義する構造体が多く大変になってしまいます。
そこで今回はshurcooL/graphqlというGoパッケージを活用します。
詳細な使い方は、パッケージのリポジトリや、後述するサンプルプログラムを参照ください。

このパッケージはSQLのクエリビルダのような役割をしてくれることと、JSONをデシリアライズする際の構造体定義を楽にしてくれます。

API仕様

GitHubAPI v4の仕様はこちらから確認できます。
https://docs.github.com/ja/graphql
有り難いことに公式からクエリのWebクライアントが提供されています。
https://docs.github.com/ja/graphql/overview/explorer
エクスプローラー

こちらを活用することで、プログラムするまえにクエリを試すことができます。
使用するにはGitHubアカウントでのSignInが必要になります。

また、コンソール左側の本のマークからデータ構造を検索することができます。
ドキュメント こちらのおかげで、ドキュメントをあちこち参照する必要が無くなり、開発効率によいです!

完成したプログラムがこちらになります。

GetContributions()でユーザー名、取得開始日、取得終了日を入力すると週刻みで日毎のContribution数と、草のカラーコードを返してきます。

  • サーバーサイドでのContribution数を取得
import (
    "context"
    "fmt"
    "net/url"
    "time"

    "github.com/shurcooL/graphql"
    "golang.org/x/oauth2"
)

type GitHubV4APIClient struct {
    githubPersonalAccessToken string
}

func NewGitHubV4APIClient(
    githubPersonalAccessToken string,
) *GitHubV4APIClient {
    return &GitHubV4APIClient{
        githubPersonalAccessToken: githubPersonalAccessToken,
    }
}

type GitHubContributionWeeks []struct {
    ContributionDays []struct {
        Date              string `json:"date"`
        Color             string `json:"color"`
        ContributionCount int    `json:"contributionCount"`
    } `json:"contributionDays"`
}

func (g *GitHubV4APIClient) GetContributions(ctx context.Context, username string, fromDateUTC time.Time, toDateUTC time.Time) (GitHubContributionWeeks, error) {
    apiUrl, err := url.Parse("https://api.github.com/graphql")
    if err != nil {
        return nil, fmt.Errorf("failed to parse url: %v", err)
    }

    var query struct {
        User struct {
            ContributionCollection struct {
                ContributionCalendar struct {
                    Weeks GitHubContributionWeeks `json:"weeks"`
                }
            } `graphql:"contributionsCollection(from: $from, to: $to)"`
        } `graphql:"user(login: $login)"`
    }
    src := oauth2.StaticTokenSource(
        &oauth2.Token{AccessToken: g.githubPersonalAccessToken, TokenType: "Bearer"},
    )
    httpClient := oauth2.NewClient(context.Background(), src)
    client := graphql.NewClient(apiUrl.String(), httpClient)

    type DateTime struct{ time.Time }
    variables := map[string]interface{}{
        "login": graphql.String(username),
        "from":  DateTime{fromDateUTC},
        "to":    DateTime{toDateUTC},
    }
    if err := client.Query(context.Background(), &query, variables); err != nil {
        return nil, fmt.Errorf("failed to query: %v", err)
    }

    return query.User.ContributionCollection.ContributionCalendar.Weeks, nil
}

フロントエンドでの描画は下記のコードになります。

①ContributionTile > ②ContributionColumn > ③GitHubContributionsGrid

①ContributionTile: 草1マスを表現
②ContributionColumn: 草の縦1列を表現(1週間分)
③GitHubContributionsGrid: ContributionColumnを横方向に並べる

  • フロントエンドで草を描画
import { GitHubContributions } from '@/types/api'
import styled from 'styled-components'

type GitHubContributionsProps = {
  contributions: GitHubContributions[]
}

const ContributionTile = styled.div<{ color: string }>`
  width: 0.7rem;
  height: 0.7rem;
  ${({ color }) => `background-color: ${color};`}
  border: none;
  border-radius: 20%;
`

const ContributionColumn = (props: { contribution: GitHubContributions }) => {
  const { contribution } = props
  const Column = styled.div`
    display: flex;
    flex-direction: column;
  `
  return (
    <Column>
      {contribution.contributionDays.map((cd, idx) => {
        return (
          <div
            style={{
              paddingBottom:
                contribution.contributionDays.length - 1 === idx
                  ? '0'
                  : '0.1rem',
            }}
          >
            <ContributionTile color={cd.color}></ContributionTile>
          </div>
        )
      })}
    </Column>
  )
}

export const GitHubContributionsGrid = (props: GitHubContributionsProps) => {
  const { contributions } = props

  const Row = styled.div`
    &::-webkit-scrollbar{
      display: none;
    }
    display: flex;
    flex-direction: row;
    justify-content: end;
    overflow-x: scroll;
    padding: 0 0.5rem;
  `

  return (
    <>
      <a href="https://github.com/shoet" target="_black">
        <Row style={{ backgroundColor: '' }}>
          {contributions.map((c, idx) => {
            return (
              <div
                style={{
                  marginLeft: 0 === idx ? '0' : '0.1rem',
                }}
              >
                <ContributionColumn contribution={c} />
              </div>
            )
          })}
        </Row>
      </a>
    </>
  )
}

ソース全体は、このブログのソースコードになります。

サーバーサイドのソース

フロントエンドのソース

初めてGraphQLを触った所感としては、
利用する側としては、ネストされた関係にあるデータの取得はしやすいように感じました。
たとえばユーザーのデータを取得してしまえば、その配下にあるユーザープロフィールのようなデータは同時に取得するといったケースです。
今度は、サーバー提供するケースを素振りしてみたいと思います!

コメント
profile
匿名ユーザー(ID: )
Markdown
Preview
エンジニア。
エンジニアリングで価値提供できるよう、
日々自己研磨。
AWSAWS LambdaChormiumCloudFrontDockerECSGitHubGoGraphQLLambdaLocalStackNext.jsOpenAIPlanetScaleReactReduxServerlessFrameworkTypeScriptUpstashViteWebSocket