GraphQLサーバとRESTサーバをさっと立ちあげて、実際に触ってみる。つづき。戻り電文の型を変更する

こんにちは。@masatomix です。

最近、現場でGraphQLとRESTサーバを構築する機会があったので、そのとき得られたナレッジを備忘としてまとめているのですが、その続きです。 

さてさて前回のコードを地道にリファクタリングやいじってナレッジを記録していきます。

https://qiita.com/masatomix/items/d52ef0bdeb131f9dd6f7

## 今回やること

- スキーマファイルを外出しファイルにします。
- Unionを使うことで、実行結果によって戻り電文の型を変更してみます。

## やってみる

### スキーマ情報を外だしします

コード上のGraphQLのスキーマ情報ですが、

```graphql:src/schema.ts
export const typeDefs = gql`
  type Query {
    users: [User]
    companies: [Company]
    findUserById(id: ID!): User
    findCompanyById(id: ID!): Company
  }
  type Mutation {
    createUser(user: UserInput): User
    updateUser(user: UserInput): SuccessResponse
  }

  type SuccessResponse {
    success: Boolean!
    message: String
    error: String
  }

  type User{
    id: ID!
    firstName: String
    lastName: String
    email: String
    age: Int
    companyCode:String
    company: Company
  }

  type Company{
    code: ID!
    name: String
  }
  
  input UserInput {
    userId: ID!
    firstName: String
    lastName: String
    email: String
    age: Int
    companyCode:String
  }
`;
```

ってコード上にベタがきしていましたが、
下記の通り別ファイルにしておくと見通しが良さそうです。

```typescript:src/schema.ts
import { readFileSync } from 'fs';
import { join } from 'path';

export const typeDefs = gql(readFileSync(join(__dirname, 'schema.gql'), 'utf8'));
```

```graphql:src/schema.gql (こちらに外だし)
type Query {
  users: [User]
  companies: [Company]
  user(id: ID!): User
  company(id: ID!): Company
}

type Mutation {
  createUser(user: UserInput): User
  updateUser(user: UserInput): Response
}

type SuccessResponse {
  success: Boolean!
}

type FailResponse {
  success: Boolean!
  message: String
  error: String
}

union Response = SuccessResponse | FailResponse

type User {
  id: ID!
  firstName: String
  lastName: String
  email: String
  age: Int
  companyCode: String
  company: Company
}

type Company {
  code: ID!
  name: String
}

input UserInput {
  userId: ID!
  firstName: String
  lastName: String
  email: String
  age: Int
  companyCode: String
}

```

### unionを使って、実行結果によって戻り電文の型を変更する

たとえば処理が成功したときは``success``プロパティだけだけど、失敗したときはエラーメッセージなど追加情報プロパティを付与したいなどがあります。

具体的には、先日のコードでは、
```typescript
  SuccessResponse: {
    success: (parent: Response) => {
      return parent.result ?
        parent.result.status >= 200 && parent.result.status < 300 :
        false
    },
    message: (parent: Response) => {
      return parent.result ?
        parent.result.status :
        parent.error.message
    },
    error: (parent: Response) => {
      return parent.result ?
        parent.result.status :
        JSON.stringify(parent.error)
    },
  },
```

と戻りの型は固定的でしたが、失敗したときだけ message やerror  をつけたいな、みたいなケースです。上記のように**スキーマは固定的のままで不要なプロパティは値をnullにする**、でも良いのですが、戻り電文の型の仕様自体を変更したい場合は、 GraphQLのunionという機構を使います。

unionを使ったスキーマ定義はこんな感じ。

```graphql
type SuccessResponse {
  success: Boolean!
}

type FailResponse {
  success: Boolean!
  message: String
  error: String
}

union Response = SuccessResponse | FailResponse
```

成功(``SuccessResponse``)と失敗(``FailResponse``)それぞれ別のプロパティの型を定義し、``Response``はそれらのunionと定義しました。**状況に応じてどっちか、みたいな意味**です。

さてそれぞれのリゾルバです。

```typescript
  SuccessResponse: {
    success: checkSuccess,
  },

  FailResponse: {
    success: checkSuccess,
    message: (parent: RestResponse) => {
      return parent.result ?
        parent.result.status :
        parent.error.message
    },
    error: (parent: RestResponse) => {
      return parent.result ?
        parent.result.status :
        JSON.stringify(parent.error)
    },
  },
  ```
 ここまではいいですよね。成功の時は ``success`` 、失敗の時はその他のプロパティも返しています。で、Responseのリゾルバは、
  ```  
  Response: {// GraphQLでのunion型は、どちらの型を返すかのロジックを定義しないといけない
    __resolveType(obj: { result: any; }, context: any, info: any) {
      return obj.result ? 'SuccessResponse' : 'FailResponse'
    }
  },
```
としました。unionである``Response``については、**どちらの型を返すかの判定ロジックを記述**しています。

