graphql-codegenのプラグインを自分で書いた ~ 副題：「GraphQLのクエリーを書けば、上手に型をつけてApolloClientでクエリを飛ばすやつを自動で生成したいと思ったので、Typescriptを書き出すJavascriptを書いた」

# 概要
[GraphQL](https://graphql.org)クエリを書いたら、[graphql-codegen](https://graphql-code-generator.com)が型を吐いてくれるけど、型をいちいち書くのもラクしたかったので、graphql-codegenのプラグインを自分で書いた。

# できた奴

### 1.スキーマの例
こんなスキーマがあるとして、

```Graphql:schema.graphql
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ファイルを作るとする。

```graphql:tweet.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`ごとにクラスを生やしてくれる）

```ts:generated/class.ts
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タグとか書かずに、補完バリバリで気持ちよく書ける。嬉しい。

```ts:main.ts
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でよしなに入れる。

```json:package.json
{
  "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にすれば適当にサーバが立つ。


<details><summary> <font color="#b01">モックサーバーの立て方の例</font> </summary><div>

```Js:server.js
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}`);
});
```

起動

```terminal:terminal
yarn server
```

</div></details>

## Graphql-codegenを走らす
[graphql-codegen](https://graphql-code-generator.com)の設定ファイルを書く。

```codegen.yaml
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`を走らせる。

```terminal:terminal
yarn codegen
```

すると`./generated/types.ts`に型を吐くし、`./generated/nodes.ts`にクエリに`gql`タグ付けた定数が吐かれる。また、これらは`queries`のファイルの変化に応じて逐次更新される。

なお、`"codegen": "graphql-codegen --config codegen.yml --watch"`から`watch`オプションを外せば、自動更新は無効になる。

<details><summary> <font color="#b01">ts:generated/nodes.ts(自動生成)</font> </summary><div>

```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
    }
  }
`;

```

</div></details>

<details><summary> <font color="#b01">ts:generated/types.ts(自動生成)</font> </summary><div>

```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">
  >;
};

```

</div></details>

## オレオレプラグインを書く

graphql_codegenのプラグインを自分で書こう。`return`が`tsの文字列`さえ吐ければ良い。

**参考:** [Write your first Plugin · GraphQL Code Generator](https://graphql-code-generator.com/docs/custom-codegen/write-your-plugin)

```js:プラグイン名.js
module.exports = {
  plugin: (schema, documents, config) => {
    //graphql_codegenがかき集めた、おおよそ人が読むようにできていない、documentsを読み込みながら、jsでts（文字列）を書く。
    //ここでtsをreturnする。
    //出力は勝手にgraphql_codegenがprettierが走らせて整形してくれるので、改行とかタブとか気にせず書く。
  }
};
```

自分が作ったプラグインの読み込み

```codegen.yaml
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](https://github.com/blakeembrey/change-case)パッケージが助かった。

# 結局何を書いたか？
結局こう言うコードを書いた。

```type-apollo-class.js
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");
  }
};
```

そして設定ファイルをこう書いた

```codegen.yaml
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
```

ごちゃっとしているけど、余は満足。

## できた奴
https://github.com/NanimonoDemonai/graphql-query-apollo-client-code-sample/tree/master

# まとめ
* GraphQL楽しい
* JSでTS(文字列)を書くと楽しい。