TIL/Node.js

TIL. 100 Node.js Project_javascript

codermun 2021. 1. 16. 23:07
728x90
반응형

출처: 위코드 춤추는 개발자

> wecode | 위코드 | 코딩 부트캠프 | 코딩교육

 

WeCode | 위코드 | 코딩 부트캠프 | 코딩교육

WeCode(위코드)의 부트캠프를 통해 개발자로서 커리어를 시작하세요.

wecode.co.kr

 

Node.js 백엔드 API 학습자료 정리

javascript 와 Typescript가 어떻게 다른지 동일한 로직을 언어만 다르게 구성한 2가지 자료를 함께 보면

javascript를 모르더라도 Python 지식만으로도 코드를 분석할 수 있었다.

 

지극히 Django 와 비교하여 알아보고있는점을 참고하자.

아직까지는 Django가 편한 것이 사실이다!

javascript_version

구조

 

1. 모듈화

Error catch 함수 모듈화

validation Token 함수 모듈화

validation Fields 함수 모듈화

 

2. Django 와 비교

Project/urls.py -> routes/index.js

app/urls.py -> appRouter.js

app/views.py -> app/controller + app/service

controller -> 전체 로직 처리(검증)

service -> DB 접근하는 모든 로직 처리 (ORM : prisma)

utills.py -> 

middwaresd  <-- validationtoken.js

utills <-- validationFirelds.js

 

3. 추후 업데이트 사항 (Question)

3.1 UserRouter.js -> body를 별도로 import 하여 사용하는 이유

(Typescript 에서는 import 해서 사용하지 않음)

 

3.2 UserController -> Token salt를 env 파일에서 굳이 임포트해 사용하는 이유

 

3.3 ArticleController -> validationFields를 아래와 같이 사용하는 이유 (단지 모듈화인지?)

 

3.4 CommentController -> Error catch가 되어있지 않는 이유

 

3.5 utils -> makeDataForCreate.js // makeQueryOption.js 질문

3.6 server.js 세팅 질문


routes

UserRouter.js

const express = require('express')
const router = express.Router()
const { body } = require('express-validator')

const { UserController } = require('../controllers')

router.post(
  '/login',
  body('email').isEmail(),
  body('password').isLength({ min: 5 }),
  UserController.logIn
)
router.post(
  '/signup',
  body('email').isEmail(),
  body('password').isLength({ min: 5 }),
  UserController.signUp
)

module.exports = router

// 요청이 들어올때 Body에 대한 내용을 확인하기 위해 body를 따로 임포트함
// 유저 라우터와 유저 컨트롤러 커넥트 현재 유저 컨트롤러 안의 회원가입/로그인 함수를 불러와 사용함
// logIn/ signUp

// body에 담겨오는 패스워드의 경우 최소 5글자 이상이여야 하는것으로 보임
// body를 임포트 해와야한다는 것!

ArticleRouter.js

const express = require('express')
const router = express.Router()
const { ArticleController, CommentController } = require('../controllers')
const { validateToken } = require('../middlewares')

// articles router
router.get('/', ArticleController.getArticles)
router.get('/:articleId', ArticleController.getOneArticle)
router.post('/', validateToken, ArticleController.postOneArticle)
router.put('/:articleId', validateToken, ArticleController.updateOneArticle)
router.put('/publish/:articleId', validateToken, ArticleController.publishOneArticle)
router.delete('/:articleId', validateToken, ArticleController.deleteOneArticle)

// comments router
router.get('/:articleId/comments', CommentController.getComments)
router.post('/:articleId/comments', validateToken, CommentController.postComment)
router.put('/:articleId/comments/:commentId', validateToken, CommentController.updateComment)
router.delete('/:articleId/comments/:commentId', validateToken, CommentController.deleteComment)

module.exports = router

// 라우트 함수 임포트
// 컨트롤러의 함수를 임포트해오면서 라우트와 컨트롤러 커넥트
// 토큰 인가 임포트 ==> 토큰 인가를 미들웨어 위치시킴!

// 접근 허가 권한을 나누어 게시물CRUD URL 설정


// 댓글 URL 설정

// 여기서 핵심은 서로 관련있는 기사와 댓글을 한 군데 작성하였다는 것

Controllers

UserController.js

const { AUTH_TOKEN_SALT } = process.env
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const { UserService } = require('../services')
const { errorWrapper, errorGenerator } = require('../errors')

const signUp = errorWrapper(async (req, res) => {
  const { email, password } = req.body
  const hashedPassword = await bcrypt.hash(password, 10)

  const foundUser = await UserService.findUser({ email })
  if (foundUser) errorGenerator({ statusCode: 409, message: 'duplicated' })

  const createdUser = await UserService.createUser({
    email,
    password: hashedPassword,
  })

  res.status(201).json({
    message: 'user created',
    email: createdUser.email,
  })
})

