From 17d21954af0be57d1885d4e74f205a38cc4f7f1e Mon Sep 17 00:00:00 2001 From: Dustin Do Date: Thu, 27 Jun 2024 11:03:09 +0700 Subject: [PATCH] feat(api): add category APIs --- .vscode/settings.json | 5 +- apps/api/v1/index.ts | 2 + apps/api/v1/routes/budgets.ts | 2 +- apps/api/v1/routes/categories.ts | 98 +++++++++++++++++++ apps/api/v1/services/category.service.ts | 116 +++++++++++++++++++++++ packages/validation/src/category.zod.ts | 20 ++++ packages/validation/src/index.ts | 1 + 7 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 apps/api/v1/routes/categories.ts create mode 100644 apps/api/v1/services/category.service.ts create mode 100644 packages/validation/src/category.zod.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index bda6cc30..bf96fa62 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,8 @@ "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, - "editor.formatOnSave": true + "editor.formatOnSave": true, + "cSpell.words": [ + "hono" + ] } \ No newline at end of file diff --git a/apps/api/v1/index.ts b/apps/api/v1/index.ts index 293b81b7..9f0709ea 100644 --- a/apps/api/v1/index.ts +++ b/apps/api/v1/index.ts @@ -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' @@ -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) diff --git a/apps/api/v1/routes/budgets.ts b/apps/api/v1/routes/budgets.ts index 8b15ad41..cc5bebff 100644 --- a/apps/api/v1/routes/budgets.ts +++ b/apps/api/v1/routes/budgets.ts @@ -134,7 +134,7 @@ const router = new Hono() await deleteBudget({ budgetId }) - return c.json(budget) + return c.json(budget, 204) }) /** Generate sharable invitation link */ diff --git a/apps/api/v1/routes/categories.ts b/apps/api/v1/routes/categories.ts new file mode 100644 index 00000000..bcc763a8 --- /dev/null +++ b/apps/api/v1/routes/categories.ts @@ -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 diff --git a/apps/api/v1/services/category.service.ts b/apps/api/v1/services/category.service.ts new file mode 100644 index 00000000..b88662f1 --- /dev/null +++ b/apps/api/v1/services/category.service.ts @@ -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: + user, +}: { user: User }): Promise { + return true +} + +// biome-ignore lint/correctness/noEmptyPattern: +export async function canUserReadCategory({}: { + user: User + category: Category +}): Promise { + return true +} + +export async function isUserCategoryOwner({ + user, + category, +}: { + user: User + category: Category +}): Promise { + return category.userId === user.id +} + +export async function canUserUpdateCategory({ + user, + category, +}: { + user: User + category: Category +}): Promise { + return isUserCategoryOwner({ user, category }) +} + +export async function canUserDeleteCategory({ + user, + category, +}: { + user: User + category: Category +}): Promise { + 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 { + return prisma.category.findMany({ + where: { userId: user.id }, + }) +} diff --git a/packages/validation/src/category.zod.ts b/packages/validation/src/category.zod.ts new file mode 100644 index 00000000..ffbd9187 --- /dev/null +++ b/packages/validation/src/category.zod.ts @@ -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 + +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 diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 2a938f1d..62763cd2 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -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'