# 初めてのGraphQL読書会 Vol.2[記録用] ## 概要 途中から来た方へ 「初めてのGraphQL」を読む 読む範囲は基本的に4〜5章(※おまかせします) ZoomをONにする枠が、共通のHackMDに読書ログを記入 【大まかな段取り】 〜10時45分 読書 〜11時25分 読んだ内容の共有、その他導入事例など雑談 **学んだ内容を共有してもいいよ!っていう方は、あとで共有できるようにmarkdownで読書メモを取ってください!** ## meijin ### スキーマの設計 - GraphQLはデザインプロセスを変える存在です - →個々のレスポンスを毎回定義するのではなく、型の集合として捉えて設計するようになるということかな - スキーマを定義するための言語:スキーマ定義言語(SDL) - カスタムスカラー型 - 組み込み(Int、Float、String、Boolean、ID)以外の型 - →ID型ってなに・・・? - DateTime型といったカスタムスカラー型を定義できる - 型自体にバリデーションが必要な場合に宣言できる - →Post等の更新時に便利か? - →Getのときにバリデーションするのであれば、InValidなデータをGetしたときはどうなるのか? - Enum - リスト - [Int!]: 必ずIntが入っている配列だけど、それ自体がNULLになることはあり得る - 忘れそう・・・というか、ほとんどNOT NULLが普通だと思うので基本!が付くのは邪魔そう - リレーション - フィールドの型に他のTypeを設定することでリレーションを表現 - Typeのリスト型にすることで、1:多の接続にできる - 多対多は、双方向に1:多にすればよい(直感的!) - スルー型 - リレーションそのものに付随する情報を持たっせ、専用の型にしたもの ```graphql= type Friendship { friends: [User!]! howLong: Int! whereWeMet: Location } ``` - ユニオン型 - TSのユニオン型に似ている - むしろクエリを書くときが書き方が変わっている気がする - インターフェース - 要は継承? - 普通の設計と同様、本質的に継承したほうが良い関係性のときに使うのがよさそう - 引数、ページング、ソート - クエリの型定義をするときに使う表現 - →GraphQLのどのフィールドにも追加できます、の意味がよくわからない - 入力型 - 多くの引数を扱うために存在する - →待ってました - input {Type名}で作成する - 引数としてしか利用することが**できない** ### GraphQLサーバーの実装 - Apollo Server - リゾルバは特定のフィールドのデータを返す関数 - スキーマと同じ名前のリゾルバ関数を作る - Mutationのリゾルバ関数の第1引数は親オブジェクト(?)への参照 - 型リゾルバ type Queryに対してPhoto自身をリゾルバ関数で取得する ```graphql= Photo: (parent, args, { db }) => db.collection('photos') .findOne({ _id: ObjectID(args.id) }), ``` nameとかはそのままDBから返していいけど、他のフィールドはparent(元になるPhoto型オブジェクト)を経由して、なんらかの整形や追加のDBアクセスをして取得する必要があるときに、こうやって個別でフィールドの定義をする。 ```graphql= id: parent => parent.id || parent._id, url: parent => `/img/photos/${parent._id}.jpg`, postedBy: (parent, args, { db }) => db.collection('users') .findOne({ githubLogin: parent.userID }), taggedUsers: async (parent, args, { db }) => { const tags = await db.collection('tags').find().toArray() const logins = tags .filter(t => t.photoID === parent._id.toString()) .map(t => t.githubLogin) return db.collection('users') .find({ githubLogin: { $in: logins }}) .toArray() } }, ``` こうやって定義されたresolverは最終的にApollo Serverのコンストラクタの引数のresolversフィールドに入れられる ```javascript= const server = new ApolloServer({ typeDefs, resolvers, context: async ({ req }) => { const githubToken = req.headers.authorization const currentUser = await db.collection('users').findOne({ githubToken }) return { db, currentUser } } }) ``` カスタムスカラー型も、resolverオブジェクトのトップに指定する(むしろこれ以外の方法はある?) ```javascript= DateTime: new GraphQLScalarType({ name: 'DateTime', description: 'A valid date time value.', parseValue: value => new Date(value), serialize: value => new Date(value).toISOString(), parseLiteral: ast => ast.value }) ``` ### コンテキスト - ReactのuseContextみたいなやつか・・・ - Apollo Serverのコンストラクタで指定したContextは、リゾルバ関数の第3引数に入ってくる - meクエリを実装して、自分自身をクエリできるようにする - ここまでの実装で、任意の認証サービスの利用に応用できそう。 - ただし、同一のGraphQLサーバーに対して、複数の認証手法でクエリを複数のクライアントから飛ばされてくるケースにはどう対応する?LaravelだとAuth Facadeという仕組みで差し替え可能な認証機能がある ### PHPのリゾルバ実装を見てみよう https://webonyx.github.io/graphql-php/schema-definition/ ```php= $queryType = new ObjectType([ 'name' => 'Query', 'fields' => [ 'hello' => [ 'type' => Type::string(), 'resolve' => function() { return 'Hello World!'; } ], 'hero' => [ 'type' => $characterInterface, 'args' => [ 'episode' => [ 'type' => $episodeEnum ] ], 'resolve' => function ($rootValue, $args) { return StarWarsData::getHero(isset($args['episode']) ? $args['episode'] : null); }, ] ] ]); ``` Nodeで書いたほうが1,000倍楽にかけそう 連想配列ベースなのがしんどい感じはする でもさっき読んだ内容のおかげで何をやっているかはだいたいわかる https://lighthouse-php.com/5/getting-started/installation.html --- ゆいと # スキーマファースト スキーマを初めに考えて開発を進めていく?スキーマさえできていればバックエンドの実装なくても自動で型生成とかして開発進められそう。 RESTでいう先にインターフェースを合意していくのと似ているが、スキーマなので画面の開発ごとに発生しない利点がある。 # カスタムスカラー Datetimeなどのシリアライズを指定したりできる。IDやNameなどの振る舞いをもった値オブジェクトも扱える # GraphQLサーバーの実装 ```jsx // 大体.graphqlファイルに切り出す? const typeDefs = ` type Photo { id: ID! url: String! } type Query { allPhotos: [Photo!]! } ` const resolvers = { Query: { allPhotos: () => photoUseCase.listAllPhotos() } } ``` ![](https://i.imgur.com/PiinKS9.png) オニオンアーキテクチャ風にいうとスキーマやクエリはUI層? スキーマとモデル(DDDだとエンティティ)の棲み分けはどうすべきだろう? ```jsx // 大体.graphqlファイルに切り出す? const typeDefs = ` type User { id: ID! name: String! photos: [Photo!]! } type Photo { id: ID! url: String! } type Query { allUsers: [User!]! } ` const Query = { allUsers: () => userUseCase.listAllUsers() // UserというよりfindUserとかfindOneUserとかのほうができせつ? User: (parent, args) => userUseCase.findUser(args.id), // photosが必要ないときにロードしないみたいなのはどうすればいいだろう? User: (parent, args) => { ...userUseCase.findUser(),   // parentで親のパラメータ(user)がとれるらしい photos: parent => photoUseCase.listByUserId(parent.id) // こう切り出せばいけそう? } } const resolver = { Query } // ``` --- Kamimura ## GitHubの認可プロセス GitHubアプリを認可するプロセスは、クライアントとサーバーで行われる。以下認可プロセス 1. [クライアント] GitHubにclient_idを含むURLへコード要求 2. [ユーザー] クライアントアプリケーションがGitHub上のアカウント情報へアクセスすることを許可 3. コードをOAuthのリダイレクトURLであるhttp://localhost:3000?code=XYZに送る 4. [クライアント] GraphQLのミューテーションgithubAuth(code) でcodeを送信 5. [API] client_id、client_secret、client_codeを用いてGitHubaccess_tokenを要求 6. [GitHub] 今後のリクエストで使うためのaccess_tokenを返す 7. [API] access_tokenを用いてユーザー情報を要求 8. [GitHub] ユーザー情報を返す (name, avatar) 9. [API] authUser (code) ミューテーションをトークンとユーザー情報を含むAuthPayloadで解決 10. [クライアント] 今後のGraphQLリクエストで使用トークンを保存 ### githubAuthミューテーション GItHubのミューテーションを利用してユーザーの認証を行う AuthPayload型はトークンと、認証されたユーザーの情報が含まれる。 ```javascript type AuthPayload { token: String! user: User! } type Mutation { githubAuth(code: String!): AuthPayload! } ``` GitHub APIリクエストを処理する関数 トークンを取得するため、GitHubのAPIにPOSTリクエストを送る。credentialsのclient_id、client_secret、codeの3つで構成されて、bodyに含める。 ```javascript const requestGitHubToken = credentials => fetch( `https://github.com/login/oauth/access_token`, { method: `POST`, headers: { `Content-Type`: `application/json`, Accept: `application/json` }, body: JSON.stringify(credentials) } ).then(res => res.json()) .catch(error => { throw new Error(JSON.stringify(error)) }) ``` 次の処理はtokenをユーザー情報にアクセスする。 ```javascript const requestGithubUserAccount = token => fetch(`https://api.github.com/user?access_token=${token}`) .then(toJSON) .catch(throwError) ``` 非同期でaccess_tokenとgithubUser (ユーザー情報) を取得。 ```javasript async authorizedWithGithub(credentials) { const { access_token } = await requestGithubToken(credentials) const githubUser = await requestGithubUserAccount(access_token) return { ...githubUser, access_token } } ``` 最後に、githubAuthの実装。取得したユーザー情報を保存して、保存したデータとトークン情報を返却 ```javascript async githubAuth(parent, { code }, { db }) { let { message, access_token, avatar_url, login, name } = await authorizeWithGithub({ client_id: 'hogehoge'. client_secret: 'hogehoge', code }) if (message) { throw new Error(message) } let latestUserInfo = { name, githubLogin: login, githubToken: access_token, avatar: avatar_url } const { ops:[user] } = await db .collection(`user`) .replaceOne({ githubLogin: login }, latestUserInfo, { upsert: true }) return { user, token: access_token } } ``` このミューテーションは、現在のユーザーを認可し、そのユーザーに関する情報とトークンを返す。今後のリクエストではヘッダーにトークンを格納して送る必要がある。 ```javascript githubAuth(code: `XYZ`) { token user { githubLogin name avatar } } ``` --- りゅーそう GraphQL = スキーマ定義言語 データに対応するフィールドを持つ ```graphql "カスタムスカラー型" scalar DateTime type Photo { id: ID! name: String! url: String! description: String! created: DateTime! } ``` スカラー型(Int, Float, String, Boolean,ID)をもつ **graphql-custom-types** - GraphQLEmail - The Email scalar type represents E-Mail addresses compliant to RFC 822. - GraphQLURL - GraphQLDateTime - GraphQLLimitedString(min, max, alphabet) - GraphQLPassword(min, max, alphabet, complexity) - GraphQLUUID Enum型 ## コネクション nullではないことを !で表す。 ・一対一の接続 ・一対多の接続:無効グラフ、双方向にエッジを作成する ルート型で頻繁に作成 ・多対多の接続:双方向に一対多の接続を定義 関係自体に意味を持たせるにはスルー型を用いる ```graphql type Friendship { "friendsはnullかもしれないが、中身はnullではない" friends: [User!] howLong: Int! whereWeMet: Location } ``` ### 異なる型のリスト ・ユニオン型  複数の型が全く異なる場合 ・インターフェース  共通のフィールドがある場合 ### 引数 ・フィルタリング ・データページング ・ソート ## Mutation アプリケーションで使われる**動詞**と対応づくのが望ましい ```graphql type Mutation { postPhoto( name: String! description: String category: PhotoCategory=PORTRAIT ): Photo! } "使う奴はschemaに定義する" schema { Query Mutation Subscription } ``` ### 入力型 - オブジェクト型と似ているが、引数でしか利用できない - ページングやソートにも利用出来る - 任意のフィールドに引数として利用できる。多くのフィールドの引数で使い回すのがスキーマ設計の鍵 ```graphql input PostPhotoInput { name: String! description: String category: PhotoCategory=PORTRAIT } type Mutation { postPhoto(input: PostPhotoInput!):Photo! } ``` ### 返却型 返り値も加えることが出来る。tokenを!をつけることで、必須にする例 ```graphql "Userに加えて、tokenを必須にしてログイン" type AuthPayload { user: User! token: String! } type Mutation { githubAuth(code: String!): AuthPayload! } ```