const logIn = errorWrapper(async (req, res) => {
  const { email, password: inputPassword } = req.body

  const foundUser = await UserService.findUser({ email })
  if (!foundUser) errorGenerator({ statusCode: 400, message: 'client input invalid' })

  const { id, password: hashedPassword } = foundUser
  const isValidPassword = await bcrypt.compare(inputPassword, hashedPassword)
  if (!isValidPassword) errorGenerator({ statusCode: 400, message: 'client input invalid' })

  const token = jwt.sign({ id }, AUTH_TOKEN_SALT)
  res.status(200).json({ message: 'login success!', token })
})

module.exports = {
  logIn,
  signUp,
}

// 토큰 솔트 값을 따로 env 파일에서 임포트해서 사용함
// 토큰 및 암호화 임포트
// 컨트롤러와 서비스 커넥트

// 어떠한 요청에 대한 응답으로 에러가 가는 경우를 안의 로직에 사용하지 않고 모듈화로 별도의 
// 에러 캐치 함수를 생성 후 임포트하여 재사용하고 있음

// (Signup과 logIn에서 사용하는 founduser는 유니크 Key 값을 가지고있음)

// SignUp
// SignUp 함수는 에러감지 함수를 인자로 받고 그 함수는 요청과 응답을 인자로 받음
// 요청 body 내용을 읽어 이메일, 비밀번호 객체로 선언
// 요청으로 들어오 비밀번호를 암호화하여 암호된 비밀번호 변수에 할당 (Q : 이때 10의 의미는?)
// 회원가입을 시도한 이메일을 DB 안에 해당 이메일이 있는지 확인함 (해당 과정은 서비스에서 담당)
// 만약 유저가 존재한다면 (값이 있다면) 에러제너레이터 함수에 코드와 메시지를 인자로 전달하여 응답
// 같은 유저가 존재하지 않는다면 이메일, 비밀번호 객체를 createUser 함수의 인자로 전달하여 회원가입 성공 메시지와 status 코드를 응답해줌

// logIn
// login 함수 처음 시작은 위와 동일
// 요청 body의 내용을 이메일과 비밀번호 객체로 선언
// 로그인을 요청한 이메일을 확인하여 DB 안에 해당 이메일을 가진 유저가 있는지 확인
// 만약 이메일이 없다면 (회원이 아닌경우) 에러제너레이터 함수에 코드와 메시지를 인자로 전달 및 응답
// 이메일이 존재한다면(회원인 경우) founduser 값의 id를 사용하여 입력된 패스워드를 암호화된 패스워드와 비교 compare 함수가 알아서 확인해주는것으로 보임
// 만약 비밀번호가 다르다면 에러 함수에 코드와 메시지를 인자로 전달 및 응답
// 비밀번호가 같다면 Salt 와 접속한 유저의 id를 이용해 jwt 토큰을 token에 선언해줌
// 메시지와 발행한 토큰함께 응답

// 여기서 에러 캐시 함수 모듈화 하였으며 장고에서와 달리 DB 히트하는 부분은 전적으로 서비스가 담당하고 있음
// 따라서 역할이 나누어져있음을 확인할 수 있음

 

ArticleController.js

const { ArticleService } = require('../services')
const { errorWrapper, errorGenerator } = require('../errors')
const { validateFields } = require('../utils')

const getArticles = errorWrapper(async (req, res) => {
  const articles = await ArticleService.findArticles(req.query)
  res.status(200).json({ articles })
})

const getOneArticle = errorWrapper(async (req, res) => {
  const { articleId } = req.params
  const article = await ArticleService.findArticle({ id: articleId })

  if (article.deleted_at) return res.status(200).json({ message: 'deleted ' })

  res.status(200).json({ article })
})

const postOneArticle = errorWrapper(async (req, res) => {
  const { id: userId } = req.foundUser
  const { title, body } = req.body

  if (!title || !body) errorGenerator({ statusCode: 400, message: 'invalid key error' })

  const createdArticle = await ArticleService.createArticle({
    userId,
    title,
    body,
  })

  res.status(201).json({ createdArticle })
})

const updateOneArticle = errorWrapper(async (req, res) => {
  const { id: userIdFromToken } = req.foundUser
  const { articleId } = req.params
  const requestedFields = req.body
  const allowedFields = ['title', 'body']

  const isValidFields = validateFields(requestedFields, allowedFields)

  if (!isValidFields) errorGenerator({ statusCode: 400, message: 'invalid requested fields' })

  const foundArticle = await ArticleService.findArticle({ id: articleId })
  const { user_id: userIdFromArticle } = foundArticle

  if (userIdFromToken !== userIdFromArticle)
    errorGenerator({ statusCode: 403, message: 'unauthorized' })

  const updatedArticle = await ArticleService.updateArticle({
    articleId,
    requestedFields,
  })

  res.status(201).json({ updatedArticle })
})

