feat: api v1 routes for calendar lists and notes
This commit is contained in:
@@ -5,52 +5,60 @@ import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
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 { calendarEvents, calendars } from "../schema";
|
||||
import { canSeeCalendar } from "./queries";
|
||||
import { canSeeCalendarForScope, type ApiScope, type CalendarEventDto } from "./queries";
|
||||
import { calendarInput, calendarUpdateInput, eventInput, eventUpdateInput } from "./schemas";
|
||||
|
||||
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"),
|
||||
});
|
||||
async function resolveOwnerId(scope: ApiScope): Promise<string> {
|
||||
if (scope.userId) return scope.userId;
|
||||
|
||||
const eventBaseInput = 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(),
|
||||
remindMinutesBefore: z.number().int().min(0).nullable().optional(),
|
||||
});
|
||||
const [member] = await db
|
||||
.select({ userId: householdMembers.userId })
|
||||
.from(householdMembers)
|
||||
.where(
|
||||
and(eq(householdMembers.householdId, scope.householdId), eq(householdMembers.role, "owner")),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
const eventInput = eventBaseInput.refine((value) => value.endAt >= value.startAt, {
|
||||
path: ["endAt"],
|
||||
message: "End must be after start",
|
||||
});
|
||||
if (!member) throw new Error("No household owner found");
|
||||
return member.userId;
|
||||
}
|
||||
|
||||
const eventUpdateInput = eventBaseInput.partial().refine(
|
||||
(value) => {
|
||||
if (!value.startAt || !value.endAt) return true;
|
||||
return value.endAt >= value.startAt;
|
||||
},
|
||||
{
|
||||
path: ["endAt"],
|
||||
message: "End must be after start",
|
||||
},
|
||||
);
|
||||
async function assertCanModifyCalendar(scope: ApiScope, calendarId: string) {
|
||||
if (scope.userId) {
|
||||
await assertOwnsCalendar(scope.userId, calendarId);
|
||||
return;
|
||||
}
|
||||
|
||||
export async function createCalendar(input: z.input<typeof calendarInput>) {
|
||||
const [calendar] = await db
|
||||
.select({ visibility: calendars.visibility, householdId: calendars.householdId })
|
||||
.from(calendars)
|
||||
.where(eq(calendars.id, calendarId))
|
||||
.limit(1);
|
||||
|
||||
if (!calendar || calendar.householdId !== scope.householdId)
|
||||
throw new Error("Calendar not found");
|
||||
if (calendar.visibility !== "household") throw new Error("Forbidden");
|
||||
}
|
||||
|
||||
export async function createCalendarForScope(
|
||||
scope: ApiScope,
|
||||
input: z.input<typeof calendarInput>,
|
||||
) {
|
||||
const parsed = calendarInput.parse(input);
|
||||
const { user, household } = await getCurrentSession();
|
||||
if (!scope.userId && parsed.visibility === "private") {
|
||||
throw new Error("Bearer tokens cannot create private calendars");
|
||||
}
|
||||
|
||||
const ownerId = await resolveOwnerId(scope);
|
||||
const [calendar] = await db
|
||||
.insert(calendars)
|
||||
.values({
|
||||
householdId: household.id,
|
||||
ownerId: user.id,
|
||||
householdId: scope.householdId,
|
||||
ownerId,
|
||||
name: parsed.name,
|
||||
color: parsed.color ?? null,
|
||||
visibility: parsed.visibility,
|
||||
@@ -58,31 +66,64 @@ export async function createCalendar(input: z.input<typeof calendarInput>) {
|
||||
.returning();
|
||||
|
||||
if (!calendar) throw new Error("Calendar was not created");
|
||||
await logActivity({
|
||||
await logActivityForScope(scope, {
|
||||
entityType: "calendar.calendar",
|
||||
entityId: calendar.id,
|
||||
action: "create",
|
||||
payload: { name: calendar.name },
|
||||
});
|
||||
return calendar;
|
||||
}
|
||||
|
||||
export async function createCalendar(input: z.input<typeof calendarInput>) {
|
||||
const { user, household } = await getCurrentSession();
|
||||
const calendar = await createCalendarForScope(
|
||||
{ householdId: household.id, userId: user.id },
|
||||
input,
|
||||
);
|
||||
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);
|
||||
export async function updateCalendarForScope(
|
||||
scope: ApiScope,
|
||||
input: { id: string } & z.input<typeof calendarUpdateInput>,
|
||||
) {
|
||||
const parsed = z.object({ id: z.string().uuid() }).and(calendarUpdateInput).parse(input);
|
||||
await assertCanModifyCalendar(scope, parsed.id);
|
||||
|
||||
if (!scope.userId && parsed.visibility === "private") {
|
||||
throw new Error("Bearer tokens cannot set private visibility");
|
||||
}
|
||||
|
||||
await db
|
||||
.update(calendars)
|
||||
.set({ name: parsed.name, updatedAt: new Date() })
|
||||
.set({
|
||||
name: parsed.name,
|
||||
visibility: parsed.visibility,
|
||||
color: parsed.color === undefined ? undefined : (parsed.color ?? null),
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(calendars.id, parsed.id));
|
||||
|
||||
await logActivity({
|
||||
await logActivityForScope(scope, {
|
||||
entityType: "calendar.calendar",
|
||||
entityId: parsed.id,
|
||||
action: "update",
|
||||
payload: { name: parsed.name },
|
||||
payload: {
|
||||
...(parsed.name ? { name: parsed.name } : {}),
|
||||
...(parsed.visibility ? { visibility: parsed.visibility } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function renameCalendar(input: { id: string; name: string }) {
|
||||
const { user, household } = await getCurrentSession();
|
||||
const parsed = z.object({ id: z.string().uuid(), name: calendarInput.shape.name }).parse(input);
|
||||
await updateCalendarForScope(
|
||||
{ householdId: household.id, userId: user.id },
|
||||
{ id: parsed.id, name: parsed.name },
|
||||
);
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
@@ -90,51 +131,52 @@ export async function setCalendarVisibility(input: {
|
||||
id: string;
|
||||
visibility: "private" | "household";
|
||||
}) {
|
||||
const { user } = await getCurrentSession();
|
||||
const { user, household } = 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));
|
||||
|
||||
await logActivity({
|
||||
entityType: "calendar.calendar",
|
||||
entityId: parsed.id,
|
||||
action: "update",
|
||||
payload: { visibility: parsed.visibility },
|
||||
});
|
||||
await updateCalendarForScope(
|
||||
{ householdId: household.id, userId: user.id },
|
||||
{ id: parsed.id, visibility: parsed.visibility },
|
||||
);
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
export async function setCalendarColor(input: { id: string; color: string | null }) {
|
||||
const { user } = await getCurrentSession();
|
||||
const { user, household } = 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));
|
||||
|
||||
await updateCalendarForScope(
|
||||
{ householdId: household.id, userId: user.id },
|
||||
{ id: parsed.id, color: parsed.color },
|
||||
);
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
export async function deleteCalendarForScope(scope: ApiScope, input: { id: string }) {
|
||||
const parsed = z.object({ id: z.string().uuid() }).parse(input);
|
||||
await assertCanModifyCalendar(scope, parsed.id);
|
||||
await logActivityForScope(scope, {
|
||||
entityType: "calendar.calendar",
|
||||
entityId: parsed.id,
|
||||
action: "delete",
|
||||
});
|
||||
await db.delete(calendars).where(eq(calendars.id, parsed.id));
|
||||
}
|
||||
|
||||
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 logActivity({ entityType: "calendar.calendar", entityId: parsed.id, action: "delete" });
|
||||
await db.delete(calendars).where(eq(calendars.id, parsed.id));
|
||||
const { user, household } = await getCurrentSession();
|
||||
await deleteCalendarForScope({ householdId: household.id, userId: user.id }, input);
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
export async function createEvent(input: z.input<typeof eventInput>) {
|
||||
export async function createEventForScope(
|
||||
scope: ApiScope,
|
||||
input: z.input<typeof eventInput>,
|
||||
): Promise<CalendarEventDto> {
|
||||
const parsed = eventInput.parse(input);
|
||||
const { user, household } = await getCurrentSession();
|
||||
if (!(await canSeeCalendar(user.id, parsed.calendarId))) throw new Error("Forbidden");
|
||||
if (!(await canSeeCalendarForScope(scope, parsed.calendarId))) throw new Error("Forbidden");
|
||||
|
||||
const ownerId = await resolveOwnerId(scope);
|
||||
const [event] = await db
|
||||
.insert(calendarEvents)
|
||||
.values({
|
||||
@@ -143,7 +185,7 @@ export async function createEvent(input: z.input<typeof eventInput>) {
|
||||
startAt: parsed.startAt,
|
||||
endAt: parsed.endAt,
|
||||
allDay: parsed.allDay,
|
||||
ownerId: user.id,
|
||||
ownerId,
|
||||
location: parsed.location || null,
|
||||
notes: parsed.notes || null,
|
||||
})
|
||||
@@ -151,26 +193,26 @@ export async function createEvent(input: z.input<typeof eventInput>) {
|
||||
|
||||
if (!event) throw new Error("Event was not created");
|
||||
|
||||
if (parsed.remindMinutesBefore != null) {
|
||||
if (parsed.remindMinutesBefore != null && scope.userId) {
|
||||
const fireAt = new Date(parsed.startAt.getTime() - parsed.remindMinutesBefore * 60_000);
|
||||
if (fireAt > new Date()) {
|
||||
await scheduleReminder({
|
||||
householdId: household.id,
|
||||
householdId: scope.householdId,
|
||||
entityType: "calendar.event",
|
||||
entityId: event.id,
|
||||
fireAt,
|
||||
createdBy: user.id,
|
||||
createdBy: scope.userId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await logActivity({
|
||||
await logActivityForScope(scope, {
|
||||
entityType: "calendar.event",
|
||||
entityId: event.id,
|
||||
action: "create",
|
||||
payload: { title: event.title },
|
||||
});
|
||||
revalidatePath("/calendar");
|
||||
|
||||
return {
|
||||
...event,
|
||||
startAt: event.startAt.toISOString(),
|
||||
@@ -178,9 +220,18 @@ export async function createEvent(input: z.input<typeof eventInput>) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateEvent(input: { id: string } & Partial<z.input<typeof eventInput>>) {
|
||||
export async function createEvent(input: z.input<typeof eventInput>) {
|
||||
const { user, household } = await getCurrentSession();
|
||||
const event = await createEventForScope({ householdId: household.id, userId: user.id }, input);
|
||||
revalidatePath("/calendar");
|
||||
return event;
|
||||
}
|
||||
|
||||
export async function updateEventForScope(
|
||||
scope: ApiScope,
|
||||
input: { id: string } & Partial<z.input<typeof eventInput>>,
|
||||
) {
|
||||
const parsed = z.object({ id: z.string().uuid() }).and(eventUpdateInput).parse(input);
|
||||
const { user } = await getCurrentSession();
|
||||
const [existing] = await db
|
||||
.select({ calendarId: calendarEvents.calendarId })
|
||||
.from(calendarEvents)
|
||||
@@ -189,7 +240,7 @@ export async function updateEvent(input: { id: string } & Partial<z.input<typeof
|
||||
|
||||
if (!existing) throw new Error("Event not found");
|
||||
const calendarId = parsed.calendarId ?? existing.calendarId;
|
||||
if (!(await canSeeCalendar(user.id, calendarId))) throw new Error("Forbidden");
|
||||
if (!(await canSeeCalendarForScope(scope, calendarId))) throw new Error("Forbidden");
|
||||
|
||||
await db
|
||||
.update(calendarEvents)
|
||||
@@ -205,29 +256,42 @@ export async function updateEvent(input: { id: string } & Partial<z.input<typeof
|
||||
})
|
||||
.where(eq(calendarEvents.id, parsed.id));
|
||||
|
||||
await logActivity({
|
||||
await logActivityForScope(scope, {
|
||||
entityType: "calendar.event",
|
||||
entityId: parsed.id,
|
||||
action: "update",
|
||||
payload: parsed.title ? { title: parsed.title } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateEvent(input: { id: string } & Partial<z.input<typeof eventInput>>) {
|
||||
const { user, household } = await getCurrentSession();
|
||||
await updateEventForScope({ householdId: household.id, userId: user.id }, input);
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
export async function deleteEvent(input: { id: string }) {
|
||||
export async function deleteEventForScope(scope: ApiScope, 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 logActivity({ entityType: "calendar.event", entityId: parsed.id, action: "delete" });
|
||||
if (!existing) throw new Error("Event not found");
|
||||
if (!(await canSeeCalendarForScope(scope, existing.calendarId))) throw new Error("Forbidden");
|
||||
await logActivityForScope(scope, {
|
||||
entityType: "calendar.event",
|
||||
entityId: parsed.id,
|
||||
action: "delete",
|
||||
});
|
||||
await cancelReminder("calendar.event", parsed.id);
|
||||
await db.delete(calendarEvents).where(eq(calendarEvents.id, parsed.id));
|
||||
}
|
||||
|
||||
export async function deleteEvent(input: { id: string }) {
|
||||
const { user, household } = await getCurrentSession();
|
||||
await deleteEventForScope({ householdId: household.id, userId: user.id }, input);
|
||||
revalidatePath("/calendar");
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export 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"),
|
||||
});
|
||||
|
||||
export const calendarUpdateInput = z.object({
|
||||
name: calendarInput.shape.name.optional(),
|
||||
visibility: calendarInput.shape.visibility.optional(),
|
||||
color: calendarInput.shape.color,
|
||||
});
|
||||
|
||||
export const eventBaseInput = 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(),
|
||||
remindMinutesBefore: z.number().int().min(0).nullable().optional(),
|
||||
});
|
||||
|
||||
export const eventInput = eventBaseInput.refine((value) => value.endAt >= value.startAt, {
|
||||
path: ["endAt"],
|
||||
message: "End must be after start",
|
||||
});
|
||||
|
||||
export const eventUpdateInput = eventBaseInput.partial().refine(
|
||||
(value) => {
|
||||
if (!value.startAt || !value.endAt) return true;
|
||||
return value.endAt >= value.startAt;
|
||||
},
|
||||
{
|
||||
path: ["endAt"],
|
||||
message: "End must be after start",
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user