Implement lists module and dev login setup
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
"use server";
|
||||
|
||||
import { and, eq, max } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import { db } from "@/lib/db";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { listItems, lists } from "../schema";
|
||||
import { getOrCreateDefaultList } from "./defaults";
|
||||
import { canAccessList, getList } from "./queries";
|
||||
import { notifyListChanged } from "./realtime";
|
||||
|
||||
const listInput = z.object({
|
||||
type: z.string().trim().min(1).max(80),
|
||||
name: z.string().trim().min(1).max(120),
|
||||
});
|
||||
|
||||
const itemInput = z.object({
|
||||
listId: z.string().uuid(),
|
||||
text: z.string().trim().min(1).max(300),
|
||||
qty: z.string().trim().max(80).nullable().optional(),
|
||||
notes: z.string().trim().max(2000).nullable().optional(),
|
||||
dueAt: z.coerce.date().nullable().optional(),
|
||||
assigneeId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
const updateItemInput = z.object({
|
||||
id: z.string().uuid(),
|
||||
text: z.string().trim().min(1).max(300).optional(),
|
||||
qty: z.string().trim().max(80).nullable().optional(),
|
||||
notes: z.string().trim().max(2000).nullable().optional(),
|
||||
dueAt: z.coerce.date().nullable().optional(),
|
||||
assigneeId: z.string().uuid().nullable().optional(),
|
||||
});
|
||||
|
||||
export async function createList(input: z.input<typeof listInput>) {
|
||||
const parsed = listInput.parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
const [list] = await db
|
||||
.insert(lists)
|
||||
.values({
|
||||
householdId: household.id,
|
||||
type: parsed.type,
|
||||
name: parsed.name,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!list) throw new Error("List was not created");
|
||||
revalidatePath("/lists");
|
||||
return list;
|
||||
}
|
||||
|
||||
export async function renameList(input: { id: string; name: string }) {
|
||||
const parsed = z.object({ id: z.string().uuid(), name: listInput.shape.name }).parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
await assertCanAccessList(parsed.id, household.id);
|
||||
await db.update(lists).set({ name: parsed.name }).where(eq(lists.id, parsed.id));
|
||||
revalidatePath("/lists");
|
||||
revalidatePath(`/lists/${parsed.id}`);
|
||||
await notifyListChanged(parsed.id);
|
||||
}
|
||||
|
||||
export async function archiveList(input: { id: string }) {
|
||||
const parsed = z.object({ id: z.string().uuid() }).parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
await assertCanAccessList(parsed.id, household.id);
|
||||
await db.update(lists).set({ archived: true }).where(eq(lists.id, parsed.id));
|
||||
revalidatePath("/lists");
|
||||
revalidatePath(`/lists/${parsed.id}`);
|
||||
await notifyListChanged(parsed.id);
|
||||
}
|
||||
|
||||
export async function addItem(input: z.input<typeof itemInput>) {
|
||||
const parsed = itemInput.parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
await assertCanAccessList(parsed.listId, household.id);
|
||||
|
||||
const [positionRow] = await db
|
||||
.select({ maxPosition: max(listItems.position) })
|
||||
.from(listItems)
|
||||
.where(eq(listItems.listId, parsed.listId));
|
||||
|
||||
const [item] = await db
|
||||
.insert(listItems)
|
||||
.values({
|
||||
listId: parsed.listId,
|
||||
text: parsed.text,
|
||||
qty: parsed.qty || null,
|
||||
notes: parsed.notes || null,
|
||||
dueAt: parsed.dueAt ?? null,
|
||||
assigneeId: parsed.assigneeId ?? null,
|
||||
position: (positionRow?.maxPosition ?? -1) + 1,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!item) throw new Error("List item was not created");
|
||||
revalidatePath(`/lists/${parsed.listId}`);
|
||||
await notifyListChanged(parsed.listId);
|
||||
return getList(parsed.listId);
|
||||
}
|
||||
|
||||
export async function addItemToDefaultList(input: { type: string; text: string }) {
|
||||
const parsed = z
|
||||
.object({
|
||||
type: listInput.shape.type,
|
||||
text: itemInput.shape.text,
|
||||
})
|
||||
.parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
const list = await getOrCreateDefaultList({ householdId: household.id, type: parsed.type });
|
||||
return addItem({ listId: list.id, text: parsed.text });
|
||||
}
|
||||
|
||||
export async function toggleItem(input: { id: string; done?: boolean }) {
|
||||
const parsed = z.object({ id: z.string().uuid(), done: z.boolean().optional() }).parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
const existing = await getAuthorizedItem(parsed.id, household.id);
|
||||
const done = parsed.done ?? !existing.done;
|
||||
|
||||
await db
|
||||
.update(listItems)
|
||||
.set({ done, updatedAt: new Date() })
|
||||
.where(eq(listItems.id, parsed.id));
|
||||
|
||||
revalidatePath(`/lists/${existing.listId}`);
|
||||
await notifyListChanged(existing.listId);
|
||||
return getList(existing.listId);
|
||||
}
|
||||
|
||||
export async function updateItem(input: z.input<typeof updateItemInput>) {
|
||||
const parsed = updateItemInput.parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
const existing = await getAuthorizedItem(parsed.id, household.id);
|
||||
|
||||
await db
|
||||
.update(listItems)
|
||||
.set({
|
||||
text: parsed.text,
|
||||
qty: parsed.qty === undefined ? undefined : parsed.qty || null,
|
||||
notes: parsed.notes === undefined ? undefined : parsed.notes || null,
|
||||
dueAt: parsed.dueAt === undefined ? undefined : parsed.dueAt,
|
||||
assigneeId: parsed.assigneeId === undefined ? undefined : parsed.assigneeId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(listItems.id, parsed.id));
|
||||
|
||||
revalidatePath(`/lists/${existing.listId}`);
|
||||
await notifyListChanged(existing.listId);
|
||||
return getList(existing.listId);
|
||||
}
|
||||
|
||||
export async function deleteItem(input: { id: string }) {
|
||||
const parsed = z.object({ id: z.string().uuid() }).parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
const existing = await getAuthorizedItem(parsed.id, household.id);
|
||||
await db.delete(listItems).where(eq(listItems.id, parsed.id));
|
||||
revalidatePath(`/lists/${existing.listId}`);
|
||||
await notifyListChanged(existing.listId);
|
||||
return getList(existing.listId);
|
||||
}
|
||||
|
||||
export async function reorderItems(input: { listId: string; itemIds: string[] }) {
|
||||
const parsed = z
|
||||
.object({ listId: z.string().uuid(), itemIds: z.array(z.string().uuid()) })
|
||||
.parse(input);
|
||||
const { household } = await getCurrentSession();
|
||||
await assertCanAccessList(parsed.listId, household.id);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
for (const [position, id] of parsed.itemIds.entries()) {
|
||||
await tx
|
||||
.update(listItems)
|
||||
.set({ position, updatedAt: new Date() })
|
||||
.where(and(eq(listItems.id, id), eq(listItems.listId, parsed.listId)));
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath(`/lists/${parsed.listId}`);
|
||||
await notifyListChanged(parsed.listId);
|
||||
return getList(parsed.listId);
|
||||
}
|
||||
|
||||
async function assertCanAccessList(listId: string, householdId: string) {
|
||||
if (!(await canAccessList(listId, householdId))) throw new Error("Forbidden");
|
||||
}
|
||||
|
||||
async function getAuthorizedItem(itemId: string, householdId: string) {
|
||||
const [item] = await db
|
||||
.select({
|
||||
id: listItems.id,
|
||||
listId: listItems.listId,
|
||||
done: listItems.done,
|
||||
})
|
||||
.from(listItems)
|
||||
.innerJoin(lists, eq(listItems.listId, lists.id))
|
||||
.where(and(eq(listItems.id, itemId), eq(lists.householdId, householdId)))
|
||||
.limit(1);
|
||||
|
||||
if (!item) throw new Error("Item not found");
|
||||
return item;
|
||||
}
|
||||
Reference in New Issue
Block a user