DenoでREST API(mysql)

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

## 注意事項

- RESTのrouterの部分は[参考](https://github.com/bradtraversy/deno-rest-api)にした部分を丸パクリです。そちらは目をつぶってほしです。
- テストの画面は[postman](https://www.postman.com/)を使っています。
- denonを知る前のソースなので環境変数のやり方が古いです


## プログラム構成

```
├─routes.ts (ルーティング用）
├─simpleServer.ts (mainの起動プログラム）
├─types.ts (型、interface）
│
└─controllers
  └─　products.ts（DBのやり方のプログラム）
```
わかりにくいのでgithubにアップしています  

https://github.com/masalib/deno_rest_api_mysql


## 事前作業

### mysql table 作成 

```
CREATE TABLE `Product` (
  `id` varchar(64) NOT NULL,
  `name` varchar(256) DEFAULT NULL,
  `description` varchar(1024) DEFAULT NULL,
  `price` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

### 環境変数の設定

windowsの場合

```
set MYSQL_HOST_ENV=XXX.XXX.XXX.XXX
set MYSQL_USERNAME_ENV=XXXXXXXXXX
set MYSQL_DB_ENV=XXXXXXXXXX
set MYSQL_PASSWORD_ENV=XXXXXXXXXX
set MYSQL_PORT_ENV=3306
```

Linuxの場合

```
export MYSQL_HOST_ENV=XXX.XXX.XXX.XXX
export MYSQL_USERNAME_ENV=XXXXXXXXXX
export MYSQL_DB_ENV=XXXXXXXXXX
export MYSQL_PASSWORD_ENV=XXXXXXXXXX
export MYSQL_PORT_ENV=3306
```

## 実行
```
deno run --allow-net --allow-env server.ts
```

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

## ソース解説

### simpleServer.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と違い型宣言できるのはいいですね。

### products.ts 
長いの分割になります。全文を見たい場合は[githubのソース](https://github.com/masalib/deno_rest_api_mysql
)を参照してください。

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

```typescript
import { v4 } from "https://deno.land/std/uuid/mod.ts";
import { Client } from "https://deno.land/x/mysql/mod.ts";
import { Product } from '../types.ts'
```

- /std/uuid/mod.tsはuuidというユニークのIDを作ってくれる関数です。stdなので標準ライブラリーです。  
- /x/mysql/mod.tsはmysqlに接続するためのプログラムです。xがついているのでサードパーティのライブラリーです。  
- /types.tsは自分がつくった型宣言用のプログラムです。

####mysqlの接続

```typescript
//環境変数
const hostname = Deno.env.get("MYSQL_HOST_ENV") as string
const username = Deno.env.get("MYSQL_USERNAME_ENV") as string
const db = Deno.env.get("MYSQL_DB_ENV") as string
const password = Deno.env.get("MYSQL_PASSWORD_ENV") as string
const port = parseInt(Deno.env.get("MYSQL_PORT_ENV") as string) //環境変数はstringなので　string to numberにする

//mysqlに接続する
const client = await new Client().connect({
  hostname: hostname,
  username: username,
  db: db,
  password: password,
  port:port
});
```
環境変数を変数に設定してmysqlのconnectionのClientを作成しています。本番で使うならtrycatchした方がいいかも。

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

```typescript
const getProducts = async({ response }: { response: any }) => {
  try{
    const data = await client.query(`SELECT id , name , description, price  FROM Product;`)
    //判定が微妙な感じ・・・
    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の記載が必須です。
- データの取得判定は「**data.toString()**」で配列を文字列に変換する事でおこなっています。単純にif ( data)を記載してしまうと0件でも成功したと判定されてしまいます。色々と試してみたけど2020/06/03時点だとこれしかないみたい
- 念のためにtrycatchをしています。
- 取得後にconnectionを開放する処理はいれていません。いれると同時アクセスで落ちる事がありました。
- 全件取得していますが、本番で動かすなら件数を絞り込むべき

![20200603021635.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/151573d1-3685-6870-f234-4d97c0dbbbf3.png)



#### データ取得（１件）
```typescript
const getProduct = async({ params ,response }: { params:{id :string },response: any }) => {
  try{
    const data = await client.query(
      "select id,name,description,price from Product where id = ? ",
      [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のパラメータをもとにSQLを作っています。SQLインジェクションはできていました（16進数では試していません）

![20200603021820.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/7a3239a2-ec16-51e5-d9a1-e8cb8eb0a863.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{
      let result = await client.execute(`
      INSERT INTO Product
        (id,name, description, price)
        VALUES
        (?,?,?,?);
        `, [
          product.id,
          product.name,
          product.description,
          product.price
      ]);
      //成功時には{ affectedRows: 1, lastInsertId: 0 }が返ってくる。もう少し欲しいけど・・・
      if (result.affectedRows ===1 ){
        response.status = 200
        response.body = {
          success: true,
          data: product
        }
      } else {
        response.status = 400
        response.body = {
          success: false,
          msg: 'Insert false'
        } 
      }

    } 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 }) => {
```

- postする時のJSONのサンプルは以下です

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

![20200603022619.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/d1ecce68-f60b-01a5-34d8-22dc8e9b841f.png)




#### 更新

```typescript
const updateProduct = async({ params, request, response }: { params: { id: string }, 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 = params.id

    try{
      let result = await client.execute(`
      UPDATE Product
      SET
      name = ?,
      description = ?,
      price = ?
      WHERE id = ?;
      `, [
          product.name,
          product.description,
          product.price,
          product.id,
      ]);

      //成功時には{ affectedRows: 1, lastInsertId: 0 }が返ってくる。もう少し欲しいけど・・・
      if (result.affectedRows ===1 ){
        response.status = 200
        response.body = {
          success: true,
          data: product
        }
      } else {
        response.status = 404
        response.body = {
          success: false,
          msg: 'No product found'
        } 
      }

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

- パラメータとリクエストデータを使って更新しています.
- 更新の有無はaffectedRows: 1で判別しています。個人的にはイケてないな～と思うけどこれしかない。
- 更新に失敗した場合はデータがなかったと判別しています。


![20200603023416.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/9607/8a0385c8-bb08-ad67-f256-5ff9a0c29650.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{
      let result = await client.execute(`
      DELETE FROM Product
      WHERE id = ?;
      `, [
        params.id,
      ]);

      //成功時には{ affectedRows: 1, lastInsertId: 0 }が返ってくる。もう少し欲しいけど・・・
      if (result.affectedRows ===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'
      } 
    }  
  }
}
```
- パラメータを使ってDELETEのSQLを作っています。
- IDの有無ぐらいしかチェックしていません

## 感想

- ベースとなる部分ができたのでうれしい。
- 他のDBの部分もClientの部分を変えればいけると思うので挑戦したい
- [mysqlのORラッパー](https://github.com/manyuanrong/dso)もあるみたいなので次はそちらで作りたい


## 参考URL


https://youtu.be/NHHhiqwcfRM?list=WL

https://github.com/bradtraversy/deno-rest-api