From d03e502f2f14faaaa2894b853f3308a34ef5f383 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 23 May 2025 14:55:22 +0700 Subject: [PATCH] 23: cache --- package-lock.json | 86 +++++++++++++++++++++++++++-- package.json | 2 + src/posts/httpCache.interceptor.ts | 19 +++++++ src/posts/posts.controller.ts | 6 +- src/posts/posts.module.ts | 12 +++- src/posts/posts.service.ts | 25 ++++++++- src/posts/postsCacheKey.constant.ts | 1 + 7 files changed, 142 insertions(+), 9 deletions(-) create mode 100644 src/posts/httpCache.interceptor.ts create mode 100644 src/posts/postsCacheKey.constant.ts diff --git a/package-lock.json b/package-lock.json index faa3f06..a15641f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@elastic/elasticsearch": "^9.0.2", "@hapi/joi": "^17.1.1", "@neondatabase/serverless": "^1.0.0", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -29,6 +30,7 @@ "@types/uuid": "^10.0.0", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", + "cache-manager": "^6.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", @@ -2008,6 +2010,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", + "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -2407,6 +2442,19 @@ "node": ">=19.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.1.tgz", + "integrity": "sha512-4UxTnR0fsmKL5YDalU2eLFVnL+OBebWUpX+hEduKGncrVKH4PPNoiRn1kXyOCjmzb0UvWgqubpssNouc8e0MCw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0", + "cache-manager": ">=6", + "keyv": ">=5", + "rxjs": "^7.8.1" + } + }, "node_modules/@nestjs/cli": { "version": "11.0.7", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.7.tgz", @@ -5818,6 +5866,15 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.3.tgz", + "integrity": "sha512-VV5eq/QQ5rIVix7/aICO4JyvSeEv9eIQuKL5iFwgM2BrcYoE0A/D1mNsAHJAsB0WEbNdBlKkn6Tjz6fKzh/cKQ==", + "license": "MIT", + "dependencies": { + "keyv": "^5.3.3" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -5847,6 +5904,16 @@ "node": ">=14.16" } }, + "node_modules/cacheable-request/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -7664,6 +7731,16 @@ "node": ">=16" } }, + "node_modules/flat-cache/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/flatbuffers": { "version": "24.12.23", "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-24.12.23.tgz", @@ -9613,13 +9690,12 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.3.3.tgz", + "integrity": "sha512-Rwu4+nXI9fqcxiEHtbkvoes2X+QfkTRo1TMkPfwzipGsJlJO/z69vqB4FNl9xJ3xCpAcbkvmEabZfPzrwN3+gQ==", "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.0.3" } }, "node_modules/kind-of": { diff --git a/package.json b/package.json index ad190cf..f205ba9 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@elastic/elasticsearch": "^9.0.2", "@hapi/joi": "^17.1.1", "@neondatabase/serverless": "^1.0.0", + "@nestjs/cache-manager": "^3.0.1", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", @@ -40,6 +41,7 @@ "@types/uuid": "^10.0.0", "aws-sdk": "^2.1692.0", "bcrypt": "^5.1.1", + "cache-manager": "^6.4.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "cookie-parser": "^1.4.7", diff --git a/src/posts/httpCache.interceptor.ts b/src/posts/httpCache.interceptor.ts new file mode 100644 index 0000000..4f77122 --- /dev/null +++ b/src/posts/httpCache.interceptor.ts @@ -0,0 +1,19 @@ +import { CACHE_KEY_METADATA, CacheInterceptor, } from '@nestjs/cache-manager'; +import {ExecutionContext, Injectable} from "@nestjs/common"; + +@Injectable() +export class HttpCacheInterceptor extends CacheInterceptor { + trackBy(context: ExecutionContext): string | undefined { + const cacheKey = this.reflector.get( + CACHE_KEY_METADATA, + context.getHandler(), + ); + + if (cacheKey) { + const request = context.switchToHttp().getRequest(); + return `${cacheKey}-${request._parsedUrl.query}`; + } + + return super.trackBy(context); + } +} \ No newline at end of file diff --git a/src/posts/posts.controller.ts b/src/posts/posts.controller.ts index dfd0c83..1e5ce07 100644 --- a/src/posts/posts.controller.ts +++ b/src/posts/posts.controller.ts @@ -17,6 +17,8 @@ import {UpdatePostDto} from './dto/update-post.dto'; import JwtAuthenticationGuard from "../authentication/jwt-authentication.guard"; import RequestWithUser from "../authentication/requestWithUser.interface"; import {PaginationParams} from "../utils/types/paginationParams"; +import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'; +import {GET_POSTS_CACHE_KEY} from "./postsCacheKey.constant"; @Controller('posts') @UseInterceptors(ClassSerializerInterceptor) @@ -30,7 +32,9 @@ export class PostsController { return this.postsService.createPost(post, req.user); } - + @UseInterceptors(CacheInterceptor) + @CacheKey(GET_POSTS_CACHE_KEY) + @CacheTTL(120) @Get() async getPosts( @Query('search') search: string, diff --git a/src/posts/posts.module.ts b/src/posts/posts.module.ts index 8f5c6ad..c7dfae3 100644 --- a/src/posts/posts.module.ts +++ b/src/posts/posts.module.ts @@ -6,10 +6,20 @@ import User from "../users/entities/user.entity"; import Post from "./entities/post.entity"; import PostsSearchService from "./postsSearch.service"; import {SearchModule} from "../search/search.module"; +import { CacheModule } from '@nestjs/cache-manager'; @Module({ controllers: [PostsController], - imports: [TypeOrmModule.forFeature([Post]), SearchModule], + imports: [ + CacheModule.register( + { + ttl: 5, + max: 100 + } + ), + TypeOrmModule.forFeature([Post]), + SearchModule + ], providers: [PostsService, PostsSearchService], exports: [PostsService, PostsSearchService], }) diff --git a/src/posts/posts.service.ts b/src/posts/posts.service.ts index 4d787b3..5b1dc39 100644 --- a/src/posts/posts.service.ts +++ b/src/posts/posts.service.ts @@ -1,4 +1,4 @@ -import {Injectable} from '@nestjs/common'; +import {Inject, Injectable} from '@nestjs/common'; import {CreatePostDto} from './dto/create-post.dto'; import {UpdatePostDto} from './dto/update-post.dto'; import User from "../users/entities/user.entity"; @@ -8,22 +8,39 @@ import Post from './entities/post.entity'; import {PostNotFoundException} from "./exception/postNotFound.exception"; import PostsSearchService from "./postsSearch.service"; import {In} from "typeorm"; +import { Cache } from 'cache-manager'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import {GET_POSTS_CACHE_KEY} from "./postsCacheKey.constant"; @Injectable() export class PostsService { constructor( @InjectRepository(Post) private repo: Repository, - private postsSearchService: PostsSearchService + private postsSearchService: PostsSearchService, + @Inject(CACHE_MANAGER) private cacheManager: Cache ) { } + + async clearCache() { + // const keys: string[] = this.cacheManager.stores.keys(); + // + // await Promise.all( + // keys + // .filter((key) => key.startsWith(GET_POSTS_CACHE_KEY)) + // .map((key) => this.cacheManager.del(key)) // note: 'del', not 'delete' + // ); + } + async createPost(post: CreatePostDto, user: User) { const newPost = this.repo.create({ ...post, author: user }); await this.repo.save(newPost); + await this.postsSearchService.indexPost(newPost); + await this.clearCache(); return newPost; } @@ -91,6 +108,8 @@ export class PostsService { }); if (updatedPost) { + await this.postsSearchService.update(updatedPost); + await this.clearCache(); return updatedPost } throw new PostNotFoundException(id); @@ -114,6 +133,7 @@ export class PostsService { }); if (updatedPost) { await this.postsSearchService.update(updatedPost); + await this.clearCache(); return updatedPost; } throw new PostNotFoundException(id); @@ -125,5 +145,6 @@ export class PostsService { throw new PostNotFoundException(id); } await this.postsSearchService.remove(id); + await this.clearCache(); } } diff --git a/src/posts/postsCacheKey.constant.ts b/src/posts/postsCacheKey.constant.ts new file mode 100644 index 0000000..78140f6 --- /dev/null +++ b/src/posts/postsCacheKey.constant.ts @@ -0,0 +1 @@ +export const GET_POSTS_CACHE_KEY = 'GET_POSTS_CACHE'; \ No newline at end of file