Webの勉強はじめてみた その27〜モジュールごとの実装とデータベース〜

N予備校「プログラミング入門Webアプリ」を受講しています。
今回は第3章20節〜26節です。
承認されたユーザーだけが使える匿名掲示板の作成。
気をつけたい箇所や気付いた点だけをまとめました。

<h2>設計の進め方</h2>
1. システム要件の定義
2. UIの設計: ページに何をどう配置するか
3. URIの設計: パスの作り方(RESTful)
4. モジュールの設計  

<h3>URIとモジュール</h3>
:::note
リクエストを具体的な処理に振り分けることをルーティング、
リクエストに対し具体的な処理をする関数を リクエストハンドラという
:::

サーバーを起動するもの
リクエストを処理するもの
ルーティングを行うもの

など、それぞれ役割を決めて実装する。
メインとなる機能から実装していくのが良い。
処理を実装する前にそれぞれの機能などをコメントする。

<h3>覚えておきたいこと</h3>
:::note
テンプレートエンジンなどを使う場合、のちにhtmlになるものはviewsディレクトリに格納する。
:::

以下はPOSTされた時の処理

```javascript
// POSTの処理
      let body =[];
      req.on('data', (chunk) => {
        body.push(chunk);
      }).on('end', ()=>{
        //  クエリから値を取得
        body = Buffer.concat(body).toString();
        const params = new URLSearchParams(body);
        const content = params.get('content');
        console.info(`投稿されました: ${content}`);                
      });
```

`querystring`が非推奨になったので`URLSearchParam`を使う。URIエンコードされた文字列のデコードも同時に行うので、`decodeURIComponent`はいらない。

<h3>認証機能</h3>
`http-auth`モジュールの、ファイルを利用した認証

```plaintext:users.htpasswd
admin:apple
guest1:1234
guest2:5678
```
```javascript
const basic = auth.basic({
  realm: 'Enter username and password.',
  file: './users.htpasswd'
})
```

:::note
Basic認証では、特定のURLにアクセスした際、ステータスコード 401 - Unauthorized を返すことでログアウトされる。
:::

```javascript
function handleLogout(req, res){
  handleStatus(req, res, 401, 'ログアウトしました');  
}

function handleNotFound(req, res){
  handleStatus(req, res, 404, 'ページが見つかりません');  
}

function handleBadRequest(req, res){
  handleStatus(req, res, 400, '未対応のメソッドです');
}
/**
 * HTTPレスポンスの共通処理
 * @param {Object} req HTTP Request
 * @param {Object} res HTTP Response
 * @param {Number} statusCode 
 * @param {String} msg Message
 */
function handleStatus(req, res, statusCode, msg){
  res.writeHead(statusCode, {
    'Content-Type' : 'text/plain; charset=utf-8'
  });
  res.end(msg);
}
```

<h2>データベース</h2>
今回はPostgreSQLを`sequelize`モジュールで操作。

```yaml:docker-compose.yml
services:
  app:
    depends_on:
        - db
  db:
    image: postgres:12
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: secret_board
      TZ: "Asia/Tokyo"
```

`docker-compose.yml`に`db`というサービス名でPostgreSQLのコンテナを追加。
仮想環境上で`app`と`db`の二つのサーバーが稼働する状態。

<h3>Sequelize</h3>

```bash
yarn add sequelize@6.5.0
yarn add pg@8.5.1
yarn add pg-hstore@2.3.3
```

:::note
データベースの構成を設定することを、データモデリングという
:::

```javascript:post.js
// sequelizeの基本設定
const { Sequelize, DataTypes } = require('sequelize');
// データベース全体の設定
const sequelize = new Sequelize(
  // IDとパスワードを渡す
  'postgres://postgres:postgres@db/secret_board',
  {
    // 起動ログなどのログを出力しない
    logging: false
  }
);
// データモデリング
const Post = sequelize.define(
  // テーブル名
  'Post',
  {
    // データ定義
    id: {
      type: DataTypes.INTEGER,
      autoIncrement: true,
      primaryKey: true
    },
    content: {
      type: DataTypes.TEXT
    },
    postedBy: {
      type: DataTypes.STRING
    },
    trackingCookie: {
      type: DataTypes.STRING
    }
  },
  {
    // テーブル名の固定
    freezeTableName: true,
    // createdAt, updatedAtを自動追加
    timestamps: true
  }
);
// このファイルの起動時にデータベースの設定を同期する
Post.sync();
// データモデリングしたオブジェクトをモジュールとして公開
module.exports = Post;
```

<h4>insert</h4>
```javascript
const Post = require('./post');
Post.create({
 //登録したいデータ
}).then(() => {
 //登録後に実行してほしい処理
})
```
非同期処理となる(Promiseオブジェクトが返ってくる)ので、thenメソッドを利用する。

