add service

This commit is contained in:
liuyi 2025-05-21 21:20:25 +08:00
parent 8ab109ce26
commit 4c063515ba
8 changed files with 89 additions and 19 deletions

View File

@ -7,5 +7,6 @@ export enum PostOrder {
CREATED = 'createdAt', CREATED = 'createdAt',
UPDATED = 'updatedAt', UPDATED = 'updatedAt',
PUBLISHED = 'publishedAt', PUBLISHED = 'publishedAt',
COMMENTCOUNT = 'commentCount',
CUSTOM = 'custom', CUSTOM = 'custom',
} }

View File

@ -43,7 +43,7 @@ export class CreateCategoryDto {
}) })
@ValidateIf((value) => value.parent !== null && value.parent) @ValidateIf((value) => value.parent !== null && value.parent)
@IsOptional({ always: true }) @IsOptional({ always: true })
@Transform((value) => (value === 'null' ? null : value)) @Transform(({ value }) => (value === 'null' ? null : value))
parent?: string; parent?: string;
@Transform(({ value }) => toNumber(value)) @Transform(({ value }) => toNumber(value))

View File

@ -11,6 +11,8 @@ import {
} from 'class-validator'; } from 'class-validator';
import { toNumber } from 'lodash'; import { toNumber } from 'lodash';
import { PaginateOptions } from '@/modules/database/types';
export class QueryTagDto implements PaginateOptions { export class QueryTagDto implements PaginateOptions {
@Transform(({ value }) => toNumber(value)) @Transform(({ value }) => toNumber(value))
@Min(1, { message: 'The current page must be greater than 1.' }) @Min(1, { message: 'The current page must be greater than 1.' })

View File

@ -2,7 +2,11 @@ import { Injectable } from '@nestjs/common';
import { isNil, omit } from 'lodash'; import { isNil, omit } from 'lodash';
import { EntityNotFoundError } from 'typeorm'; import { EntityNotFoundError } from 'typeorm';
import { CreateCategoryDto, QueryCategoryDto } from '@/modules/content/dtos/category.dto'; import {
CreateCategoryDto,
QueryCategoryDto,
UpdateCategoryDto,
} from '@/modules/content/dtos/category.dto';
import { CategoryEntity } from '@/modules/content/entities/CategoryEntity'; import { CategoryEntity } from '@/modules/content/entities/CategoryEntity';
import { CategoryRepository } from '@/modules/content/repositories/category.repository'; import { CategoryRepository } from '@/modules/content/repositories/category.repository';
import { treePaginate } from '@/modules/database/utils'; import { treePaginate } from '@/modules/database/utils';
@ -22,7 +26,7 @@ export class CategoryService {
} }
async detail(id: string) { async detail(id: string) {
return this.repository.findOneByOrFail({ where: { id }, relations: ['parent'] }); return this.repository.findOneOrFail({ where: { id }, relations: ['parent'] });
} }
async create(data: CreateCategoryDto) { async create(data: CreateCategoryDto) {
@ -35,7 +39,7 @@ export class CategoryService {
async update(data: UpdateCategoryDto) { async update(data: UpdateCategoryDto) {
await this.repository.update(data.id, omit(data, ['id', 'parent'])); await this.repository.update(data.id, omit(data, ['id', 'parent']));
const item = await this.repository.findOneByOrFail({ const item = await this.repository.findOneOrFail({
where: { id: data.id }, where: { id: data.id },
relations: ['parent'], relations: ['parent'],
}); });
@ -53,7 +57,7 @@ export class CategoryService {
} }
async delete(id: string) { async delete(id: string) {
const item = await this.repository.findOneByOrFail({ const item = await this.repository.findOneOrFail({
where: { id }, where: { id },
relations: ['parent', 'children'], relations: ['parent', 'children'],
}); });

View File

@ -4,10 +4,17 @@ import { isNil } from 'lodash';
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm'; import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
import { QueryCommentTreeDto } from '@/modules/content/dtos/comment.dto'; import {
CreateCommentDto,
QueryCommentDto,
QueryCommentTreeDto,
} from '@/modules/content/dtos/comment.dto';
import { CommentEntity } from '@/modules/content/entities/comment.entity'; import { CommentEntity } from '@/modules/content/entities/comment.entity';
import { treePaginate } from '@/modules/database/utils'; import { treePaginate } from '@/modules/database/utils';
import { CommentRepository } from '../repositories/comment.repository';
import { PostRepository } from '../repositories/post.repository';
@Injectable() @Injectable()
export class CommentService { export class CommentService {
constructor( constructor(
@ -44,7 +51,7 @@ export class CommentService {
async create(data: CreateCommentDto) { async create(data: CreateCommentDto) {
const parent = await this.getParent(undefined, data.parent); const parent = await this.getParent(undefined, data.parent);
if (!isNil(parent) && parent.post.id !== data.post.id) { if (!isNil(parent) && parent.post.id !== data.post) {
throw new ForbiddenException('Parent comment and child comment must belong same post!'); throw new ForbiddenException('Parent comment and child comment must belong same post!');
} }
const item = await this.repository.save({ const item = await this.repository.save({

View File

@ -0,0 +1,4 @@
export * from './category.service';
export * from './tag.service';
export * from './post.service';
export * from './comment.service';

View File

@ -1,21 +1,35 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isNil } from '@nestjs/common/utils/shared.utils'; import { isNil } from '@nestjs/common/utils/shared.utils';
import { isFunction, omit } from 'lodash'; import { isArray, isFunction, omit } from 'lodash';
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm'; import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeorm';
import { PostOrder } from '@/modules/content/constants'; import { PostOrder } from '@/modules/content/constants';
import { CreatePostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto'; import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dtos/post.dto';
import { PostEntity } from '@/modules/content/entities/post.entity'; import { PostEntity } from '@/modules/content/entities/post.entity';
import { PostRepository } from '@/modules/content/repositories/post.repository'; import { PostRepository } from '@/modules/content/repositories/post.repository';
import { PaginateOptions, QueryHook } from '@/modules/database/types'; import { QueryHook } from '@/modules/database/types';
import { paginate } from '@/modules/database/utils'; import { paginate } from '@/modules/database/utils';
import { CategoryRepository } from '../repositories/category.repository';
import { TagRepository } from '../repositories/tag.repository';
import { CategoryService } from './category.service';
type FindParams = {
[key in keyof Omit<QueryPostDto, 'limit' | 'page'>]: QueryPostDto[key];
};
@Injectable() @Injectable()
export class PostService { export class PostService {
constructor(protected repository: PostRepository) {} constructor(
protected repository: PostRepository,
protected categoryRepository: CategoryRepository,
protected categoryService: CategoryService,
protected tagRepository: TagRepository,
) {}
async paginate(options: PaginateOptions, callback?: QueryHook<PostEntity>) { async paginate(options: QueryPostDto, callback?: QueryHook<PostEntity>) {
const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback); const qb = await this.buildListQuery(this.repository.buildBaseQB(), options, callback);
return paginate(qb, options); return paginate(qb, options);
} }
@ -36,7 +50,15 @@ export class PostService {
if (!isNil(data.publish)) { if (!isNil(data.publish)) {
publishedAt = data.publish ? new Date() : null; publishedAt = data.publish ? new Date() : null;
} }
const item = await this.repository.save({ ...omit(data, ['publish']), publishedAt }); const createPostDto = {
...omit(data, ['publish']),
category: isNil(data.category)
? null
: await this.categoryRepository.findOneOrFail({ where: { id: data.category } }),
tags: isArray(data.tags) ? await this.tagRepository.findBy({ id: In(data.tags) }) : [],
publishedAt,
};
const item = await this.repository.save(createPostDto);
return this.detail(item.id); return this.detail(item.id);
} }
@ -45,8 +67,22 @@ export class PostService {
if (!isNil(data.publish)) { if (!isNil(data.publish)) {
publishedAt = data.publish ? new Date() : null; publishedAt = data.publish ? new Date() : null;
} }
const post = await this.detail(data.id);
if (data.category !== undefined) {
post.category = isNil(data.category)
? null
: await this.categoryRepository.findOneByOrFail({ id: data.category });
await this.repository.save(post, { reload: true });
}
if (isArray(data.tags)) {
await this.repository
.createQueryBuilder('post')
.relation(PostEntity, 'tags')
.of(post)
.addAndRemove(data.tags, post.tags ?? []);
}
await this.repository.update(data.id, { await this.repository.update(data.id, {
...omit(data, ['id', 'publish']), ...omit(data, ['id', 'publish', 'tags', 'category']),
publishedAt, publishedAt,
}); });
return this.detail(data.id); return this.detail(data.id);
@ -59,16 +95,22 @@ export class PostService {
protected async buildListQuery( protected async buildListQuery(
qb: SelectQueryBuilder<PostEntity>, qb: SelectQueryBuilder<PostEntity>,
options: RecordAny, options: FindParams,
callback?: QueryHook<PostEntity>, callback?: QueryHook<PostEntity>,
) { ) {
const { orderBy, isPublished } = options; const { orderBy, isPublished, category, tag } = 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);
if (category) {
await this.queryByCategory(category, qb);
}
if (tag) {
qb.where('tags.id = :id', { id: tag });
}
if (callback) { if (callback) {
return callback(qb); return callback(qb);
} }
@ -85,6 +127,8 @@ export class PostService {
return qb.orderBy('post.publishedAt', 'DESC'); return qb.orderBy('post.publishedAt', 'DESC');
case PostOrder.CUSTOM: case PostOrder.CUSTOM:
return qb.orderBy('post.customOrder', 'DESC'); return qb.orderBy('post.customOrder', 'DESC');
case PostOrder.COMMENTCOUNT:
return qb.orderBy('post.commentCount', 'DESC');
default: default:
return qb return qb
.orderBy('post.createdAt', 'DESC') .orderBy('post.createdAt', 'DESC')
@ -92,4 +136,12 @@ export class PostService {
.addOrderBy('post.publishedAt', 'DESC'); .addOrderBy('post.publishedAt', 'DESC');
} }
} }
protected async queryByCategory(id: string, qb: SelectQueryBuilder<PostEntity>) {
const root = await this.categoryService.detail(id);
const tree = await this.categoryRepository.findDescendantsTree(root);
const flatDes = await this.categoryRepository.toFlatTrees(tree.children);
const ids = [tree.id, ...flatDes.map((item) => item.id)];
return qb.where('categoryRepository.id IN (:...ids)', { ids });
}
} }

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { omit } from 'lodash'; import { omit } from 'lodash';
import { CreateTagDto, QueryTagDto } from '@/modules/content/dtos/tag.dto'; import { CreateTagDto, QueryTagDto, UpdateTagDto } from '@/modules/content/dtos/tag.dto';
import { TagRepository } from '@/modules/content/repositories/tag.repository'; import { TagRepository } from '@/modules/content/repositories/tag.repository';
import { paginate } from '@/modules/database/utils'; import { paginate } from '@/modules/database/utils';
@ -31,7 +31,7 @@ export class TagService {
} }
async delete(id: string) { async delete(id: string) {
const item = this.repository.findOneByOrFail({ id }); const item = await this.repository.findOneByOrFail({ id });
return this.repository.remove(item); return this.repository.remove(item);
} }
} }