Files
knur-app/lib/models/running.ts
Dominik Klarkowski 36407f534b init
2026-06-16 09:43:48 +02:00

97 lines
3.3 KiB
TypeScript

import { ObjectId } from "mongodb";
import { z } from "zod";
import { getDb } from "@/lib/db";
export const runningActivitySchema = z.object({
garminActivityId: z.number().int(),
name: z.string().min(1),
startTime: z.date(),
durationSec: z.number().positive(),
distanceM: z.number().nonnegative(),
avgPaceSecPerKm: z.number().nonnegative(),
avgHr: z.number().positive().optional(),
maxHr: z.number().positive().optional(),
calories: z.number().nonnegative().optional(),
elevationGainM: z.number().nonnegative().optional(),
avgCadence: z.number().nonnegative().optional(),
avgVerticalOscillationCm: z.number().nonnegative().optional(),
avgGroundContactTimeMs: z.number().nonnegative().optional(),
avgStrideLengthCm: z.number().nonnegative().optional(),
avgGroundContactBalanceLeftPct: z.number().nonnegative().optional(),
avgVerticalRatioPct: z.number().nonnegative().optional(),
vo2Max: z.number().nonnegative().optional(),
aerobicTrainingEffect: z.number().nonnegative().optional(),
anaerobicTrainingEffect: z.number().nonnegative().optional(),
trainingEffectLabel: z.string().optional(),
avgPowerW: z.number().nonnegative().optional(),
maxPowerW: z.number().nonnegative().optional(),
normPowerW: z.number().nonnegative().optional(),
avgRespirationRate: z.number().nonnegative().optional(),
hasRoute: z.boolean().optional(),
});
export type RunningActivityInput = z.infer<typeof runningActivitySchema>;
export type RoutePoint = [number, number];
export type RunningActivity = RunningActivityInput & {
_id: ObjectId;
createdAt: Date;
routePoints?: RoutePoint[];
};
const COLLECTION = "running_activities";
const SYNC_STATE_COLLECTION = "sync_state";
async function getCollection() {
const db = await getDb();
const collection = db.collection<RunningActivity>(COLLECTION);
await collection.createIndex({ garminActivityId: 1 }, { unique: true });
return collection;
}
export async function upsertRunningActivity(activity: RunningActivityInput): Promise<void> {
const collection = await getCollection();
await collection.updateOne(
{ garminActivityId: activity.garminActivityId },
{
$set: activity,
$setOnInsert: { createdAt: new Date() },
},
{ upsert: true }
);
}
export async function listRunningActivities(): Promise<RunningActivity[]> {
const collection = await getCollection();
return collection.find().sort({ startTime: -1 }).toArray();
}
export async function getRunningActivity(id: string): Promise<RunningActivity | null> {
const collection = await getCollection();
return collection.findOne({ _id: new ObjectId(id) });
}
export async function setRunningActivityRoutePoints(
garminActivityId: number,
points: RoutePoint[]
): Promise<void> {
const collection = await getCollection();
await collection.updateOne({ garminActivityId }, { $set: { routePoints: points } });
}
type SyncState = { _id: "garmin"; lastSyncAt: Date };
export async function getLastSyncAt(): Promise<Date | null> {
const db = await getDb();
const state = await db.collection<SyncState>(SYNC_STATE_COLLECTION).findOne({ _id: "garmin" });
return state?.lastSyncAt ?? null;
}
export async function setLastSyncAt(date: Date): Promise<void> {
const db = await getDb();
await db
.collection<SyncState>(SYNC_STATE_COLLECTION)
.updateOne({ _id: "garmin" }, { $set: { lastSyncAt: date } }, { upsert: true });
}