Skip to content

Commit

Permalink
validation helper function; split actions into files
Browse files Browse the repository at this point in the history
  • Loading branch information
webdevcody committed Nov 18, 2023
1 parent 8cd82ba commit 0493947
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 52 deletions.
76 changes: 42 additions & 34 deletions src/app/actions.ts → src/app/_actions/create-item-action.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,58 @@
"use server";

import { createItem, deleteItem } from "@/data-access/items";
import { ItemEntityValidationError } from "@/entites/item";
import { auth } from "@/lib/auth";
import { ValidationError, createItemUseCase } from "@/use-cases/items";
import { revalidatePath } from "next/cache";

type CreateItemState = {
errors: Partial<{ name: string }>;
form: { name: string };
error?: string;
status: "pending" | "success" | "errors" | "field-errors";
};
type CreateItemState = { form: { name: string } } & (
| {
status: "success";
}
| {
status: "error";
errors: string;
}
| {
status: "field-errors";
errors: Partial<{ name: string }>;
}
| {
status: "default";
}
);

async function handleFieldValidation(
submittedForm: any,
callback: () => Promise<any>
) {
try {
return await callback();
} catch (err) {
const error = err as Error;
if (error instanceof ValidationError) {
return {
form: submittedForm,
status: "field-errors",
errors: error.getErrors(),
};
} else {
return {
form: submittedForm,
status: "error",
errors: error.message,
};
}
}
}

export async function createItemAction(
state: CreateItemState,
formData: FormData
): Promise<CreateItemState> {
"use server";

const session = await auth();

try {
return handleFieldValidation(formData.entries(), async () => {
await createItemUseCase(
{
getUser: () => session?.user && { userId: session.user.id },
Expand All @@ -31,35 +62,12 @@ export async function createItemAction(
);
revalidatePath("/");
return {
errors: {},
form: {
name: "",
},
status: "success",
};
} catch (err) {
const error = err as Error;
if (error instanceof ValidationError) {
return {
form: {
name: formData.get("name") as string,
},
status: "field-errors",
errors: {
name: error.getErrors().name,
},
};
} else {
return {
form: {
name: formData.get("name") as string,
},
status: "errors",
error: error.message,
errors: {},
};
}
}
});
}

export async function deleteItemAction(itemId: number) {
Expand Down
9 changes: 9 additions & 0 deletions src/app/_actions/delete-item-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use server";

import { deleteItem } from "@/data-access/items";
import { revalidatePath } from "next/cache";

export async function deleteItemAction(itemId: number) {
await deleteItem(itemId);
revalidatePath("/");
}
15 changes: 7 additions & 8 deletions src/app/create-item-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { createItemAction } from "./actions";
import { useFormState, useFormStatus } from "react-dom";
import { useEffect, useRef } from "react";
import { useToast } from "@/components/ui/use-toast";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Terminal } from "lucide-react";
import { createItemAction } from "./_actions/create-item-action";

export function CreateItemForm() {
const { toast } = useToast();
const [formState, onCreateItemAction] = useFormState(createItemAction, {
form: {
name: "",
},
status: "pending",
errors: {
name: "",
},
status: "default",
});
const formRef = useRef<HTMLFormElement>(null);

Expand All @@ -35,11 +32,11 @@ export function CreateItemForm() {

return (
<>
{formState.status === "errors" && (
{formState.status === "error" && (
<Alert variant={"destructive"}>
<Terminal className="h-4 w-4" />
<AlertTitle>Uh oh!</AlertTitle>
<AlertDescription>{formState.error}</AlertDescription>
<AlertDescription>{formState.errors}</AlertDescription>
</Alert>
)}
<form
Expand All @@ -55,7 +52,9 @@ export function CreateItemForm() {
id="item-name"
autoFocus
></Input>
<Error error={formState.errors.name} />
{formState.status === "field-errors" && (
<Error error={formState.errors.name} />
)}

<SubmitButton />
</form>
Expand Down
3 changes: 1 addition & 2 deletions src/app/items-list.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Button } from "@/components/ui/button";
import { deleteItemAction } from "./actions";
import { Item } from "@/db/schema";
import { sortBy } from "lodash";
import { ItemDto } from "@/data-access/items";
import { deleteItemAction } from "./_actions/create-item-action";

export function ItemsList({ items }: { items: ItemDto[] }) {
const sortedItems = sortBy(items, ["name"]);
Expand Down
2 changes: 0 additions & 2 deletions src/entites/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ export class ItemEntity {
}

validate() {
console.log("name", this.name);
const itemSchema = z.object({
name: z.string().min(1),
userId: z.string().min(1),
Expand All @@ -56,7 +55,6 @@ export class ItemEntity {
} catch (err) {
const error = err as ZodError;
const errors = error.flatten().fieldErrors;
console.log(errors);
throw new ItemEntityValidationError({
name: errors.name?.[0],
userId: errors.userId?.[0],
Expand Down
16 changes: 10 additions & 6 deletions src/use-cases/items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class ValidationError extends Error {
}
}

export class AuthenticationdError extends Error {
export class AuthenticationError extends Error {
constructor() {
super("You must be authenticated to do this action");
}
Expand All @@ -27,14 +27,21 @@ export class AuthenticationdError extends Error {
export type CreateItem = (item: CreateItemDto) => void;
export type GetUser = () => User | undefined;

function itemToCreateItemDtoMapper(item: ItemEntity): CreateItemDto {
return {
name: item.getName(),
userId: item.getUserId(),
};
}

export async function createItemUseCase(
context: { getUser: GetUser; createItem: CreateItem },
data: { name: string }
) {
const user = context.getUser();

if (!user) {
throw new AuthenticationdError();
throw new AuthenticationError();
}

const item = new ItemEntity({
Expand All @@ -49,8 +56,5 @@ export async function createItemUseCase(
throw new ValidationError(error.getErrors());
}

await context.createItem({
name: item.getName(),
userId: item.getUserId(),
});
await context.createItem(itemToCreateItemDtoMapper(item));
}

0 comments on commit 0493947

Please sign in to comment.