init
This commit is contained in:
62
lib/models/analysis.ts
Normal file
62
lib/models/analysis.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
import { getDb } from "@/lib/db";
|
||||
|
||||
export type AiAnalysisTargetType = "running" | "strength" | "dashboard";
|
||||
|
||||
export type AiAnalysisInput = {
|
||||
targetType: AiAnalysisTargetType;
|
||||
targetId: ObjectId;
|
||||
summary: string;
|
||||
tips: string[];
|
||||
model: string;
|
||||
};
|
||||
|
||||
export type AiAnalysis = AiAnalysisInput & {
|
||||
_id: ObjectId;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
const COLLECTION = "ai_analyses";
|
||||
|
||||
async function getCollection() {
|
||||
const db = await getDb();
|
||||
return db.collection<AiAnalysis>(COLLECTION);
|
||||
}
|
||||
|
||||
export async function saveAiAnalysis(input: AiAnalysisInput): Promise<AiAnalysis> {
|
||||
const collection = await getCollection();
|
||||
const doc = { ...input, _id: new ObjectId(), createdAt: new Date() };
|
||||
await collection.insertOne(doc);
|
||||
return doc;
|
||||
}
|
||||
|
||||
export async function getLatestAnalysisForTarget(
|
||||
targetType: AiAnalysisTargetType,
|
||||
targetId: ObjectId
|
||||
): Promise<AiAnalysis | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne({ targetType, targetId }, { sort: { createdAt: -1 } });
|
||||
}
|
||||
|
||||
export async function getLatestAnalysis(): Promise<AiAnalysis | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne({}, { sort: { createdAt: -1 } });
|
||||
}
|
||||
|
||||
const DASHBOARD_TARGET_ID = new ObjectId("000000000000000000000001");
|
||||
|
||||
export async function getDashboardAnalysis(): Promise<AiAnalysis | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne(
|
||||
{ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID },
|
||||
{ sort: { createdAt: -1 } }
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveDashboardAnalysis(
|
||||
summary: string,
|
||||
tips: string[],
|
||||
model: string
|
||||
): Promise<AiAnalysis> {
|
||||
return saveAiAnalysis({ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID, summary, tips, model });
|
||||
}
|
||||
40
lib/models/garmin-auth.ts
Normal file
40
lib/models/garmin-auth.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { IOauth1Token } from "garmin-connect/dist/garmin/types";
|
||||
import { getDb } from "@/lib/db";
|
||||
import type { GarminPendingMfa } from "@/lib/garmin/sso";
|
||||
|
||||
const AUTH_COLLECTION = "garmin_auth";
|
||||
const PENDING_COLLECTION = "garmin_login_pending";
|
||||
|
||||
type GarminAuthDoc = { _id: "tokens"; oauth1Token: IOauth1Token; updatedAt: Date };
|
||||
type GarminPendingDoc = { _id: "pending"; state: GarminPendingMfa; createdAt: Date };
|
||||
|
||||
export async function getSavedOauth1Token(): Promise<IOauth1Token | null> {
|
||||
const db = await getDb();
|
||||
const doc = await db.collection<GarminAuthDoc>(AUTH_COLLECTION).findOne({ _id: "tokens" });
|
||||
return doc?.oauth1Token ?? null;
|
||||
}
|
||||
|
||||
export async function saveOauth1Token(oauth1Token: IOauth1Token): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db
|
||||
.collection<GarminAuthDoc>(AUTH_COLLECTION)
|
||||
.updateOne({ _id: "tokens" }, { $set: { oauth1Token, updatedAt: new Date() } }, { upsert: true });
|
||||
}
|
||||
|
||||
export async function savePendingMfaState(state: GarminPendingMfa): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db
|
||||
.collection<GarminPendingDoc>(PENDING_COLLECTION)
|
||||
.updateOne({ _id: "pending" }, { $set: { state, createdAt: new Date() } }, { upsert: true });
|
||||
}
|
||||
|
||||
export async function getPendingMfaState(): Promise<GarminPendingMfa | null> {
|
||||
const db = await getDb();
|
||||
const doc = await db.collection<GarminPendingDoc>(PENDING_COLLECTION).findOne({ _id: "pending" });
|
||||
return doc?.state ?? null;
|
||||
}
|
||||
|
||||
export async function clearPendingMfaState(): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db.collection<GarminPendingDoc>(PENDING_COLLECTION).deleteOne({ _id: "pending" });
|
||||
}
|
||||
96
lib/models/running.ts
Normal file
96
lib/models/running.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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 });
|
||||
}
|
||||
66
lib/models/strength.ts
Normal file
66
lib/models/strength.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ObjectId } from "mongodb";
|
||||
import { z } from "zod";
|
||||
import { getDb } from "@/lib/db";
|
||||
|
||||
export const strengthSetSchema = z.object({
|
||||
order: z.number().int().positive(),
|
||||
weightKg: z.number().positive().optional(),
|
||||
reps: z.number().int().positive().optional(),
|
||||
});
|
||||
|
||||
export const strengthExerciseSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
notes: z.string().optional(),
|
||||
sets: z.array(strengthSetSchema),
|
||||
});
|
||||
|
||||
export const strengthWorkoutSchema = z.object({
|
||||
date: z.date(),
|
||||
name: z.string().min(1),
|
||||
notes: z.string().optional(),
|
||||
exercises: z.array(strengthExerciseSchema),
|
||||
sourceUrl: z.string().optional(),
|
||||
sourceKey: z.string().min(1),
|
||||
});
|
||||
|
||||
export type StrengthSet = z.infer<typeof strengthSetSchema>;
|
||||
export type StrengthExercise = z.infer<typeof strengthExerciseSchema>;
|
||||
export type StrengthWorkoutInput = z.infer<typeof strengthWorkoutSchema>;
|
||||
|
||||
export type StrengthWorkout = StrengthWorkoutInput & {
|
||||
_id: ObjectId;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
const COLLECTION = "strength_workouts";
|
||||
|
||||
async function getCollection() {
|
||||
const db = await getDb();
|
||||
const collection = db.collection<StrengthWorkout>(COLLECTION);
|
||||
await collection.createIndex({ sourceKey: 1 }, { unique: true });
|
||||
return collection;
|
||||
}
|
||||
|
||||
export async function upsertStrengthWorkout(
|
||||
workout: StrengthWorkoutInput
|
||||
): Promise<void> {
|
||||
const collection = await getCollection();
|
||||
await collection.updateOne(
|
||||
{ sourceKey: workout.sourceKey },
|
||||
{
|
||||
$set: workout,
|
||||
$setOnInsert: { createdAt: new Date() },
|
||||
},
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function listStrengthWorkouts(): Promise<StrengthWorkout[]> {
|
||||
const collection = await getCollection();
|
||||
return collection.find().sort({ date: -1 }).toArray();
|
||||
}
|
||||
|
||||
export async function getStrengthWorkout(id: string): Promise<StrengthWorkout | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne({ _id: new ObjectId(id) });
|
||||
}
|
||||
Reference in New Issue
Block a user