const publishOneArticle = errorWrapper(async (req, res) => {
  const { id: userIdFromToken } = req.foundUser
  const { articleId } = req.params

  const foundArticle = await ArticleService.findArticle({ id: articleId })
  if (!foundArticle) errorGenerator({ statusCode: 404, message: 'article not found' })
  const { user_id: userIdFromArticle } = foundArticle

  if (userIdFromToken !== userIdFromArticle)
    errorGenerator({ statusCode: 403, message: 'unauthorized' })

  const publishedArticle = await ArticleService.publishArticle(articleId)

  res.status(201).json({ publishedArticle })
})

const deleteOneArticle = errorWrapper(async (req, res) => {
  const { id: userIdFromToken } = req.foundUser
  const { articleId } = req.params

  const foundArticle = await ArticleService.findArticle({ id: articleId })
  if (!foundArticle) errorGenerator({ statusCode: 404, message: 'article not found' })
  const { user_id: userIdFromArticle } = foundArticle

  if (userIdFromToken !== userIdFromArticle)
    errorGenerator({ statusCode: 403, message: 'unauthorized' })

  const deletedArticle = await ArticleService.deleteArticle(articleId)

  res.status(201).json({ deletedArticle })
})

module.exports = {
  getArticles,
  getOneArticle,
  postOneArticle,
  updateOneArticle,
  publishOneArticle,
  deleteOneArticle,
}

// Q : validataefiles의 역할

// getArticles
// 함수 관련 URL 요청이 들어올 경우 서비스의 findearticles 함수의 인자로 요청 쿼리를 전달 및 응답
// getOneArticle
// 함수 관련 URL 요청(id) 들어올 경우 요청의 파라미터를 articleld 객체 저장하고 이를  이용하여
// 요청이 들어온 파라미터(id)와 같은 값을 가지고있는 기사를 article에 선언
// 만약 해당 기사가 삭제(deleted_at)된 내용이 있다면 정상 URL이지만 해당 기사는 삭제되었음을 응답
// 문제 없을 경우 json 데이터로 article 응답

// postOneArticle
// 토큰 인가에 따라 요청한 user의 id , 기사 제목 및 내용을 객체로 선언
// 만약 제목 or 내용 중 하나라도 존재하지 않을 경우 에러 응답
// 요청 값을 이용해 기사(문서) 내용 DB 저장
// 201과 함께 저장된 json 데이터로 article 응답

// updateOneArticle
// 함수 관련 URL 요청(id)으로 부터 기사의 파라미터(id)값이 필요
// 토큰 인가에 따라 요청한 user의 id 와 수정하고자할 파마티터 값을 객체로 선언
// 요청 body의 내용을 변수로 선언
// 여기서 validateFields 함수를 사용하기 위해 비교할 allowedFields를 선언함
// validateFields는 요청으로 들어온 body의 내용을 갖고 있는 reqeustFieldsr가 제목, 내용을 가지고 있는지를 판단함!!
// requestFields가 제목, 내용을 포함하고 있을 경우 ## Q 답변 ##
// 포함하고 있지 않을 경우 에러 응답
// 포함하고 있을 경우 id를 가지고 기사를 찾고 기사를 작성한 user id와 요청으로 들어온 user id가 같은지 검증
// 같지 않다면 에러 응답
// 같다면 requestFields 내용대로 해당 기사 update
// 업데이트 완료 후 201과 함께 json 데이터 변경된 art 응답

// publishOneArticle
// 올린 기사의 출판 상태 여부를 변경
// 기사를 올린 후 출판이 완료되면 스테이터스 값을 변경하게 됨
// uesr id, article id를 객체로 선언
// article id에 맞는 기사가 존재하는지 확인
// 존재하지 않을 경우 에러 응답
// 존재할 경우 기사를 올린 user id 를 객체로 선언
// 출판을 요청하는 id와 기사를 올린 id가 같은지 검증
// 다를 경우 에러 , 같을 경우 status: 'PUBLISHED'로 변경됨(서비스에서 확인 가능)
// 201과 함께 json 데이터 응답

// deleteOneArticle
// user id, article id 객체로 선언
// 각각의 id가 맞는지 검증 (올린 사람이 지울 수 있다.)
// 둘다 맞을 경우 DB에서 해당 기사 삭제
// 201과 함께 json (데이터 삭제된 기사) 응답

 

CommentController.js

const { CommentService } = require('../services')
const { errorWrapper, errorGenerator } = require('../errors')

const getComments = errorWrapper(async (req, res) => {
  const { articleId } = req.params

  const comments = await CommentService.fetchComments({ article_id: Number(articleId) })
  res.status(200).json({ comments })
})

const postComment = errorWrapper(async (req, res) => {
  const { articleId } = req.params
  const { id: userIdFromToken } = req.foundUser
  const { body } = req.body

  const createdComment = await CommentService.createComment({
    article_id: Number(articleId),
    user_id: userIdFromToken,
    body,
  })

  res.status(201).json({ createdComment })
})

