import { experimental_trpcMiddleware, TRPCError } from "@trpc/server"; import { and, desc, eq, inArray, notExists } from "drizzle-orm"; import { z } from "zod"; import { db, DatabaseError, ErrorCodes } from "@lifetracker/db"; import { categories, days, hours, users } from "@lifetracker/db/schema"; import { zDaySchema, ZDay, ZHour } from "@lifetracker/shared/types/days"; import type { Context } from "../index"; import { authedProcedure, router } from "../index"; import { dateFromInput, hoursListInUTC } from "@lifetracker/shared/utils/days"; import { addDays, closestIndexTo, format, parseISO } from "date-fns"; import { TZDate } from "@date-fns/tz"; import { hoursAppRouter, hourColors, hourJoinsQuery, getHours, createHour } from "./hours"; import spacetime from "spacetime"; import { getHourFromTime, getTimeFromHour } from "@lifetracker/shared/utils/hours"; export async function createDay(date: string, ctx: Context) { return await ctx.db.transaction(async (trx) => { try { // Create the Day object const dayRes = await trx .insert(days) .values({ userId: ctx.user!.id, date: date, }) .returning({ id: days.id, date: days.date, mood: days.mood, comment: days.comment, }); return dayRes[0]; } catch (e) { if (e instanceof DatabaseError) { if (e.code == ErrorCodes.UNIQUE_VIOLATION) { throw new TRPCError({ code: "BAD_REQUEST", message: "This day already exists", }); } } throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Something went wrong", }); } }); } export async function getDay(ctx: Context, input: { dateQuery: string, timezone: string }) { const dayDate = dateFromInput(input); const dayRes = await ctx.db.select({ id: days.id, date: days.date, mood: days.mood, comment: days.comment, }) .from(days) .where(eq(days.date, dayDate)); const day = dayRes.length == 0 ? (await createDay(dayDate, ctx)) : (dayRes[0]); const dayHours: ZHour[] = await getHours(ctx, input); return { hours: dayHours, ...day } } export function listOfDates(dateRange: [Date, Date]) { const [start, end] = dateRange.map((date) => dateFromInput({ dateQuery: spacetime(date, "UTC").goto("UTC").format("iso-short"), timezone: "Etc/UTC" })); const dates = []; let currentDate = parseISO(start); while (currentDate <= parseISO(end)) { dates.push(format(currentDate, "yyyy-MM-dd")); currentDate = addDays(currentDate, 1); } return dates.length === 0 ? [format(parseISO(start), "yyyy-MM-dd")] : dates; } export function listOfDayIds(dateRange: [Date, Date]) { return listOfDates(dateRange).map((date) => db.select({ id: days.id }) .from(days) .where(eq(days.date, date)) ); } export const daysAppRouter = router({ get: authedProcedure .input(z.object({ dateQuery: z.string(), timezone: z.string(), })) .output(zDaySchema) .query(async ({ input, ctx }) => { // Get a Day return await getDay(ctx, input); }), update: authedProcedure .input( z.object({ mood: z.string().optional().or(z.number()), comment: z.string().optional(), dateQuery: z.string(), timezone: z.string().optional(), }), ) .mutation(async ({ input, ctx }) => { const { dateQuery, timezone, ...updatedProps } = input; // Convert mood to number, if it exists if (updatedProps.mood) { updatedProps.mood = parseInt(updatedProps.mood); } const res = await ctx.db .update(days) .set(updatedProps) .where(eq(days.date, dateFromInput({ dateQuery: dateQuery, timezone: timezone ?? ctx.user.timezone }) )).returning(); return res[0]; }), });