227 lines
9.5 KiB
TypeScript
227 lines
9.5 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { and, desc, eq, inArray, notExists, sql } from "drizzle-orm";
|
|
import { z, ZodNull } from "zod";
|
|
|
|
import { DatabaseError } from "@lifetracker/db";
|
|
import { colors, metrics, measurements, hours, days } from "@lifetracker/db/schema";
|
|
import { zMetricSchema, zMeasurementSchema } from "@lifetracker/shared/types/metrics";
|
|
import type { Context } from "../index";
|
|
import { authedProcedure, router } from "../index";
|
|
import { zColorSchema } from "@lifetracker/shared/types/colors";
|
|
import { titleCase } from "title-case";
|
|
import { listOfDates, listOfDayIds } from "./days";
|
|
import { getHours } from "./hours";
|
|
import { datetime } from "drizzle-orm/mysql-core";
|
|
|
|
const getMetricFromInput = async (ctx: Context, input:
|
|
{ metricId?: string | null | undefined; metricName?: string | null | undefined; }) => {
|
|
const metric = input.metricName
|
|
? await ctx.db.select().from(metrics).where(eq(metrics.name, titleCase(input.metricName)))
|
|
: await ctx.db.select().from(metrics).where(eq(metrics.id, input.metricId!));
|
|
|
|
if (!metric[0]) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Metric not found",
|
|
});
|
|
}
|
|
return metric[0];
|
|
}
|
|
|
|
|
|
export const measurementsAppRouter = router({
|
|
list: authedProcedure
|
|
.input(z.object({ hourId: z.string() }))
|
|
.output(z.array(zMeasurementSchema))
|
|
.query(async ({ input, ctx }) => {
|
|
const dbMeasurements = await ctx.db
|
|
.select()
|
|
.from(measurements)
|
|
.leftJoin(hours, eq(measurements.hourId, hours.id))
|
|
.where(and(eq(measurements.userId, ctx.user.id),
|
|
eq(hours.id, input.hourId))
|
|
);
|
|
console.log(dbMeasurements.length);
|
|
return dbMeasurements;
|
|
}),
|
|
setValue: authedProcedure
|
|
.input(z.object({
|
|
metricId: z.string().nullish(), metricName: z.string().nullish(),
|
|
hourId: z.string(), dayId: z.string(), value: z.number()
|
|
}))
|
|
.output(zMeasurementSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
if (!input.metricId && !input.metricName) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Metric name or id is required",
|
|
});
|
|
}
|
|
const metric = await getMetricFromInput(ctx, input);
|
|
|
|
// Check if there is a measurement for this metric in this hour, if so, update it, if not, create it
|
|
const existingMeasurement = await ctx.db.select().from(measurements).where(and(
|
|
eq(measurements.metricId, metric.id),
|
|
eq(measurements.hourId, input.hourId),
|
|
));
|
|
if (existingMeasurement[0]) {
|
|
const updatedMeasurement = await ctx.db.update(measurements).set({
|
|
value: input.value.toString(),
|
|
}).where(eq(measurements.id, existingMeasurement[0].id)).returning();
|
|
|
|
return {
|
|
...updatedMeasurement[0],
|
|
icon: metric.icon,
|
|
metricName: metric.name,
|
|
};
|
|
} else {
|
|
const newMeasurement = await ctx.db.insert(measurements).values({
|
|
metricId: metric.id,
|
|
hourId: input.hourId,
|
|
dayId: input.dayId,
|
|
value: input.value.toString(),
|
|
userId: ctx.user.id,
|
|
}).returning();
|
|
return {
|
|
...newMeasurement[0],
|
|
icon: metric.icon,
|
|
metricName: metric.name,
|
|
};
|
|
}
|
|
}),
|
|
incrementCount: authedProcedure
|
|
.input(z.object({ metricId: z.string().nullish(), metricName: z.string().nullish(), hourId: z.string(), dayId: z.string() }))
|
|
.output(zMeasurementSchema)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const metric = await getMetricFromInput(ctx, input);
|
|
if (!metric) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Metric not found",
|
|
});
|
|
}
|
|
// Check if there is a measurement for this metric in this hour, if so, increment it, if not, create it with value 1
|
|
const existingMeasurement = await ctx.db.select().from(measurements).where(and(
|
|
eq(measurements.metricId, input.metricId),
|
|
eq(measurements.hourId, input.hourId),
|
|
));
|
|
if (existingMeasurement[0]) {
|
|
const updatedMeasurement = await ctx.db.update(measurements).set({
|
|
value: (parseInt(existingMeasurement[0].value) + 1).toString(),
|
|
}).where(eq(measurements.id, existingMeasurement[0].id)).returning();
|
|
|
|
return {
|
|
...updatedMeasurement[0],
|
|
icon: metric.icon,
|
|
metricName: metric.name,
|
|
};
|
|
} else {
|
|
const newMeasurement = await ctx.db.insert(measurements).values({
|
|
metricId: input.metricId,
|
|
hourId: input.hourId,
|
|
dayId: input.dayId,
|
|
value: 1,
|
|
userId: ctx.user.id,
|
|
}).returning();
|
|
return {
|
|
...newMeasurement[0],
|
|
icon: metric.icon,
|
|
metricName: metric.name,
|
|
};
|
|
}
|
|
}),
|
|
decrementCount: authedProcedure
|
|
.input(z.object({ metricId: z.string(), hourId: z.string() }))
|
|
.output(zMeasurementSchema.optional())
|
|
.mutation(async ({ input, ctx }) => {
|
|
const metric = await ctx.db.select().from(metrics).where(eq(metrics.id, input.metricId));
|
|
if (!metric[0]) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Metric not found",
|
|
});
|
|
}
|
|
// Check if there is a measurement for this metric in this hour, if so, decrement it, if not, throw an error
|
|
const existingMeasurement = await ctx.db.select().from(measurements).where(and(
|
|
eq(measurements.metricId, input.metricId),
|
|
eq(measurements.hourId, input.hourId),
|
|
));
|
|
if (existingMeasurement[0]) {
|
|
if (parseInt(existingMeasurement[0].value) > 1) {
|
|
const updatedMeasurement = await ctx.db.update(measurements).set({
|
|
value: (parseInt(existingMeasurement[0].value) - 1).toString(),
|
|
}).where(eq(measurements.id, existingMeasurement[0].id)).returning();
|
|
|
|
return {
|
|
...updatedMeasurement[0],
|
|
icon: metric[0].icon,
|
|
metricName: metric[0].name,
|
|
};
|
|
} else {
|
|
// Delete the measurement if it's zerooo
|
|
await ctx.db.delete(measurements).where(eq(measurements.id, existingMeasurement[0].id));
|
|
}
|
|
} else {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Measurement not found",
|
|
});
|
|
}
|
|
}),
|
|
timeSinceLastMeasurement: authedProcedure
|
|
.input(z.object({ metricId: z.string() }))
|
|
.output(z.number())
|
|
.query(async ({ input, ctx }) => {
|
|
const lastMeasurement = await ctx.db.select().from(measurements).where(and(
|
|
eq(measurements.metricId, input.metricId),
|
|
eq(measurements.userId, ctx.user.id),
|
|
)).orderBy(desc(measurements)).limit(1);
|
|
if (lastMeasurement[0]) {
|
|
const lastMeasurementTime = new Date(lastMeasurement[0].createdAt).getTime();
|
|
const currentTime = new Date().getTime();
|
|
return (currentTime - lastMeasurementTime) / 1000;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}),
|
|
getTimeseries: authedProcedure
|
|
.input(z.object({
|
|
dateRange: z.tuple([z.date(), z.date()]),
|
|
timezone: z.string(),
|
|
metricName: z.string()
|
|
}))
|
|
.output(z.array(z.object({
|
|
id: z.string(),
|
|
value: z.string(),
|
|
unit: z.string(),
|
|
datetime: z.date(),
|
|
})
|
|
))
|
|
.query(async ({ input, ctx }) => {
|
|
const hoursList = await getHours(ctx, {
|
|
dateQuery: input.dateRange,
|
|
timezone: input.timezone,
|
|
});
|
|
const hourIds = hoursList.map((hour) => hour.id);
|
|
const metric = await ctx.db.select().from(metrics).where(eq(metrics.name, titleCase(input.metricName)));
|
|
if (!metric[0]) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Metric not found",
|
|
});
|
|
}
|
|
const measurementsList = await ctx.db.select({
|
|
id: measurements.id,
|
|
value: measurements.value,
|
|
unit: metrics.unit,
|
|
datetime: hours.datetime,
|
|
}).from(measurements)
|
|
.leftJoin(metrics, eq(measurements.metricId, metrics.id))
|
|
.leftJoin(hours, eq(measurements.hourId, hours.id))
|
|
.where(and(
|
|
eq(measurements.metricId, metric[0].id),
|
|
inArray(measurements.hourId, hourIds),
|
|
));
|
|
return measurementsList;
|
|
}),
|
|
}); |