lifetracker/packages/trpc/routers/days.ts
2025-01-31 18:09:27 -08:00

136 lines
4.4 KiB
TypeScript

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];
}),
});