2021年6月第4週レポート

インプット

📝 「The Road to GraphQL」を読んでいる

先週に引き続き、GraphQLの学習のため以下の書籍を読んでいます。

the-road-to-graphql/the-road-to-graphql: 📓The Road to GraphQL: Your journey to master pragmatic GraphQL in JavaScript (github.com)

この本で学べる内容は以下の通りです。 - Github GraphQL APIを題材として、クライアントサイドからの利用方法(1〜6章) - Reactを使ったGraphQLクライアントとプレゼンテーションの実装(7章) - Expressを使ったGraphQLサーバの実装(8章)

7章のReactを使った実装については内容が古く、クライアントパッケージのメジャーバージョンが古い、Hooksについての言及がないので、軽く流しています。

8章についてはおそらく現在も通用する内容かな、と感じています。ただ、実装についてはJavaScriptによるものになっています。自分は演習についてTypeScriptで実装しているので、適宜TypeScript向けの情報を収集しながら読み進めています。

現在8章の中盤まで到達しているのでぼちぼち終盤といったところでしょうか。読んだ感触としては入門書としてはかなり良い部類だと思います。前半の章では基礎が丁寧に解説されており、まずはライブラリを使わず生のHTTP通信を使ってGraphQLの利用方法を体感してみるという方針に好感が持てます。いきなり抽象化された便利なものを使うよりも、低めのレイヤーからのアプローチの方が、GraphQLそのものの理解を深めたい人には向いていると思います。

その後はクライアントもサーバもメジャーなGraphQLエコシステムであるApolloの利用方法に入るので、基礎から実践まである程度カバーできるんじゃないかなと思います。

📝 GraphQLサーバをTypeScriptで実装するときの諸情報

前述したとおりThe Road to GraphQLのサンプルはJavaScriptで実装されています。自分は今後TypeScriptで実装して使っていく予定なので、サンプルや演習はTypeScriptで実装しているのですが、型付けまわりでいろいろと調べていく中で参考となった記事をメモしておきます。

Apollo Server with TypeScript (zenn.dev)

何はともあれリゾルバを書くときに型のサポートが欲しくなりました。どうやらスキーマ定義から型定義を生成するのがデファクトのようです。Apolloでも出来るようですが、graphql-codegenの方がいろいろ出来ることが多いようです(まだよくわかってないけど)。

こちらの記事ではgraphql-codegenでスキーマからTypeScript用の型定義を生成するまでをパッケージのインストールからステップバイステップで解説されているので非常に参考になりました。これで型推論を効かせてリゾルバを書けるようになったので、だいぶ快適に実装ができるようになりました。

しかしある程度進んできたところで新たな悩みポイントが出てきました。Queryについてのリゾルバについては型チェックが可能になったのですが、QueryからTypeへマッピング?しながら解決していくリゾルバがうまく書けないことがわかりました。そこでいろいろ調べてみると、やりたかったことにピッタリ当てはまる記事が見つかりました。

Better Type Safety for your GraphQL resolvers with GraphQL Codegen - The Guild Blog (the-guild.dev)

graphql-codegenには設定でmapperが設定できるようで、GraphQLのTypeにアプリケーション側の型定義をマッピングできるようです。たとえば次のようなGraphQLスキーマを用意したとして

type Query {
    users: [User!]
    user(id: ID!): User
    me: User
    messages: [Message!]!
    message(id: ID!): Message!
}

type User {
    id: ID!
    username: String!
    messages: [Message!]
}

type Message {
    id: ID!
    text: String!
    user: User
}

type Mutation {
    createMessage(text: String!): Message!
    deleteMessage(id: ID!): Boolean!
    updateMessage(id: ID!, text:String!): Boolean!
}

次のようなアプリケーション側のエンティティの型定義を用意する。

たとえば、ここではGraphQLのスキーマでUser TypeはMessgage Typeのリストを持っていますが、アプリケーション側ではメッセージのIDのリストを持っている、といった差異があるとしています。(Message TypeからUser Typeへの関連についても同様)

export type UserModel = {
    id: string
    username: string
    messageIds: string[]
}

export type MessageModel = {
    id: string
    text: string
    userId: string
}

そしてgraphql-codegenでの設定ファイルでマッピング設定を行います。

overwrite: true
generates:
  ./src/types/generated/graphql.ts:
    schema: schema.graphql
    config:
      mappers:
        User: ./src/types/model/models#UserModel
        Message: ./src/types/model/models#MessageModel
      useIndexSignature: true
    plugins:
      - typescript
      - typescript-resolvers

それで生成された型定義を使ってリゾルバを書くと次のようになります。

const users: UserModel[] = [
    {
        id        : '1',
        username  : 'Nanoha Takamachi',
        messageIds: ['1']
    },
    {
        id        : '2',
        username  : 'Fate Testarossa',
        messageIds: ['2']
    },
    {
        id        : '3',
        username  : 'Hayate Yagami',
        messageIds: []
    }
]

const messages: MessageModel[] = [
    {
        id    : '1',
        text  : 'Hello, World',
        userId: '1'
    },
    {
        id    : '2',
        text  : 'Good Afternoon, World',
        userId: '2'
    },
    {
        id    : '3',
        text  : 'Good Bye, World',
        userId: '1'
    }
]

const resolvers: Resolvers = {
    Query   : {
        me      : (parent, args, context) => {
            return me
        },
        user    : (parent, args) => {
            const user = users.find(user => user.id === args.id)
            return user ?? null
        },
        users   : () => {
            return users
        },
        messages: () => {
            return messages
        },
        message : (parent, {id}) => {
            const message = messages.find(message => message.id === id)
            return message ?? {id: 'invalid', text: 'not found', userId: 'not found'}
        }
    },
    User    : {
        username: (parent, args, context, info) => {
            return parent.username
        },
        messages: (user) => {
            return messages.filter(msg => msg.userId === user.id)
        }
    },
    Message : {
        user: (message) => {
            const user = users.find(user => user.id === message.userId)
            return user ?? {id: '', username: '', messageIds: []}
        }
    }
}

たとえばUser Typeのmessagesフィールドのリゾルバのシグネチャですが、第一引数の方がUserModelとなります。マッピングしない場合はGraphQLのUser Typeと同じフィールドを持つUser型となるのですが、マッピングをすることによってアプリケーション側のエンティティ型に型付けされた形でフィールドのリゾルバを書くことができるようになります。

アウトプット

目立ったものはなし。

Profile
d_yama
元Microsoft MVP for Windows Development(2018-2020)
Sub-category : Windows Mixed Reality
Search