Skip to content

Commit

Permalink
feat(api): add category APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
duongdev committed Jun 27, 2024
1 parent b605491 commit af6eb4d
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 2 deletions.
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.formatOnSave": true
"editor.formatOnSave": true,
"cSpell.words": [
"hono"
]
}
2 changes: 2 additions & 0 deletions apps/api/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Hono } from 'hono'
import { authMiddleware } from './middlewares/auth'
import authApp from './routes/auth'
import budgetsApp from './routes/budgets'
import categoriesApp from './routes/categories'
import transactionsApp from './routes/transactions'
import usersApp from './routes/users'
import walletsApp from './routes/wallets'
Expand All @@ -12,6 +13,7 @@ export const hono = new Hono()

.route('/auth', authApp)
.route('/budgets', budgetsApp)
.route('/categories', categoriesApp)
.route('/users', usersApp)
.route('/transactions', transactionsApp)
.route('/wallets', walletsApp)
2 changes: 1 addition & 1 deletion apps/api/v1/routes/budgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const router = new Hono()

await deleteBudget({ budgetId })

return c.json(budget)
return c.json(budget, 204)
})

/** Generate sharable invitation link */
Expand Down
98 changes: 98 additions & 0 deletions apps/api/v1/routes/categories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { zCreateCategory, zUpdateCategory } from '@6pm/validation'
import { zValidator } from '@hono/zod-validator'
import { Hono } from 'hono'
import { z } from 'zod'
import { getAuthUserStrict } from '../middlewares/auth'
import {
canUserCreateCategory,
canUserDeleteCategory,
canUserReadCategory,
canUserUpdateCategory,
createCategory,
deleteCategory,
findCategoriesOfUser,
findCategory,
updateCategory,
} from '../services/category.service'

const router = new Hono()

// get all categories of the current authenticated user
.get('/', async (c) => {
const user = getAuthUserStrict(c)

const categories = await findCategoriesOfUser({ user })

return c.json(categories)
})

// create a new category
.post('/', zValidator('json', zCreateCategory), async (c) => {
const user = getAuthUserStrict(c)

if (!(await canUserCreateCategory({ user }))) {
return c.json({ message: 'user cannot create category' }, 403)
}

const createCategoryData = c.req.valid('json')

const category = await createCategory({ user, data: createCategoryData })

return c.json(category, 201)
})

// update a category
.put(
'/:categoryId',
zValidator('param', z.object({ categoryId: z.string() })),
zValidator('json', zUpdateCategory),
async (c) => {
const user = getAuthUserStrict(c)
const { categoryId } = c.req.valid('param')

const category = await findCategory({ id: categoryId })

if (!(category && (await canUserReadCategory({ user, category })))) {
return c.json({ message: 'category not found' }, 404)
}

if (!(await canUserUpdateCategory({ user, category }))) {
return c.json({ message: 'user cannot update category' }, 403)
}

const updateCategoryData = c.req.valid('json')

const updatedCategory = await updateCategory({
category,
data: updateCategoryData,
})

return c.json(updatedCategory)
},
)

// delete category
.delete(
'/:categoryId',
zValidator('param', z.object({ categoryId: z.string() })),
async (c) => {
const user = getAuthUserStrict(c)
const { categoryId } = c.req.valid('param')

const category = await findCategory({ id: categoryId })

if (!(category && (await canUserReadCategory({ user, category })))) {
return c.json({ message: 'category not found' }, 404)
}

if (!(await canUserDeleteCategory({ user, category }))) {
return c.json({ message: 'user cannot delete category' }, 403)
}

await deleteCategory({ categoryId })

return c.json(category, 204)
},
)

export default router
116 changes: 116 additions & 0 deletions apps/api/v1/services/category.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { CreateCategory, UpdateCategory } from '@6pm/validation'
import type { Category, User } from '@prisma/client'
import prisma from '../../lib/prisma'

export async function canUserCreateCategory({
// biome-ignore lint/correctness/noUnusedVariables: <explanation>
user,
}: { user: User }): Promise<boolean> {
return true
}

// biome-ignore lint/correctness/noEmptyPattern: <explanation>
export async function canUserReadCategory({}: {
user: User
category: Category
}): Promise<boolean> {
return true
}

export async function isUserCategoryOwner({
user,
category,
}: {
user: User
category: Category
}): Promise<boolean> {
return category.userId === user.id
}

export async function canUserUpdateCategory({
user,
category,
}: {
user: User
category: Category
}): Promise<boolean> {
return isUserCategoryOwner({ user, category })
}

export async function canUserDeleteCategory({
user,
category,
}: {
user: User
category: Category
}): Promise<boolean> {
return isUserCategoryOwner({ user, category })
}

export async function createCategory({
user,
data,
}: {
user: User
data: CreateCategory
}) {
const { name, type, color, description, icon } = data

const category = await prisma.category.create({
data: {
name,
type,
color,
description,
icon,
userId: user.id,
},
})

return category
}

export async function updateCategory({
category,
data,
}: {
category: Category
data: UpdateCategory
}) {
const { name, type, color, description, icon } = data

const updatedCategory = await prisma.category.update({
where: { id: category.id },
data: {
name,
type,
color,
description,
icon,
},
})

return updatedCategory
}

export async function deleteCategory({ categoryId }: { categoryId: string }) {
await prisma.category.delete({
where: { id: categoryId },
})
}

export async function findCategory({ id }: { id: string }) {
return prisma.category.findUnique({
where: { id },
})
}

export async function findCategoriesOfUser({
user,
}: {
user: User
}): Promise<Category[]> {
return prisma.category.findMany({
where: { userId: user.id },
})
}
20 changes: 20 additions & 0 deletions packages/validation/src/category.zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { z } from 'zod'
import { CategoryTypeSchema } from './prisma'

export const zCreateCategory = z.object({
type: CategoryTypeSchema,
name: z.string(),
description: z.string().optional(),
color: z.string().optional(),
icon: z.string().optional(),
})
export type CreateCategory = z.infer<typeof zCreateCategory>

export const zUpdateCategory = z.object({
type: CategoryTypeSchema.optional(),
name: z.string().optional(),
description: z.string().optional(),
color: z.string().optional(),
icon: z.string().optional(),
})
export type UpdateCategory = z.infer<typeof zUpdateCategory>
1 change: 1 addition & 0 deletions packages/validation/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './prisma'
export * from './auth.zod'
export * from './budget.zod'
export * from './category.zod'
export * from './user.zod'
export * from './wallet.zod'
export * from './transaction.zod'

0 comments on commit af6eb4d

Please sign in to comment.