【 Azure 】Azure Functions から Azure Cosmos DB へ接続する方法 (TypeScript)

# はじめに
Azure Functions ( 実行環境 : ``Node`` , 使用言語 : ``TypeScript`` ) から、
Mongo DB ( ``Azure Cosmos DB`` ) を操作する方法について調査したのでまとめておきます。

# 検証環境
- 開発用PC ( OS : Windows 10 Pro )
- Node.js ``v16.13.2``
  - mongoose ``6.1.9``
  - typescript ``3.9.10``
  - @azure/functions ``1.2.3``
-  Azure Functions Core Tools ``v4.0.3971``
- Postman ( テスト用 )
  - cURL でも対応  
- Visual Studio Code

# 手順
## 1. ローカル関数プロジェクトを作成する

Azure Functions Core Tools を使用して、ローカルプロジェクトを生成する。

```shell
> func init MongoDBTest
```

Use the up/down arrow keys to select a worker runtime: node
ランタイムは  ``node`` を選択します。

<img width="400" alt="image.png (136.9 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/88adb069-8911-4027-8341-5d4d38b78926.png">

Use the up/down arrow keys to select a language: typescript
使用言語は ``TypeScript`` を選択します。

<img width="400" alt="image.png (139.9 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/8e52ab1e-e2d2-4890-ac8e-42e0629517ec.png">

ランタイム (実行環境) と 使用言語 の選択が終了すると、
必要なファイルがローカル関数PJディレクトリ配下に生成されます。

<img width="400" alt="image.png (568.7 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/b4d8d576-3ee3-4292-8fda-d354cbbc1432.png">

## 2. ローカル関数を作成する
新規作成したプロジェクトのルートディレクトリへ移動して、
ローカル関数を作成するコマンドを実行します。

```shell
> cd ./MongoDBTest
> func new --template "Http Trigger" --name User
```

上記サンプルの場合、ルートディレクトリ配下に ``User`` ディレクトリが作成されます。
ローカル関数の ``functions.json``, ``index.tx`` はこの　``User`` ディレクトリ配下に保存されます。

ローカル関数の作成手順はこれで終了です。

ルートディレクトリに戻り、以下コマンドを実行すると
ローカル関数を起動することができます。

```shell
> cd ..
> npm install
> npm start
```

## 3. Azure Cosmos DB を作成する

