97 lines
3.3 KiB
TypeScript
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 });
|
|
}
|