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',
UPDATED = 'updatedAt',
PUBLISHED = 'publishedAt',
COMMENTCOUNT = 'commentCount',
CUSTOM = 'custom',
}

View File

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

View File

@ -11,6 +11,8 @@ import {
} from 'class-validator';
import { toNumber } from 'lodash';
import { PaginateOptions } from '@/modules/database/types';
export class QueryTagDto implements PaginateOptions {
@Transform(({ value }) => toNumber(value))
@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 { 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 { CategoryRepository } from '@/modules/content/repositories/category.repository';
import { treePaginate } from '@/modules/database/utils';
@ -22,7 +26,7 @@ export class CategoryService {
}
async detail(id: string) {
return this.repository.findOneByOrFail({ where: { id }, relations: ['parent'] });
return this.repository.findOneOrFail({ where: { id }, relations: ['parent'] });
}
async create(data: CreateCategoryDto) {
@ -35,7 +39,7 @@ export class CategoryService {
async update(data: UpdateCategoryDto) {
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 },
relations: ['parent'],
});
@ -53,7 +57,7 @@ export class CategoryService {
}
async delete(id: string) {
const item = await this.repository.findOneByOrFail({
const item = await this.repository.findOneOrFail({
where: { id },
relations: ['parent', 'children'],
});

View File

@ -4,10 +4,17 @@ import { isNil } from 'lodash';
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 { treePaginate } from '@/modules/database/utils';
import { CommentRepository } from '../repositories/comment.repository';
import { PostRepository } from '../repositories/post.repository';
@Injectable()
export class CommentService {
constructor(
@ -44,7 +51,7 @@ export class CommentService {
async create(data: CreateCommentDto) {
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!');
}
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 { isNil } from '@nestjs/common/utils/shared.utils';
import { isFunction, omit } from 'lodash';
import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm';
import { isArray, isFunction, omit } from 'lodash';
import { EntityNotFoundError, In, IsNull, Not, SelectQueryBuilder } from 'typeorm';
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 { 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 { 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()
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);
return paginate(qb, options);
}
@ -36,7 +50,15 @@ export class PostService {
if (!isNil(data.publish)) {
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);
}
@ -45,8 +67,22 @@ export class PostService {
if (!isNil(data.publish)) {
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, {
...omit(data, ['id', 'publish']),
...omit(data, ['id', 'publish', 'tags', 'category']),
publishedAt,
});
return this.detail(data.id);
@ -59,16 +95,22 @@ export class PostService {
protected async buildListQuery(
qb: SelectQueryBuilder<PostEntity>,
options: RecordAny,
options: FindParams,
callback?: QueryHook<PostEntity>,
) {
const { orderBy, isPublished } = options;
const { orderBy, isPublished, category, tag } = options;
if (typeof isPublished === 'boolean') {
isPublished
? qb.where({ publishedAt: Not(IsNull()) })
: qb.where({ publishedAt: IsNull() });
}
this.queryOrderBy(qb, orderBy);
if (category) {
await this.queryByCategory(category, qb);
}
if (tag) {
qb.where('tags.id = :id', { id: tag });
}
if (callback) {
return callback(qb);
}
@ -85,6 +127,8 @@ export class PostService {
return qb.orderBy('post.publishedAt', 'DESC');
case PostOrder.CUSTOM:
return qb.orderBy('post.customOrder', 'DESC');
case PostOrder.COMMENTCOUNT:
return qb.orderBy('post.commentCount', 'DESC');
default:
return qb
.orderBy('post.createdAt', 'DESC')
@ -92,4 +136,12 @@ export class PostService {
.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 { 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 { paginate } from '@/modules/database/utils';
@ -31,7 +31,7 @@ export class TagService {
}
async delete(id: string) {
const item = this.repository.findOneByOrFail({ id });
const item = await this.repository.findOneByOrFail({ id });
return this.repository.remove(item);
}
}