概要
GraphQLクエリを書いたら、graphql-codegenが型を吐いてくれるけど、型をいちいち書くのもラクしたかったので、graphql-codegenのプラグインを自分で書いた。
できた奴
1.スキーマの例
こんなスキーマがあるとして、
type Tweet {
id: ID!
body: String
date: String
Author: User
Stats: Stat
}
type User {
id: ID!
username: String
first_name: String
last_name: String
full_name: String
name: String @deprecated
avatar_url: String
}
type Stat {
views: Int
likes: Int
retweets: Int
responses: Int
}
type Notification {
id: ID
date: String
type: String
}
type Meta {
count: Int
}
type Comment {
id: String
content: String
}
type Query {
Tweet(id: ID!): Tweet
Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
TweetsMeta: Meta
User(id: ID!): User
Notifications(limit: Int): [Notification]
NotificationsMeta: Meta
}
type Mutation {
createTweet(body: String): Tweet
deleteTweet(id: ID!): Tweet
markTweetRead(id: ID!): Boolean
}
type Subscription {
commentAdded(repoFullName: String!): Comment
}
参考: https://github.com/marmelab/GraphQL-example/blob/master/schema.graphql
2.クエリの例
こんなクエリを書いたgraphqlファイルを作るとする。
query TweetMeta {
TweetsMeta {
count
}
}
query Tweet($id: ID!) {
Tweet(id: $id) {
body
date
Author {
full_name
}
}
}
mutation CreateTweet($body: String) {
createTweet(body: $body) {
id
}
}
subscription SubscComment($repoFullName: String!) {
commentAdded(repoFullName: $repoFullName) {
id
content
}
}
3. 生成例
するとこんな感じにts
を吐いてくれる。(監視対象の.graphql
ごとにクラスを生やしてくれる)
import * as Type from "./types";
import * as Node from "./nodes";
import * as ApolloType from "apollo-client";
import ApolloClient from "apollo-client";
export interface ClientClass {
readonly client: ApolloClient<any>;
}
export class TweetClient implements ClientClass {
constructor(readonly client: ApolloClient<any>) {}
tweetMeta = (
options?: Omit<
ApolloType.QueryOptions<Type.TweetMetaQueryVariables>,
"query"
>
) =>
this.client.query<Type.TweetMetaQuery, Type.TweetMetaQueryVariables>({
...options,
...{ query: Node.TweetMeta }
});
tweet = (
options?: Omit<ApolloType.QueryOptions<Type.TweetQueryVariables>, "query">
) =>
this.client.query<Type.TweetQuery, Type.TweetQueryVariables>({
...options,
...{ query: Node.Tweet }
});
createTweet = (
options?: Omit<
ApolloType.MutationOptions<
Type.CreateTweetMutation,
Type.CreateTweetMutationVariables
>,
"mutation"
>
) =>
this.client.mutate<
Type.CreateTweetMutation,
Type.CreateTweetMutationVariables
>({ ...options, ...{ mutation: Node.CreateTweet } });
subscComment = (
options?: Omit<
ApolloType.SubscriptionOptions<Type.SubscCommentSubscriptionVariables>,
"query"
>
) =>
this.client.subscribe<
Type.SubscCommentSubscription,
Type.SubscCommentSubscriptionVariables
>({ ...options, ...{ query: Node.SubscComment } });
}
4. 使い道
gqlタグとか書かずに、補完バリバリで気持ちよく書ける。嬉しい。
import { TweetClient } from "./generated/class";
import ApolloClient from "apollo-boost";
import "isomorphic-fetch";
const client = new TweetClient(
new ApolloClient({ uri: "http://localhost:4000/" })
);
async function main() {
const hoge = await client.tweetMeta();
console.log(JSON.stringify(hoge.data.TweetsMeta));
const huga = await client.createTweet({
variables: {
body: "aaa"
}
});
//dataはnullチェックしないと怒られる
console.log(JSON.stringify(huga.data && huga.data.createTweet));
const piyo = await client.tweet({ variables: { id: "hoga" } });
console.log(JSON.stringify(piyo.data));
}
main();
作り方
環境
yarnなり、npmでよしなに入れる。
{
"scripts": {
"codegen": "graphql-codegen --config codegen.yml --watch",
"server": "node server.js"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.3.1",
"@graphql-codegen/typescript": "^1.3.1",
"@graphql-codegen/typescript-document-nodes": "^1.3.1-alpha-21fe4751.62",
"@graphql-codegen/typescript-operations": "1.3.1",
"@types/graphql": "^14.2.2",
"apollo-client": "^2.6.3",
"change-case": "^3.1.0",
"graphql": "^14.4.2",
"prettier": "^1.18.2",
"typescript": "^3.5.2"
},
"dependencies": {
"apollo-boost": "^0.4.3",
"apollo-server": "^2.6.7",
"isomorphic-fetch": "^2.2.1"
}
}
適当にモックサーバを立てる。
ApolloServerのmockをtrueにすれば適当にサーバが立つ。
モックサーバーの立て方の例
const { ApolloServer, gql } = require("apollo-server");
const typeDefs = `
type Tweet {
id: ID!
body: String
date: String
Author: User
Stats: Stat
}
type User {
id: ID!
username: String
first_name: String
last_name: String
full_name: String
name: String @deprecated
avatar_url: String
}
type Stat {
views: Int
likes: Int
retweets: Int
responses: Int
}
type Notification {
id: ID
date: String
type: String
}
type Meta {
count: Int
}
type Comment {
id: String
content: String
}
type Query {
Tweet(id: ID!): Tweet
Tweets(limit: Int, skip: Int, sort_field: String, sort_order: String): [Tweet]
TweetsMeta: Meta
User(id: ID!): User
Notifications(limit: Int): [Notification]
NotificationsMeta: Meta
}
type Mutation {
createTweet(body: String): Tweet
deleteTweet(id: ID!): Tweet
markTweetRead(id: ID!): Boolean
}
type Subscription {
commentAdded(repoFullName: String!): Comment
}
`;
const server = new ApolloServer({
typeDefs,
mocks: true
});
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
起動
yarn server
Graphql-codegenを走らす
graphql-codegenの設定ファイルを書く。
overwrite: true
schema: "http://localhost:4000" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
./generated/types.ts:
plugins:
- typescript
- typescript-operations
./generated/nodes.ts:
plugins:
- typescript-document-nodes
監視対象として、さっきの例のtweet.graphql
をqueries/
に作っておく。
そしてgraphql-codegen
を走らせる。
yarn codegen
すると./generated/types.ts
に型を吐くし、./generated/nodes.ts
にクエリにgql
タグ付けた定数が吐かれる。また、これらはqueries
のファイルの変化に応じて逐次更新される。
なお、"codegen": "graphql-codegen --config codegen.yml --watch"
からwatch
オプションを外せば、自動更新は無効になる。
ts:generated/nodes.ts(自動生成)
import { DocumentNode } from "graphql";
import gql from "graphql-tag";
export const TweetMeta: DocumentNode = gql`
query TweetMeta {
TweetsMeta {
count
}
}
`;
export const Tweet: DocumentNode = gql`
query Tweet($id: ID!) {
Tweet(id: $id) {
body
date
Author {
full_name
}
}
}
`;
export const CreateTweet: DocumentNode = gql`
mutation CreateTweet($body: String) {
createTweet(body: $body) {
id
}
}
`;
export const SubscComment: DocumentNode = gql`
subscription SubscComment($repoFullName: String!) {
commentAdded(repoFullName: $repoFullName) {
id
content
}
}
`;
ts:generated/types.ts(自動生成)
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
Upload: any;
};
export enum CacheControlScope {
Public = "PUBLIC",
Private = "PRIVATE"
}
export type Comment = {
__typename?: "Comment";
id?: Maybe<Scalars["String"]>;
content?: Maybe<Scalars["String"]>;
};
export type Meta = {
__typename?: "Meta";
count?: Maybe<Scalars["Int"]>;
};
export type Mutation = {
__typename?: "Mutation";
createTweet?: Maybe<Tweet>;
deleteTweet?: Maybe<Tweet>;
markTweetRead?: Maybe<Scalars["Boolean"]>;
};
export type MutationCreateTweetArgs = {
body?: Maybe<Scalars["String"]>;
};
export type MutationDeleteTweetArgs = {
id: Scalars["ID"];
};
export type MutationMarkTweetReadArgs = {
id: Scalars["ID"];
};
export type Notification = {
__typename?: "Notification";
id?: Maybe<Scalars["ID"]>;
date?: Maybe<Scalars["String"]>;
type?: Maybe<Scalars["String"]>;
};
export type Query = {
__typename?: "Query";
Tweet?: Maybe<Tweet>;
Tweets?: Maybe<Array<Maybe<Tweet>>>;
TweetsMeta?: Maybe<Meta>;
User?: Maybe<User>;
Notifications?: Maybe<Array<Maybe<Notification>>>;
NotificationsMeta?: Maybe<Meta>;
};
export type QueryTweetArgs = {
id: Scalars["ID"];
};
export type QueryTweetsArgs = {
limit?: Maybe<Scalars["Int"]>;
skip?: Maybe<Scalars["Int"]>;
sort_field?: Maybe<Scalars["String"]>;
sort_order?: Maybe<Scalars["String"]>;
};
export type QueryUserArgs = {
id: Scalars["ID"];
};
export type QueryNotificationsArgs = {
limit?: Maybe<Scalars["Int"]>;
};
export type Stat = {
__typename?: "Stat";
views?: Maybe<Scalars["Int"]>;
likes?: Maybe<Scalars["Int"]>;
retweets?: Maybe<Scalars["Int"]>;
responses?: Maybe<Scalars["Int"]>;
};
export type Subscription = {
__typename?: "Subscription";
commentAdded?: Maybe<Comment>;
};
export type SubscriptionCommentAddedArgs = {
repoFullName: Scalars["String"];
};
export type Tweet = {
__typename?: "Tweet";
id: Scalars["ID"];
body?: Maybe<Scalars["String"]>;
date?: Maybe<Scalars["String"]>;
Author?: Maybe<User>;
Stats?: Maybe<Stat>;
};
export type User = {
__typename?: "User";
id: Scalars["ID"];
username?: Maybe<Scalars["String"]>;
first_name?: Maybe<Scalars["String"]>;
last_name?: Maybe<Scalars["String"]>;
full_name?: Maybe<Scalars["String"]>;
name?: Maybe<Scalars["String"]>;
avatar_url?: Maybe<Scalars["String"]>;
};
export type TweetMetaQueryVariables = {};
export type TweetMetaQuery = { __typename?: "Query" } & {
TweetsMeta: Maybe<{ __typename?: "Meta" } & Pick<Meta, "count">>;
};
export type TweetQueryVariables = {
id: Scalars["ID"];
};
export type TweetQuery = { __typename?: "Query" } & {
Tweet: Maybe<
{ __typename?: "Tweet" } & Pick<Tweet, "body" | "date"> & {
Author: Maybe<{ __typename?: "User" } & Pick<User, "full_name">>;
}
>;
};
export type CreateTweetMutationVariables = {
body?: Maybe<Scalars["String"]>;
};
export type CreateTweetMutation = { __typename?: "Mutation" } & {
createTweet: Maybe<{ __typename?: "Tweet" } & Pick<Tweet, "id">>;
};
export type SubscCommentSubscriptionVariables = {
repoFullName: Scalars["String"];
};
export type SubscCommentSubscription = { __typename?: "Subscription" } & {
commentAdded: Maybe<
{ __typename?: "Comment" } & Pick<Comment, "id" | "content">
>;
};
オレオレプラグインを書く
graphql_codegenのプラグインを自分で書こう。return
がtsの文字列
さえ吐ければ良い。
参考: Write your first Plugin · GraphQL Code Generator
module.exports = {
plugin: (schema, documents, config) => {
//graphql_codegenがかき集めた、おおよそ人が読むようにできていない、documentsを読み込みながら、jsでts(文字列)を書く。
//ここでtsをreturnする。
//出力は勝手にgraphql_codegenがprettierが走らせて整形してくれるので、改行とかタブとか気にせず書く。
}
};
自分が作ったプラグインの読み込み
overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates: # 生成先
./generated/types.ts:
plugins:
- typescript
- typescript-operations
./generated/nodes.ts: # 生成先
plugins:
- typescript-document-nodes
./generated/class.ts: # 生成先
- プラグイン名.js # プラグイン読み込み
プラグインを書くにあたっては、キャメルケースなりを変換してくれるchange-caseパッケージが助かった。
結局何を書いたか?
結局こう言うコードを書いた。
var path = require("path");
var changeCase = require("change-case");
const imp = `
import * as Type from "./types";
import * as Node from "./nodes";
import * as ApolloType from "apollo-client";
import ApolloClient from "apollo-client";
export interface ClientClass {readonly client: ApolloClient<any>;}
`;
function makeClassHeader(basename) {
return `
export class ${changeCase.pascalCase(
basename
)}Client implements ClientClass{
constructor(readonly client: ApolloClient<any>) {}
`;
}
function makeClassMethods(operations) {
return operations.map(e => {
const camelName = changeCase.camelCase(e.name);
const pascalName = changeCase.pascalCase(e.name);
const pascalOperation = changeCase.pascalCase(e.operation);
const queryType = `Type.${pascalName + pascalOperation}`;
const variableType = `Type.${pascalName + pascalOperation + "Variables"}`;
const optionType = getOptionName(e.operation, queryType, variableType);
return `
${camelName} = (options?:Omit<${optionType},"${operationQueryName[e.operation]}">) =>
this.client.${operationName[e.operation]}<${queryType},${variableType}>
({...options,...{${operationQueryName[e.operation]}:Node.${pascalName}}})
`;
});
}
const operationName = {
query: "query",
mutation: "mutate",
subscription: "subscribe"
};
function getOptionName(operation, query, variable) {
switch (operation) {
case "query":
return `ApolloType.QueryOptions<${variable}>`;
case "mutation":
return `ApolloType.MutationOptions<${query},${variable}>`;
case "subscription":
return `ApolloType.SubscriptionOptions<${variable}>`;
}
}
const operationQueryName = {
query: "query",
mutation: "mutation",
subscription: "query"
};
module.exports = {
plugin: (schema, documents, config) => {
const classes = documents
.map(doc => {
const filePath = doc.filePath;
const baseName = path.basename(filePath, path.extname(filePath));
const classHeader = makeClassHeader(baseName);
const definitions = doc.content.definitions;
const operations = definitions.map(e => ({
operation: e.operation,
name: e.name.value
}));
const methods = makeClassMethods(operations);
return [classHeader, methods.join("\n"), `}`].join("\n");
})
.join("\n");
return [imp, classes].join("\n");
}
};
そして設定ファイルをこう書いた
overwrite: true
schema: "schema.graphql" # サーバのアドレス。勝手にスキーマを呼んでくれる
documents: "queries/*.graphql" # 監視対象 ここのファイルに更新があれば自動的に型を作成
generates:
./generated/types.ts:
plugins:
- typescript
- typescript-operations
./generated/nodes.ts:
plugins:
- typescript-document-nodes
./generated/class.ts:
- type-apollo-class.js
ごちゃっとしているけど、余は満足。
できた奴
まとめ
- GraphQL楽しい
- JSでTS(文字列)を書くと楽しい。