fix: cache interecptor
This commit is contained in:
25
.eslintrc.js
25
.eslintrc.js
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"semi": false
|
||||
}
|
||||
1
.prettierrc.js
Normal file
1
.prettierrc.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('@innei-util/prettier')
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -11,5 +11,5 @@
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ NestJS 8 发布了, 打算找个时间重构一下现有的后端.
|
||||
|
||||
实习不太顺利, 门槛好高.
|
||||
|
||||
这个项目先立个项吧.
|
||||
这个项目先立个项吧.
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {}
|
||||
}
|
||||
}
|
||||
|
||||
18
package.json
18
package.json
@@ -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
6757
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,3 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
"extends": ["config:base"]
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
103
src/common/interceptors/cache.interceptor.ts
Normal file
103
src/common/interceptors/cache.interceptor.ts
Normal 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;
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
2
src/constants/system.constant.ts
Normal file
2
src/constants/system.constant.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const HTTP_ADAPTER_HOST = 'HttpAdapterHost'
|
||||
export const REFLECTOR = 'Reflector'
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
1
src/processors/cache/cache.config.service.ts
vendored
1
src/processors/cache/cache.config.service.ts
vendored
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"~": [
|
||||
"./src"
|
||||
],
|
||||
"~/*": [
|
||||
"./src/*"
|
||||
]
|
||||
"~": ["./src"],
|
||||
"~/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user