Azure API Management の GraphQL API の リゾルバー で Cosmos DB データソースを使ってみる

# はじめに

API Managementは、API を簡単に管理できるサービスですが、REST API だけでなく GraphQL の API も作成できることができます。
API Management では、これまで既存の REST API をデータソースとして GraphQL の API を作成することができましたが、Cosmos DB と Azure SQL Database をデータソースとして使用できるようになりました。**(※プレビュー段階)**
これらのデータソースを使用することで、別途バックエンドを用意せずにデータベースを直接 API として公開することができます。
前回の記事では、Azure SQL Database のデータソースを利用しましたが、この記事では Cosmos DB のデータソースを使って、API Management で GraphQL の API を作成してみたいと思います。

Azure SQL Database 版

https://qiita.com/fsdg-nagumo/items/8478c79ebb0e566220c7

# 前提

- API Management のリソースが作成済みであること。
    - この記事では API Management のリソース作成手順については説明しません。
    - 価格レベルは何でも良いと思いますが、執筆時は従量課金レベルで検証しています。
- Cosmos DB のリソースが作成済みであること。
    - この記事では Cosmos DB のリソース作成手順については説明しません。
- [Azure CLI](https://learn.microsoft.com/ja-jp/cli/azure/install-azure-cli) がインストール済みであること。
    - この記事では Azure CLI のインストール手順については説明しません。

# 事前準備

基本的にはドキュメントに従って進めていきます。

https://learn.microsoft.com/ja-jp/azure/api-management/cosmosdb-data-source-policy

## API Management のマネージド ID を有効にする

※[SQL Database 版](https://qiita.com/fsdg-nagumo/items/8478c79ebb0e566220c7#api-management-%E3%81%AE%E3%83%9E%E3%83%8D%E3%83%BC%E3%82%B8%E3%83%89-id-%E3%82%92%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B)と同様となります。

## Cosmos DB のロールの割り当て

ターミナルで以下のコマンドを実行し、マネージド ID に Cosmos DB のロールを付与します。

```bash:bash
resourceGroupName="<リソースグループ名>"
cosmosName="<Cosmos DB のリソース名>"
apimName="<API Management のリソース名>"

# API Management のプリンシパルID を取得する
apimPrincipal=$(az apim show \
                    --resource-group $resourceGroupName \
                    --name $apimName \
                    --query "identity.principalId" \
                    --output tsv)

# Cosmos DB のリソースID を取得する
cosmosdbId=$(
    az cosmosdb show \
        --resource-group $resourceGroupName \
        --name $cosmosName \
        --query id \
        --output tsv
)

# API Management の マネージド ID に Cosmos DB のロールを付与する
# ここでは「Cosmos DB Built-in Data Contributor」を付与
az cosmosdb sql role assignment create \
    --resource-group $resourceGroupName \
    --account-name $cosmosName \
    --role-definition-name "Cosmos DB Built-in Data Contributor" \
    --principal-id $apimPrincipal \
    --scope $cosmosdbId

```

## データベースとコンテナの作成

適当なデータベースとコンテナを作成し、適当なデータを投入します。


# API の作成

## Schema ファイルの作成

GraphQL API を作成する際に Schema ファイルを選択する必要があるため、事前に作成します。
今回は以下のように作成しました。

```graphql
type Query {
  getUser(id: ID!): User!
  getUsers(): UserList!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

type User {
  id: ID!
  name: String!
  age: Int!
}

type UserList {
  items: [User!]!
  hasNextPage: Boolean!
  endCursor: String
}

input UpdateUserInput {
  id: ID!
  name: String!
  age: Int!
}

input CreateUserInput {
  id: ID!
  name: String!
  age: Int!
}
```

## GraphQL API の追加

※[SQL Database 版](https://qiita.com/fsdg-nagumo/items/8478c79ebb0e566220c7#graphql-api-%E3%81%AE%E8%BF%BD%E5%8A%A0)と同様となります。

## Resolver の作成

作成した API が一覧に表示されるので、クリックすると先ほど選択した Schema が見られます。
更に行番号の左にある`＋`をクリックすると、Resolver の登録画面に行くことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/943698d3-4f2d-fe89-1d3d-d5c3cee5efd7.png)

Resolver の登録画面で Data source に`Azure Cosmos DB`を設定し、Resolver policy(後述)を入力して Create をクリック。
※Schema から ジャンプした場合は Name、Type、Field は自動的に入力されています
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/2a50ba2f-d8bc-053a-e9af-f80e5439b9b4.png)

### ポリシーの設定

#### 接続設定(共通)

```xml
<cosmosdb-data-source>
    <!-- 接続設定 -->
	<connection-info>
		<connection-string use-managed-identity="true">
            AccountEndpoint=https://<Cosmos DB リソース名>.documents.azure.com:443/;
        </connection-string>
		<database-name>demo-db</database-name>
		<container-name>users</container-name>
	</connection-info>
	<!-- 省略 -->
</cosmosdb-data-source>
```

マネージドIDで接続するため、`connection-string`の属性に`use-managed-identity="true"`を指定します。
`database-name`に Cosmos DB のデータベース名を指定します。
`container-name`に Cosmos DB のコンテナ名を指定します。

#### 単一結果の取得

単一データを取得する場合は以下の様なポリシーとなります。
(getUser)

```xml
<!-- getUser -->
<cosmosdb-data-source>
	<!-- 接続設定は省略 -->
	<read-request>
		<id>
            @(context.GraphQL.Arguments["id"].Value<string>())
        </id>
		<partition-key>
            @(context.GraphQL.Arguments["id"].Value<string>())
        </partition-key>
	</read-request>
</cosmosdb-data-source>
```

`read-request`要素を設定します。
SQL Database とは異なり、こちらはクエリを書かずにキー(`id`、`partition-key`)を指定するだけで OK です。
`id`、`partition-key`ではリクエストパラメータを参照することができます。
参照するには [ポリシー式](https://learn.microsoft.com/ja-jp/azure/api-management/api-management-policy-expressions) を指定します。
`context.GraphQL.Arguments`にリクエスト時に指定した引数が格納されています。

https://learn.microsoft.com/ja-jp/azure/api-management/api-management-policy-expressions

#### 複数データの取得

複数行の結果を取得する場合は以下の様なポリシーとなります。
(getUsers)

```xml
<!-- getUsers -->
<cosmosdb-data-source>
	<!-- 接続設定は省略 -->
	<query-request>
		<sql-statement>
            SELECT * FROM c
        </sql-statement>
	</query-request>
</cosmosdb-data-source>
```

結果セットが複数行の場合は、SQL Database の場合と同様`sql-statement`要素にクエリを記述します。

また、こちらも SQL Database と同様`parameters`に指定したパラメータを`sql-statement`内で使用することができます。

```diff_xml
<!-- getUsers -->
<cosmosdb-data-source>
	<!-- 接続設定は省略 -->
	<query-request>
		<sql-statement>
-           SELECT * FROM c
+           SELECT * FROM c WHERE CONTAINS(c.name, @name)
        </sql-statement>
+       <parameters>
+           <parameter name="@name">@(context.GraphQL.Arguments["name"])</parameter>
+       </parameters>
	</query-request>
</cosmosdb-data-source>
```

#### 更新系

更新系の場合は以下の様なポリシーとなります。
(createUserの場合)

```xml
<!-- createUser -->
<cosmosdb-data-source>
	<!-- 接続設定は省略 -->
    <write-request type="upsert">
        <partition-key>
            @(context.GraphQL.Arguments["input"]["id"].Value<string>())
        </partition-key>
        <set-body template="liquid">
            {
                "id":"{{body.arguments.input.id}}",
                "name":"{{body.arguments.input.name}}",
                "age":{{body.arguments.input.age}}
            }
        </set-body>
    </write-request>
</cosmosdb-data-source>
```

`write-request`要素を設定します。
単一データの取得と同様、クエリを書かずにキー(`partition-key`)を指定します。
逆に言うと、クエリによる更新はそもそも Cosmos DB ではサポートされていないため、できません。
`write-request`要素の`type`属性で更新の種類を指定します。
`insert`、`replace`、`upsert`が指定可能です。

:::note warn
**2023/09/03 時点**
試してみたところ、`insert`を指定しても`upsert`と同じ挙動だったり、ドキュメントには`replace`を指定した場合は`id`要素が必須となっているけど、そもそも`id`要素が指定できなかったりとまだ怪しい感じでした
:::

`set-body`要素で入力するデータ(JSON)を組み立てます。
[SQL Database](https://qiita.com/fsdg-nagumo/items/8478c79ebb0e566220c7#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B) の方でも登場しましたが、Liquid というテンプレート構文で記述します。

#### 削除

削除の場合は以下の様なポリシーとなります。
(deleteUser)

```xml
<!-- deleteUser -->
<cosmosdb-data-source>
	<!-- 接続設定は省略 -->
	<delete-request>
		<id>
            @(context.GraphQL.Arguments["id"].Value<string>())
        </id>
		<partition-key>
            @(context.GraphQL.Arguments["id"].Value<string>())
        </partition-key>
	</delete-request>
</cosmosdb-data-source>
```

`delete-request`要素を設定します。
`read-request`と同様、キー(`id`、`partition-key`)を指定します。

# API のテスト

テスト方法は [SQL Database 版](https://qiita.com/fsdg-nagumo/items/8478c79ebb0e566220c7#api-%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88)と同様です。

# おまけ

## ページング

Cosmos DB では、クエリによる複数データを取得する際、ページングを制御することができます。

### Resolver ポリシー

```diff_xml
<!-- getUsers -->
<cosmosdb-data-source>
	<!-- 接続設定は省略 -->
	<query-request>
		<sql-statement>
            SELECT * FROM c
        </sql-statement>
+		<paging>
+			<max-item-count>
+               @(context.GraphQL.Arguments["input"]["maxItemCount"]?.Value<string>())
+           </max-item-count>
+			<continuation-token>
+               @(context.GraphQL.Arguments["input"]["continuationToken"]?.Value<string>())
+			</continuation-token>
+		</paging>
    </query-request>
</cosmosdb-data-source>
```

`paging`要素を設定します。
`max-item-count`要素で1ページの取得件数を指定します。(デフォルトは 100)
`continuation-token`要素で次ページを特定するためのトークン(後述)を指定します。
`max-item-count`、`continuation-token`共にポリシー式で記述することができます。

※`max-item-count`は文字列として設定しないとエラーになりました

```xml
<max-item-count>
   <!-- ↓はエラー -->
   @(context.GraphQL.Arguments["input"]["maxItemCount"]?.Value<int>())
</max-item-count>
```

### Schema

Schema は以下のように変更しています。

```diff_graph_ql
type Query {
- getUser(id: ID!): User!
+ getUsers(input: GetUserInput!): UserList!
  getUsers(): UserList!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

type User {
  id: ID!
  name: String!
  age: Int!
}

type UserList {
  items: [User!]!
  hasNextPage: Boolean!
  endCursor: String
}

+input GetUserInput {
+  maxItemCount: Int
+  continuationToken: String
+}

"・・・省略・・・"
```

### 実行結果

最初は`max-item-count`を指定せずに実行。
全部で 3 件取得できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/00189a57-51f7-c886-120b-56e4d3dc6cf3.png)

レスポンスを見てみると`endCursor`と`hasNextPage`というプロパティがあります。

次に`max-item-count`に 2 を指定してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/d2a920b8-6bfe-ecca-5eac-61cff5c4a0d2.png)

結果が 2 件のみとなりました。
また、`endCursor`と`hasNextPage`の値が先ほどと変わりましたね。
全体の件数が`max-item-count`を超えている場合は`hasNextPage`が`true`となり、`endCursor`に次ページを特定するためのトークンがセットされます。

では、`continuation-token`に上記のトークンを指定してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2884047/93ee9323-f058-48b4-0cc1-b81e80eae8dd.png)

先ほど取得されなかった残りの 1 件が取得できました。

## その他

レスポンスのカスタマイズや、オプショナルなパラメータを指定する方法は [SQL Database 版](https://qiita.com/fsdg-nagumo/items/8478c79ebb0e566220c7#%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B9%E3%82%92%E3%82%AB%E3%82%B9%E3%82%BF%E3%83%9E%E3%82%A4%E3%82%BA%E3%81%99%E3%82%8B)と同様、こちらでも使えます！

# おわりに

API Management と データベースだけで GraphQL の API を作成することができました。
また、ここでは紹介できませんでしたが、データ更新の際にストアドプロシージャを呼び出してバリデーションするといったこともできるみたいです。
他にも紹介できていない機能もありますので、気になった方は遊んでみてください！