Skip to content

Commit cf0e251

Browse files
committed
feat(caching): add redis
1 parent 07679bd commit cf0e251

File tree

11 files changed

+199
-28
lines changed

11 files changed

+199
-28
lines changed

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ CLOUDINARY_API_KEY=
22
CLOUDINARY_API_SECRET=
33
CLOUDINARY_CLOUD_NAME=
44
DATABASE_URL=
5+
REDIS_USERNAME=
6+
REDIS_PASSWORD=
7+
REDIS_HOST=
8+
REDIS_PORT=
59
GOOGLE_CLIENT_ID=
610
GOOGLE_CLIENT_SECRET=
711
PORT=

app/config/env.config.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@ const envSchema = z.object({
1111
DATABASE_URL: z.string(),
1212
GOOGLE_CLIENT_ID: z.string(),
1313
GOOGLE_CLIENT_SECRET: z.string(),
14+
REDIS_USERNAME: z.string(),
15+
REDIS_PASSWORD: z.string(),
16+
REDIS_HOST: z.string(),
17+
REDIS_PORT: z.preprocess(
18+
v => (v ? v : undefined),
19+
z.coerce.number().int().positive()
20+
),
1421
ACCESS_JWT_SECRET: z.string().transform(v => new TextEncoder().encode(v)),
1522
REFRESH_JWT_SECRET: z.string().transform(v => new TextEncoder().encode(v)),
16-
PORT: z.preprocess(v => (v ? v : undefined), z.coerce.number().int()),
23+
PORT: z.preprocess(
24+
v => (v ? v : undefined),
25+
z.coerce.number().int().positive()
26+
),
1727
API_PREFIX: z.string(),
1828
NODE_ENV: z.enum(['development', 'production']),
1929
ALLOWED_ORIGINS: z

app/config/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { transport } from './mailer.config'
22
export { default } from './cloudinary.config'
33
export { env } from './env.config'
4+
export { redis } from './redis.config'

app/config/redis.config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Redis from 'ioredis'
2+
3+
import { env } from './env.config'
4+
5+
export const redis = new Redis({
6+
username: env.REDIS_USERNAME,
7+
password: env.REDIS_PASSWORD,
8+
host: env.REDIS_HOST,
9+
port: env.REDIS_PORT
10+
})

app/controllers/auth.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ class AuthController {
170170
logout = async ({ session }: Request, res: Response) => {
171171
await prisma.session.delete({ where: { id: session } })
172172

173-
res.status(204).send()
173+
res.sendStatus(204)
174174
}
175175

176176
private getNewTokens = async (payload: JwtPayload) => {

app/controllers/board.controller.ts

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,54 @@ import type { NextFunction, Request, Response } from 'express'
1313
import { prisma } from '@/prisma'
1414
import { NotFound } from 'http-errors'
1515

16+
import { redis } from '@/config'
1617
import boardImages from '@/data/board-bg-images.json'
1718

1819
class BoardController {
1920
getAll = async ({ user }: Request, res: Response) => {
20-
const boards = await prisma.board.findMany({ where: { userId: user.id } })
21+
const cacheKey = `boards:user:${user.id}:all`
2122

22-
res.json(boards)
23+
const cachedBoards = await redis.get(cacheKey)
24+
25+
if (cachedBoards) {
26+
res.json(JSON.parse(cachedBoards))
27+
} else {
28+
const boards = await prisma.board.findMany({ where: { userId: user.id } })
29+
30+
await redis.set(cacheKey, JSON.stringify(boards))
31+
32+
res.json(boards)
33+
}
2334
}
2435

2536
getById = async (
2637
{ user, params }: TypedRequestParams<typeof BoardParamsSchema>,
2738
res: Response,
2839
next: NextFunction
2940
) => {
30-
const board = await prisma.board.findFirst({
31-
where: { id: params.boardId, userId: user.id },
32-
include: {
33-
columns: {
34-
orderBy: { order: 'asc' },
35-
include: { cards: { orderBy: { order: 'asc' } } }
41+
const cacheKey = `board:${params.boardId}:user:${user.id}`
42+
43+
const cachedBoard = await redis.get(cacheKey)
44+
45+
if (cachedBoard) {
46+
res.json(JSON.parse(cachedBoard))
47+
} else {
48+
const board = await prisma.board.findFirst({
49+
where: { id: params.boardId, userId: user.id },
50+
include: {
51+
columns: {
52+
orderBy: { order: 'asc' },
53+
include: { cards: { orderBy: { order: 'asc' } } }
54+
}
3655
}
37-
}
38-
})
56+
})
57+
58+
if (!board) return next(NotFound('Board not found'))
3959

40-
if (!board) return next(NotFound('Board not found'))
60+
await redis.set(cacheKey, JSON.stringify(board))
4161

42-
res.json(board)
62+
res.json(board)
63+
}
4364
}
4465

4566
add = async (
@@ -54,6 +75,8 @@ class BoardController {
5475
}
5576
})
5677

78+
await redis.del(`boards:user:${user.id}:all`)
79+
5780
res.json(newBoard)
5881
}
5982

@@ -76,6 +99,9 @@ class BoardController {
7699

77100
if (!updatedBoard) return next(NotFound('Board not found'))
78101

102+
await redis.del(`board:${updatedBoard.id}:user:${user.id}`)
103+
await redis.del(`boards:user:${user.id}:all`)
104+
79105
res.json(updatedBoard)
80106
}
81107

@@ -90,7 +116,10 @@ class BoardController {
90116

91117
if (!deletedBoard) return next(NotFound('Board not found'))
92118

93-
res.status(204).send()
119+
await redis.del(`board:${deletedBoard.id}:user:${user.id}`)
120+
await redis.del(`boards:user:${user.id}:all`)
121+
122+
res.sendStatus(204)
94123
}
95124
}
96125

app/controllers/card.controller.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type { NextFunction, Response } from 'express'
1212
import { prisma } from '@/prisma'
1313
import { BadRequest, NotFound } from 'http-errors'
1414

15+
import { redis } from '@/config'
16+
1517
class CardController {
1618
add = async (
1719
{
@@ -22,7 +24,8 @@ class CardController {
2224
next: NextFunction
2325
) => {
2426
const column = await prisma.column.findFirst({
25-
where: { id: params.columnId }
27+
where: { id: params.columnId },
28+
select: { id: true, boardId: true, board: { select: { userId: true } } }
2629
})
2730

2831
if (!column) return next(NotFound('Column not found'))
@@ -33,6 +36,8 @@ class CardController {
3336
data: { ...body, columnId: column.id, order: newOrder }
3437
})
3538

39+
await redis.del(`board:${column.boardId}:user:${column.board.userId}`)
40+
3641
res.json(newCard)
3742
}
3843

@@ -46,11 +51,20 @@ class CardController {
4651
) => {
4752
const updatedCard = await prisma.card.updateIgnoreNotFound({
4853
where: { id: params.cardId },
49-
data: body
54+
data: body,
55+
select: {
56+
column: {
57+
select: { boardId: true, board: { select: { userId: true } } }
58+
}
59+
}
5060
})
5161

5262
if (!updatedCard) return next(NotFound('Card not found'))
5363

64+
await redis.del(
65+
`board:${updatedCard.column.boardId}:user:${updatedCard.column.board.userId}`
66+
)
67+
5468
res.json(updatedCard)
5569
}
5670

@@ -63,7 +77,8 @@ class CardController {
6377
next: NextFunction
6478
) => {
6579
const column = await prisma.column.findFirst({
66-
where: { id: params.columnId }
80+
where: { id: params.columnId },
81+
select: { id: true, boardId: true, board: { select: { userId: true } } }
6782
})
6883

6984
if (!column) return next(NotFound('Column not found'))
@@ -77,6 +92,9 @@ class CardController {
7792

7893
try {
7994
const updatedCards = await prisma.$transaction(transaction)
95+
96+
await redis.del(`board:${column.boardId}:user:${column.board.userId}`)
97+
8098
res.json(updatedCards)
8199
} catch {
82100
return next(BadRequest('Invalid order'))
@@ -89,7 +107,8 @@ class CardController {
89107
next: NextFunction
90108
) => {
91109
const column = await prisma.column.findFirst({
92-
where: { id: params.newColumnId }
110+
where: { id: params.newColumnId },
111+
select: { id: true, boardId: true, board: { select: { userId: true } } }
93112
})
94113

95114
if (!column) return next(NotFound('Column not found'))
@@ -103,6 +122,8 @@ class CardController {
103122

104123
if (!updatedCard) return next(NotFound('Card not found'))
105124

125+
await redis.del(`board:${column.boardId}:user:${column.board.userId}`)
126+
106127
res.json(updatedCard)
107128
}
108129

@@ -112,12 +133,21 @@ class CardController {
112133
next: NextFunction
113134
) => {
114135
const deletedCard = await prisma.card.deleteIgnoreNotFound({
115-
where: { id: params.cardId }
136+
where: { id: params.cardId },
137+
select: {
138+
column: {
139+
select: { boardId: true, board: { select: { userId: true } } }
140+
}
141+
}
116142
})
117143

118144
if (!deletedCard) return next(NotFound('Card not found'))
119145

120-
res.status(204).send()
146+
await redis.del(
147+
`board:${deletedCard.column.boardId}:user:${deletedCard.column.board.userId}`
148+
)
149+
150+
res.sendStatus(204)
121151
}
122152

123153
private getNewCardOrder = async (columnId: string) => {

app/controllers/column.controller.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { NextFunction, Response } from 'express'
1111
import { prisma } from '@/prisma'
1212
import { BadRequest, NotFound } from 'http-errors'
1313

14+
import { redis } from '@/config'
15+
1416
class ColumnController {
1517
add = async (
1618
{
@@ -39,6 +41,8 @@ class ColumnController {
3941
data: { ...body, order: newOrder, boardId: board.id }
4042
})
4143

44+
await redis.del(`board:${params.boardId}:user:${user.id}`)
45+
4246
res.json(column)
4347
}
4448

@@ -52,11 +56,16 @@ class ColumnController {
5256
) => {
5357
const updatedColumn = await prisma.column.updateIgnoreNotFound({
5458
where: { id: params.columnId },
59+
select: { boardId: true, board: { select: { userId: true } } },
5560
data: body
5661
})
5762

5863
if (!updatedColumn) return next(NotFound('Column not found'))
5964

65+
await redis.del(
66+
`board:${updatedColumn.boardId}:user:${updatedColumn.board.userId}`
67+
)
68+
6069
res.json(updatedColumn)
6170
}
6271

@@ -83,6 +92,9 @@ class ColumnController {
8392

8493
try {
8594
const updatedColumns = await prisma.$transaction(transaction)
95+
96+
await redis.del(`board:${board.id}:user:${board.userId}`)
97+
8698
res.json(updatedColumns)
8799
} catch {
88100
return next(BadRequest('Invalid order'))
@@ -95,12 +107,17 @@ class ColumnController {
95107
next: NextFunction
96108
) => {
97109
const deletedColumn = await prisma.column.deleteIgnoreNotFound({
98-
where: { id: params.columnId }
110+
where: { id: params.columnId },
111+
select: { boardId: true, board: { select: { userId: true } } }
99112
})
100113

101114
if (!deletedColumn) return next(NotFound('Column not found'))
102115

103-
res.status(204).send()
116+
await redis.del(
117+
`board:${deletedColumn.boardId}:user:${deletedColumn.board.userId}`
118+
)
119+
120+
res.sendStatus(204)
104121
}
105122
}
106123

app/controllers/user.controller.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,27 @@ import { prisma } from '@/prisma'
88
import { hash } from 'argon2'
99
import { Conflict, InternalServerError, NotAcceptable } from 'http-errors'
1010

11-
import { env } from '@/config'
11+
import { env, redis } from '@/config'
1212
import cloudinary from '@/config/cloudinary.config'
1313
import { transport } from '@/config/mailer.config'
1414

1515
class UserController {
1616
me = async (req: Request, res: Response) => {
17-
const user = await prisma.user.findFirst({
18-
where: { id: req.user.id }
19-
})
17+
const cacheKey = `user:${req.user.id}`
18+
19+
const cachedUser = await redis.get(cacheKey)
20+
21+
if (cachedUser) {
22+
res.json(JSON.parse(cachedUser))
23+
} else {
24+
const user = await prisma.user.findFirst({
25+
where: { id: req.user.id }
26+
})
2027

21-
res.json(user)
28+
await redis.set(cacheKey, JSON.stringify(user))
29+
30+
res.json(user)
31+
}
2232
}
2333

2434
update = async (
@@ -74,6 +84,8 @@ class UserController {
7484
data: updateData
7585
})
7686

87+
await redis.del(`user:${updatedUser.id}`)
88+
7789
res.json(updatedUser)
7890
}
7991

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"express": "^5.1.0",
2626
"google-auth-library": "^10.1.0",
2727
"http-errors": "^2.0.0",
28+
"ioredis": "^5.6.1",
2829
"jose": "^6.0.11",
2930
"morgan": "1.10.0",
3031
"multer": "^2.0.1",

0 commit comments

Comments
 (0)