Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/controllers/board.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ class BoardController {
include: {
columns: {
orderBy: { order: 'asc' },
include: { tasks: { orderBy: { order: 'asc' } } }
include: {
tasks: { include: { labels: true }, orderBy: { order: 'asc' } }
}
}
}
})
Expand Down
1 change: 1 addition & 0 deletions app/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { authController } from './auth.controller'
export { boardController } from './board.controller'
export { taskController } from './task.controller'
export { labelController } from './label.controller'
export { columnController } from './column.controller'
export { userController } from './user.controller'
89 changes: 89 additions & 0 deletions app/controllers/label.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type {
CreateLabelSchema,
EditLabelSchema,
LabelParamsSchema
} from '@/schemas'
import type {
TypedRequest,
TypedRequestBody,
TypedRequestParams
} from '@/types'
import type { NextFunction, Request, Response } from 'express'
import type { ZodType } from 'zod'

import { prisma } from '@/prisma'
import { NotFound } from 'http-errors'

import { redisClient } from '@/config'

class LabelController {
getAll = async ({ user }: Request, res: Response) => {
const cacheKey = `labels:user:${user.id}`

const cachedLabels = await redisClient.get(cacheKey)

if (cachedLabels) {
res.json(JSON.parse(cachedLabels))
} else {
const labels = await prisma.label.findMany({ where: { userId: user.id } })

await redisClient.set(cacheKey, JSON.stringify(labels), 'EX', 5 * 60)

res.json(labels)
}
}

createLabel = async (
{ body, user }: TypedRequestBody<typeof CreateLabelSchema>,
res: Response
) => {
const { name, color } = body

const label = await prisma.label.create({
data: { name, color, userId: user.id }
})

await redisClient.del(`labels:user:${user.id}`)

res.json(label)
}

updateById = async (
{
params,
body,
user
}: TypedRequest<typeof LabelParamsSchema, ZodType, typeof EditLabelSchema>,
res: Response,
next: NextFunction
) => {
const updatedLabel = await prisma.label.updateIgnoreNotFound({
where: { id: params.labelId, userId: user.id },
data: body
})

if (!updatedLabel) return next(NotFound('Label not found'))

await redisClient.del(`labels:user:${user.id}`)

res.json(updatedLabel)
}

deleteById = async (
{ params, user }: TypedRequestParams<typeof LabelParamsSchema>,
res: Response,
next: NextFunction
) => {
const deletedLabel = await prisma.label.deleteIgnoreNotFound({
where: { id: params.labelId, userId: user.id }
})

if (!deletedLabel) return next(NotFound('Label not found'))

await redisClient.del(`labels:user:${user.id}`)

res.sendStatus(204)
}
}

