feat: api v1 routes for calendar lists and notes

This commit is contained in:
ginnoir
2026-07-04 19:03:08 -05:00
parent ea5d1d050c
commit d4304b005c
25 changed files with 1141 additions and 236 deletions
+106 -14
View File
@@ -7,6 +7,11 @@ import { getCurrentSession } from "@/lib/session";
import { householdMembers } from "@/modules/_core/schema";
import { calendarEvents, calendars } from "../schema";
export type ApiScope = {
householdId: string;
userId: string | null;
};
export type CalendarDto = {
id: string;
name: string;
@@ -32,6 +37,30 @@ const listEventsSchema = z.object({
calendarIds: z.union([z.literal("all"), z.array(z.string().uuid())]),
});
function calendarVisibilityFilter(scope: ApiScope) {
if (scope.userId) {
return or(eq(calendars.visibility, "household"), eq(calendars.ownerId, scope.userId));
}
return eq(calendars.visibility, "household");
}
export async function canSeeCalendarForScope(scope: ApiScope, calendarId: string) {
const [row] = await db
.select({
visibility: calendars.visibility,
ownerId: calendars.ownerId,
householdId: calendars.householdId,
})
.from(calendars)
.where(eq(calendars.id, calendarId))
.limit(1);
if (!row || row.householdId !== scope.householdId) return false;
if (row.visibility === "household") return true;
if (!scope.userId) return false;
return row.ownerId === scope.userId;
}
export async function canSeeCalendar(userId: string, calendarId: string) {
const [row] = await db
.select({
@@ -56,8 +85,7 @@ export async function canSeeCalendar(userId: string, calendarId: string) {
return row.memberUserId === userId;
}
export async function listCalendars(): Promise<CalendarDto[]> {
const { user, household } = await getCurrentSession();
export async function listCalendarsForScope(scope: ApiScope): Promise<CalendarDto[]> {
const rows = await db
.select({
id: calendars.id,
@@ -67,12 +95,7 @@ export async function listCalendars(): Promise<CalendarDto[]> {
ownerId: calendars.ownerId,
})
.from(calendars)
.where(
and(
eq(calendars.householdId, household.id),
or(eq(calendars.visibility, "household"), eq(calendars.ownerId, user.id)),
),
)
.where(and(eq(calendars.householdId, scope.householdId), calendarVisibilityFilter(scope)))
.orderBy(asc(calendars.name));
return rows.map((row) => ({
@@ -81,13 +104,48 @@ export async function listCalendars(): Promise<CalendarDto[]> {
}));
}
export async function listEvents(input: {
from: Date | string;
to: Date | string;
calendarIds: "all" | string[];
}): Promise<CalendarEventDto[]> {
export async function listCalendars(): Promise<CalendarDto[]> {
const { user, household } = await getCurrentSession();
return listCalendarsForScope({ householdId: household.id, userId: user.id });
}
export async function getCalendarForScope(scope: ApiScope, id: string): Promise<CalendarDto> {
const parsed = z.string().uuid().parse(id);
const [row] = await db
.select({
id: calendars.id,
name: calendars.name,
color: calendars.color,
visibility: calendars.visibility,
ownerId: calendars.ownerId,
householdId: calendars.householdId,
})
.from(calendars)
.where(and(eq(calendars.id, parsed), eq(calendars.householdId, scope.householdId)))
.limit(1);
if (!row) throw new Error("Calendar not found");
if (!(await canSeeCalendarForScope(scope, row.id))) throw new Error("Calendar not found");
return {
id: row.id,
name: row.name,
color: row.color,
visibility: row.visibility as "private" | "household",
ownerId: row.ownerId,
};
}
export async function listEventsForScope(
scope: ApiScope,
input: {
from: Date | string;
to: Date | string;
calendarIds: "all" | string[];
},
): Promise<CalendarEventDto[]> {
const parsed = listEventsSchema.parse(input);
const visibleCalendars = await listCalendars();
const visibleCalendars = await listCalendarsForScope(scope);
const visibleIds = new Set(visibleCalendars.map((calendar) => calendar.id));
const calendarIds =
parsed.calendarIds === "all"
@@ -120,6 +178,40 @@ export async function listEvents(input: {
return rows.map(toEventDto);
}
export async function listEvents(input: {
from: Date | string;
to: Date | string;
calendarIds: "all" | string[];
}): Promise<CalendarEventDto[]> {
const { user, household } = await getCurrentSession();
return listEventsForScope({ householdId: household.id, userId: user.id }, input);
}
export async function getEventForScope(scope: ApiScope, id: string): Promise<CalendarEventDto> {
const parsed = z.string().uuid().parse(id);
const [row] = await db
.select({
id: calendarEvents.id,
calendarId: calendarEvents.calendarId,
title: calendarEvents.title,
startAt: calendarEvents.startAt,
endAt: calendarEvents.endAt,
allDay: calendarEvents.allDay,
location: calendarEvents.location,
notes: calendarEvents.notes,
householdId: calendars.householdId,
})
.from(calendarEvents)
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
.where(and(eq(calendarEvents.id, parsed), eq(calendars.householdId, scope.householdId)))
.limit(1);
if (!row) throw new Error("Event not found");
if (!(await canSeeCalendarForScope(scope, row.calendarId))) throw new Error("Event not found");
return toEventDto(row);
}
export async function searchCalendars(query: string, householdId: string) {
const rows = await db
.select({ id: calendars.id, name: calendars.name })