LoginSignup
1
0

More than 3 years have passed since last update.

ハンズオン Node.jsの7章データストレージ(file-system)をTypescriptで試したメモ

Last updated at Posted at 2020-11-28

概要

ハンズオン Node.jsの7章データストレージを試す。
型をつけて、typescriptで書いてみる。
今回はfile-systemの章。
環境はnodeのexpressをtsで作って無料でazureに公開したメモで作成したものを利用した

ソース

環境

package.json
{
  "name": "node-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node ./bin/www",
    "file-system": "node ./bin/www",
    "tsc": "tsc",
    "watch": "tsc --watch"
  },
  "keywords": [],
  "author": "",
  "license": "MIT",
  "engines": {
    "node": "14.x"
  },
  "dependencies": {
    "express": "^4.17.1",
    "uuid": "^8.3.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.9",
    "@types/uuid": "^8.3.0",
    "@typescript-eslint/eslint-plugin": "^4.8.2",
    "@typescript-eslint/parser": "^4.8.2",
    "eslint": "^7.14.0",
    "eslint-config-prettier": "^6.15.0",
    "eslint-plugin-prettier": "^3.1.4",
    "isomorphic-fetch": "^3.0.0",
    "prettier": "^2.2.0",
    "typescript": "^4.1.2"
  }
}

ソース

メインのソース

  • ステータスコードは数字でみても分かりにくいので定数にした
app.ts
import express from 'express'
import { v4 as uuidv4 } from 'uuid'
import { statusCode, paths } from './constants'
import type { Todo, DataStorage, HttpError, MiddlewareHandler } from './types'

// const dataStorage: DataStorage<Todo> = require(`./${process.env.npm_lifecycle_event}`)
//   .default
const dataStorage: DataStorage<Todo> = require('./file-system').default

const app = express()
app.use(express.json())

// ToDo一覧の取得
app.get(paths.todos, (req, res, next) => {
  if (!req.query.completed) {
    return dataStorage.fetchAll().then((todos) => res.json(todos), next)
  }
  const completed = req.query.completed === 'true'
  dataStorage.fetchByCompleted(completed).then((todos) => res.json(todos), next)
})

// ToDoの新規登録
app.post(paths.todos, (req, res, next) => {
  const { title } = req.body
  if (typeof title !== 'string' || !title) {
    const err: HttpError = new Error('title is required')
    err.statusCode = statusCode.BadRequest
    return next(err)
  }
  const todo = { id: uuidv4(), title, completed: false }
  dataStorage
    .create(todo)
    .then(() => res.status(statusCode.Created).json(todo), next)
})

// Completedの設定、解除の共通処理
function completedHandler(completed: boolean): MiddlewareHandler {
  return (req, res, next) =>
    dataStorage.update(req.params.id, { completed }).then((todo) => {
      if (todo) {
        return res.json(todo)
      }
      const err: HttpError = new Error('Todo not found')
      err.statusCode = statusCode.NotFound
      next(err)
    }, next)
}

// ToDoのCompoetedの設定、解除
app
  .route(`${paths.todos}/:id/completed`)
  .put(completedHandler(true))
  .delete(completedHandler(false))

// Todoの削除
app.delete(`${paths.todos}/:id`, (req, res, next) =>
  dataStorage.remove(req.params.id).then((id) => {
    if (id !== null) {
      return res.status(statusCode.NoContent).end()
    }
    const err: HttpError = new Error('Todo not found')
    err.statusCode = statusCode.NotFound
    next(err)
  }, next),
)
export default app
file-system/index.ts
import { extname } from 'path'
import { readdir, readFile, writeFile, unlink } from 'fs/promises'
import type { Todo, DataStorage } from '../types'

