From 8d730b58830a8a0e6be6bf0fe86b3021a2d473eb Mon Sep 17 00:00:00 2001 From: Naufil <145439464+Z-xus@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:53:51 +0530 Subject: [PATCH] chore(api): Add user cache for optimization (#386) --- apps/api/src/app/app.module.ts | 4 +- .../auth/controller/auth.controller.spec.ts | 17 +++- .../src/auth/guard/auth/auth.guard.spec.ts | 2 +- apps/api/src/auth/guard/auth/auth.guard.ts | 18 +++-- .../api/src/auth/service/auth.service.spec.ts | 17 +++- apps/api/src/auth/service/auth.service.ts | 6 +- apps/api/src/cache/cache.module.ts | 9 +++ apps/api/src/cache/cache.service.spec.ts | 34 ++++++++ apps/api/src/cache/cache.service.ts | 54 +++++++++++++ pnpm-lock.yaml | 79 +------------------ 10 files changed, 150 insertions(+), 90 deletions(-) create mode 100644 apps/api/src/cache/cache.module.ts create mode 100644 apps/api/src/cache/cache.service.spec.ts create mode 100644 apps/api/src/cache/cache.service.ts diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 2f492558..0bc84ee0 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -23,6 +23,7 @@ import { ScheduleModule } from '@nestjs/schedule' import { EnvSchema } from '../common/env/env.schema' import { IntegrationModule } from '../integration/integration.module' import { FeedbackModule } from '../feedback/feedback.module' +import { CacheModule } from '../cache/cache.module' @Module({ controllers: [AppController], @@ -53,7 +54,8 @@ import { FeedbackModule } from '../feedback/feedback.module' SocketModule, ProviderModule, IntegrationModule, - FeedbackModule + FeedbackModule, + CacheModule ], providers: [ { diff --git a/apps/api/src/auth/controller/auth.controller.spec.ts b/apps/api/src/auth/controller/auth.controller.spec.ts index d51abbd4..a8721683 100644 --- a/apps/api/src/auth/controller/auth.controller.spec.ts +++ b/apps/api/src/auth/controller/auth.controller.spec.ts @@ -10,6 +10,8 @@ import { ConfigService } from '@nestjs/config' import { GithubOAuthStrategyFactory } from '../../config/factory/github/github-strategy.factory' import { GoogleOAuthStrategyFactory } from '../../config/factory/google/google-strategy.factory' import { GitlabOAuthStrategyFactory } from '../../config/factory/gitlab/gitlab-strategy.factory' +import { CacheService } from '../../cache/cache.service' +import { REDIS_CLIENT } from '../../provider/redis.provider' describe('AuthController', () => { let controller: AuthController @@ -25,7 +27,20 @@ describe('AuthController', () => { ConfigService, { provide: MAIL_SERVICE, useClass: MockMailService }, JwtService, - PrismaService + PrismaService, + CacheService, + { + provide: REDIS_CLIENT, + useValue: { + publisher: { + setEx: jest.fn(), + set: jest.fn(), + get: jest.fn(), + del: jest.fn(), + keys: jest.fn() + } + } + } ] }) .overrideProvider(PrismaService) diff --git a/apps/api/src/auth/guard/auth/auth.guard.spec.ts b/apps/api/src/auth/guard/auth/auth.guard.spec.ts index 2da36533..c018193d 100644 --- a/apps/api/src/auth/guard/auth/auth.guard.spec.ts +++ b/apps/api/src/auth/guard/auth/auth.guard.spec.ts @@ -2,6 +2,6 @@ import { AuthGuard } from './auth.guard' describe('AuthGuard', () => { it('should be defined', () => { - expect(new AuthGuard(null, null, null)).toBeDefined() + expect(new AuthGuard(null, null, null, null)).toBeDefined() }) }) diff --git a/apps/api/src/auth/guard/auth/auth.guard.ts b/apps/api/src/auth/guard/auth/auth.guard.ts index ffdff70a..c0248c55 100644 --- a/apps/api/src/auth/guard/auth/auth.guard.ts +++ b/apps/api/src/auth/guard/auth/auth.guard.ts @@ -13,6 +13,7 @@ import { ONBOARDING_BYPASSED } from '../../../decorators/bypass-onboarding.decor import { AuthenticatedUserContext } from '../../auth.types' import { toSHA256 } from '../../../common/to-sha256' import { EnvSchema } from '../../../common/env/env.schema' +import { CacheService } from '../../../cache/cache.service' const X_E2E_USER_EMAIL = 'x-e2e-user-email' const X_KEYSHADE_TOKEN = 'x-keyshade-token' @@ -24,7 +25,8 @@ export class AuthGuard implements CanActivate { constructor( private readonly jwtService: JwtService, private readonly prisma: PrismaService, - private reflector: Reflector + private reflector: Reflector, + private cache: CacheService ) {} async canActivate(context: ExecutionContext): Promise { @@ -104,11 +106,15 @@ export class AuthGuard implements CanActivate { secret: process.env.JWT_SECRET }) - user = await this.prisma.user.findUnique({ - where: { - id: payload['id'] - } - }) + const cachedUser = await this.cache.getUser(payload['id']) + if (cachedUser) user = cachedUser + else { + user = await this.prisma.user.findUnique({ + where: { + id: payload['id'] + } + }) + } } catch { throw new ForbiddenException() } diff --git a/apps/api/src/auth/service/auth.service.spec.ts b/apps/api/src/auth/service/auth.service.spec.ts index 57c89764..34cd05dc 100644 --- a/apps/api/src/auth/service/auth.service.spec.ts +++ b/apps/api/src/auth/service/auth.service.spec.ts @@ -5,6 +5,8 @@ import { MAIL_SERVICE } from '../../mail/services/interface.service' import { JwtService } from '@nestjs/jwt' import { PrismaService } from '../../prisma/prisma.service' import { mockDeep } from 'jest-mock-extended' +import { CacheService } from '../../cache/cache.service' +import { REDIS_CLIENT } from '../../provider/redis.provider' describe('AuthService', () => { let service: AuthService @@ -15,7 +17,20 @@ describe('AuthService', () => { AuthService, { provide: MAIL_SERVICE, useClass: MockMailService }, JwtService, - PrismaService + PrismaService, + CacheService, + { + provide: REDIS_CLIENT, + useValue: { + publisher: { + setEx: jest.fn(), + set: jest.fn(), + get: jest.fn(), + del: jest.fn(), + keys: jest.fn() + } + } + } ] }) .overrideProvider(PrismaService) diff --git a/apps/api/src/auth/service/auth.service.ts b/apps/api/src/auth/service/auth.service.ts index 5dac0e89..1c82a738 100644 --- a/apps/api/src/auth/service/auth.service.ts +++ b/apps/api/src/auth/service/auth.service.ts @@ -18,6 +18,7 @@ import { PrismaService } from '../../prisma/prisma.service' import createUser from '../../common/create-user' import { AuthProvider } from '@prisma/client' import generateOtp from '../../common/generate-otp' +import { CacheService } from '../../cache/cache.service' @Injectable() export class AuthService { @@ -26,7 +27,8 @@ export class AuthService { constructor( @Inject(MAIL_SERVICE) private mailService: IMailService, private readonly prisma: PrismaService, - private jwt: JwtService + private jwt: JwtService, + private cache: CacheService ) { this.logger = new Logger(AuthService.name) } @@ -82,7 +84,7 @@ export class AuthService { } } }) - + this.cache.setUser(user) // Save user to cache this.logger.log(`User logged in: ${email}`) const token = await this.generateToken(user.id) diff --git a/apps/api/src/cache/cache.module.ts b/apps/api/src/cache/cache.module.ts new file mode 100644 index 00000000..c8dddd24 --- /dev/null +++ b/apps/api/src/cache/cache.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common' +import { CacheService } from './cache.service' + +@Global() +@Module({ + exports: [CacheService], + providers: [CacheService] +}) +export class CacheModule {} diff --git a/apps/api/src/cache/cache.service.spec.ts b/apps/api/src/cache/cache.service.spec.ts new file mode 100644 index 00000000..de3d09bd --- /dev/null +++ b/apps/api/src/cache/cache.service.spec.ts @@ -0,0 +1,34 @@ +import { Test, TestingModule } from '@nestjs/testing' +import { CacheService } from './cache.service' +import { REDIS_CLIENT } from '../provider/redis.provider' + +describe('CacheService', () => { + let service: CacheService + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CacheService, + { + provide: REDIS_CLIENT, + useValue: { + publisher: { + // Add minimal mock methods as needed + setEx: jest.fn(), + set: jest.fn(), + get: jest.fn(), + del: jest.fn(), + keys: jest.fn() + } + } + } + ] + }).compile() + + service = module.get(CacheService) + }) + + it('should be defined', () => { + expect(service).toBeDefined() + }) +}) diff --git a/apps/api/src/cache/cache.service.ts b/apps/api/src/cache/cache.service.ts new file mode 100644 index 00000000..d5ecced3 --- /dev/null +++ b/apps/api/src/cache/cache.service.ts @@ -0,0 +1,54 @@ +import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common' +import { RedisClientType } from 'redis' +import { User } from '@prisma/client' +import { REDIS_CLIENT } from '../provider/redis.provider' + +@Injectable() +export class CacheService implements OnModuleDestroy { + private static readonly USER_PREFIX = 'user-' + + constructor( + @Inject(REDIS_CLIENT) private redisClient: { publisher: RedisClientType } + ) {} + + private getUserKey(userId: string): string { + return `${CacheService.USER_PREFIX}${userId}` + } + + async setUser(user: User, expirationInSeconds?: number): Promise { + const key = this.getUserKey(user.id) + const userJson = JSON.stringify(user) + if (expirationInSeconds) { + await this.redisClient.publisher.setEx(key, expirationInSeconds, userJson) + } else { + await this.redisClient.publisher.set(key, userJson) + } + } + + async getUser(userId: string): Promise { + const key = this.getUserKey(userId) + const userData = await this.redisClient.publisher.get(key) + if (userData) { + return JSON.parse(userData) as User + } + return null + } + + async deleteUser(userId: string): Promise { + const key = this.getUserKey(userId) + return await this.redisClient.publisher.del(key) + } + + async clearAllUserCache(): Promise { + const keys = await this.redisClient.publisher.keys( + `${CacheService.USER_PREFIX}*` + ) + if (keys.length > 0) { + await this.redisClient.publisher.del(keys) + } + } + + async onModuleDestroy() { + await this.redisClient.publisher.quit() + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b44b358..5603dba6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -471,9 +471,6 @@ importers: tsconfig: specifier: workspace:* version: link:../../packages/tsconfig - typescript: - specifier: ^4.5.3 - version: 4.9.5 apps/web: dependencies: @@ -11349,17 +11346,6 @@ snapshots: - supports-color - typescript - '@svgr/core@8.1.0(typescript@5.5.3)': - dependencies: - '@babel/core': 7.24.7 - '@svgr/babel-preset': 8.1.0(@babel/core@7.24.7) - camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.5.3) - snake-case: 3.0.4 - transitivePeerDependencies: - - supports-color - - typescript - '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: '@babel/types': 7.24.7 @@ -11375,7 +11361,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@4.9.5))(typescript@4.9.5)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.5.3))(typescript@5.5.3)': dependencies: '@svgr/core': 8.1.0(typescript@5.5.3) cosmiconfig: 8.3.6(typescript@5.5.3) @@ -11384,20 +11370,6 @@ snapshots: transitivePeerDependencies: - typescript - '@svgr/webpack@8.1.0(typescript@4.9.5)': - dependencies: - '@babel/core': 7.24.7 - '@babel/plugin-transform-react-constant-elements': 7.24.7(@babel/core@7.24.7) - '@babel/preset-env': 7.24.7(@babel/core@7.24.7) - '@babel/preset-react': 7.24.7(@babel/core@7.24.7) - '@babel/preset-typescript': 7.24.7(@babel/core@7.24.7) - '@svgr/core': 8.1.0(typescript@5.5.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.5.3)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.5.3))(typescript@5.5.3) - transitivePeerDependencies: - - supports-color - - typescript - '@svgr/webpack@8.1.0(typescript@5.5.3)': dependencies: '@babel/core': 7.24.7 @@ -11493,11 +11465,6 @@ snapshots: mini-svg-data-uri: 1.4.4 tailwindcss: 3.4.6(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3)) - '@tailwindcss/forms@0.5.7(tailwindcss@3.4.6(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3)))': - dependencies: - mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.6(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3)) - '@tanstack/react-table@8.19.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/table-core': 8.19.3 @@ -13073,15 +13040,6 @@ snapshots: optionalDependencies: typescript: 5.5.3 - cosmiconfig@8.3.6(typescript@5.5.3): - dependencies: - import-fresh: 3.3.0 - js-yaml: 4.1.0 - parse-json: 5.2.0 - path-type: 4.0.0 - optionalDependencies: - typescript: 5.5.3 - cosmiconfig@9.0.0(typescript@5.5.3): dependencies: env-paths: 2.2.1 @@ -16648,14 +16606,6 @@ snapshots: postcss: 8.4.39 ts-node: 10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3) - postcss-load-config@4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3)): - dependencies: - lilconfig: 3.1.2 - yaml: 2.4.5 - optionalDependencies: - postcss: 8.4.39 - ts-node: 10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3) - postcss-nested@6.0.1(postcss@8.4.39): dependencies: postcss: 8.4.39 @@ -17693,33 +17643,6 @@ snapshots: transitivePeerDependencies: - ts-node - tailwindcss@3.4.6(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3)): - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.2 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.6 - lilconfig: 2.1.0 - micromatch: 4.0.7 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.0.1 - postcss: 8.4.39 - postcss-import: 15.1.0(postcss@8.4.39) - postcss-js: 4.0.1(postcss@8.4.39) - postcss-load-config: 4.0.2(postcss@8.4.39)(ts-node@10.9.2(@swc/core@1.7.0)(@types/node@20.14.11)(typescript@5.5.3)) - postcss-nested: 6.0.1(postcss@8.4.39) - postcss-selector-parser: 6.1.1 - resolve: 1.22.8 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node - tapable@2.2.1: {} temp-dir@3.0.0: {}