Compare commits
9 Commits
50dbb06b29
...
5452f890ec
Author | SHA1 | Date | |
---|---|---|---|
5452f890ec | |||
02ccf58457 | |||
7af6efc642 | |||
996f887d73 | |||
fdd9d80310 | |||
37f11a0097 | |||
87aba1d0eb | |||
b7bb509b70 | |||
427997f1cb |
@ -30,6 +30,7 @@
|
|||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"meilisearch": "^0.50.0",
|
||||||
"mysql2": "^3.14.1",
|
"mysql2": "^3.14.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
|
@ -35,6 +35,9 @@ importers:
|
|||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
|
meilisearch:
|
||||||
|
specifier: ^0.50.0
|
||||||
|
version: 0.50.0
|
||||||
mysql2:
|
mysql2:
|
||||||
specifier: ^3.14.1
|
specifier: ^3.14.1
|
||||||
version: 3.14.1
|
version: 3.14.1
|
||||||
@ -2744,6 +2747,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
meilisearch@0.50.0:
|
||||||
|
resolution: {integrity: sha512-9IzIkobvnuS18Eg4dq/eJB9W+eXqeLZjNRgq/kKMswSmVYYSQsXqGgSuCA0JkF+o5RwJlwIsieQee6rh313VhA==}
|
||||||
|
|
||||||
memfs@3.5.3:
|
memfs@3.5.3:
|
||||||
resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
|
resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
|
||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
@ -7316,6 +7322,8 @@ snapshots:
|
|||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
meilisearch@0.50.0: {}
|
||||||
|
|
||||||
memfs@3.5.3:
|
memfs@3.5.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
fs-monkey: 1.0.6
|
fs-monkey: 1.0.6
|
||||||
|
@ -4,7 +4,10 @@ import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
|||||||
|
|
||||||
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
import { AppInterceptor } from '@/modules/core/providers/app.interceptor';
|
||||||
|
|
||||||
import { database } from './config';
|
import { MEILI_CONFIG } from '@/modules/meilisearch/meili.config';
|
||||||
|
import { MeiliModule } from '@/modules/meilisearch/meili.module';
|
||||||
|
|
||||||
|
import { content, database } from './config';
|
||||||
|
|
||||||
import { DEFAULT_VALIDATION_CONFIG } from './modules/content/constants';
|
import { DEFAULT_VALIDATION_CONFIG } from './modules/content/constants';
|
||||||
import { ContentModule } from './modules/content/content.module';
|
import { ContentModule } from './modules/content/content.module';
|
||||||
@ -14,7 +17,12 @@ import { AppPipe } from './modules/core/providers/app.pipe';
|
|||||||
import { DatabaseModule } from './modules/database/database.module';
|
import { DatabaseModule } from './modules/database/database.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ContentModule, CoreModule.forRoot(), DatabaseModule.forRoot(database)],
|
imports: [
|
||||||
|
ContentModule.forRoot(content),
|
||||||
|
CoreModule.forRoot(),
|
||||||
|
DatabaseModule.forRoot(database),
|
||||||
|
MeiliModule.forRoot(MEILI_CONFIG),
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: APP_PIPE,
|
provide: APP_PIPE,
|
||||||
|
5
src/config/content.config.ts
Normal file
5
src/config/content.config.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { ContentConfig } from '@/modules/content/types';
|
||||||
|
|
||||||
|
export const content = (): ContentConfig => ({
|
||||||
|
SearchType: 'meili',
|
||||||
|
});
|
@ -1 +1,2 @@
|
|||||||
export * from './database.config';
|
export * from './database.config';
|
||||||
|
export * from './content.config';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { DynamicModule, Module, ModuleMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
@ -6,21 +6,68 @@ import * as controllers from '@/modules/content/controllers';
|
|||||||
import * as entities from '@/modules/content/entities';
|
import * as entities from '@/modules/content/entities';
|
||||||
import * as repositories from '@/modules/content/repositories';
|
import * as repositories from '@/modules/content/repositories';
|
||||||
import * as services from '@/modules/content/services';
|
import * as services from '@/modules/content/services';
|
||||||
|
import { SearchService } from '@/modules/content/services';
|
||||||
import { SanitizeService } from '@/modules/content/services/SanitizeService';
|
import { SanitizeService } from '@/modules/content/services/SanitizeService';
|
||||||
|
|
||||||
|
import { PostService } from '@/modules/content/services/post.service';
|
||||||
import { PostSubscriber } from '@/modules/content/subscribers/post.subscriber';
|
import { PostSubscriber } from '@/modules/content/subscribers/post.subscriber';
|
||||||
|
import { ContentConfig } from '@/modules/content/types';
|
||||||
import { DatabaseModule } from '@/modules/database/database.module';
|
import { DatabaseModule } from '@/modules/database/database.module';
|
||||||
|
|
||||||
@Module({
|
@Module({})
|
||||||
imports: [
|
export class ContentModule {
|
||||||
TypeOrmModule.forFeature(Object.values(entities)),
|
static forRoot(configRegister?: () => ContentConfig): DynamicModule {
|
||||||
DatabaseModule.forRepository(Object.values(repositories)),
|
const config: Required<ContentConfig> = {
|
||||||
],
|
SearchType: 'mysql',
|
||||||
controllers: Object.values(controllers),
|
...(configRegister ? configRegister() : {}),
|
||||||
providers: [...Object.values(services), PostSubscriber, SanitizeService],
|
};
|
||||||
exports: [
|
const providers: ModuleMetadata['providers'] = [
|
||||||
...Object.values(services),
|
...Object.values(services),
|
||||||
DatabaseModule.forRepository(Object.values(repositories)),
|
SanitizeService,
|
||||||
],
|
PostSubscriber,
|
||||||
})
|
{
|
||||||
export class ContentModule {}
|
provide: PostService,
|
||||||
|
inject: [
|
||||||
|
repositories.PostRepository,
|
||||||
|
repositories.CategoryRepository,
|
||||||
|
repositories.TagRepository,
|
||||||
|
services.CategoryService,
|
||||||
|
{ token: services.SearchService, optional: true },
|
||||||
|
],
|
||||||
|
useFactory(
|
||||||
|
postRepository: repositories.PostRepository,
|
||||||
|
categoryRepository: repositories.CategoryRepository,
|
||||||
|
tagRepository: repositories.TagRepository,
|
||||||
|
categoryService: services.CategoryService,
|
||||||
|
searchService: SearchService,
|
||||||
|
) {
|
||||||
|
return new PostService(
|
||||||
|
postRepository,
|
||||||
|
categoryRepository,
|
||||||
|
categoryService,
|
||||||
|
tagRepository,
|
||||||
|
searchService,
|
||||||
|
config.SearchType,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
if (config.SearchType === 'meili') {
|
||||||
|
providers.push(services.SearchService);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
module: ContentModule,
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature(Object.values(entities)),
|
||||||
|
DatabaseModule.forRepository(Object.values(repositories)),
|
||||||
|
],
|
||||||
|
controllers: Object.values(controllers),
|
||||||
|
providers,
|
||||||
|
exports: [
|
||||||
|
...Object.values(services),
|
||||||
|
PostService,
|
||||||
|
DatabaseModule.forRepository(Object.values(repositories)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
import {
|
import { Body, Controller, Delete, Get, Post, Query, SerializeOptions } from '@nestjs/common';
|
||||||
Body,
|
|
||||||
Controller,
|
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Param,
|
|
||||||
ParseUUIDPipe,
|
|
||||||
Post,
|
|
||||||
Query,
|
|
||||||
SerializeOptions,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
|
|
||||||
import { CreateCommentDto, QueryCommentDto, QueryCommentTreeDto } from '../dtos/comment.dto';
|
import {
|
||||||
|
CreateCommentDto,
|
||||||
|
DeleteCommentDto,
|
||||||
|
QueryCommentDto,
|
||||||
|
QueryCommentTreeDto,
|
||||||
|
} from '../dtos/comment.dto';
|
||||||
import { CommentService } from '../services';
|
import { CommentService } from '../services';
|
||||||
|
|
||||||
@Controller('comment')
|
@Controller('comment')
|
||||||
@ -38,9 +33,9 @@ export class CommentController {
|
|||||||
return this.service.create(data);
|
return this.service.create(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete()
|
||||||
@SerializeOptions({ groups: ['comment-detail'] })
|
@SerializeOptions({ groups: ['comment-detail'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Body() data: DeleteCommentDto) {
|
||||||
return this.service.delete(id);
|
return this.service.delete(data.ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ import {
|
|||||||
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 { DeleteWithTrashDto, RestoreDto } from '../dtos/delete.with.trash.dto';
|
||||||
|
|
||||||
@Controller('posts')
|
@Controller('posts')
|
||||||
export class PostController {
|
export class PostController {
|
||||||
constructor(private postService: PostService) {}
|
constructor(private postService: PostService) {}
|
||||||
@ -51,9 +53,18 @@ export class PostController {
|
|||||||
return this.postService.update(data);
|
return this.postService.update(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete()
|
||||||
@SerializeOptions({ groups: ['post-detail'] })
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Body() data: DeleteWithTrashDto) {
|
||||||
return this.postService.delete(id);
|
return this.postService.delete(data.ids, data.trash);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('restore')
|
||||||
|
@SerializeOptions({ groups: ['post-detail'] })
|
||||||
|
async restore(
|
||||||
|
@Body()
|
||||||
|
data: RestoreDto,
|
||||||
|
) {
|
||||||
|
return this.postService.restore(data.ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import {
|
|||||||
SerializeOptions,
|
SerializeOptions,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
|
import { DeleteDto } from '@/modules/content/dtos/delete.dto';
|
||||||
|
|
||||||
import { CreateTagDto, QueryTagDto, UpdateTagDto } from '../dtos/tag.dto';
|
import { CreateTagDto, QueryTagDto, UpdateTagDto } from '../dtos/tag.dto';
|
||||||
import { TagService } from '../services';
|
import { TagService } from '../services';
|
||||||
|
|
||||||
@ -51,9 +53,9 @@ export class TagController {
|
|||||||
return this.service.update(date);
|
return this.service.update(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete()
|
||||||
@SerializeOptions({})
|
@SerializeOptions({})
|
||||||
async delete(@Param('id', new ParseUUIDPipe()) id: string) {
|
async delete(@Body() data: DeleteDto) {
|
||||||
return this.service.delete(id);
|
return this.service.delete(data.ids);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,13 +46,13 @@ export class QueryCommentTreeDto extends PickType(QueryCommentDto, ['post']) {}
|
|||||||
|
|
||||||
@DtoValidation()
|
@DtoValidation()
|
||||||
export class CreateCommentDto {
|
export class CreateCommentDto {
|
||||||
@MaxLength(1000, { message: '' })
|
@MaxLength(1000, { message: 'The length of the comment content cannot exceed $constraint1' })
|
||||||
@IsNotEmpty({ message: '' })
|
@IsNotEmpty({ message: 'Comment content cannot be empty' })
|
||||||
body: string;
|
body: string;
|
||||||
|
|
||||||
@IsDataExist(PostEntity, { message: 'The post does not exist' })
|
@IsDataExist(PostEntity, { message: 'The post does not exist' })
|
||||||
@IsUUID(undefined, { message: 'The ID format is incorrect' })
|
@IsUUID(undefined, { message: 'The ID format is incorrect' })
|
||||||
@IsDefined({ message: 'The ID must be specified' })
|
@IsDefined({ message: 'The post ID must be specified' })
|
||||||
post: string;
|
post: string;
|
||||||
|
|
||||||
@IsDataExist(CommentEntity, { message: 'The parent comment does not exist' })
|
@IsDataExist(CommentEntity, { message: 'The parent comment does not exist' })
|
||||||
@ -62,3 +62,11 @@ export class CreateCommentDto {
|
|||||||
@Transform(({ value }) => (value === 'null' ? null : value))
|
@Transform(({ value }) => (value === 'null' ? null : value))
|
||||||
parent?: string;
|
parent?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DtoValidation()
|
||||||
|
export class DeleteCommentDto {
|
||||||
|
@IsDataExist(CommentEntity)
|
||||||
|
@IsUUID(undefined, { each: true, always: true, message: 'The ID format is incorrect' })
|
||||||
|
@IsDefined({ each: true, message: 'The ID must be specified' })
|
||||||
|
ids: string[];
|
||||||
|
}
|
||||||
|
10
src/modules/content/dtos/delete.dto.ts
Normal file
10
src/modules/content/dtos/delete.dto.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { IsUUID, IsDefined } from 'class-validator';
|
||||||
|
|
||||||
|
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||||
|
|
||||||
|
@DtoValidation()
|
||||||
|
export class DeleteDto {
|
||||||
|
@IsUUID(undefined, { each: true, always: true, message: 'The ID format is incorrect' })
|
||||||
|
@IsDefined({ each: true, message: 'The ID must be specified' })
|
||||||
|
ids: string[];
|
||||||
|
}
|
19
src/modules/content/dtos/delete.with.trash.dto.ts
Normal file
19
src/modules/content/dtos/delete.with.trash.dto.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
import { IsBoolean, IsDefined, IsOptional, IsUUID } from 'class-validator';
|
||||||
|
import { toBoolean } from 'validator';
|
||||||
|
|
||||||
|
import { DeleteDto } from './delete.dto';
|
||||||
|
|
||||||
|
export class DeleteWithTrashDto extends DeleteDto {
|
||||||
|
@Transform(({ value }) => toBoolean(value))
|
||||||
|
@IsBoolean()
|
||||||
|
@IsOptional()
|
||||||
|
trash?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RestoreDto {
|
||||||
|
@IsUUID(undefined, { each: true, always: true, message: 'The ID format is incorrect' })
|
||||||
|
@IsDefined({ each: true, message: 'The ID must be specified' })
|
||||||
|
ids: string[];
|
||||||
|
}
|
@ -19,6 +19,7 @@ import { isNil, toNumber } from 'lodash';
|
|||||||
import { PostOrder } from '@/modules/content/constants';
|
import { PostOrder } from '@/modules/content/constants';
|
||||||
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
|
||||||
import { toBoolean } from '@/modules/core/helpers';
|
import { toBoolean } from '@/modules/core/helpers';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { IsDataExist } from '@/modules/database/constraints/data.exist.constraint';
|
import { IsDataExist } from '@/modules/database/constraints/data.exist.constraint';
|
||||||
import { PaginateOptions } from '@/modules/database/types';
|
import { PaginateOptions } from '@/modules/database/types';
|
||||||
|
|
||||||
@ -31,11 +32,14 @@ export class QueryPostDto implements PaginateOptions {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
isPublished?: boolean;
|
isPublished?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
@IsEnum(PostOrder, {
|
@IsEnum(PostOrder, {
|
||||||
message: `The sorting rule must be one of ${Object.values(PostOrder).join(',')}`,
|
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, { always: true, message: 'The current page must be greater than 1.' })
|
@Min(1, { always: true, message: 'The current page must be greater than 1.' })
|
||||||
@ -52,6 +56,10 @@ export class QueryPostDto implements PaginateOptions {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
limit = 10;
|
limit = 10;
|
||||||
|
|
||||||
|
@IsEnum(SelectTrashMode)
|
||||||
|
@IsOptional()
|
||||||
|
trashed?: SelectTrashMode;
|
||||||
|
|
||||||
@IsDataExist(CategoryEntity, { always: true, message: 'The category does not exist' })
|
@IsDataExist(CategoryEntity, { always: true, message: 'The category does not exist' })
|
||||||
@IsUUID(undefined, { message: 'The ID format is incorrect' })
|
@IsUUID(undefined, { message: 'The ID format is incorrect' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
|
DeleteDateColumn,
|
||||||
Entity,
|
Entity,
|
||||||
JoinTable,
|
JoinTable,
|
||||||
ManyToMany,
|
ManyToMany,
|
||||||
@ -64,6 +65,11 @@ export class PostEntity extends BaseEntity {
|
|||||||
@UpdateDateColumn({ comment: '更新时间', nullable: true })
|
@UpdateDateColumn({ comment: '更新时间', nullable: true })
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
@Type(() => Date)
|
||||||
|
@DeleteDateColumn({ comment: '删除时间' })
|
||||||
|
deleteAt: Date;
|
||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
commentCount: number;
|
commentCount: number;
|
||||||
|
|
||||||
@ -76,7 +82,7 @@ export class PostEntity extends BaseEntity {
|
|||||||
|
|
||||||
@Expose()
|
@Expose()
|
||||||
@Type(() => TagEntity)
|
@Type(() => TagEntity)
|
||||||
@ManyToMany(() => TagEntity, (tag) => tag.posts, { cascade: true })
|
@ManyToMany(() => TagEntity, (tag) => tag.posts, { cascade: ['insert', 'update', 'remove'] })
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
tags: Relation<TagEntity>[];
|
tags: Relation<TagEntity>[];
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { pick, unset } from 'lodash';
|
import { isNil, pick, unset } from 'lodash';
|
||||||
import { FindOptionsUtils, FindTreeOptions, TreeRepository, TreeRepositoryUtils } from 'typeorm';
|
import { FindOptionsUtils, FindTreeOptions, TreeRepository, TreeRepositoryUtils } from 'typeorm';
|
||||||
|
|
||||||
import { CategoryEntity } from '@/modules/content/entities/category.entity';
|
import { CategoryEntity } from '@/modules/content/entities/category.entity';
|
||||||
@ -111,4 +111,17 @@ export class CategoryRepository extends TreeRepository<CategoryEntity> {
|
|||||||
}
|
}
|
||||||
return data as CategoryEntity[];
|
return data as CategoryEntity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async flatAncestorsTree(item: CategoryEntity) {
|
||||||
|
let data: Omit<CategoryEntity, 'children'>[] = [];
|
||||||
|
const category = await this.findAncestorsTree(item);
|
||||||
|
const { parent } = category;
|
||||||
|
unset(category, 'children');
|
||||||
|
unset(category, 'item');
|
||||||
|
data.push(item);
|
||||||
|
if (!isNil(parent)) {
|
||||||
|
data = [...(await this.flatAncestorsTree(parent)), ...data];
|
||||||
|
}
|
||||||
|
return data as CategoryEntity[];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
import { isNil } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
|
|
||||||
import { EntityNotFoundError, SelectQueryBuilder } from 'typeorm';
|
import { EntityNotFoundError, In, SelectQueryBuilder } from 'typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CreateCommentDto,
|
CreateCommentDto,
|
||||||
@ -63,9 +63,9 @@ export class CommentService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(ids: string[]) {
|
||||||
const comment = await this.repository.findOneOrFail({ where: { id: id ?? null } });
|
const comments = await this.repository.find({ where: { id: In(ids) } });
|
||||||
return this.repository.remove(comment);
|
return this.repository.remove(comments);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getPost(id: string) {
|
protected async getPost(id: string) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from './category.service';
|
export * from './category.service';
|
||||||
export * from './tag.service';
|
export * from './tag.service';
|
||||||
export * from './post.service';
|
|
||||||
export * from './comment.service';
|
export * from './comment.service';
|
||||||
|
export * from './search.service';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 { isArray, isFunction, omit } from 'lodash';
|
import { isArray, isFunction, omit, pick } from 'lodash';
|
||||||
import { EntityNotFoundError, In, 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';
|
||||||
@ -9,6 +9,9 @@ import { CreatePostDto, QueryPostDto, UpdatePostDto } from '@/modules/content/dt
|
|||||||
import { PostEntity } from '@/modules/content/entities/post.entity';
|
import { PostEntity } from '@/modules/content/entities/post.entity';
|
||||||
import { CategoryRepository } from '@/modules/content/repositories';
|
import { CategoryRepository } from '@/modules/content/repositories';
|
||||||
import { PostRepository } from '@/modules/content/repositories/post.repository';
|
import { PostRepository } from '@/modules/content/repositories/post.repository';
|
||||||
|
import { SearchService } from '@/modules/content/services/search.service';
|
||||||
|
import { SearchType } from '@/modules/content/types';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
import { QueryHook } from '@/modules/database/types';
|
import { QueryHook } from '@/modules/database/types';
|
||||||
import { paginate } from '@/modules/database/utils';
|
import { paginate } from '@/modules/database/utils';
|
||||||
|
|
||||||
@ -27,9 +30,17 @@ export class PostService {
|
|||||||
protected categoryRepository: CategoryRepository,
|
protected categoryRepository: CategoryRepository,
|
||||||
protected categoryService: CategoryService,
|
protected categoryService: CategoryService,
|
||||||
protected tagRepository: TagRepository,
|
protected tagRepository: TagRepository,
|
||||||
|
protected searchService?: SearchService,
|
||||||
|
protected searchType: SearchType = 'mysql',
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async paginate(options: QueryPostDto, callback?: QueryHook<PostEntity>) {
|
async paginate(options: QueryPostDto, callback?: QueryHook<PostEntity>) {
|
||||||
|
if (!isNil(this.searchService) && !isNil(options.search) && this.searchType === 'meili') {
|
||||||
|
return this.searchService.search(
|
||||||
|
options.search,
|
||||||
|
pick(options, ['trashed', 'page', 'limit']),
|
||||||
|
);
|
||||||
|
}
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -59,7 +70,11 @@ export class PostService {
|
|||||||
publishedAt,
|
publishedAt,
|
||||||
};
|
};
|
||||||
const item = await this.repository.save(createPostDto);
|
const item = await this.repository.save(createPostDto);
|
||||||
return this.detail(item.id);
|
const result = await this.detail(item.id);
|
||||||
|
if (!isNil(this.searchService)) {
|
||||||
|
await this.searchService.create(result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(data: UpdatePostDto) {
|
async update(data: UpdatePostDto) {
|
||||||
@ -85,12 +100,57 @@ export class PostService {
|
|||||||
...omit(data, ['id', 'publish', 'tags', 'category']),
|
...omit(data, ['id', 'publish', 'tags', 'category']),
|
||||||
publishedAt,
|
publishedAt,
|
||||||
});
|
});
|
||||||
return this.detail(data.id);
|
const result = await this.detail(data.id);
|
||||||
|
if (!isNil(this.searchService)) {
|
||||||
|
await this.searchService.update([result]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(ids: string[], trash?: boolean) {
|
||||||
const item = await this.repository.findOneByOrFail({ id });
|
const items = await this.repository
|
||||||
return this.repository.remove(item);
|
.buildBaseQB()
|
||||||
|
.where('post.id IN (:...ids)', { ids })
|
||||||
|
.withDeleted()
|
||||||
|
.getMany();
|
||||||
|
let result: PostEntity[];
|
||||||
|
if (trash) {
|
||||||
|
const directs = items.filter((item) => !isNil(item.deleteAt));
|
||||||
|
const softs = items.filter((item) => isNil(item.deleteAt));
|
||||||
|
result = [
|
||||||
|
...(await this.repository.remove(directs)),
|
||||||
|
...(await this.repository.softRemove(softs)),
|
||||||
|
];
|
||||||
|
if (!isNil(this.searchService)) {
|
||||||
|
await this.searchService.delete(directs.map((item) => item.id));
|
||||||
|
await this.searchService.update(softs);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = await this.repository.remove(items);
|
||||||
|
if (!isNil(this.searchService)) {
|
||||||
|
await this.searchService.delete(ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restore(ids: string[]) {
|
||||||
|
const items = await this.repository
|
||||||
|
.buildBaseQB()
|
||||||
|
.where('post.id IN (:...ids)', { ids })
|
||||||
|
.withDeleted()
|
||||||
|
.getMany();
|
||||||
|
const trashes = items.filter((item) => !isNil(item.deleteAt));
|
||||||
|
await this.searchService.update(trashes);
|
||||||
|
const trashedIds = trashes.map((item) => item.id);
|
||||||
|
if (trashedIds.length < 1) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
await this.repository.restore(trashedIds);
|
||||||
|
const qb = await this.buildListQuery(this.repository.buildBaseQB(), {}, async (qbuilder) =>
|
||||||
|
qbuilder.andWhereInIds(trashedIds),
|
||||||
|
);
|
||||||
|
return qb.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async buildListQuery(
|
protected async buildListQuery(
|
||||||
@ -98,12 +158,21 @@ export class PostService {
|
|||||||
options: FindParams,
|
options: FindParams,
|
||||||
callback?: QueryHook<PostEntity>,
|
callback?: QueryHook<PostEntity>,
|
||||||
) {
|
) {
|
||||||
const { orderBy, isPublished, category, tag } = options;
|
const { orderBy, isPublished, category, tag, trashed, search } = 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() });
|
||||||
}
|
}
|
||||||
|
if (!isNil(search)) {
|
||||||
|
this.buildSearchQuery(qb, search);
|
||||||
|
}
|
||||||
|
if (trashed === SelectTrashMode.ALL || trashed === SelectTrashMode.ONLY) {
|
||||||
|
qb.withDeleted();
|
||||||
|
if (trashed === SelectTrashMode.ONLY) {
|
||||||
|
qb.where('post.deletedAt is not null');
|
||||||
|
}
|
||||||
|
}
|
||||||
this.queryOrderBy(qb, orderBy);
|
this.queryOrderBy(qb, orderBy);
|
||||||
if (category) {
|
if (category) {
|
||||||
await this.queryByCategory(category, qb);
|
await this.queryByCategory(category, qb);
|
||||||
@ -117,6 +186,15 @@ export class PostService {
|
|||||||
return qb;
|
return qb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected buildSearchQuery(qb: SelectQueryBuilder<PostEntity>, search: string) {
|
||||||
|
qb.orWhere('title LIKE :search', { search: `%${search}%` })
|
||||||
|
.orWhere('summary LIKE :search', { search: `%${search}%` })
|
||||||
|
.orWhere('body LIKE :search', { search: `%${search}%` })
|
||||||
|
.orWhere('category.name LIKE :search', { search: `%${search}%` })
|
||||||
|
.orWhere('tags.name LIKE :search', { search: `%${search}%` });
|
||||||
|
return qb;
|
||||||
|
}
|
||||||
|
|
||||||
protected queryOrderBy(qb: SelectQueryBuilder<PostEntity>, orderBy?: PostOrder) {
|
protected queryOrderBy(qb: SelectQueryBuilder<PostEntity>, orderBy?: PostOrder) {
|
||||||
switch (orderBy) {
|
switch (orderBy) {
|
||||||
case PostOrder.CREATED:
|
case PostOrder.CREATED:
|
||||||
|
95
src/modules/content/services/search.service.ts
Normal file
95
src/modules/content/services/search.service.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { ForbiddenException, Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { isNil, omit } from 'lodash';
|
||||||
|
import { MeiliSearch } from 'meilisearch';
|
||||||
|
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
import {
|
||||||
|
CategoryRepository,
|
||||||
|
CommentRepository,
|
||||||
|
PostRepository,
|
||||||
|
} from '@/modules/content/repositories';
|
||||||
|
import { SearchOption } from '@/modules/content/types';
|
||||||
|
import { getSearchData, getSearchItem } from '@/modules/content/utils';
|
||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
|
import { MeiliService } from '@/modules/meilisearch/meili.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchService implements OnModuleInit {
|
||||||
|
private index = 'content';
|
||||||
|
|
||||||
|
protected client: MeiliSearch;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected meiliService: MeiliService,
|
||||||
|
protected categoryRepository: CategoryRepository,
|
||||||
|
protected commentRepository: CommentRepository,
|
||||||
|
protected postRepository: PostRepository,
|
||||||
|
) {
|
||||||
|
this.client = this.meiliService.getClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit(): Promise<any> {
|
||||||
|
await this.client.deleteIndex(this.index);
|
||||||
|
this.client.index(this.index).updateFilterableAttributes(['deletedAt', 'publishedAt']);
|
||||||
|
this.client.index(this.index).updateSortableAttributes(['updatedAt', 'commentCount']);
|
||||||
|
const posts = await this.postRepository.buildBaseQB().withDeleted().getMany();
|
||||||
|
await this.client
|
||||||
|
.index(this.index)
|
||||||
|
.addDocuments(
|
||||||
|
await getSearchData(posts, this.categoryRepository, this.commentRepository),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient() {
|
||||||
|
if (isNil(this.client)) {
|
||||||
|
throw new ForbiddenException('Has no meili search client!');
|
||||||
|
}
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(text: string, param: SearchOption = {}) {
|
||||||
|
const option = { page: 1, limit: 10, trashed: SelectTrashMode.ONLY, ...param };
|
||||||
|
const limit = isNil(option.limit) || option.limit < 1 ? 1 : option.limit;
|
||||||
|
const page = isNil(option.page) || option.page < 1 ? 1 : option.page;
|
||||||
|
let filter = ['deletedAt IS NULL'];
|
||||||
|
if (option.trashed === SelectTrashMode.ALL) {
|
||||||
|
filter = [];
|
||||||
|
} else if (option.trashed === SelectTrashMode.ONLY) {
|
||||||
|
filter = ['deletedAt IS NOT NULL'];
|
||||||
|
}
|
||||||
|
if (option.isPublished) {
|
||||||
|
filter.push('publishedAt IS NOT NULL');
|
||||||
|
}
|
||||||
|
const result = await this.client
|
||||||
|
.index(this.index)
|
||||||
|
.search(text, { page, limit, sort: ['updatedAt:desc', 'commentCount:desc'], filter });
|
||||||
|
return {
|
||||||
|
item: result.hits,
|
||||||
|
currentPage: result.page,
|
||||||
|
perPage: result.hitsPerPage,
|
||||||
|
totalItems: result.estimatedTotalHits,
|
||||||
|
itemCount: result.totalHits,
|
||||||
|
...omit(result, ['hits', 'page', 'hitsPerPage', 'estimatedTotalHits', 'totalHits']),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(post: PostEntity) {
|
||||||
|
return this.getClient()
|
||||||
|
.index(this.index)
|
||||||
|
.addDocuments(
|
||||||
|
await getSearchItem(this.categoryRepository, this.commentRepository, post),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(posts: PostEntity[]) {
|
||||||
|
return this.getClient()
|
||||||
|
.index(this.index)
|
||||||
|
.updateDocuments(
|
||||||
|
await getSearchData(posts, this.categoryRepository, this.commentRepository),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(ids: string[]) {
|
||||||
|
return this.getClient().index(this.index).deleteDocuments(ids);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
|
|
||||||
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
import { CreateTagDto, QueryTagDto, UpdateTagDto } 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';
|
||||||
@ -30,8 +32,10 @@ export class TagService {
|
|||||||
return this.detail(data.id);
|
return this.detail(data.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string) {
|
async delete(ids: string[]) {
|
||||||
const item = await this.repository.findOneByOrFail({ id });
|
const items = await this.repository.find({
|
||||||
return this.repository.remove(item);
|
where: { id: In(ids) },
|
||||||
|
});
|
||||||
|
return this.repository.remove(items);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
src/modules/content/types.ts
Normal file
14
src/modules/content/types.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { SelectTrashMode } from '@/modules/database/constants';
|
||||||
|
|
||||||
|
export type SearchType = 'mysql' | 'meili';
|
||||||
|
|
||||||
|
export interface ContentConfig {
|
||||||
|
SearchType?: SearchType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchOption {
|
||||||
|
trashed?: SelectTrashMode;
|
||||||
|
isPublished?: boolean;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
53
src/modules/content/utils.ts
Normal file
53
src/modules/content/utils.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { instanceToPlain } from 'class-transformer';
|
||||||
|
import { isNil, pick } from 'lodash';
|
||||||
|
|
||||||
|
import { PostEntity } from '@/modules/content/entities';
|
||||||
|
import { CategoryRepository, CommentRepository } from '@/modules/content/repositories';
|
||||||
|
|
||||||
|
export async function getSearchItem(
|
||||||
|
categoryRepository: CategoryRepository,
|
||||||
|
commentRepository: CommentRepository,
|
||||||
|
post: PostEntity,
|
||||||
|
) {
|
||||||
|
const categories = isNil(post.category)
|
||||||
|
? []
|
||||||
|
: (await categoryRepository.flatAncestorsTree(post.category)).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
name: item.name,
|
||||||
|
}));
|
||||||
|
const comments = (
|
||||||
|
await commentRepository.find({
|
||||||
|
relations: ['post'],
|
||||||
|
where: { post: { id: post.id } },
|
||||||
|
})
|
||||||
|
).map((item) => ({ id: item.id, name: item.body }));
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...pick(instanceToPlain(post), [
|
||||||
|
'id',
|
||||||
|
'title',
|
||||||
|
'body',
|
||||||
|
'summary',
|
||||||
|
'commentCount',
|
||||||
|
'deleteAt',
|
||||||
|
'publishedAt',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
]),
|
||||||
|
categories,
|
||||||
|
comments,
|
||||||
|
tags: post.tags.map((item) => ({ id: item.id, name: item.name })),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSearchData = async (
|
||||||
|
posts: PostEntity[],
|
||||||
|
categoryRepository: CategoryRepository,
|
||||||
|
commentRepository: CommentRepository,
|
||||||
|
) =>
|
||||||
|
(
|
||||||
|
await Promise.all(
|
||||||
|
posts.map((post) => getSearchItem(categoryRepository, commentRepository, post)),
|
||||||
|
)
|
||||||
|
).reduce((o, n) => [...o, ...n], []);
|
@ -1 +1,10 @@
|
|||||||
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
|
export const CUSTOM_REPOSITORY_METADATA = 'CUSTOM_REPOSITORY_METADATA';
|
||||||
|
|
||||||
|
export enum SelectTrashMode {
|
||||||
|
// ALL: 包含已软删除和未软删除的数据(同时查询正常数据和回收站中的数据)
|
||||||
|
ALL = 'all',
|
||||||
|
// ONLY: 只包含软删除的数据 (只查询回收站中的数据)
|
||||||
|
ONLY = 'only',
|
||||||
|
// NONE: 只包含未软删除的数据 (只查询正常数据)
|
||||||
|
NONE = 'none',
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
ValidationOptions,
|
ValidationOptions,
|
||||||
registerDecorator,
|
registerDecorator,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
import { isArray, isNil } from 'lodash';
|
||||||
import { ObjectType, Repository, DataSource } from 'typeorm';
|
import { ObjectType, Repository, DataSource } from 'typeorm';
|
||||||
|
|
||||||
type Condition = {
|
type Condition = {
|
||||||
@ -18,9 +19,11 @@ type Condition = {
|
|||||||
export class DataExistConstraint implements ValidatorConstraintInterface {
|
export class DataExistConstraint implements ValidatorConstraintInterface {
|
||||||
constructor(private dataSource: DataSource) {}
|
constructor(private dataSource: DataSource) {}
|
||||||
|
|
||||||
|
errorValues: any[] = [];
|
||||||
|
|
||||||
async validate(value: any, validationArguments?: ValidationArguments) {
|
async validate(value: any, validationArguments?: ValidationArguments) {
|
||||||
let repo: Repository<any>;
|
let repo: Repository<any>;
|
||||||
if (!value) {
|
if (isNil(value)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let map = 'id';
|
let map = 'id';
|
||||||
@ -30,6 +33,22 @@ export class DataExistConstraint implements ValidatorConstraintInterface {
|
|||||||
} else {
|
} else {
|
||||||
repo = this.dataSource.getRepository(validationArguments.constraints[0]);
|
repo = this.dataSource.getRepository(validationArguments.constraints[0]);
|
||||||
}
|
}
|
||||||
|
if (isArray(value)) {
|
||||||
|
const values = value as any[];
|
||||||
|
const validationResults = await Promise.all(
|
||||||
|
values.map(async (val) => {
|
||||||
|
if (isNil(val)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const item = await repo.findOne({ where: { [map]: val } });
|
||||||
|
if (isNil(item)) {
|
||||||
|
this.errorValues.push(val);
|
||||||
|
}
|
||||||
|
return !isNil(item);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return validationResults.every((isValid) => isValid);
|
||||||
|
}
|
||||||
const item = await repo.findOne({ where: { [map]: value } });
|
const item = await repo.findOne({ where: { [map]: value } });
|
||||||
return !!item;
|
return !!item;
|
||||||
}
|
}
|
||||||
@ -37,6 +56,11 @@ export class DataExistConstraint implements ValidatorConstraintInterface {
|
|||||||
if (!validationArguments.constraints[0]) {
|
if (!validationArguments.constraints[0]) {
|
||||||
return 'Model not been specified!';
|
return 'Model not been specified!';
|
||||||
}
|
}
|
||||||
|
if (this.errorValues.length > 0) {
|
||||||
|
return `All instance of ${
|
||||||
|
validationArguments.constraints[0].name
|
||||||
|
} must been exists in databse!Errors are ${this.errorValues.join(',')}`;
|
||||||
|
}
|
||||||
return `All instance of ${validationArguments.constraints[0].name} must been exists in databse!`;
|
return `All instance of ${validationArguments.constraints[0].name} must been exists in databse!`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
9
src/modules/meilisearch/meili.config.ts
Normal file
9
src/modules/meilisearch/meili.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { MeiliConfig } from '@/modules/meilisearch/types';
|
||||||
|
|
||||||
|
export const MEILI_CONFIG = (): MeiliConfig => [
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
host: 'http://localhost:7700',
|
||||||
|
apiKey: 'masterKey',
|
||||||
|
},
|
||||||
|
];
|
28
src/modules/meilisearch/meili.module.ts
Normal file
28
src/modules/meilisearch/meili.module.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { DynamicModule, Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { MeiliService } from '@/modules/meilisearch/meili.service';
|
||||||
|
import { MeiliConfig } from '@/modules/meilisearch/types';
|
||||||
|
import { createMeiliOptions } from '@/modules/meilisearch/utils';
|
||||||
|
|
||||||
|
@Module({})
|
||||||
|
export class MeiliModule {
|
||||||
|
static forRoot(configRegister: () => MeiliConfig): DynamicModule {
|
||||||
|
return {
|
||||||
|
global: true,
|
||||||
|
module: MeiliModule,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MeiliService,
|
||||||
|
useFactory: async () => {
|
||||||
|
const service = new MeiliService(
|
||||||
|
await createMeiliOptions(configRegister()),
|
||||||
|
);
|
||||||
|
await service.createClients();
|
||||||
|
return service;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [MeiliService],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
42
src/modules/meilisearch/meili.service.ts
Normal file
42
src/modules/meilisearch/meili.service.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { isNil } from 'lodash';
|
||||||
|
import { MeiliSearch } from 'meilisearch';
|
||||||
|
|
||||||
|
import { MeiliConfig } from '@/modules/meilisearch/types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MeiliService {
|
||||||
|
protected options: MeiliConfig;
|
||||||
|
|
||||||
|
protected clients: Map<string, MeiliSearch> = new Map();
|
||||||
|
|
||||||
|
constructor(options: MeiliConfig) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptions() {
|
||||||
|
return this.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createClients() {
|
||||||
|
for (const option of this.options) {
|
||||||
|
this.clients.set(option.name, new MeiliSearch(option));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient(name?: string): MeiliSearch {
|
||||||
|
let key = 'default';
|
||||||
|
if (!isNil(name)) {
|
||||||
|
key = name;
|
||||||
|
}
|
||||||
|
if (!this.clients.has(key)) {
|
||||||
|
throw new Error(`No client found for ${name}`);
|
||||||
|
}
|
||||||
|
return this.clients.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
getClients(): Map<string, MeiliSearch> {
|
||||||
|
return this.clients;
|
||||||
|
}
|
||||||
|
}
|
5
src/modules/meilisearch/types.ts
Normal file
5
src/modules/meilisearch/types.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { Config } from 'meilisearch';
|
||||||
|
|
||||||
|
export type MeiliConfig = MeiliOption[];
|
||||||
|
|
||||||
|
export type MeiliOption = Config & { name: string };
|
18
src/modules/meilisearch/utils.ts
Normal file
18
src/modules/meilisearch/utils.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { MeiliConfig } from '@/modules/meilisearch/types';
|
||||||
|
|
||||||
|
export const createMeiliOptions = async (config: MeiliConfig): Promise<MeiliConfig | undefined> => {
|
||||||
|
if (config.length < 0) {
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
let options: MeiliConfig = [...config];
|
||||||
|
const names = options.map(({ name }) => name);
|
||||||
|
if (!names.includes('default')) {
|
||||||
|
options[0].name = 'default';
|
||||||
|
} else if (names.filter((name) => name === 'default').length > 0) {
|
||||||
|
options = options.reduce(
|
||||||
|
(o, n) => (o.map(({ name }) => name).includes('default') ? o : [...o, n]),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user