- 教程地址: pincman.com/docs/courses/nestjs-pr...
- 影片地址: www.bilibili.com/video/BV1pG4y167c...
- qq: yjosscom(加微信後拉群交流)
注意: 此處文件只起配合作用,為了您的身心愉悅,請看B站影片教程,不要直接略過影片直接看這個文件,這樣你將什麼都看不到?
檢視上一節的程式碼我們會發現有很多重複性的CRUD方法在複製貼上,比如CategoryService
與PostService
,CategoryController
與PostController
等。所以本節的目標是對這些CRUD類進行抽象化
- 實現CRUD的Service基類
- 實現CRUD的Controller基類
核心模組
服務基類
repository
屬性為預設的儲存類,透過子類的constructor
注入來賦值,必須繼承自BaseRepository
或BaseTreeRepository
enabl_trash
屬性用於確定是否包含軟刪除操作list
用於獲取資料列表paginate
用於獲取分頁資料detail
用於獲取資料詳情creatte
用於建立資料(注意:此方法由子類實現,子類不實現則呼叫時丟擲403update
方法與create
一樣由子類實現delete
用於刪除單條資料deleteList
用於批次刪除資料deletePaginate
與deleteList
的區別在於刪除資料後返回的列表是分頁資料restore
,restoreList
,restorePaginate
用於恢復資料,返回的列表與刪除雷同buildItemQuery
用於構建單條資料的查詢器buildListQuery
用於構建資料列表的查詢器
// src/modules/core/crud/service.ts
export abstract class BaseService<
E extends ObjectLiteral,
R extends BaseRepository<E> | BaseTreeRepository<E>,
P extends QueryListParams<E> = QueryListParams<E>,
M extends IPaginationMeta = IPaginationMeta,
> {
protected repository: R;
protected enable_trash = false;
constructor(repository: R) {
this.repository = repository;
if (
!(
this.repository instanceof BaseRepository ||
this.repository instanceof BaseTreeRepository
)
) {
throw new Error(
'Repository must instance of BaseRepository or BaseTreeRepository in DataService!',
);
}
}
async list(params?: P, callback?: QueryHook<E>): Promise<E[]>
async paginate(
options: PaginateDto<M> & P,
callback?: QueryHook<E>,
): Promise<Pagination<E, M>>
async detail(id: string, trashed?: boolean, callback?: QueryHook<E>): Promise<E>
create(data: any): Promise<E>
update(data: any): Promise<E>
async delete(id: string, trash = true)
async deleteList(data: string[], params?: P, trash?: boolean, callback?: QueryHook<E>)
async deletePaginate(
data: string[],
options: PaginateDto<M> & P,
trash?: boolean,
callback?: QueryHook<E>,
)
async restore(id: string, callback?: QueryHook<E>)
async restoreList(data: string[], params?: P, callback?: QueryHook<E>)
async restorePaginate(data: string[], options: PaginateDto<M> & P, callback?: QueryHook<E>)
protected async buildItemQuery(query: SelectQueryBuilder<E>, callback?: QueryHook<E>)
protected async buildListQuery(qb: SelectQueryBuilder<E>, options: P, callback?: QueryHook<E>)
}
控制器
控制器基類
// src/modules/core/crud/controller.ts
export abstract class BaseController<
S,
P extends QueryListParams<any> = QueryListParams<any>,
M extends IPaginationMeta = IPaginationMeta,
> {
protected service: S;
constructor(service: S) {
this.setService(service);
}
private setService(service: S) {
this.service = service;
}
@Get()
async list(@Query() options: PaginateDto<M> & P & TrashedDto, ...args: any[]) {
return (this.service as any).paginate(options);
}
@Get(':item')
async detail(
@Query() { trashed }: QueryDetailDto,
@Param('item', new ParseUUIDPipe())
item: string,
...args: any[]
) {
return (this.service as any).detail(item, trashed);
}
@Post()
async store(
@Body()
data: any,
...args: any[]
) {
return (this.service as any).create(data);
}
@Patch()
async update(
@Body()
data: any,
...args: any[]
) {
return (this.service as any).update(data);
}
@Delete(':item')
async delete(
@Param('item', new ParseUUIDPipe())
item: string,
@Body()
{ trash }: DeleteDto,
...args: any[]
) {
return (this.service as any).delete(item, trash);
}
@Delete()
async deleteMulti(
@Query()
options: PaginateDto<M> & TrashedDto & P,
@Body()
{ trash, items }: DeleteMultiDto,
...args: any[]
) {
return (this.service as any).deletePaginate(items, options, trash);
}
@Patch('restore/:item')
async restore(
@Param('item', new ParseUUIDPipe())
item: string,
...args: any[]
) {
return (this.service as any).restore(item);
}
@Patch('restore')
async restoreMulti(
@Query()
options: PaginateDto<M> & TrashedDto & P,
@Body()
{ items }: DeleteRestoreDto,
...args: any[]
) {
return (this.service as any).restorePaginate(items, options);
}
}
由於控制器的方法和引數中用到一些裝飾器,比如繫結DTO的@Query
,序列化資料的@SerializeOptions
等等,所以新增一個裝飾器,並使用metadata來儲存傳入這些裝飾器的資料
我們把這個命名為Crud
,其作用如下
- 為控制器的方法新增上
DTO
類 - 為方法的響應新增上序列化選項
- 對於沒有在
enabled
啟用的方法丟擲404
在編寫裝飾器之前需要先定義一下要用到的型別
// src/modules/core/types.ts
/**
* CURD控制器方法列表
*/
export type CurdMethod =
| 'detail'
| 'delete'
| 'restore'
| 'list'
| 'store'
| 'update'
| 'deleteMulti'
| 'restoreMulti';
/**
* CRUD裝飾器的方法選項
*/
export interface CrudMethodOption {
/**
* 該方法是否允許匿名訪問
*/
allowGuest?: boolean;
/**
* 序列化選項,如果為`noGroup`則不傳引數,否則根據`id`+方法匹配來傳參
*/
serialize?: ClassTransformOptions | 'noGroup';
}
/**
* 每個啟用方法的配置
*/
export interface CurdItem {
name: CurdMethod;
option?: CrudMethodOption;
}
/**
* CRUD裝飾器選項
*/
export interface CurdOptions {
id: string;
// 需要啟用的方法
enabled: Array<CurdMethod | CurdItem>;
// 一些方法要使用到的自定義DTO
dtos: {
[key in 'query' | 'create' | 'update']?: Type<any>;
};
}
裝飾器程式碼
// src/modules/core/decorators/crud.decorator.ts
export const Crud =
(options: CurdOptions) =>
<T extends BaseController<any>>(Target: Type<T>) => {
Reflect.defineMetadata(CRUD_OPTIONS, options, Target);
const { id, enabled, dtos } = Reflect.getMetadata(CRUD_OPTIONS, Target) as CurdOptions;
const changed: Array<CurdMethod> = [];
// 新增驗證DTO類
for (const value of enabled) {
// 新增驗證DTO類
}
for (const key of changed) {
// 新增序列化選項以及是否允許匿名訪問等metadata
}
const fixedProperties = ['constructor', 'service', 'setService'];
for (const key of Object.getOwnPropertyNames(BaseController.prototype)) {
// 對於不啟用的方法返回404
}
return Target;
};
內容模組
現在更改一下內容模組中的服務類和控制器即可,程式碼會變得異常簡潔,以CategoryService
和CategoryController
為例
// src/modules/content/services/category.service.ts
@Injectable()
export class CategoryService extends BaseService<CategoryEntity, CategoryRepository> {
constructor(protected categoryRepository: CategoryRepository) {
super(categoryRepository);
}
protected enable_trash = true;
async findTrees() {
return this.repository.findTrees();
}
async create(data: CreateCategoryDto) {
// ...
}
async update(data: UpdateCategoryDto) {
// ...
}
protected async getParent(id?: string) {
// ...
}
}
// src/modules/content/controllers/category.controller.ts
@Crud({
id: 'category',
enabled: [
'list',
'detail',
'store',
'update',
'delete',
'restore',
'deleteMulti',
'restoreMulti',
],
dtos: {
query: QueryCategoryDto,
create: CreateCategoryDto,
update: UpdateCategoryDto,
},
})
@Controller('categories')
export class CategoryController extends BaseController<CategoryService> {
constructor(protected categoryService: CategoryService) {
super(categoryService);
}
@Get('tree')
@SerializeOptions({ groups: ['category-tree'] })
async index() {
this.service;
return this.service.findTrees();
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結