refactor(categories): move mutations into use cases

This commit is contained in:
2026-06-04 22:12:06 +02:00
parent 0af25417ab
commit f48ccb8c50
7 changed files with 183 additions and 80 deletions
@@ -7,8 +7,12 @@ import {
createCategorySchema,
type UpdateCategoryFormType,
updateCategorySchema,
} from "@/lib/schemas/category.schemas"
import { CategoryService } from "@/services/category.service"
} from "@/schemas/category.schema"
import {
createCategoryUseCase,
deleteCategoryUseCase,
updateCategoryUseCase,
} from "@/use-cases/category.use-cases"
export async function createCategoryAction(formData: CreateCategoryFormType) {
const validatedFields = createCategorySchema.safeParse(formData)
@@ -21,21 +25,12 @@ export async function createCategoryAction(formData: CreateCategoryFormType) {
}
try {
const existingCategory = await CategoryService.findByName(
validatedFields.data.name,
)
const result = await createCategoryUseCase(validatedFields.data)
if (existingCategory) {
return {
success: false,
errors: {
name: ["Category already exists"],
},
}
if (!result.success) {
return result
}
await CategoryService.create(validatedFields.data)
revalidatePath("/inventory/categories")
return {
@@ -64,36 +59,13 @@ export async function updateCategoryAction(formData: UpdateCategoryFormType) {
}
}
const { id, name } = validatedFields.data
try {
const existingCategory = await CategoryService.findById(id)
const result = await updateCategoryUseCase(validatedFields.data)
if (!existingCategory) {
return {
success: false,
errors: { id: ["Category not found"] },
}
if (!result.success) {
return result
}
if (existingCategory.name === name) {
return {
success: false,
errors: { name: ["Category name is the same as the old one"] },
}
}
if (await CategoryService.findByName(name)) {
return {
success: false,
errors: { name: ["Category already exists"] },
}
}
await CategoryService.update(id, {
name,
})
revalidatePath("/inventory/categories")
return {
@@ -113,40 +85,27 @@ export async function deleteCategoryAction(formData: FormData) {
const { id } = Object.fromEntries(formData) as { id: string }
try {
const existingCategory = await CategoryService.findAllWithItemsCount()
const category = existingCategory.find((category) => category.id === id)
const result = await deleteCategoryUseCase(id)
if (!category) {
if (!result.success) {
return {
success: false,
errors: {
id: ["Category not found"],
},
...result,
message: "Failed to delete category",
}
}
if (category._count.items && category._count.items > 0) {
return {
success: false,
errors: {
id: ["Category has items"],
},
}
}
await CategoryService.delete(id)
revalidatePath("/inventory/categories")
return {
success: true,
success: true as const,
message: "Category deleted successfully",
}
} catch (error) {
console.error("Database error:", error)
return {
success: false,
success: false as const,
message: "Failed to delete category",
errors: {},
}
}
}
@@ -4,9 +4,8 @@ import { Trash } from "lucide-react"
import { useRouter } from "next/navigation"
import { useTransition } from "react"
import { toast } from "sonner"
import { deleteCategoryAction } from "@/actions/category.actions"
import { Button } from "@/components/ui/button"
import { deleteCategoryAction } from "@/lib/actions/category.actions"
export default function DeleteCategoryButton({
categoryId,
@@ -4,14 +4,13 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { updateCategoryAction } from "@/actions/category.actions"
import { SubmitButton } from "@/components/forms/submitButton"
import { updateCategoryAction } from "@/lib/actions/category.actions"
import {
type UpdateCategoryFormType,
updateCategorySchema,
} from "@/lib/schemas/category.schemas"
import type { CategorySummary } from "@/lib/types"
} from "@/schemas/category.schema"
import type { CategorySummary } from "@/types"
export default function EditCategoryForm({
category,
@@ -4,13 +4,12 @@ import { zodResolver } from "@hookform/resolvers/zod"
import { useRouter } from "next/navigation"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { createCategoryAction } from "@/actions/category.actions"
import { SubmitButton } from "@/components/forms/submitButton"
import { createCategoryAction } from "@/lib/actions/category.actions"
import {
type CreateCategoryFormType,
createCategorySchema,
} from "@/lib/schemas/category.schemas"
} from "@/schemas/category.schema"
export default function NewCategoryForm() {
const router = useRouter()
@@ -4,7 +4,7 @@ export const createCategorySchema = z.object({
name: z
.string()
.min(3, {
error: "Name is required and must be at least 3 characters long"
error: "Name is required and must be at least 3 characters long",
})
.nonempty("Name is required and must be at least 3 characters long"),
})
+37 -10
View File
@@ -1,7 +1,7 @@
import type { Prisma } from "@/generated/prisma/client"
import { paginate } from "@/lib/paginate"
import prisma from "@/lib/prisma"
import type { Category, CategorySummary, CategoryWithItemsCount } from "@/lib/types"
import type { Category, CategorySummary, CategoryWithItemsCount } from "@/types"
export const CategoryService = {
findAll: async (): Promise<CategorySummary[]> => {
@@ -53,28 +53,55 @@ export const CategoryService = {
})
},
findByName: async (name: string): Promise<Category | null> => {
return prisma.category.findFirst({
findByName: async (
name: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category | null> => {
return db.category.findFirst({
where: { name: { equals: name, mode: "insensitive" } },
})
},
findById: async (id: string): Promise<Category | null> => {
return prisma.category.findUnique({ where: { id } })
findById: async (
id: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category | null> => {
return db.category.findUnique({ where: { id } })
},
create: async (data: Prisma.CategoryCreateInput): Promise<Category> => {
return prisma.category.create({ data })
findByIdWithItemsCount: async (
id: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<CategoryWithItemsCount | null> => {
return db.category.findUnique({
where: { id },
select: {
id: true,
name: true,
_count: { select: { items: true } },
},
})
},
create: async (
data: Prisma.CategoryCreateInput,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category> => {
return db.category.create({ data })
},
update: async (
id: string,
data: Prisma.CategoryUpdateInput,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category> => {
return prisma.category.update({ where: { id }, data })
return db.category.update({ where: { id }, data })
},
delete: async (id: string): Promise<Category> => {
return prisma.category.delete({ where: { id } })
delete: async (
id: string,
db: Prisma.TransactionClient | typeof prisma = prisma,
): Promise<Category> => {
return db.category.delete({ where: { id } })
},
}
+120
View File
@@ -0,0 +1,120 @@
import { Prisma } from "@/generated/prisma/client"
import prisma from "@/lib/prisma"
import type {
CreateCategoryFormType,
UpdateCategoryFormType,
} from "@/schemas/category.schema"
import { CategoryService } from "@/services/category.service"
type FieldErrors = Record<string, string[]>
type CategoryUseCaseResult =
| {
success: true
}
| {
success: false
errors: FieldErrors
}
function categoryError(errors: FieldErrors): CategoryUseCaseResult {
return {
success: false,
errors,
}
}
function isUniqueConstraintError(error: unknown) {
return (
error instanceof Prisma.PrismaClientKnownRequestError &&
error.code === "P2002"
)
}
export async function createCategoryUseCase(
input: CreateCategoryFormType,
): Promise<CategoryUseCaseResult> {
try {
return await prisma.$transaction(async (tx) => {
const existingCategory = await CategoryService.findByName(input.name, tx)
if (existingCategory) {
return categoryError({ name: ["Category already exists"] })
}
await CategoryService.create(input, tx)
return {
success: true,
}
})
} catch (error) {
if (isUniqueConstraintError(error)) {
return categoryError({ name: ["Category already exists"] })
}
throw error
}
}
export async function updateCategoryUseCase(
input: UpdateCategoryFormType,
): Promise<CategoryUseCaseResult> {
const { id, name } = input
try {
return await prisma.$transaction(async (tx) => {
const existingCategory = await CategoryService.findById(id, tx)
if (!existingCategory) {
return categoryError({ id: ["Category not found"] })
}
if (existingCategory.name === name) {
return categoryError({
name: ["Category name is the same as the old one"],
})
}
const categoryWithName = await CategoryService.findByName(name, tx)
if (categoryWithName) {
return categoryError({ name: ["Category already exists"] })
}
await CategoryService.update(id, { name }, tx)
return {
success: true,
}
})
} catch (error) {
if (isUniqueConstraintError(error)) {
return categoryError({ name: ["Category already exists"] })
}
throw error
}
}
export async function deleteCategoryUseCase(
id: string,
): Promise<CategoryUseCaseResult> {
return prisma.$transaction(async (tx) => {
const category = await CategoryService.findByIdWithItemsCount(id, tx)
if (!category) {
return categoryError({ id: ["Category not found"] })
}
if (category._count.items && category._count.items > 0) {
return categoryError({ id: ["Category has items"] })
}
await CategoryService.delete(id, tx)
return {
success: true,
}
})
}