|
|
|
@@ -1,39 +1,49 @@
|
|
|
|
|
"use server";
|
|
|
|
|
|
|
|
|
|
import { eq } from "drizzle-orm";
|
|
|
|
|
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 { logActivity } from "@/modules/_core/activity";
|
|
|
|
|
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, getNote } from "./queries";
|
|
|
|
|
import { canAccessNote, getNoteForScope, type NoteDto } from "./queries";
|
|
|
|
|
import { noteInput, updateNoteInput } from "./schemas";
|
|
|
|
|
|
|
|
|
|
const noteInput = z.object({
|
|
|
|
|
title: z.string().trim().min(1).max(200),
|
|
|
|
|
body: z.string().max(20000).default(""),
|
|
|
|
|
pinned: z.boolean().default(false),
|
|
|
|
|
remindAt: z.coerce.date().nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
function toScope(ctx: ApiAuthContext) {
|
|
|
|
|
return { householdId: ctx.householdId, userId: ctx.userId };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateNoteInput = z.object({
|
|
|
|
|
id: z.string().uuid(),
|
|
|
|
|
title: noteInput.shape.title.optional(),
|
|
|
|
|
body: z.string().max(20000).optional(),
|
|
|
|
|
pinned: z.boolean().optional(),
|
|
|
|
|
remindAt: z.coerce.date().nullable().optional(),
|
|
|
|
|
});
|
|
|
|
|
async function resolveAuthorId(scope: ApiAuthContext): Promise<string> {
|
|
|
|
|
if (scope.userId) return scope.userId;
|
|
|
|
|
|
|
|
|
|
export async function createNote(input: z.input<typeof noteInput>) {
|
|
|
|
|
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<typeof noteInput>,
|
|
|
|
|
): Promise<NoteDto> {
|
|
|
|
|
const parsed = noteInput.parse(input);
|
|
|
|
|
const { household, user } = await getCurrentSession();
|
|
|
|
|
const authorId = await resolveAuthorId(scope);
|
|
|
|
|
|
|
|
|
|
const [note] = await db
|
|
|
|
|
.insert(notes)
|
|
|
|
|
.values({
|
|
|
|
|
householdId: household.id,
|
|
|
|
|
authorId: user.id,
|
|
|
|
|
householdId: scope.householdId,
|
|
|
|
|
authorId,
|
|
|
|
|
title: parsed.title,
|
|
|
|
|
body: parsed.body,
|
|
|
|
|
pinned: parsed.pinned,
|
|
|
|
@@ -43,30 +53,51 @@ export async function createNote(input: z.input<typeof noteInput>) {
|
|
|
|
|
|
|
|
|
|
if (!note) throw new Error("Note was not created");
|
|
|
|
|
|
|
|
|
|
if (parsed.remindAt) {
|
|
|
|
|
if (parsed.remindAt && scope.userId) {
|
|
|
|
|
await scheduleReminder({
|
|
|
|
|
householdId: household.id,
|
|
|
|
|
householdId: scope.householdId,
|
|
|
|
|
entityType: "notes.note",
|
|
|
|
|
entityId: note.id,
|
|
|
|
|
fireAt: parsed.remindAt,
|
|
|
|
|
createdBy: user.id,
|
|
|
|
|
createdBy: scope.userId,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await logActivity({
|
|
|
|
|
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<typeof noteInput>) {
|
|
|
|
|
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 updateNote(input: z.input<typeof updateNoteInput>) {
|
|
|
|
|
export async function updateNoteForScope(
|
|
|
|
|
scope: ApiAuthContext,
|
|
|
|
|
input: z.input<typeof updateNoteInput>,
|
|
|
|
|
): Promise<NoteDto> {
|
|
|
|
|
const parsed = updateNoteInput.parse(input);
|
|
|
|
|
const { household, user } = await getCurrentSession();
|
|
|
|
|
await assertCanAccessNote(parsed.id, household.id);
|
|
|
|
|
await assertCanAccessNote(parsed.id, scope.householdId);
|
|
|
|
|
|
|
|
|
|
const [note] = await db
|
|
|
|
|
.update(notes)
|
|
|
|
@@ -82,34 +113,43 @@ export async function updateNote(input: z.input<typeof updateNoteInput>) {
|
|
|
|
|
|
|
|
|
|
if (!note) throw new Error("Note was not updated");
|
|
|
|
|
|
|
|
|
|
if (parsed.remindAt !== undefined) {
|
|
|
|
|
if (parsed.remindAt !== undefined && scope.userId) {
|
|
|
|
|
if (parsed.remindAt) {
|
|
|
|
|
await scheduleReminder({
|
|
|
|
|
householdId: household.id,
|
|
|
|
|
householdId: scope.householdId,
|
|
|
|
|
entityType: "notes.note",
|
|
|
|
|
entityId: note.id,
|
|
|
|
|
fireAt: parsed.remindAt,
|
|
|
|
|
createdBy: user.id,
|
|
|
|
|
createdBy: scope.userId,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
await cancelReminder("notes.note", note.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await logActivity({
|
|
|
|
|
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<typeof updateNoteInput>) {
|
|
|
|
|
const { household, user } = await getCurrentSession();
|
|
|
|
|
const note = await updateNoteForScope(
|
|
|
|
|
{ householdId: household.id, userId: user.id, role: null },
|
|
|
|
|
input,
|
|
|
|
|
);
|
|
|
|
|
revalidatePath("/notes");
|
|
|
|
|
revalidatePath(`/notes/${parsed.id}`);
|
|
|
|
|
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 } = await getCurrentSession();
|
|
|
|
|
const { household, user } = await getCurrentSession();
|
|
|
|
|
await assertCanAccessNote(parsed.id, household.id);
|
|
|
|
|
|
|
|
|
|
await db
|
|
|
|
@@ -117,34 +157,40 @@ export async function setNotePinned(input: { id: string; pinned: boolean }) {
|
|
|
|
|
.set({ pinned: parsed.pinned, updatedAt: new Date() })
|
|
|
|
|
.where(eq(notes.id, parsed.id));
|
|
|
|
|
|
|
|
|
|
const note = await getNote(parsed.id);
|
|
|
|
|
await logActivity({
|
|
|
|
|
entityType: "notes.note",
|
|
|
|
|
entityId: parsed.id,
|
|
|
|
|
action: parsed.pinned ? "pin" : "unpin",
|
|
|
|
|
payload: note ? { title: note.title } : undefined,
|
|
|
|
|
});
|
|
|
|
|
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 deleteNote(input: { id: string }) {
|
|
|
|
|
export async function deleteNoteForScope(scope: ApiAuthContext, input: { id: string }) {
|
|
|
|
|
const parsed = z.object({ id: z.string().uuid() }).parse(input);
|
|
|
|
|
const { household } = await getCurrentSession();
|
|
|
|
|
await assertCanAccessNote(parsed.id, household.id);
|
|
|
|
|
await assertCanAccessNote(parsed.id, scope.householdId);
|
|
|
|
|
|
|
|
|
|
const note = await getNote(parsed.id);
|
|
|
|
|
await logActivity({
|
|
|
|
|
const note = await getNoteForScope(scope.householdId, parsed.id);
|
|
|
|
|
await logActivityForScope(toScope(scope), {
|
|
|
|
|
entityType: "notes.note",
|
|
|
|
|
entityId: parsed.id,
|
|
|
|
|
action: "delete",
|
|
|
|
|
payload: note ? { title: note.title } : undefined,
|
|
|
|
|
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");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|