Compare commits
8 Commits
7988850063
...
de0d2fe333
Author | SHA1 | Date | |
---|---|---|---|
de0d2fe333 | |||
a7b685b2e1 | |||
e487cf7555 | |||
e2f1180a14 | |||
0b98e1c899 | |||
db5802d8e6 | |||
7fd3bd83f8 | |||
46d0567921 |
@ -8,17 +8,22 @@ import {
|
|||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
|
SerializeOptions,
|
||||||
|
UseInterceptors,
|
||||||
ValidationPipe,
|
ValidationPipe,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto';
|
import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto';
|
||||||
import { PostService } from '@/modules/content/services/post.service';
|
import { PostService } from '@/modules/content/services/post.service';
|
||||||
|
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
||||||
|
|
||||||
|
@UseInterceptors(AppInterceptor)
|
||||||
@Controller('posts')
|
@Controller('posts')
|
||||||
export class PostController {
|
export class PostController {
|
||||||
constructor(private postService: PostService) {}
|
constructor(private postService: PostService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@SerializeOptions({ groups: ['post-list'] })
|
||||||
async list(
|
async list(
|
||||||
@Query(
|
@Query(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
@ -35,11 +40,13 @@ export class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async show(@Param('id', new ParseUUIDPipe()) id: string) {
|
async show(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.postService.detail(id);
|
return this.postService.detail(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async store(
|
async store(
|
||||||
@Body(
|
@Body(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
@ -57,6 +64,7 @@ export class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch()
|
@Patch()
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async update(
|
async update(
|
||||||
@Body(
|
@Body(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
@ -74,6 +82,7 @@ export class PostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
||||||
return this.postService.delete(id);
|
return this.postService.delete(id);
|
||||||
}
|
}
|
||||||
|
@ -26,18 +26,20 @@ export class QueryPostDto implements PaginateOptions {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
isPublished?: boolean;
|
isPublished?: boolean;
|
||||||
|
|
||||||
@IsEnum(PostOrder, { message: `` })
|
@IsEnum(PostOrder, {
|
||||||
|
message: `The sorting rule must be one of ${Object.values(PostOrder).join(',')}`,
|
||||||
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
orderBy: PostOrder;
|
orderBy: PostOrder;
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
@Transform(({ value }) => toNumber(value))
|
||||||
@Min(1, { message: '' })
|
@Min(1, { message: 'The current page must be greater than 1.' })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
page = 1;
|
page = 1;
|
||||||
|
|
||||||
@Transform(({ value }) => toNumber(value))
|
@Transform(({ value }) => toNumber(value))
|
||||||
@Min(1, { message: '' })
|
@Min(1, { message: 'The number of data displayed per page must be greater than 1.' })
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
limit = 10;
|
limit = 10;
|
||||||
|
37
src/modules/content/entities/CategoryEntity.ts
Normal file
37
src/modules/content/entities/CategoryEntity.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
Entity,
|
||||||
|
OneToMany,
|
||||||
|
PrimaryColumn,
|
||||||
|
Relation,
|
||||||
|
Tree,
|
||||||
|
TreeChildren,
|
||||||
|
TreeParent,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||||
|
|
||||||
|
@Entity('content_category')
|
||||||
|
@Tree('materialized-path')
|
||||||
|
export class CategoryEntity extends BaseEntity {
|
||||||
|
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ comment: '分类名称', unique: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ comment: '分类排序', default: 0 })
|
||||||
|
customOrder: number;
|
||||||
|
|
||||||
|
@OneToMany(() => PostEntity, (post) => post.category, { cascade: true })
|
||||||
|
posts: Relation<PostEntity>[];
|
||||||
|
|
||||||
|
depth = 0;
|
||||||
|
|
||||||
|
@TreeParent({ onDelete: 'NO ACTION' })
|
||||||
|
parent: Relation<CategoryEntity> | null;
|
||||||
|
|
||||||
|
@TreeChildren({ cascade: true })
|
||||||
|
children: Relation<CategoryEntity>[];
|
||||||
|
}
|
42
src/modules/content/entities/comment.entity.ts
Normal file
42
src/modules/content/entities/comment.entity.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import {
|
||||||
|
BaseEntity,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryColumn,
|
||||||
|
Relation,
|
||||||
|
Tree,
|
||||||
|
TreeChildren,
|
||||||
|
TreeParent,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||||
|
|
||||||
|
@Entity('content_comment')
|
||||||
|
@Tree('materialized-path')
|
||||||
|
export class CommentEntity extends BaseEntity {
|
||||||
|
@PrimaryColumn({ type: 'varchar', length: 36, generated: 'uuid' })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ comment: '评论内容', type: 'text' })
|
||||||
|
body: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ comment: '创建时间' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => PostEntity, (post) => post.comments, {
|
||||||
|
nullable: false,
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE',
|
||||||
|
})
|
||||||
|
post: Relation<PostEntity>;
|
||||||
|
|
||||||
|
depth = 0;
|
||||||
|
|
||||||
|
@TreeParent({ onDelete: 'CASCADE' })
|
||||||
|
parent: Relation<CommentEntity> | null;
|
||||||
|
|
||||||
|
@TreeChildren({ cascade: true })
|
||||||
|
children: Relation<CommentEntity>[];
|
||||||
|
}
|
@ -1,45 +1,80 @@
|
|||||||
import { Expose } from 'class-transformer';
|
import { Exclude, Expose, Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
|
JoinTable,
|
||||||
|
ManyToMany,
|
||||||
|
OneToMany,
|
||||||
PrimaryColumn,
|
PrimaryColumn,
|
||||||
|
Relation,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
import { PostBodyType } from '@/modules/content/constants';
|
import { PostBodyType } from '@/modules/content/constants';
|
||||||
|
import { CategoryEntity } from '@/modules/content/entities/CategoryEntity';
|
||||||
|
import { CommentEntity } from '@/modules/content/entities/comment.entity';
|
||||||
|
import { TagEntity } from '@/modules/content/entities/tag.entity';
|
||||||
|
|
||||||
|
@Exclude()
|
||||||
@Entity('content_posts')
|
@Entity('content_posts')
|
||||||
export class PostEntity extends BaseEntity {
|
export class PostEntity extends BaseEntity {
|
||||||
|
@Expose()
|
||||||
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '文章标题' })
|
@Column({ comment: '文章标题' })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@Expose({ groups: ['post-detail'] })
|
||||||
@Column({ comment: '文章内容', type: 'text' })
|
@Column({ comment: '文章内容', type: 'text' })
|
||||||
body: string;
|
body: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '文章描述', nullable: true })
|
@Column({ comment: '文章描述', nullable: true })
|
||||||
summary?: string;
|
summary?: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Expose()
|
@Expose()
|
||||||
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
@Column({ comment: '关键字', type: 'simple-array', nullable: true })
|
||||||
keywords?: [];
|
keywords?: string[];
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '文章类型', type: 'enum', enum: PostBodyType, default: PostBodyType.HTML })
|
@Column({ comment: '文章类型', type: 'enum', enum: PostBodyType, default: PostBodyType.HTML })
|
||||||
type: PostBodyType;
|
type: PostBodyType;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '发布时间', type: 'varchar', nullable: true })
|
@Column({ comment: '发布时间', type: 'varchar', nullable: true })
|
||||||
publishedAt?: Date | null;
|
publishedAt?: Date | null;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
@Column({ comment: '自定义文章排序', default: 0 })
|
@Column({ comment: '自定义文章排序', default: 0 })
|
||||||
customOrder: number;
|
customOrder: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@Type(() => Date)
|
||||||
@CreateDateColumn({ comment: '创建时间' })
|
@CreateDateColumn({ comment: '创建时间' })
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
|
|
||||||
@UpdateDateColumn({ comment: '更新时间' })
|
@Expose()
|
||||||
|
@Type(() => Date)
|
||||||
|
@UpdateDateColumn({ comment: '更新时间', nullable: true })
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@OneToMany(() => CategoryEntity, (category) => category.posts, {
|
||||||
|
nullable: true,
|
||||||
|
onDelete: 'SET NULL',
|
||||||
|
})
|
||||||
|
category: Relation<CategoryEntity>;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ManyToMany(() => TagEntity, (tag) => tag.posts, { cascade: true })
|
||||||
|
@JoinTable()
|
||||||
|
tags: Relation<TagEntity>[];
|
||||||
|
|
||||||
|
@OneToMany(() => CommentEntity, (comment) => comment.post, { cascade: true })
|
||||||
|
comments: Relation<CommentEntity>[];
|
||||||
}
|
}
|
||||||
|
20
src/modules/content/entities/tag.entity.ts
Normal file
20
src/modules/content/entities/tag.entity.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Expose } from 'class-transformer';
|
||||||
|
import { Column, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
|
||||||
|
|
||||||
|
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||||
|
|
||||||
|
@Entity('content_tag')
|
||||||
|
export class TagEntity {
|
||||||
|
@PrimaryColumn({ type: 'varchar', generated: 'uuid', length: 36 })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ comment: '标签名称', unique: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ comment: '标签描述', nullable: true })
|
||||||
|
desc?: string;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@ManyToMany(() => PostEntity, (post) => post.tags)
|
||||||
|
posts: Relation<PostEntity>[];
|
||||||
|
}
|
@ -65,7 +65,7 @@ export class PostService {
|
|||||||
const { orderBy, isPublished } = options;
|
const { orderBy, isPublished } = options;
|
||||||
if (typeof isPublished === 'boolean') {
|
if (typeof isPublished === 'boolean') {
|
||||||
isPublished
|
isPublished
|
||||||
? qb.where({ publishedAt: Not(IsNull) })
|
? qb.where({ publishedAt: Not(IsNull()) })
|
||||||
: qb.where({ publishedAt: IsNull() });
|
: qb.where({ publishedAt: IsNull() });
|
||||||
}
|
}
|
||||||
this.queryOrderBy(qb, orderBy);
|
this.queryOrderBy(qb, orderBy);
|
||||||
@ -84,7 +84,7 @@ export class PostService {
|
|||||||
case PostOrder.PUBLISHED:
|
case PostOrder.PUBLISHED:
|
||||||
return qb.orderBy('post.publishedAt', 'DESC');
|
return qb.orderBy('post.publishedAt', 'DESC');
|
||||||
case PostOrder.CUSTOM:
|
case PostOrder.CUSTOM:
|
||||||
return qb.orderBy('post.custom', 'DESC');
|
return qb.orderBy('post.customOrder', 'DESC');
|
||||||
default:
|
default:
|
||||||
return qb
|
return qb
|
||||||
.orderBy('post.createdAt', 'DESC')
|
.orderBy('post.createdAt', 'DESC')
|
||||||
|
35
src/modules/core/providers/app.interceptor.ts
Normal file
35
src/modules/core/providers/app.interceptor.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
ClassSerializerContextOptions,
|
||||||
|
ClassSerializerInterceptor,
|
||||||
|
PlainLiteralObject,
|
||||||
|
StreamableFile,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { isArray, isNil, isObject } from 'lodash';
|
||||||
|
|
||||||
|
export class AppInterceptor extends ClassSerializerInterceptor {
|
||||||
|
serialize(
|
||||||
|
response: PlainLiteralObject | Array<PlainLiteralObject>,
|
||||||
|
options: ClassSerializerContextOptions,
|
||||||
|
): PlainLiteralObject | Array<PlainLiteralObject> {
|
||||||
|
if ((!isObject(response) && !isArray(response)) || response instanceof StreamableFile) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isArray(response)) {
|
||||||
|
return (response as PlainLiteralObject[]).map((item) => {
|
||||||
|
return !isObject(item) ? item : this.transformToPlain(item, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('meta' in response && 'items' in response) {
|
||||||
|
const items = !isNil(response.items) && isArray(response.items) ? response.items : [];
|
||||||
|
return {
|
||||||
|
...response,
|
||||||
|
items: (items as PlainLiteralObject[]).map((item) => {
|
||||||
|
return isObject(item) ? this.transformToPlain(item, options) : item;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return super.transformToPlain(response, options);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user