const updateComment = errorWrapper(async (req, res) => {
  const { articleId, commentId } = req.params
  const { body } = req.body
  const { id: userIdFromToken } = req.foundUser

  const comments = await CommentService.fetchComments({ article_id: Number(articleId) })

  const foundComment = comments.find((comment) => comment.id === Number(commentId))
  if (!foundComment) errorGenerator({ message: 'not found', statusCode: 404 })

  const isValidUser = foundComment.user_id === userIdFromToken // true or false
  if (!isValidUser) errorGenerator({ message: 'unauthorized', statusCode: 403 })

  const updatedComment = await CommentService.updateComment({
    comment_id: Number(commentId),
    body,
  })

  res.status(200).json({ updatedComment })
})

const deleteComment = errorWrapper(async (req, res) => {
  const { articleId, commentId } = req.params
  const { id: userIdFromToken } = req.foundUser

  const comments = await CommentService.fetchComments({ article_id: Number(articleId) })

  const foundComment = comments.find((comment) => comment.id === Number(commentId))
  if (!foundComment) errorGenerator({ message: 'not found', statusCode: 404 })

  const isValidUser = foundComment.user_id === userIdFromToken
  if (!isValidUser) errorGenerator({ message: 'unauthorized', statusCode: 403 })

  const deletedComment = await CommentService.deleteComment({
    comment_id: Number(commentId),
  })

  res.status(200).json({ deletedComment })
})

module.exports = {
  getComments,
  postComment,
  updateComment,
  deleteComment,
}

// 당연히 컨트롤러와 서비스 커넥트
// getComments
// 함수 관련 URL 요청시 파라미터(id) 값 객체로 선언
// 해당 article에 관련된 댓글 응답 (Q : 서비스상에서 댓글 body내용을 출력해주지 않음)

// postComment
// 토큰 인가를 통한 user id, article id, 댓글 내용(요청.body)를 객체로 선언
// 토큰 인가만 문제없다면 바로 DB에 댓글 생성 (Q : Error catch가 되어있지 않음!)

// updateComment 
// user id, article id 말고도 comment id값도 객체로 선언해줌
// comments 변수에 해당 기사의 댓글을 가져옴 (이때는 서비스상 atricle id와 deleted_ad 여부만 가져옴)
// 앞서 comments 값에서 req(comment id)값을 이용해 요청으로 들어온 comment id와 같은 id를 가진 댓글이 있는지 검증
// 만약 일치하는 댓글이 없다면(!False)=> (True) 에러 응답
// 일치하는 댓글이 있다면 해당 하는 댓글을 올린 작성자가 맞는지를 검증
// 다른 작성자라면 에러 응답
// 같은 작성자라면 해당 댓글 내용 업데이트 

// deletecomment
// user id, article id 말고도 comment id값도 객체로 선언해줌
// update과정과 동일
// id 값이 모두 일치할 경우 해당 댓글 삭제

Services

UserService.js

const prisma = require('../prisma')

const createUser = (fields) => {
  return prisma.users.create({ data: fields })
}

const findUser = (field) => {
  const [uniqueKey] = Object.keys(field)

  const isKeyId = uniqueKey === 'id'
  const value = isKeyId ? Number(field[uniqueKey]) : field[uniqueKey]

  return prisma.users.findUnique({ where: { [uniqueKey]: value } })
}

module.exports = {
  createUser,
  findUser,
}

// prisma ORM 사용!으로 인한 임포트
// 컨트롤러에서 회원가입시 받는 이메일과 암호화된 비밀번호를 fields 라는 인자로 받아 prisma ORM을 이용해 데이터 users 테이터 생성
// uniquekey 값이 'id' 일 경우 isKeyId = True
// isKeyId 값이 True 일 경우 value = Number(field[uniqueKey]) 선언 (불린)
// isKeyId 값이 False 일 경우 value = field[uniqueKey] 선언 (삼항연산자)


ArticleService.js

const { body } = require('express-validator')
const prisma = require('../prisma')
const { makeQueryOption } = require('../utils')

const ARTICLES_DEFAULT_OFFSET = 0
const ARTICLES_DEFAULT_LIMIT = 5

const findArticles = (query) => {
  const { offset, limit, ...fields } = query
  const where = makeQueryOption(fields)

  return prisma.articles.findMany({
    where,
    skip: Number(offset) || ARTICLES_DEFAULT_OFFSET,
    take: Number(limit) || ARTICLES_DEFAULT_LIMIT,
    orderBy: {
      created_at: 'desc',
    },
  })
}// pagination 발견!

const findArticle = (field) => {
  const [uniqueKey] = Object.keys(field)

  const isKeyId = uniqueKey === 'id'
  const value = isKeyId ? Number(field[uniqueKey]) : field[uniqueKey]

  return prisma.articles.findUnique({
    where: { [uniqueKey]: value },
    include: {
      users: {
        select: {
          id: true,
          email: true,
        },
      },
      comments: {
        where: {
          deleted_at: null,
        },
      },
    },
  })
}

