refactor: get database writing model

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei
2024-02-20 20:15:36 +08:00
parent 6ed5e40fa1
commit aeb85099b5
16 changed files with 144 additions and 117 deletions

View File

@@ -5,7 +5,6 @@ import wcmatch from 'wildcard-match'
import type { LogLevel } from '@nestjs/common'
import type { NestFastifyApplication } from '@nestjs/platform-fastify'
import { ValidationPipe } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { CROSS_DOMAIN, DEBUG_MODE, PORT } from './app.config'
@@ -14,6 +13,7 @@ import { fastifyApp } from './common/adapters/fastify.adapter'
import { RedisIoAdapter } from './common/adapters/socket.adapter'
import { SpiderGuard } from './common/guards/spider.guard'
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
import { ExtendedValidationPipe } from './common/pipes/validation.pipe'
import { logger } from './global/consola.global'
import { isMainProcess, isTest } from './global/env.global'
import { migrateDatabase } from './migration/migrate'
@@ -70,16 +70,7 @@ export async function bootstrap() {
app.useGlobalInterceptors(new LoggingInterceptor())
}
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
errorHttpStatusCode: 422,
forbidUnknownValues: true,
enableDebugMessages: isDev,
stopAtFirstError: true,
}),
)
app.useGlobalPipes(ExtendedValidationPipe.shared)
app.useGlobalGuards(new SpiderGuard())
!isTest && app.useWebSocketAdapter(new RedisIoAdapter(app))

View File

@@ -0,0 +1,19 @@
import type { ValidationPipeOptions } from '@nestjs/common'
import { Injectable, ValidationPipe } from '@nestjs/common'
@Injectable()
export class ExtendedValidationPipe extends ValidationPipe {
public static readonly options: ValidationPipeOptions = {
transform: true,
whitelist: true,
errorHttpStatusCode: 422,
forbidUnknownValues: true,
enableDebugMessages: isDev,
stopAtFirstError: true,
}
public static readonly shared = new ExtendedValidationPipe(
ExtendedValidationPipe.options,
)
}

View File

@@ -10,3 +10,5 @@ export const DB_CONNECTION_TOKEN = '__db_connection_token__'
export const DB_MODEL_TOKEN_SUFFIX = '__db_model_token_suffix__'
export const SKIP_LOGGING_METADATA = '__skipLogging__'
export const VALIDATION_PIPE_INJECTION = '__VALIDATION_PIPE__'

View File

