fix: cache interecptor

This commit is contained in:
Innei
2021-08-31 14:30:59 +08:00
parent eaf4a6bede
commit c4ab247ca6
20 changed files with 6917 additions and 82 deletions

View File

@@ -1,24 +1,3 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
extends: ['@innei-util/eslint-config-ts'],
}

View File

@@ -1,5 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all",
"semi": false
}

1
.prettierrc.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require('@innei-util/prettier')

View File

@@ -11,5 +11,5 @@
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
}
}
}

View File

@@ -6,4 +6,4 @@ NestJS 8 发布了, 打算找个时间重构一下现有的后端.
实习不太顺利, 门槛好高.
这个项目先立个项吧.
这个项目先立个项吧.

View File

@@ -2,4 +2,4 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {}
}
}

View File

@@ -47,7 +47,7 @@
"@nestjs/swagger": "^5.0.9",
"@nestjs/websockets": "^8.0.6",
"@typegoose/auto-increment": "^0.9.0",
"@typegoose/typegoose": "8.1.1",
"@typegoose/typegoose": "8.2.0",
"@types/bcrypt": "^5.0.0",
"argv": "^0.0.2",
"bcrypt": "^5.0.1",
@@ -58,12 +58,10 @@
"class-validator": "^0.13.1",
"dayjs": "^1.10.6",
"fastify-swagger": "^4.9.0",
"ioredis": "4.27.8",
"lodash": "^4.17.21",
"mongoose": "^5.13.7",
"mongoose-lean-virtuals": "^0.8.0",
"nanoid": "^3.1.25",
"nest-winston": "1.5.0",
"nestjs-typegoose": "^7.1.38",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
@@ -71,14 +69,14 @@
"redis": "3.1.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.3.0",
"snakecase-keys": "^4.0.2",
"winston": "3.3.3"
"snakecase-keys": "^4.0.2"
},
"devDependencies": {
"@innei-util/eslint-config-ts": "^0.2.3",
"@innei-util/prettier": "^0.1.3",
"@nestjs/cli": "^8.1.1",
"@nestjs/schematics": "^8.0.2",
"@nestjs/testing": "^8.0.6",
"@types/ioredis": "4.26.7",
"@types/jest": "^27.0.1",
"@types/lodash": "^4.14.172",
"@types/node": "^16.7.1",
@@ -87,14 +85,10 @@
"@types/redis": "2.8.31",
"@types/socket.io": "3.0.2",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "4.29.2",
"@typescript-eslint/parser": "4.29.2",
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"husky": "^7.0.1",
"jest": "27.0.6",
"jest": "27.1.0",
"lint-staged": "^11.1.2",
"prettier": "^2.3.2",
"rimraf": "^3.0.2",
@@ -108,4 +102,4 @@
"webpack": "^5.51.1",
"webpack-node-externals": "^3.0.0"
}
}
}

6757
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
{
"extends": [
"config:base"
]
"extends": ["config:base"]
}

View File

@@ -1,4 +1,5 @@
import argv from 'argv'
import { isDev } from './utils'
export const CROSS_DOMAIN = {
allowedOrigins: [
'innei.ren',
@@ -24,5 +25,8 @@ export const REDIS = {
port: argv.redis_port || 6379,
password: (argv.redis_password || null) as string,
ttl: null,
defaultCacheTTL: 60 * 60 * 24,
httpCacheTTL: 5,
max: 5,
disableApiCache:
(isDev || argv.disableCache) && !process.env['ENABLE_CACHE_DEBUG'],
}

View File

@@ -1,6 +1,8 @@
import { Module } from '@nestjs/common'
import { CacheInterceptor, Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_INTERCEPTOR } from '@nestjs/core'
import { AppController } from './app.controller'
import { HttpCacheInterceptor } from './common/interceptors/cache.interceptor'
import { AuthModule } from './modules/auth/auth.module'
// must after post
import { CategoryModule } from './modules/category/category.module'
@@ -38,5 +40,11 @@ import { HelperModule } from './processors/helper/helper.module'
HelperModule,
],
controllers: [AppController],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: HttpCacheInterceptor,
},
],
})
export class AppModule {}