コード全体をのせると以下の通りです。

```typescript
import { gql } from 'apollo-server-express';
import {
  AppUserEntityControllerApi,
  AppUserRequestBody,
  CompanyEntityControllerApi,
  EntityModelAppUser,
  EntityModelCompany
} from './generated'
import axios, { AxiosResponse } from 'axios';
import DataLoader from 'dataloader';

import { readFileSync } from 'fs';
import { join } from 'path';

export const typeDefs = gql(readFileSync(join(__dirname, 'schema.gql'), 'utf8'));

type UserInput = {
  userId: string
  firstName?: string
  lastName?: string
  email?: string
  age?: number
  companyCode?: string
}

type RestResponse = {
  result?: AxiosResponse<EntityModelAppUser, any>;
  error?: any;
}

const checkSuccess = (parent: RestResponse): boolean => {
  return parent.result ?
    parent.result.status >= 200 && parent.result.status < 300 :
    false
}

const dataLoader = new DataLoader<string, EntityModelCompany>(async (ids): Promise<EntityModelCompany[]> => {
  console.log('batch start')
  console.log(ids)

  const idsStr = ids.join(',')
  const data = (await axios.get('http://localhost:8080/companies/search/findAllByIdIn',
    {
      params: { ids: idsStr }
    })).data;
  const companies: EntityModelCompany[] = data._embedded?.company
  // 戻り値のデータは、引数のデータの順番通りとはかぎらない

  // 引数のidsの順番にあうように、結果を並べ替える処理
  // companyCodeとCompanyのMap
  type CompanyMap = { [key: string]: EntityModelCompany }
  const companyMap: CompanyMap = companies.reduce((map, company) => {
    map[company.companyCode!] = company
    return map
  }, {} as CompanyMap)

  return ids.map(id => companyMap[id])
})

export const resolvers = {
  Query: {
    users: async () => {
      const api = new AppUserEntityControllerApi()
      const data = (await api.getCollectionResourceAppuserGet1()).data
      console.table(data._embedded?.user)
      return data._embedded?.user
    },

    companies: async () => {
      const api = new CompanyEntityControllerApi()
      const data = (await api.getCollectionResourceCompanyGet1()).data
      console.table(data._embedded?.company)
      return data._embedded?.company
    },

    user: async (parent: {}, args: { id: string }) => {
      const p = new AppUserEntityControllerApi().getItemResourceAppuserGet(args.id)
      const data = (await p).data
      console.log(data)
      return data
    },

    company: async (parent: {}, args: { id: string }) => {
      const p = new CompanyEntityControllerApi().getItemResourceCompanyGet(args.id)
      const data = (await p).data
      console.log(data)
      return data
    }

  },

  User: {
    id: (parent: EntityModelAppUser) => `${parent.userId}`,
    // company: (parent: EntityModelAppUser) => parent._links!.company.href
    company: async (parent: EntityModelAppUser) => {
      // DataLoaderを使う方法。ここではこのコードの登録(？)のみが行われ、
      // 実際は全部のcompanyがそろった時点で、DataLoader.BatchLoadFn 関数がコールされる
      const data = (await dataLoader.load(parent.companyCode!))

      // この方式だと、PK指定でN+1問題が発生しちゃう
      // const api = new CompanyEntityControllerApi()
      // const data = (await api.getItemResourceCompanyGet(parent.companyCode!)).data

      console.log(data)
      return data
    }
  },

  Company: {
    code: (parent: EntityModelCompany) => `${parent.companyCode}`,
    name: (parent: EntityModelCompany) => `${parent.companyName}`,
  },

  Response: {// GraphQLでのunion型は、どちらの型を返すかのロジックを定義しないといけない
    __resolveType(obj: { result: any; }, context: any, info: any) {
      return obj.result ? 'SuccessResponse' : 'FailResponse'
    }
  },

  SuccessResponse: {
    success: checkSuccess,
  },

  FailResponse: {
    success: checkSuccess,
    message: (parent: RestResponse) => {
      return parent.result ?
        parent.result.status :
        parent.error.message
    },
    error: (parent: RestResponse) => {
      return parent.result ?
        parent.result.status :
        JSON.stringify(parent.error)
    },
  },

  Mutation: {
    createUser: async (parent: {}, args: { user: UserInput }) => {
      const api = new AppUserEntityControllerApi()
      return (await api.postCollectionResourceAppuserPost(args.user)).data
    },

    updateUser: async (parent: {}, args: { user: UserInput }): Promise<RestResponse> => {
      try {
        const userPromise = new AppUserEntityControllerApi()
          .getItemResourceAppuserGet(args.user.userId)
        const instance = (await userPromise).data
        console.log(instance)

        const userInput = args.user
        console.log(userInput)

        const body: AppUserRequestBody = {
          userId: userInput.userId,
          firstName: userInput.firstName ?? instance.firstName,
          lastName: userInput.lastName ?? instance.lastName,
          age: userInput.age ?? instance.age,
          companyCode: userInput.companyCode ?? instance.companyCode,
          email: userInput.email ?? instance.email,
        }

        try {
          const updatePromise = new AppUserEntityControllerApi()
            .postCollectionResourceAppuserPost(body)
          const result = await updatePromise
          return { result }
        } catch (error) {
          console.log(error)
          return { error } // 例外のときもがんばって正常終了させる
        }
      } catch (error) {
        console.log(error)
        return { error } // 例外のときもがんばって正常終了させる
      }
    }
  }
}
```