const exportsObj: DataStorage<Todo> = {
  fetchAll: async () => {
    const files = (await readdir(__dirname)).filter(
      (file) => extname(file) === '.json',
    )
    return Promise.all(
      files.map((file) =>
        readFile(`${__dirname}/${file}`, 'utf8').then(JSON.parse),
      ),
    )
  },
  fetchByCompleted: (completed) =>
    exportsObj
      .fetchAll()
      .then((all) => all.filter((todo) => todo.completed === completed)),
  create: (todo) =>
    writeFile(`${__dirname}/${todo.id}.json`, JSON.stringify(todo)),
  update: async (id, update) => {
    const filename = `${__dirname}/${id}.json`
    return readFile(filename, 'utf8').then((content) => {
      const todo = { ...JSON.parse(content), ...update }
      return writeFile(filename, JSON.stringify(todo)).then(() => todo)
    })
  },
  remove: (id) =>
    unlink(`${__dirname}/${id}.json`).then(
      () => id,
      (err) => (err.code === 'ENOENT' ? null : Promise.reject(err)),
    ),
}
export default exportsObj
constants.ts
import type { ValueOf } from './types'
export const statusCode = {
  Created: 201,
  NoContent: 204,
  BadRequest: 400,
  NotFound: 404,
} as const
export type StatusCode = ValueOf<typeof statusCode>

export const paths = { todos: '/api/todos' }

型定義

  • 各ファイルに分かれた定義をまとめてexport
types/index.d.ts
export type * from './storage'
export type * from './todo'
export type * from './utils'
export type * from './http'
  • ハンズオンに書かれていたStorageの仕様を型にした
types/storage.d.ts
import type { ID } from './common'
import type { OptionalKeys } from './utils'
export interface DataStorage<T> {
  fetchAll: () => Promise<T[]>
  fetchByCompleted: (completed: boolean) => Promise<T[]>
  create: (todo: T) => Promise<void>
  update: (id: ID, update: OptionalKeys<T>) => Promise<T | null>
  remove: (id: ID) => Promise<ID | null>
}
  • 型を定義するための汎用的な型をutilsとしてまとめた
types/utils.d.ts
export type ValueOf<T> = T[keyof T]
export type OptionalKeys<T> = { [K in keyof T]?: T[K] | null }
  • IDはエイリアスを切っただけ
types/common.d.ts
export type ID = string
types/todo.d.ts
import type { ID } from './common'
export interface Todo {
  id: ID
  title: string
  completed: boolean
}
  • ErrorにはstatusCodeプロパティがないので自前で定義
  • expressの、引数を3つとるMiddlewareの型がなかったのでRequestParamHandlerを参考に定義
http.d.ts
import type { Request, Response, NextFunction } from 'express'
import type { StatusCode } from '../constants'
export interface HttpError extends Error {
  statusCode?: StatusCode
}
export type MiddlewareHandler = (
  req: Request,
  res: Response,
  next: NextFunction,
) => void

テスト

  • テストはVsCodeのREST Client拡張を使用した

image.png

tools/connectionTest/todos.azure.http
GET https://az-node-app.azurewebsites.net/api/todos HTTP/1.1

###
POST https://az-node-app.azurewebsites.net/api/todos HTTP/1.1
content-type: application/json

{"title": "テスト"}

###
# タイトルがないと400エラー
POST https://az-node-app.azurewebsites.net/api/todos HTTP/1.1
content-type: application/json

{}

###
# 一つ目の要素をcompletedにする
PUT https://az-node-app.azurewebsites.net/api/todos/29010728-d64e-4db2-b49e-d7c2daf09a9a/completed HTTP/1.1

###
# 一つ目の要素のcompletedを解除
DELETE https://az-node-app.azurewebsites.net/api/todos/29010728-d64e-4db2-b49e-d7c2daf09a9a/completed HTTP/1.1

###
# 一つ目の要素を削除
DELETE https://az-node-app.azurewebsites.net/api/todos/29010728-d64e-4db2-b49e-d7c2daf09a9a HTTP/1.1

参考

ハンズオン Node.js

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0