iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 24
0
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 24

[今晚我想來點 Express 佐 MVC 分層架構] DAY 24 - 實作會員關聯資料

在有會員機制下的 TodoList 中,每一筆的 Todo 都應該有一個擁有者,這樣才能知道該筆 Todo 是誰的,誰才有權限可以存取它,所以建立關聯就變得很重要,這也是我們目前還沒補上的機制,廢話不多說,趕快來修改我們的 TodoList 吧!

調整 Model

修改 TodoSchema,主要是新增 owner 欄位,存放 User 的 id,這樣就可以產生關聯:

const TodoSchema = new Schema(
  {
    content: {
      type: String,
      required: true
    },
    completed: {
      type: Boolean,
      required: true
    },
    owner: {
      type: Types.ObjectId,
      required: true,
      ref: 'User'
    }
  },
  {
    timestamps: true
  }
);

以及調整 TodoDocument

export interface TodoDocument extends CoreDocument {
  content: string;
  completed: boolean;
  owner: string;
};

新增 DTO

前面有提到透過 express-jwt 將使用者資訊存放在 req.payload 中,不過我們可能不需要整個 payload 的資訊,所以用 DTO 進行包裝,在 dtos 資料夾下新增 jwt-payload.dto.ts

export class JWTPayloadDTO {

  public readonly _id: string;
  public readonly username: string;

  constructor(payload: JWTPayloadDTO) {
    this._id = payload._id;
    this.username = payload.username;
  }

}

修改 Controller

要來修改 TodoController 中所有的方法,因為目前在 Todo 下的操作都需要有會員身份才能執行:

import { Request } from 'express';

import { ControllerBase } from '../../../bases/controller.base';

import { TodoService } from './todo.service';

import { JWTPayloadDTO } from '../../../dtos/jwt-payload.dto';
import { ResponseObject } from '../../../common/response/response.object';

import { HttpStatus } from '../../../types/response.type';

export class TodoController extends ControllerBase {

  private readonly todoSvc = new TodoService();

  public async getTodos(req: Request): Promise<ResponseObject> {
    const { limit, skip } = req.query;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dtos = await this.todoSvc.getTodos(payload, Number(limit), Number(skip));
    return this.formatResponse(dtos, HttpStatus.OK);
  }

  public async getTodo(req: Request): Promise<ResponseObject> {
    const { id } = req.params;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.getTodo(payload, id);
    if ( !dto ) {
        return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
    }
    return this.formatResponse(dto, HttpStatus.OK);
  }

  public async addTodo(req: Request): Promise<ResponseObject> {
    const { content } = req.body;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.addTodo(payload, content);
    return this.formatResponse(dto, HttpStatus.CREATED);
  }

  public async completedTodo(req: Request): Promise<ResponseObject> {
    const { id } = req.params;
    const { completed } = req.body;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.completedTodo(payload, id, completed);
    if ( !dto ) {
      return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
    }
    return this.formatResponse(dto, HttpStatus.OK);
  }

  public async removeTodo(req: Request): Promise<ResponseObject> {
    const { id } = req.params;
    const payload = new JWTPayloadDTO((req as any).payload);
    const dto = await this.todoSvc.removeTodo(payload, id);
    if ( !dto ) {
      return this.formatResponse('Not found.', HttpStatus.NOT_FOUND);
    }
    return this.formatResponse(null, HttpStatus.NO_CONTENT);
  }

}

修改 Service

可以從 Controller 看到每個操作都多了取得 payload 並傳入 Service 的部分,要在 Service 中處理好相關資訊傳給 Repository 去存取資料,所以我們修改 TodoService

import { TodoRepository } from '../../../repositories/todo.repository';

import { TodoDTO } from '../../../dtos/todo.dto';
import { JWTPayloadDTO } from '../../../dtos/jwt-payload.dto';
import { DefaultQuery } from '../../../types/request.type';

export class TodoService {

  private readonly todoRepo = new TodoRepository();

  public async getTodos(
    payload: JWTPayloadDTO,
    limit: number = DefaultQuery.LIMIT,
    skip: number = DefaultQuery.SKIP
  ): Promise<TodoDTO[]> {
    const todos = await this.todoRepo.getTodos(
      payload._id,
      Math.min(limit, DefaultQuery.MAX_LIMIT),
      skip
    );
    const dtos = todos.map(todo => new TodoDTO(todo));
    return dtos;
  }

  public async getTodo(payload: JWTPayloadDTO, id: string): Promise<TodoDTO | null> {
    const todo = await this.todoRepo.getTodo(payload._id, id);
    const dto = todo ? new TodoDTO(todo) : null;
    return dto;
  }

  public async addTodo(payload: JWTPayloadDTO, content: string): Promise<TodoDTO> {
    const document = await this.todoRepo.addTodo(payload._id, content);
    const dto = new TodoDTO(document);
    return dto;
  }

  public async completedTodo(
    payload: JWTPayloadDTO,
    id: string,
    completed: boolean
  ): Promise<TodoDTO | null> {
    const todo = await this.todoRepo.completedTodo(payload._id, id, completed);
    const dto =  todo ? new TodoDTO(todo) : null;
    return dto;
  }

  public async removeTodo(payload: JWTPayloadDTO, id: string): Promise<TodoDTO | null> {
    const todo = await this.todoRepo.removeTodo(payload._id, id);
    const dto = todo ? new TodoDTO(todo) : null;
    return dto;
  }

}

修改 Repository

要透過會員的 _id 來進行 CRUD,所以修改 TodoRepository

import { QueryFindOneAndUpdateOptions } from 'mongoose';
import { TodoModel, TodoDocument } from '../models/todo.model';

export class TodoRepository {

  public async addTodo(userId: string, content: string): Promise<TodoDocument> {
    const todo = new TodoModel({ content, completed: false, owner: userId });
    const document = await todo.save();
    return document;
  }

  public async getTodo(userId: string, id: string): Promise<TodoDocument | null> {
    const todo = await TodoModel.findOne({ _id: id, owner: userId });
    return todo;
  }

  public async getTodos(
    userId: string,
    limit: number,
    skip: number
  ): Promise<TodoDocument[]> {
    const todos = await TodoModel.find({ owner: userId }).skip(skip).limit(limit);
    return todos;
  }

  public async completedTodo(userId: string, id: string, completed: boolean) {
    const options: QueryFindOneAndUpdateOptions = {
      new: true,
      runValidators: true,
      useFindAndModify: false
    };
    const todo = await TodoModel.findOneAndUpdate(
      { _id: id, owner: userId },
      { completed },
      options
    );
    return todo;
  }

  public async removeTodo(userId: string, id: string) {
    const todo = await TodoModel.findOneAndRemove({ _id: id, owner: userId });
    return todo;
  }

}

這樣就完成了用會員身份進行操作的功能了!

小結

今天的內容是很多系統都有的需求,也算是蠻重要又很基本的議題,如果要取得關聯的資料,可以透過 mongoose 的 populate 方法進行查詢,由於 TodoList 沒有此需求,這邊就提及而已沒有實作出來,還請多包涵/images/emoticon/emoticon16.gif


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 23 - Guard
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 25 - 玩轉 DTO 與 ResponseObject
系列文
今晚我想來點 Express 佐 MVC 分層架構30

尚未有邦友留言

立即登入留言