DenoでREST API(MongoDB)

[ブログ](https://masalib.hatenablog.com/entry/2020/06/09/000000)にも記載したのですが誰も突っ込んでもらえないのでこちらに記載。

## 注意事項

- RESTのrouterの部分は[参考](https://github.com/bradtraversy/deno-rest-api)にした部分を丸パクリです。そちらは目をつぶってほしです。
- テストの画面は[postman](https://www.postman.com/)を使っています。

- 実行されると.deno_pluginsというフォルダが作成されてそこにDLLがインストールされます。
これはmongodbのモジュールが不安定のためです。バージョンアップされると改善されるかと思います


## プログラム構成

```
├─routes.ts (ルーティング用）
├─server.ts (mainの起動プログラム）
├─denon.json (denonという監視プログラムの設定ファイル）
├─.env (DBの接続情報）
├─types.ts (型、interface）
│
└─controllers
  └─　products.ts（DBのやり方のプログラム）
```

わかりにくいのでgithubにアップしています  

https://github.com/masalib/deno_rest_api_mongodb/tree/6c817cacddf9be30940b3f134b8a1e7ea0095af2


## 事前作業

MongoDBをローカルに作ってコレクションを作成してください。
もしできない場合は500Mまで無料で使えるMlabというサービスがありますのでそちらを使ってください。
登録方法は
https://masalib.hatenablog.com/entry/2018/04/19/211340


### 環境変数の設定

.envのサンプルは以下です（ローカルの環境によって違うのでもし使う場合は修正してください）

```
mongo_host=XXX.mlab.com
mongo_user=denouser
mongo_password=password
mongo_db=deno_db
mongo_collection=denocollection 
mongo_port=nnnnn
```
このファイルはgithubにあがっていないので、もし動かす場合は作成してください


### denon install

変更時に自動的に再起動してくれるツールのインストール。環境設定とかも楽です。

```
deno install --allow-read --allow-run --allow-write -f --unstable https://deno.land/x/denon/denon.ts
```

## 実行
```
denon start
```

## Routes
```
GET      /api/v1/products
GET      /api/v1/products/:id
POST     /api/v1/products
PUT      /api/v1/products/:id
DELETE   /api/v1/products/:id
```

## ソース解説

### server.ts
```typescript
import {Application,Router} from "https://deno.land/x/oak/mod.ts";
import router from './routes.ts'

const port =  5000 
const app = new Application()
app.use(router.routes())
app.use(router.allowedMethods())
console.log(`Server Running on port ${port}`)

await app.listen({port })
```
起動用のプログラムなのでルーティング用のファイルを読んでいて適用させているだけです。

### routes.ts 

```typescript
import {Router} from "https://deno.land/x/oak/mod.ts";
import {getProducts,getProduct, addProduct ,updateProduct ,deleteProduct} from './controllers/products.ts'

const router = new Router()
router.get('/api/v1/products', getProducts )
    .get('/api/v1/products/:id', getProduct )
    .post('/api/v1/products', addProduct )
    .put('/api/v1/products/:id', updateProduct )
    .delete('/api/v1/products/:id', deleteProduct )

export default router
```
ルーティング用のプログラムで振るまい（コントローラー）の設定をしています
:id になっているところはgetのパラメータです。

### types.ts

```typescript
export interface Product{
    id: string;
    name: string;
    description: string;
    price: number;
}
```
型宣言してるだけです。javascriptと違い型宣言できるのはいいですね。

### denon.json

```
{
  "$schema": "https://deno.land/x/denon/schema.json",
  "scripts": {
    "start": {
      "cmd": "deno run server.ts",
      "allow": [
        "env",
        "read",
        "write",
        "net",
        "plugin"
      ],
      "unstable": true
    }
  },
  "logger": {
    "debug": true
  },
  "watcher": {
    "interval": 350,
    "exts": ["js", "ts", "json"],
    "match": ["*.*"],
    "skip": ["*/.git/*" ,"README.md" ],
    "legacy": false
  }
}
```
- 起動時の設定や監視ファイルの無視ファイルを設定しています。allowの設定とか多くて困る・・・この設定があると本当に楽～♪


### products.ts 
長いの分割になります。全文をみたい場合はgithubのソースを参照してください。

####ライブラリーの読み込み

```typescript
import { v4 } from "https://deno.land/std/uuid/mod.ts";
import { config } from "https://deno.land/x/dotenv/mod.ts";
import { MongoClient } from "https://deno.land/x/mongo@v0.7.0/mod.ts";
import { Product } from '../types.ts'
```

- /std/uuid/mod.tsはuuidというユニークのIDを作ってくれる関数です。stdなので標準ライブラリーです。  
- /x/dotenv/mod.tsは環境変数をファイルから読み込むプログラムです。
- /x/mongo@v0.7.0/mod.tsはmongodbに接続するためのプログラムです。xがついているのでサードパーティのライブラリーです。  
- /types.tsは自分がつくった型宣言用のプログラムです。


####mongodbの接続

```typescript
//環境変数
const ENV_PATH = '.env';//deno runしたところをカレントディレクトリになるみたい
const config_env:any = config({ path: ENV_PATH });
const user:string = config_env.mongo_user
const password_data:string = config_env.mongo_password
const host:string = config_env.mongo_host
const port_num:number = config_env.mongo_port
const db_name:string = config_env.mongo_db
//mongodb://<dbuser>:<dbpassword>@<host>:<port>/<db>
const url:string = 'mongodb://' + user + ':' + password_data + '@' + host + ':' + port_num + '/' + db_name

//mongoDB接続
console.log("mongodb connection start")
const client = new MongoClient()
client.connectWithUri(url)

//DBの設定
const db = client.database(db_name)

//コレクションの設定
const collection_name:string = config_env.mongo_collection
const datas = db.collection(collection_name)
```

環境変数を変数に設定してmongodbのconnectionのClientを作成しています。本番で使うならtrycatchした方がいいかも。

#### データ取得（全件）

```typescript
const getProducts = async({ response }: { response: any }) => {
  try{
    const data = await datas.find();
    //判定が微妙な感じ・・・
    if ( data.toString() ===""){
      response.status = 404
      response.body = {
        success: false,
        msg: 'No Product found'
      }
    } else {
      response.body = {
          success: true,
          data: data
      }
    }
  } catch(error){
    response.status = 500
    response.body = {
      success: false,
      msg: 'Server Error'
    }
  }
}
```
- データを取得するのでasyncとawaitの記載が必須です。
- 「datas.find()」で全件取得しています。本番で動かすなら件数の制限や取得順の指定をするべき
- データの取得判定は「data.toString()」で配列を文字列に変換する事でおこなっています。単純にif ( data)を記載してしまうと0件でも成功したと判定されてしまいます。色々と試してみたけど2020/06/03時点だとこれしかないみたい
- 念のためにtrycatchをしています。

![20200606204929.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/549b7309-2397-2375-2a6d-8266c5540d70.png)



#### データ取得（１件）
```typescript
const getProduct = async({ params ,response }: { params:{id :string },response: any }) => {
  try{
    const data = await datas.find({ "id": params.id });
    //console.log(params.id)
  
    //判定が微妙な感じ・・・
    if ( data.toString() ===""){
      response.status = 404
      response.body = {
        success: false,
        msg: 'No Product found'
      }
    } else {
      response.status = 200
      response.body = {
        success: true,
        data:data
      }
    }
  } catch (error){
    response.status = 500
    response.body = {
      success: false,
      msg: 'Server Error'
    }
  } 
}
```
- getのIDのパラメータをもとに条件式にいれています。ユニークの設定をしていないと複数件とれてしまいますｗｗｗ

![1ken.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/e96414e0-ad21-b62f-7e59-ca688bd26498.png)



#### 新規作成

```typescript
const addProduct = async ({ request, response }: { request: any , response: any }) => {
  const body = await request.body()

  if (!request.hasBody){
    response.status = 400
    response.body = {
      success: false,
      msg: 'No Data'
    } 
  }else {

    //バリデーション・・・省略
    const product: Product = body.value
    product.id = v4.generate()
    try{
      const insertId = await datas.insertOne(product);
      const product_data = await datas.findOne({ _id: insertId });//$oidを表示するため

      if ( product_data.toString() ===""  ){
        response.status = 400
        response.body = {
          success: false,
          msg: 'Insert false'
        } 
      } else {
        response.status = 200
        response.body = {
          success: true,
          data: product_data
        }
      }
    } catch(error){
      response.status = 500
      response.body = {
        success: false,
        msg: 'Server Error'
      } 
  
    }  
  }
}
```

- バリデーションはめんどくさいのしていません
- idはuuidでユニークのIDを作成しています
- パラメータではなくリクエストの内容（JSON）を利用してデータをインサートしてます
- 関数の部分が１件取得時と違います

```
１件取得　　　　　　　　　↓パラメータ　　　　↓パラメータ
const getProduct = async({ params ,response }: { params:{id :string },response: any }) => {

新規作成　　　　　　　　　↓リクエスト　　　　↓リクエスト
const addProduct = async ({ request, response }: { request: any , response: any }) => {
```

- JSONのサンプル

```
 {
 	"name": "Product1_add",
    "description": "description1_add",
    "price": 200
  }
```

mysqlと違いmongodbが独自に発行しているIDが付与されています。

![add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/e4b16ece-e0ab-a446-0f96-dee9951df1eb.png)


#### 更新

```typescript
const updateProduct = async({ params, request, response }: { params: { id: string }, request: any, response: any }) => {
  //console.log("params.id:"+ params.id)

  const body = await request.body()

  if (!request.hasBody){
    response.status = 400
    response.body = {
      success: false,
      msg: 'No Data'
    } 
  }else {
    //バリデーション・・・省略
    const product: Product = body.value
    product.id = params.id
  
    try{
      const { matchedCount, modifiedCount, upsertedId } = await datas.updateOne(
        { "id": params.id },
        { $set: { 
          "name": product.name,
          "description": product.description,
          "price": product.price
          } }
      );
      console.log("upsertedId:" + upsertedId) //NULLがセット？？どうやったらセットされるのか不明
      console.log("matchedCount:" + matchedCount) 
      console.log("modifiedCount:" + modifiedCount)
      const data = await datas.find({ "id": params.id }); //$oidを表示するため

      //成功時には{ matchedCount 1, modifiedCount 1 }が返ってくる。もう少し欲しいけど・・・
      if (matchedCount ===1 &&  modifiedCount ===1  ){
        response.status = 200
        response.body = {
          success: true,
          data: data
        }
      } else {
        response.status = 404
        response.body = {
          success: false,
          msg: 'No product found'
        } 
      }
    } catch(error){
      response.status = 500
      response.body = {
        success: false,
        msg: 'Server Error'
      } 
    }  
  }
}
```

- パラメータとリクエストデータを使って更新しています.
- bodyをそのまま渡す事ができず・・・分解してセットしています
- 公式に書いてあるとおりにやっているのですが、upsertedId にIDがセットされません。NULLがセットされるので今回は使っていません
- 更新の有無はmatchedCount: 1とmodifiedCount: 1 で判別しています。


![update.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/b7d10c54-ebc7-f21d-1bd9-d2c42a0be322.png)


#### 削除

```typescript
const deleteProduct = async({ params, response }: { params: { id: string }, response: any }) => {

  if (params.id ===""){
    response.status = 400
    response.body = {
      success: false,
      msg: 'No Data'
    } 
  }else {

    //バリデーション・・・省略
    try{
      const data = await datas.find({ "id": params.id });
      //console.log(params.id)
    
      //判定が微妙な感じ・・・
      if ( data.toString() ===""){
        response.status = 404
        response.body = {
          success: false,
          msg: 'No Product found'
        }
      } else{
        //const deleteCount = await datas.deleteOne({ "id": params.id }); mongodbのIDじゃないといけないのか？わからないのでmanyに変更
        const deleteCount = await datas.deleteMany({ "id": params.id });
        //console.log(deleteCount)
        //成功時には{ deleteCount 1 }が返ってくる。もう少し欲しいけど・・・
        if (deleteCount ===1 ){
          response.status = 200
          response.body = {
            success: true,
            msg: 'Product removed'
          }
        } else {
          response.status = 404
          response.body = {
            success: false,
            msg: 'No product found'
          } 
        }
      }

    } catch(error){
      response.status = 500
      response.body = {
        success: false,
        msg: 'Server Error'
      } 
    }  
  }
}
```

- パラメータを使って削除しています。deleteOneだとうまくいかなかったのでdeleteManyでやっています
- IDの有無と存在チェックをしています。



## 感想

- 更新の部分がjsonのまま渡せないのはめんどくさい。
- 更新時にupsertedIdが何もセットされずはまった。バージョンアップしたら何か変わるかも。