<h4>コンソール上でのデータ確認</h4>
dbコンテナへ入る

```bash
docker-compose exec db bash
```
`su postgres` : ユーザーの変更(今回はpostgresというユーザー)
`psql` : Postgres用コンソールに入る
`¥c secret_borad` : データベースに接続(今回は`secret_board`というデータベース)
`¥q` : 終了

<h4>select</h4>
データの全件取得

```javascript
// データの全件取得
Post.findAll({order:[['id', 'DESC']]}).then((posts) => {
    res.end(
      pug.renderFile('./views/posts.pug', { posts })
      );
  });
```

`renderFile`の引数に変数を指定することで、pugで参照可能となる。

<h4>delete</h4>
`destroy`で指定したキーのデータを削除
投稿者自身か管理者のみが削除できるようにする

```javascript
function handleDelete(req, res){
  switch(req.method){
    case 'POST':
      let body = [];
      req.on('data', (chunk) => {
        body.push(chunk);
      }).on('end', () =>{
        body = Buffer.concat(body).toString();
        const params = new URLSearchParams(body);
        const id = params.get('id');
        Post.findByPk(id).then((post) => {
          if(req.user === post.postedBy || req.user === 'admin'){
            post.destroy().then(() => {
              handleRedirectPosts(req, res);    
            })
          } 
        })
      })
      break;
    default:
      util.handleBadRequest(req, res);
      break;
  }
}
```

:::note warn
curlなどで第三者がdeleteを送る可能性があるため、サーバー側でもユーザーのチェックをすること。
:::

:::note
実行するとデータベースから実際にデータを削除することを物理削除、実際にデータを削除する代わりに「削除された」ことを表すフラグを立ててデータが削除されたとみなすことを論理削除という。
:::

削除処理は慎重に。実用的にも論理削除が良い。

<h4>データベースの永続化</h4>
dockerを破棄するとデータも破棄されるので、PC側の別の場所にファイルを保存する。

```yaml:docker-compose.yml
db:
    image: postgres:12
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: secret_board
      TZ: "Asia/Tokyo"
    volumes:
      - ../secret-board-db:/var/lib/postgresql/data
```

PC側: `../secret-board-db`
docker側: `var/lib/postgresql/data`

docker起動時に同期させる

<h2>トラッキングCookie</h2>
:::note
ユーザーの行動を追跡するために付与される Cookie のことをトラッキング Cookie と呼ぶ。
:::

`cookies`というnpmモジュールを利用する

```bash
yarn add cookies@0.8.0
```

```javascript
const Cookies = require('cookies');
const trackingIdKey = 'tracking_id';
// TrackingCookie
const cookies = new Cookies(req, res);
addTrackingCookie(cookies);

function addTrackingCookie(cookies){
  // Cookieが存在しない場合
  if(!cookies.get(trackingIdKey)){
    // ランダムな数値を登録
    const trackingId = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
    // 有効期限を翌日(24時間後)まで
    const tomorrow = new Date(Date.now() + (1000 * 60 * 60 * 24));
    cookies.set(trackingIdKey, trackingId, {expires: tomorrow});
  }
}
```

`MAX_SAFE_INTEGER` : JavaScriptで扱える最大値

<h2>PUG側</h2>
```pug
    each post in posts       
      - let isPostedAdmin = (post.postedBy === 'admin')    
      if isPostedAdmin
        h3 #{post.id} : 管理人 ★
      else 
        h3 #{post.id} : ID:#{post.trackingCookie}
      p!= post.content
      p 投稿日時: #{post.createdAt}
        - let isAdmin = (user === 'admin')
      if isAdmin
        p 投稿者: #{post.postedBy}
      - let isDeletable = (user === post.postedBy || isAdmin)      
      if isDeletable 
        form(method="post" action="/posts?delete=1")
          input(type="hidden" name="id" value=post.id)
          button(type="submit") 削除          
      hr
```

`each in`　
渡されてきたコレクションをループ。
`-` 
pugテンプレート内にJavaScriptを直接記述することができる。
`p!= post.content`
pugテンプレート内で文字列に含まれたタグを認識させる。


<h2>まとめ</h2>
モジュールごとの実装、わかりにくかったのは名前のせいもあるのかな。
`form`からの`POST`メソッド、データベースの`Post`、データベースモデリングの`post.js`、URIクエリの`posts`。
データベースからの削除は、なるほどと思った。
実際に削除するとなるとほかのテーブルも意識しないといけないので、実務的にも慎重になる事案。
しかし設計の重要性というのをつくづく感じる今日この頃。