refactor(categories): move mutations into use cases
This commit is contained in:
@@ -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"),
|
||||
})
|
||||
@@ -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 } })
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user