const createArticle = (fields) => {
  const { userId: user_id, ...dataFields } = fields

  return prisma.articles.create({
    data: {
      ...dataFields,
      user_id,
    },
  })
}

const updateArticle = (fields) => {
  const { articleId, requestedFields } = fields

  return prisma.articles.update({
    where: {
      id: Number(articleId),
    },
    data: {
      ...requestedFields,
      updated_at: new Date(),
    },
  })
}

const publishArticle = (articleId) => {
  return prisma.articles.update({
    where: {
      id: Number(articleId),
    },
    data: {
      status: 'PUBLISHED',
      updated_at: new Date(),
    },
  })
}

const deleteArticle = (articleId) => {
  return prisma.articles.update({
    where: {
      id: Number(articleId),
    },
    data: {
      deleted_at: new Date(),
    },
  })
}

module.exports = {
  findArticles,
  findArticle,
  createArticle,
  updateArticle,
  publishArticle,
  deleteArticle,
}

 

CommentService.js

const prisma = require('../prisma')

const fetchComments = ({ article_id }) => {
  return prisma.comments.findMany({
    where: {
      article_id,
      deleted_at: null,
    },
  })
}

const createComment = ({ article_id, user_id, body }) => {
  return prisma.comments.create({
    data: {
      article_id,
      user_id,
      body,
    },
  })
}

const updateComment = ({ comment_id, body }) => {
  return prisma.comments.update({
    where: {
      id: comment_id,
    },
    data: {
      body,
    },
  })
}

const deleteComment = ({ comment_id }) => {
  return prisma.comments.update({
    where: {
      id: comment_id,
    },
    data: {
      deleted_at: new Date(),
    },
  })
}

module.exports = {
  fetchComments,
  createComment,
  updateComment,
  deleteComment,
}

errors

errorWrapper.js

const { validationResult } = require('express-validator')
const errorGenerator = require('./errorGenerator')

const errorWrapper = (controller) => async (req, res, next) => {
  try {
    const errors = validationResult(req)
    if (!errors.isEmpty()) errorGenerator({ statusCode: 400 })

    await controller(req, res, next)
  } catch (err) {
    next(err)
  }
}

module.exports = errorWrapper

 

errorGenerator.js

const DEFAULT_HTTP_STATUS_MESSAGES = {
  400: 'Bad Requests',
  401: 'Unauthorized',
  403: 'Foribdden',
  404: 'Not Found',
  500: 'Internal Server Error',
  503: 'Temporary Unavailable',
}

const errorGenerator = ({ message = '', statusCode = 500 }) => {
  const err = new Error(message || DEFAULT_HTTP_STATUS_MESSAGES[statusCode])
  err.statusCode = statusCode
  throw err
}

module.exports = errorGenerator


// 만약 에러가 발생한다면
// err 변수에 statuscode에 따른 에러 메시지를 반환하고
// 응답으로 err을 전달해줌

middlewares

validationToken.js

const { errorWrapper, errorGenerator } = require('../errors')
const { UserService } = require('../services')
const { AUTH_TOKEN_SALT } = process.env
const jwt = require('jsonwebtoken')

const validateToken = errorWrapper(async (req, res, next) => {
  const [bearer, token] = req.headers.authorization.split(' ')
  const { id } = jwt.verify(token, AUTH_TOKEN_SALT)

  const foundUser = await UserService.findUser({ id })
  if (!foundUser) errorGenerator({ statusCode: 404, message: 'user not found' })

  req.foundUser = foundUser
  next()
})

module.exports = validateToken

utills

makeDataForCreate.js

const makeDataForCreate = (fields) =>
  Object.entries(fields).reduce((data, [key, value]) => {
    data[key] = value
    return data
  }, {})

module.exports = makeDataForCreate

makeQueryOption.js

const DEFAULT_QUERY_OPTION = {
  deleted_at: null,
  // status: 'PUBLISHED',
}

const getQueryOption = (key, value) => {
  const mapper = {
    user_id: { [key]: Number(value) },
    comments: { [key]: { some: { body: { contains: value } } } },
  }

  const matched = mapper[key]
  if (matched) return matched

  return { [key]: { contains: value } }
}

const entriesAndMap = (fields, cb) => Object.entries(fields).map(cb)

const makeQueryOption = (fields) => {
  if (!fields) return DEFAULT_QUERY_OPTION

  const defaultQueryOptions = entriesAndMap(DEFAULT_QUERY_OPTION, ([key, value]) => ({
    [key]: value,
  }))
  const queryOptins = entriesAndMap(fields, ([key, value]) => getQueryOption(key, value))
  const where = { AND: [...defaultQueryOptions, ...queryOptins] }
  return where
}

module.exports = makeQueryOption

 

validationFields.js

const validateFields = (requested, allowed) =>
  Object.keys(requested).every((field) => allowed.includes(field))

module.exports = validateFields

app.js

