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; export type RoutePoint = [number, number]; export type RunMetrics = { distanceKm: number[]; hrBpm?: number[]; cadenceSpm?: number[]; gctMs?: number[]; gcbLeftPct?: number[]; paceSec?: number[]; }; export type RunningActivity = RunningActivityInput & { _id: ObjectId; userId: string; createdAt: Date; routePoints?: RoutePoint[]; elevationProfile?: number[]; runMetrics?: RunMetrics; }; const COLLECTION = "running_activities"; const SYNC_STATE_COLLECTION = "sync_state"; async function getCollection() { const db = await getDb(); const collection = db.collection(COLLECTION); await collection.createIndex({ userId: 1, garminActivityId: 1 }, { unique: true }); return collection; } export async function upsertRunningActivity( userId: string, activity: RunningActivityInput ): Promise { const collection = await getCollection(); await collection.updateOne( { userId, garminActivityId: activity.garminActivityId }, { $set: { ...activity, userId }, $setOnInsert: { createdAt: new Date() }, }, { upsert: true } ); } export async function listRunningActivities(userId: string): Promise { const collection = await getCollection(); return collection.find({ userId }).sort({ startTime: -1 }).toArray(); } export async function getRunningActivity( userId: string, id: string ): Promise { const collection = await getCollection(); return collection.findOne({ _id: new ObjectId(id), userId }); } export async function setRunningActivityMetrics( userId: string, garminActivityId: number, metrics: RunMetrics ): Promise { const collection = await getCollection(); await collection.updateOne({ userId, garminActivityId }, { $set: { runMetrics: metrics } }); } export async function setRunningActivityRoutePoints( userId: string, garminActivityId: number, points: RoutePoint[], elevationProfile: number[] ): Promise { const collection = await getCollection(); await collection.updateOne( { userId, garminActivityId }, { $set: { routePoints: points, elevationProfile } } ); } type SyncState = { _id: string; lastSyncAt: Date }; export async function getLastSyncAt(userId: string): Promise { const db = await getDb(); const state = await db .collection(SYNC_STATE_COLLECTION) .findOne({ _id: userId }); return state?.lastSyncAt ?? null; } export async function setLastSyncAt(userId: string, date: Date): Promise { const db = await getDb(); await db .collection(SYNC_STATE_COLLECTION) .updateOne({ _id: userId }, { $set: { lastSyncAt: date } }, { upsert: true }); }