Skip to content

Commit

Permalink
chore(api): Add user cache for optimization (#386)
Browse files Browse the repository at this point in the history
  • Loading branch information
Z-xus authored and rajdip-b committed Jul 29, 2024
1 parent be6aabf commit 8d730b5
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 90 deletions.
4 changes: 3 additions & 1 deletion apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -53,7 +54,8 @@ import { FeedbackModule } from '../feedback/feedback.module'
SocketModule,
ProviderModule,
IntegrationModule,
FeedbackModule
FeedbackModule,
CacheModule
],
providers: [
{
Expand Down
17 changes: 16 additions & 1 deletion apps/api/src/auth/controller/auth.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/auth/guard/auth/auth.guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
18 changes: 12 additions & 6 deletions apps/api/src/auth/guard/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<boolean> {
Expand Down Expand Up @@ -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()
}
Expand Down
17 changes: 16 additions & 1 deletion apps/api/src/auth/service/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/auth/service/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/cache/cache.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common'
import { CacheService } from './cache.service'

@Global()
@Module({
exports: [CacheService],
providers: [CacheService]
})
export class CacheModule {}
34 changes: 34 additions & 0 deletions apps/api/src/cache/cache.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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>(CacheService)
})

it('should be defined', () => {
expect(service).toBeDefined()
})
})
54 changes: 54 additions & 0 deletions apps/api/src/cache/cache.service.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<User | null> {
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<number> {
const key = this.getUserKey(userId)
return await this.redisClient.publisher.del(key)
}

async clearAllUserCache(): Promise<void> {
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()
}
}
79 changes: 1 addition & 78 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8d730b5

Please sign in to comment.