在有會員機制下的 TodoList 中,每一筆的 Todo 都應該有一個擁有者,這樣才能知道該筆 Todo 是誰的,誰才有權限可以存取它,所以建立關聯就變得很重要,這也是我們目前還沒補上的機制,廢話不多說,趕快來修改我們的 TodoList 吧!
修改 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;
};
前面有提到透過 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;
}
}
要來修改 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);
}
}
可以從 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;
}
}
要透過會員的 _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 沒有此需求,這邊就提及而已沒有實作出來,還請多包涵