// app.js
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import 'express-async-errors';

import tweetsRouter from './router/tweets.js';
import authRouter from './router/auth.js';

const app = express();

// 기본 미들웨어 설정
app.use(express.json());
app.use(cors());
app.use(helmet());
app.use(morgan('tiny'));

// Route(tweets)
app.use('/tweets', tweetsRouter);
// Route(Auth)
app.use('/auth', authRouter);

// 404 Error
app.use((req, res, next) => {
  res.sendStatus(404);
});

// Error
app.use((error, req, res, next) => {
  console.log(error);
  res.status(500).send('Server Error');
});

app.listen(config.host.port);

// DataBase Connection
db.getConnection();

View

import express from 'express';
import 'express-async-errors';
import { body, param, validationResult } from 'express-validator';

import * as tweetController from '../controller/tweet.js';
import { validate } from '../middleware/validator.js';
import { isAuth } from '../middleware/auth.js';

const router = express.Router();

const validateTweet = [
  body('text') // text : body 안에 포함되어 있는 req 값
		.trim()
		.isLength({ min: 3 })
		.withMessage('text should be at least 3 characters'),
  validate
];

// GET /tweets
// GET /tweets?username=:username
router.get('/', isAuth, tweetController.getTweets);

// GET /tweets/:id
router.get('/:id', isAuth, tweetController.getTweet);

// POST /tweets
router.post('/', isAuth, validateTweet, tweetController.createTweet);

// PUT /tweets/:id
router.put('/:id', isAuth, validateTweet, tweetController.updateTweet);

// DELETE /tweets/:id
router.delete('/:id', isAuth, tweetController.deleteTweet);

export default router;

Controller

import * as tweetRepository from '../data/tweet.js';

// tweets 다건 조회
export async function getTweets(req, res, next) {
  const username = req.query.username;
  const tweets = await (username
    ? tweetRepository.getAllByUsername(username)
    : tweetRepository.getAll());
  res.status(200).json(tweets);
}

// tweets 단건 조회
export async function getTweet(req, res, next) {
  const id = req.params.id;
  const tweet = await tweetRepository.getById(id);
  if (!tweet) {
    return res.status(404).json({message: `Tweet id(${id}) not found`});
  }
  res.status(200).json(tweet);
}

// tweet 생성
export async function createTweet(req, res, next) {
  const { text } = req.body;
  const tweet = await tweetRepository.create(text, req.userId);
  res.status(201).json(tweet);
}

// tweet 수정
export async function updateTweet(req, res, next) {
  const text = req.body.text;
  const id = req.params.id;
  const tweet = await tweetRepository.getById(id);
	// 수정 대상 없음
  if (!tweet) {
    return res.status(404).json({message: `Tweet id(${id}) not found`});
  }
  // 다른 사용자 수정 방지
  if (tweet.userId !== req.userId) {
    return res.sendStatus(403);
  }
  const updated = await tweetRepository.update(text, id);
  res.status(200).json(updated);
}

// tweet 삭제
export async function deleteTweet(req, res, next) {
  const id = req.params.id;
  const tweet = await tweetRepository.getById(id);
	// 삭제 대상 없음
  if (!tweet) {
    return res.status(404).json({message: `Tweet id(${id}) not found`});
  }
  // 다른 사용자 삭제 방지
  if (tweet.userId !== req.userId) {
    return res.sendStatus(403);
  }
  const deleted = await tweetRepository.remove(id);
  res.status(204).json(deleted);
}

Model (MySQL)

import { db } from '../db/database.js';

const SELECT_JOIN = 'SELECT T1.id AS id, T1.userId AS userId, T1.text AS text, T1.createdAt AS createdAt, T2.username AS username, T2.name AS name, T2.url AS url FROM dwitter.tweets T1 JOIN dwitter.users T2 ON T1.userId = T2.id';
const ORDER_DESC = 'ORDER BY CREATEDAT DESC';
// Auths 데이터와 Tweets 데이터 Join 헤서 출력 데이터 가공하기
// tweets 다건 조회
export async function getAll() {
  return db
    .execute(`${SELECT_JOIN} ${ORDER_DESC}`)
    .then((result) => result[0]);
}

// tweets 다건 조회
export async function getAllByUsername(username) {
  return db
    .execute(`${SELECT_JOIN} WHERE T2.username = ? ${ORDER_DESC}`, [username])
    .then((result) => result[0]);
}

// tweet 단건 조회
export async function getById(id) {
  return db
    .execute(`${SELECT_JOIN} WHERE T1.id = ?`, [id])
    .then((result) => result[0][0]);
}

// tweet 생성
export async function create(text, userId) {
  return db
    .execute(`INSERT INTO dwitter.tweets (text, createdAt, userId) VALUES (?, ?, ?)`, [text, new Date(), userId])
    .then((result) => getById(result[0].insertId));
}

// tweet 수정
export async function update(text, id) {
  return db
    .execute('UPDATE dwitter.tweets SET text = ? WHERE id = ?', [text, id])
    .then(() => getById(id));
}

// tweet 삭제
export async function remove(id) {
  return db
    .execute('DELETE FROM dwitter.tweets WHERE id = ?', [id])
    .then(() => getById(id));
}

Model (MySQL)

import MongoDB from 'mongodb';
import { getTweets } from '../db/mongo.js';
import * as userReository from '../data/auth_mongo.js';

// ---------------------------------
// [ MVC ( Model ) ]
// ---------------------------------
// - server 에서의 model
// - 데이터의 로직이 변경되어야 한다면 Model 에서만 변경해 주면 된다.
// - 데이터베이스

// 다건
// find() 는 커서(cursor) 형태로 데이터를 하나 하나씩 읽어온다.
// sort() 는 정렬 방식으로 해당 데이터에 양수면 오름차순, 음수면 내림차순.
export async function getAll() {
  return getTweets()
    .find()
    .sort({ createdAt: -1 })
    .toArray()
    .then(mapTweets);
}

// 다건
export async function getAllByUsername(username) {
  return getTweets()
    .find({ username })
    .sort({ createdAt: -1 })
    .toArray()
    .then(mapTweets);
}

// 단건
// 단건은 find() 가 아닌 findOne() 을 사용해야 한다.
export async function getById(id) {
  return getTweets()
    .findOne({ _id: new MongoDB.ObjectId(id) })
    .then(mapOptionalTweet);
}

export async function create(text, userId) {
  const { username, name, url } = await userReository.findById(userId);
  const tweet = {
    text,
    createdAt: new Date(),
    userId,
    username,
    name,
    url
  }
  return getTweets()
    .insertOne(tweet)
    .then((data) => mapOptionalTweet({ ...tweet, _id: data.insertedId }));
}

// updateOne() : void
// -> 업데이트를 하고 아무 값도 받아오지 않는다면
// findOneAndUpdate() : object
// -> 업데이트를 하고 반환 값을 받아온다면
export async function update(text, id) {
  return getTweets()
    .findOneAndUpdate(
      { _id: new MongoDB.ObjectId(id) }, // 업데이트할 대상 선택
      { $set: { text } },
      { returnDocumnet: 'after' } // before: 업데이트 이전 값 반환, after: 업데이트 이후 값 반환
    )
    .then((result) => result.value)
    .then(mapOptionalTweet);
}

export async function remove(id) {
  return getTweets()
    .deleteOne({ _id: new MongoDB.ObjectId(id) });
}

function mapOptionalTweet(tweet) {
  return tweet ? { ...tweet, id: tweet._id.toString() } : tweet;
}

function mapTweets(tweets) {
  return tweets.map(mapOptionalTweet);
}