データを保存するための Azure Cosmos DB を作成します。
( Mongo DB on Azure Cosmos DB 作成方法は、[公式ドキュメント](https://docs.microsoft.com/ja-jp/azure/cosmos-db/sql/create-cosmosdb-resources-portal)をご覧ください。)

Cosmos DB のデプロイが完了したら、ナビゲーションメニューより
**[設定] > [接続文字列]** を選択します。

<img width="700" alt="image.png (246.7 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/45913806-304e-404d-94eb-d2ca2b90a174.png">

接続文字列が表示されるので、**プライマリ接続文字列** をコピーします。

<img width="700" alt="image.png (246.7 kB)" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180432/38bbaa78-ec95-d161-8139-20201c3184c2.png">


※ プライマリ接続文字列は、Node アプリケーションから
 Cosmos DB ( Mongo DB ) へ接続する際に使用します。


## 4. ローカル関数から Cosmos DB にデータを保存する
ローカル関数から Mongo DB ( Cosmos DB ) へ接続するために、
``mongoose`` をインストールします。

```shell
> npm install mongoose --save
```

ルートディレクトリの ``local.settings.json`` を開き、
Value オブジェクトに  ``CosmosDBConnectionString`` を追加します。

```local.settings.json
{
  "Values": {
    "FUNCTIONS_WOKER_RUNTIME": "node",
    "CosmosDBConnectionString": "<--- Azure Cosmos DB 接続文字列 --->"
  }
}
```

- local.settings.json は ローカル関数を実行する際に読み込まれる設定になります。
Azure 上へデプロイする際には含めることができないので、Key Vault 等を使用して
接続文字列等の機密性の高い情報を取り扱うようにしてください。
- [Azure Functions をローカルでコーディングしてテストする](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-develop-local#local-settings-file)

Mongo DB 接続用の TypeScript ファイルを lib フォルダ配下に追加します。
( ※ lib フォルダが存在しない場合は、新規作成します。)

```azure-cosmosdb-mongodb.ts
import { Schema, model, connect } from "mongoose";

let db = null;

const UserSchema = new Schema(
  { name: String },
  { timestamps: true }
);
const UserModel = model("User", UserSchema, "Users");

export const init = async () => {
  if(!db) {
    db = await connect(process.env["CosmosDbConnectionString"]);
  }
};
export const addItem = async (doc) => {
  const modelToInsert = new UserModel();
  modelToInsert["name"] = doc.name;

  return await modelToInsert.save();
};
export const findItemById = async (id) => {
  return await UserModel.findById(id);
};
export const findItems = async (query = {}) => {
  return await UserModel.find({});
};
export const deleteItemById = async (id) => {
  return await UserModel.findByIdAndDelete(id);
};
```

``azure-cosmosdb-mongodb.ts`` で export した関数をインポートして、
ローカル関数から Mongo DB ( Cosmos DB ) への接続、データのストアを行います。

```index.ts
import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import * as db from "../lib/azure-cosmosdb-mongodb";

const User: AzureFunction = async function (
  context: Context,
  req: HttpRequest
): Promise<void> {
  try {
    let response = null;

    // Mongo DB ( Cosmos DB ) への接続
    await db.init();

    // Request メソッドに応じて処理を振り分け
    switch (req.method) {
      case "GET":
        if (req?.query.id || (req?.body && req?.body?.id)) {
          response = {
            documentResponse: await db.findItemById(req?.body?.id),
          };
        } else {
          // リクエストパラメーターが存在しない場合は、コレクション一覧を取得する
          const dbQuery =
            req?.query?.dbQuery || (req?.body && req?.body?.dbQuery);
          response = {
            documentResponse: await db.findItems(dbQuery),
          };
        }
        break;
      case "POST":
        if (req?.body?.document) {
          const insertOneResponse = await db.addItem(req?.body?.document);
          response = {
            documentResponse: insertOneResponse,
          };
        } else {
          throw Error("No document found");
        }

        break;
      case "DELETE":
        if (req?.query?.id || (req?.body && req?.body?.id)) {
          response = {
            documentResponse: await db.deleteItemById(req?.body?.id),
          };
        } else {
          throw Error("No id found");
        }

        break;
      default:
        throw Error(`${req.method} not allowed`)
    }

    context.res = {
      body: response,
    };
  } catch (err) {
    context.log(`*** Error throw: ${JSON.stringify(err)}`);

    context.res = {
      status: 500,
      body: err,
    };
  }
};

export default User;
```

``functions.json`` を開き、``bindings`` オブジェクトの
 ``methods`` 配列に "delete" を追加します。

```function.json
{
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": [
        "get",
        "post",
        "delete"
      ]
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    }
  ],
  "scriptFile": "../dist/User/index.js"
}
```

以上で、ローカル関数 ( Azure Functions ) から Mongo DB へ接続、
データを保存することが可能になります。

## 5. 実際にローカル関数を動かしてみる

ローカル関数 ( Azure Functions ) をローカルで起動します。

```shell
> npm start
```

起動後、以下出力があるので、URL をコピーします。

```shell
Functions:
  User: [GET, POST, DELETE] http://localhost:7071/api/User
```

まず GET (リクエストパラメーターなし) でリクエストしてみます。
→ まだデータを登録していないので、空のオブジェクトが返ってきました。

<img width="1037.25" alt="image.png (60.4 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/3cb6dba0-fc27-4ae2-977d-e35bb1602be9.png">

続いて、POST (リクエストボディ/ name : XXXXXXX) でリクエストしてみます。
→ データが正常登録されました。

<img width="1044" alt="image.png (73.6 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/a027891d-597b-4dd7-b99e-dd15e7c1397b.png">

再度、GET (リクエストパラメーターなし) でリクエストしてみると...
→ User コレクション一覧を取得することができました。

<img width="1034.25" alt="image.png (71.5 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/9d127259-a6a9-4a88-bcc2-a12c154ae655.png">

最後に Delete ( リクエストボディ / id : xxxxxxxxxxxxxxxxxxxxx ) を実行。
→ 対象のデータが削除できました。

<img width="1039.5" alt="image.png (71.4 kB)" src="https://img.esa.io/uploads/production/attachments/3505/2022/02/02/10862/85b0918c-9f7c-4c79-bc2b-3e8ea58894de.png">

以上、Azure Functions から Mongo DB を操作する方法でした。


# 終わりに
``Azure Functions Core Tools`` を使うと、コマンド一発で Azure 上にもデプロイできるので凄い便利ですね。今は Azure CLI でリソースグループの作成、リソースのデプロイをやってるんですが、慣れてきたら ARM ( Azure Resource Manager ) テンプレートを使ってデプロイしてみたいなと思います。Azure は楽しい！

# 参考文献
- [Store data in MongoDB with Azure Function | MS Docs](https://docs.microsoft.com/en-us/azure/developer/javascript/tutorial/azure-function-cosmos-db-mongo-api)
- [mongoose | 公式ページ](https://mongoosejs.com/)
- [Automattic/mongoose | GitHub](https://github.com/Automattic/mongoose)