【GraphQL】クエリで取得するデータの制限をする

## はじめに
GraphQLクライアントからクエリを呼び出す際、取得するデータに制限がない場合

意図しない膨大なデータ量のリクエストや、悪意のある攻撃を受けてしまう危険性があります。

そういった事態を防ぐために、GraphQLサーバー側で対策をすることができます。


## 環境
- GraphQL Server
    - Node.js
    - Express
    - MongoDB
    - Apollo Server
- GraphQL Client
    - React.js
    - Apollo Client

## データの制限
下記のようなスキーマ・クエリを想定します。

```graphql:server/typeDefs.graphql
type Photo {
    id: ID!
    url: String!
    name: String!
    postedBy: User!
    taggedUsers: [User!]!
    created: DateTime!
}

type User {
    name: String
    avatar: String
    postedPhotos: [Photo!]!
    inPhotos: [Photo!]!
}
```

```graphql:client/query.graphql
query allPhotos {
    allPhotos(first=99999) {
        name
        url
        postedBy {
            name
            avatar
        }
    }
} 
```

クエリの引数に```first```を追加すると、一つのデータページが返却するデータ件数を指定することができます。
上記のクエリでは、```first```に```99999```を指定しているため、99999のデータ取得が発生してしまいます。


### リゾルバに制限を加える
クエリリゾルバに引数のチェック処理を加えることで、データサイズを制限します。

```javascript:server/resolver.js
allPhots: (root, data, context) {
    if (data.first > 100) {
        throw new Erroe('Only 100 photos can be requested at a time')
    }
}
```

## クエリ深さの制限
GraphQLがクエリに提供する利点の一つとして、関係するデータを一度に問い合わせられることです。
例えば、下記のようなPhoto APIがあったとします。

```graphql:client/query.graphql
query getPhoto($id: ID!) {
    Photo(id: $id) {
        name
        url
        postedBy {
            name
            avatar
            postedPhotos {
                name
                url
            }
        }
    }
} 
```

このクエリは、```postedBy```と```postedPhoto```という2つの関連するフィールドを問い合わせているので、深さは3であると言えます。

```graphql:client/query.graphql
query getPhoto($id: ID!) {
    Photo(id: $id) {
        name # 深さ：1
        url  # 深さ：1
        postedBy {
            name    # 深さ：2
            avatar  # 深さ：2
            postedPhotos {
                name   # 深さ：3
                url    # 深さ：3
            }
        }
    }
} 
```

さらに、上記のクエリを活用すると、下記のように、深さがより深くなるクエリも発行できてしまいます。

```graphql:client/query.graphql
query getPhoto($id: ID!) {
    Photo(id: $id) {
        name # 深さ：1
        url  # 深さ：1
        postedBy {
            name    # 深さ：2
            avatar  # 深さ：2
            postedPhotos {
                name   # 深さ：3
                url    # 深さ：3
                taggedUsers {
                    name    # 深さ：4
                    avatar  # 深さ：4
                    postedPhots {
                        name    # 深さ：5
                        url     # 深さ：5
                    }
                }
            }
        }
    }
} 
```

クエリの深さが深くなると、サーバーへかかる負荷は指数関数的に増大してしまいます。

こういったクエリを許容しないために、クエリの深さ制限を実現するnpmパッケージもあります。

```
npm install graphql-depth-limit
```

上記パッケージをinstallすると、depthLimit関数を私用してGraphQLサーバーの設定にバリデーションルールを追加できます。

```javascript:server/index.js
const { ApolloServer } = require(`apollo-server-express`);
const depthLimit = require('graphql-depth-limit');
.....

const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [depthLimit(5)],
    .....
})

```


## クエリの複雑さ制限
次に```クエリ複雑度```が高い場合のクエリを考えます。
上記のように深さがそこまで深くないクエリであったとしても、問い合わせられるフィールドの両が多いことで負荷が高くなるクエリがあります。

```graphql:client/query.graphql
query everything($id: ID!) {
    totalUsers  # 深さ：1
    Photo(id: $id) {
        name    # 深さ：1
        url     # 深さ：1
    }
    allUsers {
        id      # 深さ：2
        name    # 深さ：2
        avatar  # 深さ：2
        postedPhotos {
            name    # 深さ：3
            url     # 深さ：3
        }
        inPhotos {
            name    # 深さ：3
            url     # 深さ：3
            taggedUsers {
                id  # 深さ：4
            }
        }
    }
} 
```

everythingクエリはクエリの深さ制限5以内には収まっていますが、問い合わせられるフィールドの数が多く、非常に高コストな処理になります。

クエリ複雑度の制限の実装をサポートするnpmパッケージもあります。

```
npm install graphql-validation-complexity
```

この```graphql-validation-complexity```にはクエリの複雑度を決定するためのルールが定められています。
スカラーフィールドはそれぞれ値1が設定され、フィールドがリストなら10倍ずつ値を増やしたものが設定されています。

```graphql:client/query.graphql
query everything($id: ID!) {
    totalUsers  # 複雑度：1
    Photo(id: $id) {
        name    # 複雑度：1
        url     # 複雑度：1
    }
    allUsers {
        id      # 複雑度：10
        name    # 複雑度：10
        avatar  # 複雑度：10
        postedPhotos {
            name    # 複雑度：100
            url     # 複雑度：100
        }
        inPhotos {
            name    # 複雑度：100
            url     # 複雑度：100
            taggedUsers {
                id  # 複雑度：1000
            }
        }
    }
}       # 総複雑度1433
```

クエリの複雑度の制限を1000にすると、こういった特殊なクエリの実行を防ぐことができます。

```javascript:server/index.js
const { createComplexityLimitRule } = require('graphql-validation-complexity')

.....

const server = new ApolloServer({
    typeDefs,
    resolvers,
    validationRules: [
      depthLimit(5),
      createComplexityLimitRule(1000, {
          onCost: cost => console.log('query cost: ', cost)
      })
    ],
    .....
}
```

この例では、最大複雑度を1000に制限しました。

また、それぞれのクエリの総コストが計算されたときに、その値を引数として呼び出されるonCost関数も実装しています。

先ほどのクエリは最大複雑度が1000を超えているので、この条件下では実行が許可されません。


## 最後に
GraphQL Server/Clientにそれぞれ設定を入れることで、負荷がかかるクエリの発行を未然に防ぐことができる、ということがわかりました。

膨大なデータの要求をしなくてよいだけでなく、悪意のある攻撃からも一定の効果があると思いますので、実務にも取り入れられるところは積極的に取り入れていたいです。