"use server"; import { and, eq } from "drizzle-orm"; import { revalidatePath } from "next/cache"; import { z } from "zod"; import type { ApiAuthContext } from "@/lib/api-auth"; import { db } from "@/lib/db"; import { getCurrentSession } from "@/lib/session"; import { logActivityForScope } from "@/modules/_core/activity"; import { householdMembers } from "@/modules/_core/schema"; import { scheduleReminder, cancelReminder } from "@/modules/_core/reminders"; import { notes } from "../schema"; import { canAccessNote, getNoteForScope, type NoteDto } from "./queries"; import { noteInput, updateNoteInput } from "./schemas"; function toScope(ctx: ApiAuthContext) { return { householdId: ctx.householdId, userId: ctx.userId }; } async function resolveAuthorId(scope: ApiAuthContext): Promise { if (scope.userId) return scope.userId; const [member] = await db .select({ userId: householdMembers.userId }) .from(householdMembers) .where( and(eq(householdMembers.householdId, scope.householdId), eq(householdMembers.role, "owner")), ) .limit(1); if (!member) throw new Error("No household owner found"); return member.userId; } export async function createNoteForScope( scope: ApiAuthContext, input: z.input, ): Promise { const parsed = noteInput.parse(input); const authorId = await resolveAuthorId(scope); const [note] = await db .insert(notes) .values({ householdId: scope.householdId, authorId, title: parsed.title, body: parsed.body, pinned: parsed.pinned, remindAt: parsed.remindAt ?? null, }) .returning(); if (!note) throw new Error("Note was not created"); if (parsed.remindAt && scope.userId) { await scheduleReminder({ householdId: scope.householdId, entityType: "notes.note", entityId: note.id, fireAt: parsed.remindAt, createdBy: scope.userId, }); } await logActivityForScope(toScope(scope), { entityType: "notes.note", entityId: note.id, action: "create", payload: { title: note.title }, }); return { id: note.id, householdId: note.householdId, authorId: note.authorId, title: note.title, body: note.body, pinned: note.pinned, remindAt: note.remindAt?.toISOString() ?? null, createdAt: note.createdAt.toISOString(), updatedAt: note.updatedAt.toISOString(), }; } export async function createNote(input: z.input) { const { household, user } = await getCurrentSession(); const note = await createNoteForScope( { householdId: household.id, userId: user.id, role: null }, input, ); revalidatePath("/notes"); return note; } export async function updateNoteForScope( scope: ApiAuthContext, input: z.input, ): Promise { const parsed = updateNoteInput.parse(input); await assertCanAccessNote(parsed.id, scope.householdId); const [note] = await db .update(notes) .set({ title: parsed.title, body: parsed.body, pinned: parsed.pinned, remindAt: parsed.remindAt === undefined ? undefined : parsed.remindAt, updatedAt: new Date(), }) .where(eq(notes.id, parsed.id)) .returning(); if (!note) throw new Error("Note was not updated"); if (parsed.remindAt !== undefined && scope.userId) { if (parsed.remindAt) { await scheduleReminder({ householdId: scope.householdId, entityType: "notes.note", entityId: note.id, fireAt: parsed.remindAt, createdBy: scope.userId, }); } else { await cancelReminder("notes.note", note.id); } } await logActivityForScope(toScope(scope), { entityType: "notes.note", entityId: note.id, action: "update", payload: { title: note.title }, }); return getNoteForScope(scope.householdId, note.id); } export async function updateNote(input: z.input) { const { household, user } = await getCurrentSession(); const note = await updateNoteForScope( { householdId: household.id, userId: user.id, role: null }, input, ); revalidatePath("/notes"); revalidatePath(`/notes/${input.id}`); return note; } export async function setNotePinned(input: { id: string; pinned: boolean }) { const parsed = z.object({ id: z.string().uuid(), pinned: z.boolean() }).parse(input); const { household, user } = await getCurrentSession(); await assertCanAccessNote(parsed.id, household.id); await db .update(notes) .set({ pinned: parsed.pinned, updatedAt: new Date() }) .where(eq(notes.id, parsed.id)); const note = await getNoteForScope(household.id, parsed.id); await logActivityForScope( { householdId: household.id, userId: user.id }, { entityType: "notes.note", entityId: parsed.id, action: parsed.pinned ? "pin" : "unpin", payload: { title: note.title }, }, ); revalidatePath("/notes"); revalidatePath(`/notes/${parsed.id}`); return note; } export async function deleteNoteForScope(scope: ApiAuthContext, input: { id: string }) { const parsed = z.object({ id: z.string().uuid() }).parse(input); await assertCanAccessNote(parsed.id, scope.householdId); const note = await getNoteForScope(scope.householdId, parsed.id); await logActivityForScope(toScope(scope), { entityType: "notes.note", entityId: parsed.id, action: "delete", payload: { title: note.title }, }); await cancelReminder("notes.note", parsed.id); await db.delete(notes).where(eq(notes.id, parsed.id)); } export async function deleteNote(input: { id: string }) { const { household, user } = await getCurrentSession(); await deleteNoteForScope({ householdId: household.id, userId: user.id, role: null }, input); revalidatePath("/notes"); } async function assertCanAccessNote(noteId: string, householdId: string) { if (!(await canAccessNote(noteId, householdId))) throw new Error("Forbidden"); }