const express = require('express')
const routes = require('./routes')
const logger = require('morgan')('dev')

const app = express()

app.use(express.json())
app.use(logger)
app.use(routes)

// general error handler
app.use((err, req, res, next) => {
  const { statusCode, message } = err
  console.error(err)
  res.status(statusCode || 500).json({ message })
})

module.exports = app

Sserver.js

require('dotenv').config()
const { PORT } = process.env
const http = require('http')
const app = require('./app')
const server = http.createServer(app)
const prisma = require('./prisma')

const start = async () => {
  try {
    server.listen(PORT, () => console.log(`Server is listening on ${PORT}`))
  } catch (err) {
    console.error(err)
    await prisma.$disconnect()
  }
}

start()

 

Typescript_version

구조

 

1. 모듈화

Error catch 함수 모듈화 (JS와 동일)

validation Token 함수 모듈화 (JS와 동일)

Article.ts 하나로 JS에서의 utills를 포함 시킴 (JS diff)

 

2. Django 와 비교

동일

3. 추후 업데이트 사항 (Question)

3.1 Service에서 userCreateInput / userUniqueSearchInput 등 인스턴스를 새로 생성하여 사용하는 이유

UserController

 

UserService

 

3.2 app.js /ts && server.js/ts 코드 리뷰

3.3 Article.ts 에 대한 코드 리뷰 요청


routes

UserRouter.ts

import { Router } from 'express'
import { UserController } from '../controllers'

const router = Router()

router.use('/signup', UserController.signUp)
router.use('/login', UserController.logIn)

export default router
// JS와 달리 해당 부분에서 body를 별도로 임포트하지 않음

 

ArticleRouter.ts

import { Router } from 'express' 
import { ArticleController } from '../controllers'
import { validateToken } from '../middlewares'

const router = Router()

router.get('/', ArticleController.getArticles)
router.get('/:article_id', ArticleController.getArticle)
router.post('/', validateToken, ArticleController.postArticle)
router.put('/:article_id', validateToken, ArticleController.updateArticle)
router.put('/publish/:article_id', validateToken, ArticleController.publishArticle)
router.delete('/:article_id', validateToken, ArticleController.deleteArticle)

export default router

Controller

UserController.ts

import bcrypt, { hash } from 'bcryptjs' // js diff
import jwt from 'jsonwebtoken'
import { UserService } from '../services'
import { errorWrapper, errorGenerator} from '../errors'
import { Request, Response } from 'express' // js diff
import { userCreateInput } from '../services/UserService' // js diff
const { AUTH_TOKEN_SALT } = process.env

const signUp = errorWrapper(async (req: Request, res: Response) => {
  const { email, password }: userCreateInput = req.body
  if (!email || !password) errorGenerator({ statusCode: 400 })

  const foundUser = await UserService.findUser({ email })
  if (foundUser) errorGenerator({ statusCode: 409 })

  const hashedPassword = await bcrypt.hash(password, 10)

  const createdUser = await UserService.createUser({
    email,
    password: hashedPassword
  })

  res.status(201).json({
    message: 'user created',
    email: createdUser.email
  })
})

const logIn = errorWrapper(async (req: Request, res: Response) => {
  const { email, password: inputPassword }: userCreateInput = req.body // js diff

  if (!email || !inputPassword) errorGenerator({ statusCode: 400 })

  const foundUser = await UserService.findUser({ email })
  if (!foundUser) errorGenerator({ statusCode: 400 })

  const { id, password: hashedPassword } = foundUser
  const isValidPassword = await bcrypt.compare(inputPassword, hashedPassword)
  if (!isValidPassword) errorGenerator({ statusCode: 400 })

  const token = jwt.sign({ id }, AUTH_TOKEN_SALT)
  res.status(200).json({ message: 'login success', token })
})

export default { // js diff
  logIn,
  signUp
}

//js와 달리 express Request, Response 임포트해 사용함
// js와 달리 userCreateInput이란 인스턴스를 서비스에 생성하고 해당 인스턴스에 맞춰 내용을 집어넣고?사용하는 듯 하다.
// Q userCreateInput 과 같은 인스턴스를 추가로 생성하여 이를 활용하는 이유
// 

 

ArticleController.ts

import { ArticleService } from '../services'
import { errorWrapper, errorGenerator } from '../errors'
import { Request, Response } from 'express' // js diff 
import { articleCreateInput } from '../services/ArticleService' // js diff
import { Article } from '../utils' // js diff

const getArticles = errorWrapper(async (req: Request, res: Response) => {
  const articles = await ArticleService.findArticles(req.query)
  res.status(200).json({ articles })
})

const getArticle = errorWrapper(async (req: Request, res: Response) => {
  const { article_id } = req.params
  const article = await ArticleService.findArticle({ id: Number(article_id) })

  if (article.deleted_at) return res.status(200).json({ message: 'deleted' })

  res.status(200).json({ article })
})

