Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(api): add category APIs #73

Merged
merged 1 commit into from
Jun 27, 2024
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
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'