"use server"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import { db } from "@/lib/db"; import { getCurrentSession } from "@/lib/session"; import { calendarEvents, calendars } from "../schema"; import { canSeeCalendar } from "./queries"; const calendarInput = z.object({ name: z.string().trim().min(1).max(120), color: z.string().trim().min(1).max(32).nullable().optional(), visibility: z.enum(["private", "household"]).default("household"), }); const eventBaseInput = z.object({ calendarId: z.string().uuid(), title: z.string().trim().min(1).max(200), startAt: z.coerce.date(), endAt: z.coerce.date(), allDay: z.boolean().default(false), location: z.string().trim().max(300).nullable().optional(), notes: z.string().trim().max(3000).nullable().optional(), }); const eventInput = eventBaseInput.refine((value) => value.endAt >= value.startAt, { path: ["endAt"], message: "End must be after start", }); const eventUpdateInput = eventBaseInput.partial().refine( (value) => { if (!value.startAt || !value.endAt) return true; return value.endAt >= value.startAt; }, { path: ["endAt"], message: "End must be after start", }, ); export async function createCalendar(input: z.input) { const parsed = calendarInput.parse(input); const { user, household } = await getCurrentSession(); const [calendar] = await db .insert(calendars) .values({ householdId: household.id, ownerId: user.id, name: parsed.name, color: parsed.color ?? null, visibility: parsed.visibility, }) .returning(); if (!calendar) throw new Error("Calendar was not created"); revalidatePath("/calendar"); return calendar; } export async function renameCalendar(input: { id: string; name: string }) { const { user } = await getCurrentSession(); const parsed = z.object({ id: z.string().uuid(), name: calendarInput.shape.name }).parse(input); await assertOwnsCalendar(user.id, parsed.id); await db .update(calendars) .set({ name: parsed.name, updatedAt: new Date() }) .where(eq(calendars.id, parsed.id)); revalidatePath("/calendar"); } export async function setCalendarVisibility(input: { id: string; visibility: "private" | "household"; }) { const { user } = await getCurrentSession(); const parsed = z .object({ id: z.string().uuid(), visibility: calendarInput.shape.visibility }) .parse(input); await assertOwnsCalendar(user.id, parsed.id); await db .update(calendars) .set({ visibility: parsed.visibility, updatedAt: new Date() }) .where(eq(calendars.id, parsed.id)); revalidatePath("/calendar"); } export async function setCalendarColor(input: { id: string; color: string | null }) { const { user } = await getCurrentSession(); const parsed = z.object({ id: z.string().uuid(), color: calendarInput.shape.color }).parse(input); await assertOwnsCalendar(user.id, parsed.id); await db .update(calendars) .set({ color: parsed.color ?? null, updatedAt: new Date() }) .where(eq(calendars.id, parsed.id)); revalidatePath("/calendar"); } export async function deleteCalendar(input: { id: string }) { const { user } = await getCurrentSession(); const parsed = z.object({ id: z.string().uuid() }).parse(input); await assertOwnsCalendar(user.id, parsed.id); await db.delete(calendars).where(eq(calendars.id, parsed.id)); revalidatePath("/calendar"); } export async function createEvent(input: z.input) { const parsed = eventInput.parse(input); const { user } = await getCurrentSession(); if (!(await canSeeCalendar(user.id, parsed.calendarId))) throw new Error("Forbidden"); const [event] = await db .insert(calendarEvents) .values({ ...parsed, ownerId: user.id, location: parsed.location || null, notes: parsed.notes || null, }) .returning(); if (!event) throw new Error("Event was not created"); revalidatePath("/calendar"); return { ...event, startAt: event.startAt.toISOString(), endAt: event.endAt.toISOString(), }; } export async function updateEvent(input: { id: string } & Partial>) { const parsed = z.object({ id: z.string().uuid() }).and(eventUpdateInput).parse(input); const { user } = await getCurrentSession(); const [existing] = await db .select({ calendarId: calendarEvents.calendarId }) .from(calendarEvents) .where(eq(calendarEvents.id, parsed.id)) .limit(1); if (!existing) throw new Error("Event not found"); const calendarId = parsed.calendarId ?? existing.calendarId; if (!(await canSeeCalendar(user.id, calendarId))) throw new Error("Forbidden"); await db .update(calendarEvents) .set({ calendarId, title: parsed.title, startAt: parsed.startAt, endAt: parsed.endAt, allDay: parsed.allDay, location: parsed.location === undefined ? undefined : parsed.location || null, notes: parsed.notes === undefined ? undefined : parsed.notes || null, updatedAt: new Date(), }) .where(eq(calendarEvents.id, parsed.id)); revalidatePath("/calendar"); } export async function deleteEvent(input: { id: string }) { const parsed = z.object({ id: z.string().uuid() }).parse(input); const { user } = await getCurrentSession(); const [existing] = await db .select({ calendarId: calendarEvents.calendarId }) .from(calendarEvents) .where(eq(calendarEvents.id, parsed.id)) .limit(1); if (!existing) return; if (!(await canSeeCalendar(user.id, existing.calendarId))) throw new Error("Forbidden"); await db.delete(calendarEvents).where(eq(calendarEvents.id, parsed.id)); revalidatePath("/calendar"); } async function assertOwnsCalendar(userId: string, calendarId: string) { const [calendar] = await db .select({ id: calendars.id }) .from(calendars) .where(and(eq(calendars.id, calendarId), eq(calendars.ownerId, userId))) .limit(1); if (!calendar) throw new Error("Forbidden"); }