const postArticle = errorWrapper(async (req: Request, res: Response) => {
  const { id: user_id } = req.foundUser
  const { title, body }: articleCreateInput = req.body

  if (!title || !body) errorGenerator({ statusCode: 400 })

  const createdArticle = await ArticleService.createArticle({
    user_id,
    title,
    body
  })

  res.status(201).json({ createdArticle })
})

const updateArticle = errorWrapper(async (req: Request, res: Response) => {
  const { id: userIdFromToken } = req.foundUser
  const { article_id } = req.params

  const isValidFields = Article.validFields(Object.keys(req.body), ['title', 'body', 'status'])
  if (!isValidFields) errorGenerator({ statusCode: 400 })

  const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
  const { user_id: userIdFromArticle } = foundArticle

  if (userIdFromToken !== userIdFromArticle) errorGenerator({ statusCode: 403 })

  const updatedArticle = await ArticleService.updateArticle({ id: Number(article_id), ...req.body })

  res.status(201).json({ updatedArticle })
})

const publishArticle = errorWrapper(async (req: Request, res: Response) => {
  const { id: userIdFromToken } = req.foundUser
  const { article_id } = req.params

  const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
  const { user_id: userIdFromArticle } = foundArticle

  if (userIdFromToken !== userIdFromArticle) errorGenerator({ statusCode: 403 })

  const publishedArticle = await ArticleService.publishArticle(Number(article_id))

  res.status(201).json({ publishedArticle })
})

const deleteArticle = errorWrapper(async (req: Request, res: Response) => {
  const { id: userIdFromToken } = req.foundUser
  const { article_id } = req.params

  const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
  const { user_id: userIdFromArticle } = foundArticle

  if (userIdFromToken !== userIdFromArticle) errorGenerator({ statusCode: 403 })

  const deletedArticle = await ArticleService.deleteArticle(Number(article_id))

  res.status(201).json({ deletedArticle })
})

export default {
  getArticles,
  getArticle,
  postArticle,
  updateArticle,
  publishArticle,
  deleteArticle,
}


// 임포트 부분에서부터 차이점을 확인
// 전체적으로 js와 상당히 비슷함을 확인함
// req로 들어오는 요청을 서비스에서 특정 인스턴스로 만들어 활용하는 점이 다름 (User에서도 질문부분)
// js에 비해 ts 조금 더 세밀하게 지정해주는 부분들이 있음
// ex req, res ==> req:Request, res : Response
// ex   const foundArticle = await ArticleService.findArticle({ id: articleId })
// == > const foundArticle = await ArticleService.findArticle({ id: Number(article_id) })
// Article.ts 라는 모듈화를 진행한 파일에 대한 이해가 필요! (Question)

Services

UserService.ts

import prisma from '../prisma'

export interface userCreateInput {
  email: string,
  password: string
}

export interface userUniqueSearchInput {
  id?: number,
  email?: string
}

const createUser = (data: userCreateInput) => {
  return prisma.users.create({ data })
}

const findUser = (data: userUniqueSearchInput) => {
  const [uniqueKey] = Object.keys(data)
  return prisma.users.findUnique({ where: {[uniqueKey]: data[uniqueKey]}})
}

export default {
  createUser,
  findUser
}

 

ArticleService.ts

import prisma from '../prisma'
import { articles_status } from '@prisma/client'
import { Article } from '../utils'

const ARTICLES_DEFAULT_OFFSET = 0
const ARTICLES_DEFAULT_LIMIT = 5

export interface articleSearchInput {
  offset?: number
  limit?: number
  user_id?: string
  title?: string
  body?: string
  comments?: string
}

const findArticles = (query: articleSearchInput) => {
  const { offset, limit, ...fields } = query
  const where = Article.makeQueryOption(fields)

  return prisma.articles.findMany({
    where,
    skip: Number(offset) || ARTICLES_DEFAULT_OFFSET,
    take: Number(limit) || ARTICLES_DEFAULT_LIMIT,
    orderBy: {
      created_at: 'desc',
    },
  })
}

export interface articleUniqueSearchInput {
  id: number
}

const findArticle = (data: articleUniqueSearchInput)  => {
  const [uniqueKey] = Object.keys(data)

  return prisma.articles.findUnique({
    where: { [uniqueKey]: data[uniqueKey] },
    include: {
      users: {
        select: {
          id: true,
          email: true
        }
      },
      comments: {
        where: { deleted_at: null }
      }
    }
  })
}

export interface articleCreateInput {
  user_id: number,
  title: string,
  body: string,
}

const createArticle = (data: articleCreateInput) => {
  return prisma.articles.create({ data })
}

export interface articleUpdateInput {
  id: number,
  title?: string,
  body?: string,
  status?: articles_status
}

const updateArticle = (data: articleUpdateInput) => {
  const { id } = data

  return prisma.articles.update({
    where: { id },
    data: {
      ...data,
      updated_at: new Date()
    }
  })
}

