feat(tasks): integrate zod validation for task creation and updates
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 47s
This commit is contained in:
parent
3812afab5a
commit
f99d1f15c4
3 changed files with 64 additions and 23 deletions
|
|
@ -6,6 +6,7 @@
|
|||
import { useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { TaskPriority, TaskStatus, DbTask } from "@/types/task";
|
||||
import { taskSchema } from "@/lib/schemas/task";
|
||||
|
||||
/**
|
||||
* Custom React hook that encapsulates form state management, field updates, assignee toggling,
|
||||
|
|
@ -65,18 +66,14 @@ export function useTaskForm(
|
|||
};
|
||||
|
||||
/**
|
||||
* Validates form inputs, combines date and time values, and submits the payload
|
||||
* via API depending on whether the form is in create or edit mode.
|
||||
* Validates form input against Zod schemas and dispatches POST or PATCH network requests to persist task changes.
|
||||
*
|
||||
* @async
|
||||
* @param {React.SubmitEvent} e - The form submission event.
|
||||
*/
|
||||
const handleSubmit = (e: React.SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!form.title.trim()) return;
|
||||
|
||||
const finalDate = form.dueDate || todayString;
|
||||
const combinedDateTime = new Date(`${finalDate}T${form.dueTime}`);
|
||||
|
||||
|
|
@ -85,15 +82,24 @@ export function useTaskForm(
|
|||
return;
|
||||
}
|
||||
|
||||
const validationResult = taskSchema.safeParse({
|
||||
...form,
|
||||
dueDate: combinedDateTime.toISOString(),
|
||||
});
|
||||
|
||||
if (!validationResult.success) {
|
||||
setError(validationResult.error.issues[0].message);
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/tasks", {
|
||||
method: isEditMode ? "PATCH" : "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
...validationResult.data,
|
||||
id: initialData?.id,
|
||||
...form,
|
||||
dueDate: combinedDateTime.toISOString(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
/**
|
||||
* @file api/tasks/route.ts
|
||||
* @description API endpoint for creating and updating task records with strict existence checks.
|
||||
* @description API endpoint for creating and updating task records with strict Zod validation.
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { TaskService } from "@/services/task.service";
|
||||
import { TaskPayload } from "@/types/task";
|
||||
import { taskSchema } from "@/lib/schemas/task";
|
||||
|
||||
/**
|
||||
* Validates the incoming task request by checking user authentication, parsing the JSON payload,
|
||||
* and ensuring all required task fields (and optional ID if specified) are present.
|
||||
* and running Zod validation against the task schema.
|
||||
*
|
||||
* @async
|
||||
* @param {Request} request - The incoming HTTP request.
|
||||
* @param {boolean} [requireId=false] - Whether a task ID is mandatory in the payload body.
|
||||
* @returns {Promise<{ userId?: string; body?: TaskPayload & { id?: string }; error?: NextResponse }>} Validation results containing user ID, parsed body, or a NextResponse error.
|
||||
* @returns {Promise<{ userId?: string; body?: any; error?: NextResponse }>} Validation results containing user ID, parsed body, or a NextResponse error.
|
||||
*/
|
||||
async function validateTaskRequest(request: Request, requireId = false) {
|
||||
const session = await auth();
|
||||
|
|
@ -23,19 +23,22 @@ async function validateTaskRequest(request: Request, requireId = false) {
|
|||
return { error: new NextResponse("Unauthorized", { status: 401 }) };
|
||||
}
|
||||
|
||||
const body: TaskPayload & { id?: string } = await request.json();
|
||||
const jsonBody = await request.json().catch(() => null);
|
||||
if (!jsonBody) {
|
||||
return { error: new NextResponse("Invalid JSON payload", { status: 400 }) };
|
||||
}
|
||||
|
||||
if (
|
||||
(requireId && !body.id) ||
|
||||
!body.title ||
|
||||
!body.description ||
|
||||
!body.dueDate ||
|
||||
!body.status ||
|
||||
!body.priority
|
||||
) {
|
||||
return {
|
||||
error: new NextResponse("Missing required task fields", { status: 400 }),
|
||||
};
|
||||
const validationResult = taskSchema.safeParse(jsonBody);
|
||||
|
||||
if (!validationResult.success) {
|
||||
const errorMessage = validationResult.error.issues[0].message;
|
||||
return { error: new NextResponse(errorMessage, { status: 400 }) };
|
||||
}
|
||||
|
||||
const body = validationResult.data;
|
||||
|
||||
if (requireId && !body.id) {
|
||||
return { error: new NextResponse("Missing task ID", { status: 400 }) };
|
||||
}
|
||||
|
||||
return { userId: session.user.id, body };
|
||||
|
|
|
|||
32
lib/schemas/task.ts
Normal file
32
lib/schemas/task.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* @file lib/schemas/task.ts
|
||||
* @description Zod validation schema for task creation and modification operations, enforcing constraints on title, description, status, priority, dates, and assignees.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import { taskStatusEnum, taskPriorityEnum } from "@/db/schema";
|
||||
|
||||
/**
|
||||
* Zod validation schema for task entities and form submissions.
|
||||
*/
|
||||
export const taskSchema = z.object({
|
||||
id: z.uuid().optional(),
|
||||
title: z
|
||||
.string()
|
||||
.min(1, "Title is required")
|
||||
.max(255, "Title is too long (max 255 characters)"),
|
||||
description: z
|
||||
.string()
|
||||
.min(1, "Description is required")
|
||||
.max(2000, "Description is too long (max 2000 characters)"),
|
||||
status: z.enum(taskStatusEnum.enumValues),
|
||||
priority: z.enum(taskPriorityEnum.enumValues),
|
||||
dueDate: z
|
||||
.string()
|
||||
.min(1, "Due date is required")
|
||||
.transform((val) => new Date(val)),
|
||||
dueTime: z
|
||||
.string()
|
||||
.regex(/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/, "Invalid time format"),
|
||||
assignees: z.array(z.uuid()).optional(),
|
||||
});
|
||||
Loading…
Reference in a new issue