Implement calendar module
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
"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 eventInput = 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(),
|
||||
})
|
||||
.refine((value) => 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(eventInput.partial())
|
||||
.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");
|
||||
}
|
||||
Reference in New Issue
Block a user