const publishArticle = (id: number) => {
  return prisma.articles.update({
    where: { id },
    data: { status: 'PUBLISHED', updated_at: new Date() }
  })
}

const deleteArticle = (id: number) => {
  return prisma.articles.update({
    where: { id },
    data: { status: 'DELETED', deleted_at: new Date() }
  })
}

export default {
  findArticles,
  findArticle,
  createArticle,
  updateArticle,
  publishArticle,
  deleteArticle
}

// 전반적으로 Article.ts에 대한 이해가 필요
// 요청으로 들어온 쿼리를 매핑? 해주는 역할을 하는 것으로 예상..

errors

errorGenerator.ts

const DEFAULT_HTTP_STATUS_MESSAGES = {
  400: 'Bad Requests',
  401: 'Unauthorized',
  403: 'Foribdden',
  404: 'Not Found',
  409: 'duplicate',
  500: 'Internal Server Error',
  503: 'Temporary Unavailable',
}

export interface ErrorWithStatusCode extends Error {
  statusCode?: number
}

const errorGenerator = ({ message = '', statusCode = 500 }: { message?: string, statusCode: number}): void => {
  const err: ErrorWithStatusCode = new Error(message || DEFAULT_HTTP_STATUS_MESSAGES[statusCode])
  err.statusCode = statusCode
  throw err
}

export default errorGenerator

 

errorWrapper.ts

import { RequestHandler, Request, Response, NextFunction } from 'express'

 const errorWrapper = (controller: RequestHandler) => async (req: Request, res: Response, next: NextFunction) => {
  try {
    await controller(req, res, next)
  } catch (err) {
    next(err)
  }
}

export default errorWrapper 

middlewares

validationToken.ts

import jwt from 'jsonwebtoken'
import { errorGenerator, errorWrapper } from '../errors'
import { UserService } from '../services'
import { Express, Request, Response, NextFunction } from 'express'
import { users } from '@prisma/client'

const { AUTH_TOKEN_SALT } = process.env

export interface Token {
  id: number
}

declare global {
  module Express {
    export interface Request {
      foundUser: users
    }
  }
}

const validateToken = errorWrapper(async (req: Request, res: Response, next: NextFunction) => {
  const [bearer, token] = req.headers.authorization.split(' ')
  const { id } = jwt.verify(token, AUTH_TOKEN_SALT) as Token

  const foundUser = await UserService.findUser({ id })
  if (!foundUser) errorGenerator({ statusCode: 400 })

  req.foundUser = foundUser
  next()
})

export default validateToken

utills

Article.ts

import { articleSearchInput } from '../services/ArticleService'
import { articles_status } from '@prisma/client'

const DEFAULT_QUERY_OPTION: { deleted_at: null, status: articles_status } = {
  deleted_at: null,
  status: 'PUBLISHED'
}

const getQueryOption = <T>(key: string, value: T): object => {
  const mapper = {
    user_id: { [key]: Number(value) },
    comments: { [key]: { some: { body: { contains: value } } } },
  }

  const matched = mapper[key]
  if (matched) return matched

  return { [key]: { contains: value } }
}

const makeQueryOption = (fields: articleSearchInput) => {
  if (!fields) return DEFAULT_QUERY_OPTION

  const defaultQueryOptions = Object.entries(DEFAULT_QUERY_OPTION).map(([key, value]) => ({ [key]: value }))
  const queryOptins = Object.entries(fields).map(([key, value]) => getQueryOption(key, value))
  const where = { AND: [...defaultQueryOptions, ...queryOptins] }
  return where
}

const validFields = (requested: string[], allowed: string[]) => requested.every((field) => allowed.includes(field))

export default {
  makeQueryOption,
  validFields
} 

// js에는 없으나 Ts에는 있는 내용으로 요청으로 들어온 쿼리를 매핑? 해주는 함수들로 예상
// Q : Atricle.ts의 역할

app.ts

import express, { Express, ErrorRequestHandler, Request, Response, NextFunction} from 'express'
import { ErrorWithStatusCode } from './errors/errorGenerator'
import morgan from 'morgan'
import routes from './routes'

const logger = morgan('dev')

const app: Express = express()

app.use(express.json())
app.use(logger)
app.use(routes)

const generalErrorHandler: ErrorRequestHandler = (err: ErrorWithStatusCode, req: Request, res: Response, next: NextFunction) => {
  const { message, statusCode } = err
  console.error(err)
  res.status(statusCode || 500).json({ message })
}

app.use(generalErrorHandler)

export default app

server.ts

import dotenv from 'dotenv'
import http, { Server } from 'http'
import app from './app'
import prisma from './prisma'
dotenv.config()
const { PORT } = process.env

const server: Server = http.createServer(app)

const start = async () => {
  try {
    server.listen(PORT, () => console.log(`Server is listening on ${PORT} 🚀`))
  } catch (err) {
    console.error(err)
  } finally {
    await prisma.$disconnect()
  }
}

start()

 

728x90
반응형