Files
famapp/src/modules/calendar/server/actions.ts
T

189 lines
5.9 KiB
TypeScript

"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<typeof calendarInput>) {
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<typeof eventInput>) {
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<z.input<typeof eventInput>>) {
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");
}