View File

@@ -6,35 +6,36 @@
*/
import lodash from 'lodash'
import { SetMetadata, CacheKey } from '@nestjs/common'
import { SetMetadata, CacheKey, CacheTTL } from '@nestjs/common'
import * as META from '~/constants/meta.constant'
// 缓存器配置
interface ICacheOption {
ttl?: number
key?: string
disable?: boolean
}
/**
* 统配构造器
* @function HttpCache
* @description 两种用法
* @example @HttpCache(CACHE_KEY, 60 * 60)
* @example @HttpCache({ key: CACHE_KEY, ttl: 60 * 60 })
* @example @HttpCache({ disable: true })
*/
export function HttpCache(option: ICacheOption): MethodDecorator
export function HttpCache(key: string, ttl?: number): MethodDecorator
export function HttpCache(...args) {
const option = args[0]
const isOption = (value): value is ICacheOption => lodash.isObject(value)
const key: string = isOption(option) ? option.key : option
const ttl: number = isOption(option) ? option.ttl : args[1] || null
export function HttpCache(option: ICacheOption): MethodDecorator {
const { disable, key, ttl = 60 } = option
return (_, __, descriptor: PropertyDescriptor) => {
if (disable) {
SetMetadata(META.HTTP_CACHE_DISABLE, true)(descriptor.value)
return descriptor
}
if (key) {
CacheKey(key)(descriptor.value)
}
if (ttl) {
SetMetadata(META.HTTP_CACHE_TTL_METADATA, ttl)(descriptor.value)
if (typeof ttl === 'number' && !isNaN(ttl)) {
CacheTTL(ttl)(descriptor.value)
}
return descriptor
}

View File

@@ -0,0 +1,103 @@
/**
* HttpCache interceptor.
* @file 缓存拦截器
* @module interceptor/cache
* @author Surmon <https://github.com/surmon-china>
*/
import { tap } from 'rxjs/operators'
import { Observable, of } from 'rxjs'
import { Reflector } from '@nestjs/core'
import {
HttpAdapterHost,
NestInterceptor,
ExecutionContext,
CallHandler,
Inject,
Injectable,
RequestMethod,
} from '@nestjs/common'
import { CacheService } from '~/processors/cache/cache.service'
import * as SYSTEM from '~/constants/system.constant'
import * as META from '~/constants/meta.constant'
import { REDIS } from '~/app.config'
import { IncomingMessage } from 'http'
/**
* @class HttpCacheInterceptor
* @classdesc 弥补框架不支持单独定义 ttl 参数以及单请求应用的缺陷
*/
@Injectable()
export class HttpCacheInterceptor implements NestInterceptor {
constructor(
private readonly cacheManager: CacheService,
@Inject(SYSTEM.REFLECTOR) private readonly reflector: Reflector,
@Inject(SYSTEM.HTTP_ADAPTER_HOST)
private readonly httpAdapterHost: HttpAdapterHost,
) {}
// 自定义装饰器,修饰 ttl 参数
async intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Promise<Observable<any>> {
// 如果想彻底禁用缓存服务,则直接返回 -> return call$;
const call$ = next.handle()
if (REDIS.disableApiCache) {
return call$
}
const handler = context.getHandler()
const isDisableCache = this.reflector.get(META.HTTP_CACHE_DISABLE, handler)
const key =
this.trackBy(context) ||
`api-cache:${
(context.switchToHttp().getRequest() as IncomingMessage).url
}`
if (!key || isDisableCache) {
return call$
}
const target = context.getHandler()
const metaTTL = this.reflector.get(META.HTTP_CACHE_TTL_METADATA, target)
const ttl = metaTTL || REDIS.httpCacheTTL
try {
const value = await this.cacheManager.get(key)
return value
? of(value)
: call$.pipe(
tap((response) => this.cacheManager.set(key, response, { ttl })),
)
} catch (error) {
return call$
}
}
/**
* @function trackBy
* @description 目前的命中规则是:必须手动设置了 CacheKey 才会启用缓存机制,默认 ttl 为 APP_CONFIG.REDIS.defaultCacheTTL
*/
trackBy(context: ExecutionContext): string | undefined {
const request = context.switchToHttp().getRequest()
const httpServer = this.httpAdapterHost.httpAdapter
const isHttpApp = Boolean(httpServer?.getRequestMethod)
const isGetRequest =
isHttpApp &&
httpServer.getRequestMethod(request) === RequestMethod[RequestMethod.GET]
const cacheKey = this.reflector.get(
META.HTTP_CACHE_KEY_METADATA,
context.getHandler(),
)
const isMatchedCache = isHttpApp && isGetRequest && cacheKey
// const requestUrl = httpServer.getRequestUrl(request);
// console.log('isMatchedCache', isMatchedCache, 'requestUrl', requestUrl, 'cacheKey', cacheKey);
// 缓存命中策略 -> http -> GET -> cachekey -> url -> undefined
return isMatchedCache ? cacheKey : undefined
/*
return undefined;
return isMatchedCache ? requestUrl : undefined;
return isMatchedCache ? (cacheKey || requestUrl) : undefined;
*/
}
}

View File

@@ -1,4 +1,9 @@
import { CACHE_KEY_METADATA } from '@nestjs/common/cache/cache.constants'
import {
CACHE_KEY_METADATA,
CACHE_TTL_METADATA,
} from '@nestjs/common/cache/cache.constants'
export const HTTP_CACHE_KEY_METADATA = CACHE_KEY_METADATA
export const HTTP_CACHE_TTL_METADATA = '__customHttpCacheTTL__'
export const HTTP_CACHE_TTL_METADATA = CACHE_TTL_METADATA
export const HTTP_CACHE_DISABLE = 'cache_module:cache_disable'

View File

@@ -0,0 +1,2 @@
export const HTTP_ADAPTER_HOST = 'HttpAdapterHost'
export const REFLECTOR = 'Reflector'

View File

@@ -3,7 +3,7 @@ import { AppModule } from './app.module'
import { NestFastifyApplication } from '@nestjs/platform-fastify'
import { fastifyApp } from './common/adapt/fastify'
import { isDev } from './utils'
import { Logger } from '@nestjs/common'
import { CacheInterceptor, Logger } from '@nestjs/common'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
import { CROSS_DOMAIN } from './app.config'
// const PORT = parseInt(process.env.PORT) || 2333

View File

@@ -42,7 +42,7 @@ export class AuthService {
return (await this.getAccessTokens()).map((token) => ({
id: token._id,
...omit(token, ['_id', '__v', 'token']),
}))
})) as any as TokenModel[]
}
async getTokenSecret(id: string) {

View File

@@ -1,15 +1,6 @@
import { CacheKey, CacheTTL, Controller, Get } from '@nestjs/common'
import { Controller } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { HttpCache } from '~/common/decorator/cache.decorator'
@ApiTags('User Routes')
@Controller(['user', 'master'])
export class UserController {
@Get('/')
@CacheKey('a')
@CacheTTL(300)
// FIXME not working
ping() {
return 'pong'
}
}
export class UserController {}

View File

@@ -31,6 +31,7 @@ export class CacheConfigService implements CacheOptionsFactory {
// https://github.com/dabroek/node-cache-manager-redis-store/blob/master/CHANGELOG.md#breaking-changes
// Any value (undefined | null) return true (cacheable) after redisStore v2.0.0
is_cacheable_value: () => true,
max: REDIS.max,
...redisOptions,
}
}

View File

@@ -16,12 +16,8 @@
"resolveJsonModule": true,
"skipLibCheck": true,
"paths": {
"~": [
"./src"
],
"~/*": [
"./src/*"
]
"~": ["./src"],
"~/*": ["./src/*"]
}
}
}
}