@@ -1,16 +1,15 @@
import { plainToInstance } from 'class-transformer'
import { validateSync } from 'class-validator'
import { FastifyReply, FastifyRequest } from 'fastify'
import type { ValidatorOptions } from 'class-validator'
import { Body, HttpCode, Post, Req, Res, ValidationPipe } from '@nestjs/common'
import { Body, HttpCode, Inject, Post, Req, Res } from '@nestjs/common'
import { ApiController } from '~/common/decorators/api-controller.decorator'
import { Cookies } from '~/common/decorators/cookie.decorator'
import { RedisKeys } from '~/constants/cache.constant'
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
import { CountingService } from '~/processors/helper/helper.counting.service'
import { CacheService } from '~/processors/redis/cache.service'
import { getRedisKey } from '~/utils'
import { AckDto, AckEventType, AckReadPayloadDto } from './ack.dto'
@@ -19,15 +18,10 @@ export class AckController {
constructor(
private readonly cacheService: CacheService,
private readonly countingService: CountingService,
@Inject(VALIDATION_PIPE_INJECTION)
private readonly validatePipe: ExtendedValidationPipe,
) {}
private validateOptions: ValidatorOptions = {
whitelist: true,
forbidNonWhitelisted: true,
stopAtFirstError: true,
}
private validate = new ValidationPipe(this.validateOptions)
@Post('/')
@HttpCode(200)
async ack(
@@ -42,37 +36,55 @@ export class AckController {
switch (type) {
case AckEventType.READ: {
const validPayload = plainToInstance(AckReadPayloadDto, payload)
const errors = validateSync(validPayload, this.validateOptions)
const errors = validateSync(
validPayload,
ExtendedValidationPipe.options,
)
if (errors.length) {
const error = this.validate.createExceptionFactory()(errors as any[])
const error = this.validatePipe.createExceptionFactory()(
errors as any[],
)
throw error
}
const { id, type } = validPayload
if (uuidReq) {
const cacheKey = getRedisKey(RedisKeys.Read, `ack-${uuidReq}-${id}`)
const cacheValue = await this.cacheService.get(cacheKey)
if (cacheValue) {
return res.send()
}
await this.cacheService.set(cacheKey, '1', 12 * 60 * 60 * 1000)
// @ts-expect-error
await this.countingService.updateReadCount(type, id)
return res.send()
}
const cookieKey = `ack-read-${id}`
// @ts-expect-error
await this.countingService.updateReadCount(type, id)
if (cookies[cookieKey]) {
return res.send()
}
res.cookie(cookieKey, '1', {
maxAge:
// 12 hour
12 * 60 * 60 * 1000,
})
return res.send()
// disable ack read limit
// const validPayload = plainToInstance(AckReadPayloadDto, payload)
// const errors = validateSync(validPayload, this.validateOptions)
// if (errors.length) {
// const error = this.validate.createExceptionFactory()(errors as any[])
// throw error
// }
// const { id, type } = validPayload
// if (uuidReq) {
// const cacheKey = getRedisKey(RedisKeys.Read, `ack-${uuidReq}-${id}`)
// const cacheValue = await this.cacheService.get(cacheKey)
// if (cacheValue) {
// return res.send()
// }
// await this.cacheService.set(cacheKey, '1', 12 * 60 * 60 * 1000)
// // @ts-expect-error
// await this.countingService.updateReadCount(type, id)
// return res.send()
// }
// const cookieKey = `ack-read-${id}`
// // @ts-expect-error
// await this.countingService.updateReadCount(type, id)
// if (cookies[cookieKey]) {
// return res.send()
// }
// res.cookie(cookieKey, '1', {
// maxAge:
// // 12 hour
// 12 * 60 * 60 * 1000,
// })
}
}
}

View File

@@ -1,10 +1,19 @@
import { Module } from '@nestjs/common'
import { DiscoveryModule } from '@nestjs/core'
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
import { AckController } from './ack.controller'
@Module({
controllers: [AckController],
imports: [DiscoveryModule],
providers: [
{
provide: VALIDATION_PIPE_INJECTION,
useValue: ExtendedValidationPipe.shared,
},
],
})
export class AckModule {}

View File

@@ -30,7 +30,7 @@ export class ActivityController {
const { ip } = location
const { id, type } = body
await this.service.likeAndEmit(type, id, ip)
await this.service.likeAndEmit(type.toLowerCase() as any, id, ip)
return
}

View File

@@ -1,10 +1,10 @@
export interface ActivityLikePayload {
id: string
ip: string
type: 'Note' | 'Post'
type: ActivityLikeSupportType
}
export type ActivityLikeSupportType = 'Post' | 'Note'
export type ActivityLikeSupportType = 'post' | 'note'
export interface ActivityPresence {
operationTime: number

View File

@@ -12,7 +12,12 @@ import type { UpdatePresenceDto } from './dtos/presence.dto'
import { BadRequestException, Injectable, Logger } from '@nestjs/common'
import { ArticleTypeEnum } from '~/constants/article.constant'
import { BusinessEvents, EventScope } from '~/constants/business-event.constant'
import {
NOTE_COLLECTION_NAME,
POST_COLLECTION_NAME,
} from '~/constants/db.constant'
import { DatabaseService } from '~/processors/database/database.service'
import { GatewayService } from '~/processors/gateway/gateway.service'
import { WebEventsGateway } from '~/processors/gateway/web/events.gateway'
@@ -143,12 +148,12 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
const { type, id } = item.payload as ActivityLikePayload
switch (type) {
case 'Note': {
acc.Note.push(id)
case 'note': {
acc.note.push(id)
break
}
case 'Post': {
acc.Post.push(id)
case 'post': {
acc.post.push(id)
break
}
@@ -156,8 +161,8 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
return acc
},
{
Post: [],
Note: [],
post: [],
note: [],
} as Record<ActivityLikeSupportType, string[]>,
)
@@ -165,8 +170,8 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
ActivityLikeSupportType,
Collection<Document>
> = {
Note: this.databaseService.db.collection('notes'),
Post: this.databaseService.db.collection('posts'),
note: this.databaseService.db.collection(NOTE_COLLECTION_NAME),
post: this.databaseService.db.collection(POST_COLLECTION_NAME),
}
const refModelData = new Map<string, any>()
@@ -237,9 +242,17 @@ export class ActivityService implements OnModuleInit, OnModuleDestroy {
}
}
async likeAndEmit(type: ActivityLikeSupportType, id: string, ip: string) {
async likeAndEmit(type: 'post' | 'note', id: string, ip: string) {
try {
const res = await this.countingService.updateLikeCountWithIp(type, id, ip)
const mapping = {
post: ArticleTypeEnum.Post,
note: ArticleTypeEnum.Note,
}
const res = await this.countingService.updateLikeCountWithIp(
mapping[type],
id,
ip,
)
if (!res) {
throw new BadRequestException('你已经支持过啦!')
}

View File

@@ -5,6 +5,6 @@ import { MongoIdDto } from '~/shared/dto/id.dto'
import { ActivityLikeSupportType } from '../activity.interface'
export class LikeBodyDto extends MongoIdDto {
@IsEnum(['Post', 'Note'])
@IsEnum(['Post', 'Note', 'note', 'post'])
type: ActivityLikeSupportType
}

View File

@@ -100,16 +100,7 @@ export class CommentService implements OnModuleInit {
private getModelByRefType(
type: CollectionRefTypes,
): ReturnModelType<typeof WriteBaseModel> {
switch (type) {
case CollectionRefTypes.Note:
return this.databaseService.getModelByRefType('Note') as any
case CollectionRefTypes.Page:
return this.databaseService.getModelByRefType('Page') as any
case CollectionRefTypes.Post:
return this.databaseService.getModelByRefType('Post') as any
case CollectionRefTypes.Recently:
return this.databaseService.getModelByRefType('Recently') as any
}
return this.databaseService.getModelByRefType(type) as any
}
async checkSpam(doc: CommentModel) {

View File

@@ -1,11 +1,20 @@
import { Global, Module } from '@nestjs/common'
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
import { UserModule } from '../user/user.module'
import { ConfigsService } from './configs.service'
@Global()
@Module({
providers: [ConfigsService],
providers: [
ConfigsService,
{
provide: VALIDATION_PIPE_INJECTION,
useValue: ExtendedValidationPipe.shared,
},
],
imports: [UserModule],
exports: [ConfigsService],
})

View File

@@ -3,21 +3,22 @@ import { plainToInstance } from 'class-transformer'
import { validateSync } from 'class-validator'
import { cloneDeep, mergeWith } from 'lodash'
import type { ClassConstructor } from 'class-transformer'
import type { ValidatorOptions } from 'class-validator'
import { Clerk } from '@clerk/clerk-sdk-node'
import {
BadRequestException,
Inject,
Injectable,
Logger,
UnprocessableEntityException,
ValidationPipe,
} from '@nestjs/common'
import { ReturnModelType } from '@typegoose/typegoose'
import { ExtendedValidationPipe } from '~/common/pipes/validation.pipe'
import { EventScope } from '~/constants/business-event.constant'
import { RedisKeys } from '~/constants/cache.constant'
import { EventBusEvents } from '~/constants/event-bus.constant'
import { VALIDATION_PIPE_INJECTION } from '~/constants/system.constant'
import { EventManagerService } from '~/processors/helper/helper.event.service'
import { CacheService } from '~/processors/redis/cache.service'
import { SubPubBridgeService } from '~/processors/redis/subpub.service'
@@ -49,6 +50,9 @@ export class ConfigsService {
private readonly subpub: SubPubBridgeService,
private readonly eventManager: EventManagerService,
@Inject(VALIDATION_PIPE_INJECTION)
private readonly validate: ExtendedValidationPipe,
) {
this.configInit().then(() => {
this.logger.log('Config 已经加载完毕!')
@@ -182,12 +186,6 @@ export class ConfigsService {
return newData
}
private validateOptions: ValidatorOptions = {
whitelist: true,
forbidNonWhitelisted: true,
}
private validate = new ValidationPipe(this.validateOptions)
async patchAndValid<T extends keyof IConfig>(
key: T,
value: Partial<IConfig[T]>,
@@ -290,10 +288,11 @@ export class ConfigsService {
const validModel = plainToInstance(dto, value)
const errors = Array.isArray(validModel)
? (validModel as Array<any>).reduce(
(acc, item) => acc.concat(validateSync(item, this.validateOptions)),
(acc, item) =>
acc.concat(validateSync(item, ExtendedValidationPipe.options)),
[],
)
: validateSync(validModel, this.validateOptions)
: validateSync(validModel, ExtendedValidationPipe.options)
if (errors.length > 0) {
const error = this.validate.createExceptionFactory()(errors as any[])
throw error

View File

@@ -158,7 +158,7 @@ export class SearchService {
const tasks = search.hits.map((hit) => {
const { type, objectID } = hit
const model = this.databaseService.getModelByRefType(type as any)
const model = this.databaseService.getModelByRefType(type as 'post')
if (!model) {
return
}

View File

@@ -1,3 +1,6 @@
import type { ArticleTypeEnum } from '~/constants/article.constant'
import type { WriteBaseModel } from '~/shared/model/write-base.model'
import { Inject, Injectable } from '@nestjs/common'
import { mongoose, ReturnModelType } from '@typegoose/typegoose'
@@ -31,17 +34,20 @@ export class DatabaseService {
| ReturnModelType<typeof NoteModel>
| ReturnModelType<typeof PageModel>
| ReturnModelType<typeof RecentlyModel>
public getModelByRefType(type: 'Post'): ReturnModelType<typeof PostModel>
// @ts-ignore
public getModelByRefType(
type: ArticleTypeEnum,
): ReturnModelType<typeof WriteBaseModel>
public getModelByRefType(type: 'post'): ReturnModelType<typeof PostModel>
public getModelByRefType(
type: CollectionRefTypes.Post,
): ReturnModelType<typeof PostModel>
public getModelByRefType(type: 'Note'): ReturnModelType<typeof NoteModel>
public getModelByRefType(type: 'note'): ReturnModelType<typeof NoteModel>
public getModelByRefType(
type: CollectionRefTypes.Note,
): ReturnModelType<typeof NoteModel>
public getModelByRefType(type: 'Page'): ReturnModelType<typeof PageModel>
public getModelByRefType(type: 'page'): ReturnModelType<typeof PageModel>
public getModelByRefType(
type: CollectionRefTypes.Page,

View File

@@ -1,4 +1,4 @@
import type { ArticleType } from '~/constants/article.constant'
import type { ArticleTypeEnum } from '~/constants/article.constant'
import { Injectable, Logger } from '@nestjs/common'
@@ -30,40 +30,15 @@ export class CountingService {
return true
}
// public async updateReadCountWithIp(
// type: keyof typeof ArticleType,
// id: string,
// ip: string,
// ) {
// if (!this.checkIdAndIp(id, ip)) {
// return
// }
// const redis = this.redis.getClient()
// const isReadBefore = await redis.sismember(
// getRedisKey(RedisKeys.Read, id),
// ip,
// )
// if (isReadBefore) {
// this.logger.debug(`已经增加过计数了,${id}`)
// return
// }
// const doc = await this.updateReadCount(type, id)
// await redis.sadd(getRedisKey(RedisKeys.Read, doc.id), ip)
// this.logger.debug(`增加阅读计数,(${doc.title}`)
// }
public async updateLikeCountWithIp(
type: keyof typeof ArticleType,
type: ArticleTypeEnum,
id: string,
ip: string,
): Promise<boolean> {
const redis = this.redis.getClient()
const isLikeBefore = await this.getThisRecordIsLiked(id, ip)
const model = this.databaseService.getModelByRefType(type as any)
const model = this.databaseService.getModelByRefType(type)
const doc = await model.findById(id)
if (!doc) {
@@ -81,8 +56,9 @@ export class CountingService {
this.logger.debug(`增加喜欢计数,(${doc.title}`)
return true
}
public async updateReadCount(type: keyof typeof ArticleType, id: string) {
const model = this.databaseService.getModelByRefType(type as any)
public async updateReadCount(type: ArticleTypeEnum, id: string) {
const model = this.databaseService.getModelByRefType(type)
const doc = await model.findById(id)
if (!doc) throw ''

View File

@@ -33,7 +33,7 @@ export class ActivityController<ResponseWrapper> implements IController {
likeIt(type: 'Post' | 'Note', id: string) {
return this.proxy.like.post<never>({
data: {
type,
type: type.toLowerCase(),
id,
},
})