ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • TIL. 100 Node.js Project_javascript
    TIL/Node.js 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
    반응형

    'TIL > Node.js' 카테고리의 다른 글

    TIL.99 Node.js_Project Architecture  (0) 2021.01.15
    TIL.98 Node.js란?  (0) 2021.01.14
Designed by Tistory.