2021年6月第4週レポート
インプット
📝 「The Road to GraphQL」を読んでいる
先週に引き続き、GraphQLの学習のため以下の書籍を読んでいます。
この本で学べる内容は以下の通りです。 - 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
でも出来るようですが、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
型となるのですが、マッピングをすることによってアプリケーション側のエンティティ型に型付けされた形でフィールドのリゾルバを書くことができるようになります。
アウトプット
目立ったものはなし。