export const labelController = new LabelController()
18 changes: 15 additions & 3 deletions app/controllers/task.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,13 @@ class TaskController {
const newOrder = await this.getNewTaskOrder(column.id)

const newTask = await prisma.task.create({
data: { ...body, columnId: column.id, order: newOrder }
data: {
...body,
columnId: column.id,
order: newOrder,
labels: { connect: body.labels?.map(id => ({ id })) }
},
include: { labels: true }
})

await redisClient.del(`board:${column.boardId}:user:${column.board.userId}`)
Expand All @@ -59,8 +65,14 @@ class TaskController {

const updatedTask = await prisma.task.updateIgnoreNotFound({
where: { id: params.taskId },
data: body,
include: { column: { include: { board: { select: { userId: true } } } } }
data: {
...body,
labels: { set: body.labels?.map(id => ({ id })) }
},
include: {
labels: true,
column: { include: { board: { select: { userId: true } } } }
}
})

if (!updatedTask) return next(NotFound('Task not found'))
Expand Down
23 changes: 23 additions & 0 deletions app/routes/api/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Router } from 'express'

import { labelController } from '@/controllers'

import { authenticate, validateRequest } from '@/middlewares'

import { CreateLabelSchema } from '@/schemas'

export const labelRouter = Router()

labelRouter.use(authenticate)

labelRouter.get('/', labelController.getAll)

labelRouter.post(
'/',
validateRequest({ body: CreateLabelSchema }),
labelController.createLabel
)

labelRouter.patch('/:labelId', labelController.updateById)

labelRouter.delete('/:labelId', labelController.deleteById)
2 changes: 2 additions & 0 deletions app/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import swaggerUi from 'swagger-ui-express'
import { authRouter } from './api/auth'
import { boardRouter } from './api/board'
import { columnRouter } from './api/column'
import { labelRouter } from './api/label'
import { taskRouter } from './api/task'
import { userRouter } from './api/user'

Expand All @@ -26,3 +27,4 @@ apiRouter.use('/user', userRouter)
apiRouter.use('/board', boardRouter)
apiRouter.use('/column', columnRouter)
apiRouter.use('/task', taskRouter)
apiRouter.use('/label', labelRouter)
5 changes: 5 additions & 0 deletions app/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ export {
ColumnParamsSchema,
UpdateColumnOrderSchema
} from './column.schema'
export {
CreateLabelSchema,
EditLabelSchema,
LabelParamsSchema
} from './label.schema'
export { SigninSchema, SignupSchema } from './auth.schema'
export { EditUserSchema, NeedHelpSchema } from './user.schema'
13 changes: 13 additions & 0 deletions app/schemas/label.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { LabelColor } from '@prisma/client'
import * as z from 'zod'

import { objectIdSchema } from './object-id.schema'

export const CreateLabelSchema = z.object({
name: z.string().min(2),
color: z.enum(LabelColor)
})

export const EditLabelSchema = CreateLabelSchema.partial()

export const LabelParamsSchema = z.object({ labelId: objectIdSchema() })
25 changes: 16 additions & 9 deletions app/schemas/task.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,30 @@ import { objectIdSchema } from './object-id.schema'

export const AddTaskSchema = z.object({
title: z.string().min(3),
description: z.string().min(3),
description: z.optional(z.string().min(3)),
priority: z.enum(Priority),
deadline: z.iso.datetime().refine(value => {
const checkDate = new Date(value)
const today = new Date()
labels: z.optional(z.array(objectIdSchema())),
deadline: z
.optional(
z.iso.datetime().refine(value => {
const checkDate = new Date(value)
const today = new Date()

// Strip time down to the day level for fair comparison
checkDate.setHours(0, 0, 0, 0)
today.setHours(0, 0, 0, 0)
// Strip time down to the day level for fair comparison
checkDate.setHours(0, 0, 0, 0)
today.setHours(0, 0, 0, 0)

return checkDate >= today
}, 'Deadline must be today or in the future')
return checkDate >= today
}, 'Deadline must be today or in the future')
)
.transform(v => (v ? new Date(v) : null))
})

export const EditTaskSchema = z
.object({
...AddTaskSchema.shape,
description: z.nullable(z.string().min(3)),
deadline: z.nullable(z.iso.datetime()),
columnId: objectIdSchema()
})
.partial()
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"start": "node dist/app/server.js",
"build": "swc app -d dist --copy-files",
"dev": "tsx watch app/server.ts",
"dev": "tsx watch --include ./swagger.json app/server.ts",
"lint": "eslint --fix app/**/*.ts",
"format": "prettier --write ./app",
"format:check": "prettier --check ./app",
Expand Down
23 changes: 23 additions & 0 deletions prisma/models/label.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
model Label {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
color LabelColor
userId String @db.ObjectId
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
task Task? @relation(fields: [taskId], references: [id])
taskId String? @db.ObjectId

@@unique([userId, name])
@@map("labels")
}

enum LabelColor {
blue
violet
rose
gray
cyan
amber
indigo
emerald
}
27 changes: 14 additions & 13 deletions prisma/models/task.prisma
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
model Task {
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
description String
priority Priority
deadline DateTime
order Int
columnId String @map("column_id") @db.ObjectId
column Column @relation(fields: [columnId], references: [id], onDelete: Cascade)
id String @id @default(auto()) @map("_id") @db.ObjectId
title String
description String?
priority Priority
deadline DateTime?
order Int
columnId String @map("column_id") @db.ObjectId
column Column @relation(fields: [columnId], references: [id], onDelete: Cascade)
labels Label[]

@@map("tasks")
@@map("tasks")
}

enum Priority {
Without
Low
Medium
High
Without
Low
Medium
High
}
25 changes: 13 additions & 12 deletions prisma/models/user.prisma
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
email String @unique
password String?
theme Theme @default(light)
avatar String?
avatarPublicId String? @map("avatar_public_id")
boards Board[]
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
email String @unique
password String?
theme Theme @default(light)
avatar String?
avatarPublicId String? @map("avatar_public_id")
boards Board[]
labels Label[]

@@map("users")
@@map("users")
}

enum Theme {
light
dark
violet
light
dark
violet
}
Loading
Loading