DataLoader とか見 慣れないコードはまた次回。

とりあえず投げてみましょう。

![union.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73777/cb056c29-2a82-dcfd-5f10-25b0f8cecc15.jpeg)

投げるときはこのように、

```graphql
mutation Mutation($input1: UserInput) {
  updateUser(user: $input1) {
    ... on SuccessResponse {
      success
    }
    ... on FailResponse {
      error
      message
      success
    }
  }
}
```
**成功した場合と失敗した場合を並べて記述**することができます。戻り値は、

成功した場合

```json
{
  "data": {
    "updateUser": {
      "success": true
    }
  }
}
```


失敗した場合
```json
{
  "data": {
    "updateUser": {
      "error": "{\"message\":\"Request failed with status code 409\",\"name\":\"AxiosError\",\"stack\":\"AxiosError: d...割愛...{"kino1@example.com\\\"}\",\"url\":\"http://localhost:8080/users\"},\"code\":\"ERR_BAD_REQUEST\",\"status\":409}",
      "message": "Request failed with status code 409",
      "success": false
    }
  }
}
```
というように、プロパティのあるなしに違いがあるのがわかりますね！

ちなみにcurlでも投げておきましょう。

```
$ cat data.txt 
{
    "query":
    "
mutation Mutation($input1: UserInput) {
  updateUser(user: $input1) {
    ... on SuccessResponse {
      success
    }
    ... on FailResponse {
      error
      message
      success
    }
  }
}
",
    "variables":{
  "input1": {
    "companyCode": "com003",
    "userId": "u001"
  }
}
```

```
$ cat data.txt | curl --data @-  \
  --request POST \
  --header 'content-type: application/json' \
  --url http://localhost:3000/graphql | jq

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   329  100    41  100   288   1491  10474 --:--:-- --:--:-- --:--:-- 12185
{
  "data": {
    "updateUser": {
      "success": true
    }
  }
}
$ 
```

できました。

## まとめ

実際は、**成功・失敗をこのように使い分ける**か、それともシンプルに**失敗は例外をスローしちゃう**かはプロジェクトによるかとおもいます[^1]。バックエンドのAPI(用のプロキシライブラリ)が例外をスローしちゃってるのに、わざわざtry/catchして正常終了に詰め替えているのも微妙かもしれません。

[^1]: 成功失敗をunion型で判定する際、さらに失敗を種類によって型を分けて、何エラーが発生したかを呼び元でわかるようにするとか、？あんまないかなこのパタンは。

ひきつづき、こんどは**バックエンドが例外ならGraphQLサーバも例外を投げてしまうケース**や、**上記で説明をペンディングしたDataLoader**なども記事にしていきます！

おつかれさまでした。

## 関連リンク



- [GraphQLサーバとRESTサーバをさっと立ちあげて、実際に触ってみる。つづき。関連のあるデータの取得](https://qiita.com/masatomix/items/d52ef0bdeb131f9dd6f7) 前回の記事
- https://github.com/masatomix/spring-data-rest-example/releases/tag/practice_union 今回のソースです
- [Spring Data REST](https://spring.io/projects/spring-data-rest) 公式
- [Apollo Server](https://www.apollographql.com/docs/apollo-server/) 公式
- [Spring Data RESTの要点と利用方法](https://qiita.com/umiushi_1/items/b369f659bbd94576b8f4) Spring Data RESTについての記事。参考にさせてもらいました。
- [GraphQLにおけるN+1問題の解決策](https://qiita.com/nakawodayo/items/2f2f98c9833659da66f0) N+1問題についての解決策を提示しています。このDataLoaderを使って先ほどの問題を解決します