概要
ハンズオン 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
importexpressfrom'express'import{v4asuuidv4}from'uuid'import{statusCode,paths}from'./constants'importtype{Todo,DataStorage,HttpError,MiddlewareHandler}from'./types'// const dataStorage: DataStorage<Todo> = require(`./${process.env.npm_lifecycle_event}`)// .defaultconstdataStorage:DataStorage<Todo>=require('./file-system').defaultconstapp=express()app.use(express.json())// ToDo一覧の取得app.get(paths.todos,(req,res,next)=>{if(!req.query.completed){returndataStorage.fetchAll().then((todos)=>res.json(todos),next)}constcompleted=req.query.completed==='true'dataStorage.fetchByCompleted(completed).then((todos)=>res.json(todos),next)})// ToDoの新規登録app.post(paths.todos,(req,res,next)=>{const{title}=req.bodyif(typeoftitle!=='string'||!title){consterr:HttpError=newError('title is required')err.statusCode=statusCode.BadRequestreturnnext(err)}consttodo={id:uuidv4(),title,completed:false}dataStorage.create(todo).then(()=>res.status(statusCode.Created).json(todo),next)})// Completedの設定、解除の共通処理functioncompletedHandler(completed:boolean):MiddlewareHandler{return(req,res,next)=>dataStorage.update(req.params.id,{completed}).then((todo)=>{if(todo){returnres.json(todo)}consterr:HttpError=newError('Todo not found')err.statusCode=statusCode.NotFoundnext(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){returnres.status(statusCode.NoContent).end()}consterr:HttpError=newError('Todo not found')err.statusCode=statusCode.NotFoundnext(err)},next),)exportdefaultapp
file-system/index.ts
import{extname}from'path'import{readdir,readFile,writeFile,unlink}from'fs/promises'importtype{Todo,DataStorage}from'../types'constexportsObj:DataStorage<Todo>={fetchAll:async()=>{constfiles=(awaitreaddir(__dirname)).filter((file)=>extname(file)==='.json',)returnPromise.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)=>{constfilename=`${__dirname}/${id}.json`returnreadFile(filename,'utf8').then((content)=>{consttodo={...JSON.parse(content),...update}returnwriteFile(filename,JSON.stringify(todo)).then(()=>todo)})},remove:(id)=>unlink(`${__dirname}/${id}.json`).then(()=>id,(err)=>(err.code==='ENOENT'?null:Promise.reject(err)),),}exportdefaultexportsObj
constants.ts
importtype{ValueOf}from'./types'exportconststatusCode={Created:201,NoContent:204,BadRequest:400,NotFound:404,}asconstexporttypeStatusCode=ValueOf<typeofstatusCode>exportconstpaths={todos:'/api/todos'}
型定義
- 各ファイルに分かれた定義をまとめてexport
types/index.d.ts
exporttype*from'./storage'exporttype*from'./todo'exporttype*from'./utils'exporttype*from'./http'
- ハンズオンに書かれていたStorageの仕様を型にした
types/storage.d.ts
importtype{ID}from'./common'importtype{OptionalKeys}from'./utils'exportinterfaceDataStorage<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
exporttypeValueOf<T>=T[keyofT]exporttypeOptionalKeys<T>={[KinkeyofT]?:T[K]|null}
- IDはエイリアスを切っただけ
types/common.d.ts
exporttypeID=string
types/todo.d.ts
importtype{ID}from'./common'exportinterfaceTodo{id:IDtitle:stringcompleted:boolean}
- ErrorにはstatusCodeプロパティがないので自前で定義
- expressの、引数を3つとるMiddlewareの型がなかったのでRequestParamHandlerを参考に定義
http.d.ts
importtype{Request,Response,NextFunction}from'express'importtype{StatusCode}from'../constants'exportinterfaceHttpErrorextendsError{statusCode?:StatusCode}exporttypeMiddlewareHandler=(req:Request,res:Response,next:NextFunction,)=>void
テスト
- テストはVsCodeのREST Client拡張を使用した
tools/connectionTest/todos.azure.http
GEThttps://az-node-app.azurewebsites.net/api/todosHTTP/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