Files

131 lines
4.1 KiB
TypeScript
Raw Permalink Normal View History

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