init
This commit is contained in:
@@ -28,9 +28,15 @@ Jeśli podano dane z poprzednich treningów, odnieś się do progresu (np. zmian
|
||||
type PreviousRun = { run: RunningActivity; analysis: AiAnalysis | null };
|
||||
type PreviousWorkout = { workout: StrengthWorkout; analysis: AiAnalysis | null };
|
||||
|
||||
function secToMinKm(sec: number): string {
|
||||
const m = Math.floor(sec / 60);
|
||||
const s = Math.round(sec % 60);
|
||||
return `${m}:${s.toString().padStart(2, "0")} min/km`;
|
||||
}
|
||||
|
||||
function buildRunMetricsSummary(metrics: RunMetrics, totalDistanceM: number): string[] {
|
||||
const { distanceKm, hrBpm, gcbLeftPct } = metrics;
|
||||
if (!hrBpm && !gcbLeftPct) return [];
|
||||
const { distanceKm, hrBpm, gcbLeftPct, paceSec } = metrics;
|
||||
if (!hrBpm && !gcbLeftPct && !paceSec) return [];
|
||||
const n = distanceKm.length;
|
||||
if (n < 8) return [];
|
||||
|
||||
@@ -60,6 +66,11 @@ function buildRunMetricsSummary(metrics: RunMetrics, totalDistanceM: number): st
|
||||
const a = avg(vals);
|
||||
if (a !== null) parts.push(`HR śr. ${a} bpm`);
|
||||
}
|
||||
if (paceSec) {
|
||||
const vals = idx.map((i) => paceSec[i]).filter((v) => v > 0 && v < 1800);
|
||||
const a = avg(vals);
|
||||
if (a !== null) parts.push(`tempo śr. ${secToMinKm(a)}`);
|
||||
}
|
||||
if (gcbLeftPct) {
|
||||
const vals = idx.map((i) => gcbLeftPct[i]).filter((v) => v > 0);
|
||||
if (vals.length > 0) {
|
||||
@@ -184,6 +195,7 @@ function parseAnalysisResponse(text: string): { summary: string; tips: string[]
|
||||
}
|
||||
|
||||
export async function generateAnalysis(
|
||||
userId: string,
|
||||
targetType: AiAnalysisTargetType,
|
||||
targetId: string
|
||||
): Promise<AiAnalysis> {
|
||||
@@ -194,28 +206,28 @@ export async function generateAnalysis(
|
||||
|
||||
let prompt: string;
|
||||
if (targetType === "running") {
|
||||
const activity = await getRunningActivity(targetId);
|
||||
const activity = await getRunningActivity(userId, targetId);
|
||||
if (!activity) throw new Error("Nie znaleziono biegu.");
|
||||
const previousRuns = (await listRunningActivities())
|
||||
const previousRuns = (await listRunningActivities(userId))
|
||||
.filter((run) => run.startTime < activity.startTime)
|
||||
.slice(0, PREVIOUS_RUNS_LIMIT);
|
||||
const previousRunsWithAnalysis: PreviousRun[] = await Promise.all(
|
||||
previousRuns.map(async (run) => ({
|
||||
run,
|
||||
analysis: await getLatestAnalysisForTarget("running", run._id),
|
||||
analysis: await getLatestAnalysisForTarget(userId, "running", run._id),
|
||||
}))
|
||||
);
|
||||
prompt = buildRunningPrompt(activity, previousRunsWithAnalysis);
|
||||
} else {
|
||||
const workout = await getStrengthWorkout(targetId);
|
||||
const workout = await getStrengthWorkout(userId, targetId);
|
||||
if (!workout) throw new Error("Nie znaleziono treningu.");
|
||||
const previousWorkouts = (await listStrengthWorkouts())
|
||||
const previousWorkouts = (await listStrengthWorkouts(userId))
|
||||
.filter((previous) => previous.date < workout.date)
|
||||
.slice(0, PREVIOUS_WORKOUTS_LIMIT);
|
||||
const previousWorkoutsWithAnalysis: PreviousWorkout[] = await Promise.all(
|
||||
previousWorkouts.map(async (previous) => ({
|
||||
workout: previous,
|
||||
analysis: await getLatestAnalysisForTarget("strength", previous._id),
|
||||
analysis: await getLatestAnalysisForTarget(userId, "strength", previous._id),
|
||||
}))
|
||||
);
|
||||
prompt = buildStrengthPrompt(workout, previousWorkoutsWithAnalysis);
|
||||
@@ -234,6 +246,7 @@ export async function generateAnalysis(
|
||||
const { summary, tips } = parseAnalysisResponse(text);
|
||||
|
||||
return saveAiAnalysis({
|
||||
userId,
|
||||
targetType,
|
||||
targetId: new ObjectId(targetId),
|
||||
summary,
|
||||
@@ -332,18 +345,18 @@ function buildDashboardPrompt(
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export async function generateDashboardAnalysis(): Promise<AiAnalysis> {
|
||||
export async function generateDashboardAnalysis(userId: string): Promise<AiAnalysis> {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) throw new Error("Brak klucza ANTHROPIC_API_KEY w konfiguracji.");
|
||||
|
||||
const [runs, workouts] = await Promise.all([
|
||||
listRunningActivities().then((r) => r.slice(0, DASHBOARD_RUNS_LIMIT)),
|
||||
listStrengthWorkouts().then((w) => w.slice(0, DASHBOARD_WORKOUTS_LIMIT)),
|
||||
listRunningActivities(userId).then((r) => r.slice(0, DASHBOARD_RUNS_LIMIT)),
|
||||
listStrengthWorkouts(userId).then((w) => w.slice(0, DASHBOARD_WORKOUTS_LIMIT)),
|
||||
]);
|
||||
|
||||
let wellness: DayWellness[] = [];
|
||||
try {
|
||||
const garminClient = await getAuthorizedClient();
|
||||
const garminClient = await getAuthorizedClient(userId);
|
||||
wellness = await fetchRecentWellness(garminClient, DASHBOARD_WELLNESS_DAYS);
|
||||
} catch {
|
||||
// Wellness data not available, proceed without it
|
||||
@@ -361,5 +374,5 @@ export async function generateDashboardAnalysis(): Promise<AiAnalysis> {
|
||||
const textBlock = message.content.find((b) => b.type === "text");
|
||||
const text = textBlock && textBlock.type === "text" ? textBlock.text : "";
|
||||
const { summary, tips } = parseAnalysisResponse(text);
|
||||
return saveDashboardAnalysis(summary, tips, model);
|
||||
return saveDashboardAnalysis(userId, summary, tips, model);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GarminConnect } from "garmin-connect";
|
||||
import type { IActivity } from "garmin-connect/dist/garmin/types/activity";
|
||||
import type { IOauth1Token } from "garmin-connect/dist/garmin/types";
|
||||
import type { RoutePoint, RunMetrics, RunningActivityInput } from "@/lib/models/running";
|
||||
import { getSavedOauth1Token } from "@/lib/models/garmin-auth";
|
||||
import { getGarminCredentials, getSavedOauth1Token } from "@/lib/models/garmin-auth";
|
||||
import { completeMfaAndGetTicket, loginAndGetTicket, type GarminPendingMfa } from "@/lib/garmin/sso";
|
||||
|
||||
const FETCH_LIMIT = 50;
|
||||
@@ -13,6 +13,12 @@ export class GarminLoginRequiredError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class GarminCredentialsMissingError extends Error {
|
||||
constructor() {
|
||||
super("Brak danych logowania do Garmin Connect. Skonfiguruj je w Ustawieniach.");
|
||||
}
|
||||
}
|
||||
|
||||
function parseGarminDate(value: string): Date {
|
||||
return new Date(`${value.replace(" ", "T")}Z`);
|
||||
}
|
||||
@@ -154,7 +160,6 @@ export async function fetchActivityRunMetrics(
|
||||
const step = Math.max(1, Math.floor(rows.length / MAX));
|
||||
const sampled = rows.filter((_, i) => i % step === 0 || i === rows.length - 1);
|
||||
|
||||
// Try several known Garmin distance metric keys
|
||||
const distKey = ["directDistance", "sumDistance", "directCumulativeDistance"].find(
|
||||
(k) => sampled.some((row) => get(row, k) !== null && get(row, k)! > 0)
|
||||
);
|
||||
@@ -172,24 +177,21 @@ export async function fetchActivityRunMetrics(
|
||||
return values.some((v) => v > 0) ? values : undefined;
|
||||
};
|
||||
|
||||
const speedSeries = series("directSpeed", 3);
|
||||
const paceSec = speedSeries
|
||||
? speedSeries.map((v) => (v > 0.5 ? Math.round(1000 / v) : 0))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
distanceKm,
|
||||
hrBpm: series("directHeartRate"),
|
||||
cadenceSpm: series("directDoubleCadence"),
|
||||
gctMs: series("directGroundContactTime"),
|
||||
gcbLeftPct: series("directGroundContactBalanceLeft", 1),
|
||||
paceSec,
|
||||
};
|
||||
}
|
||||
|
||||
function getCredentials(): { username: string; password: string } {
|
||||
const username = process.env.GARMIN_EMAIL;
|
||||
const password = process.env.GARMIN_PASSWORD;
|
||||
if (!username || !password) {
|
||||
throw new Error("Brak danych logowania do Garmin Connect (GARMIN_EMAIL / GARMIN_PASSWORD).");
|
||||
}
|
||||
return { username, password };
|
||||
}
|
||||
|
||||
async function exchangeOauth1Token(client: GarminConnect, oauth1Token: IOauth1Token): Promise<void> {
|
||||
const http = client.client;
|
||||
if (!http.OAUTH_CONSUMER) {
|
||||
@@ -205,12 +207,8 @@ async function exchangeOauth1Token(client: GarminConnect, oauth1Token: IOauth1To
|
||||
await http.exchange({ oauth, token: oauth1Token });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a client authenticated using a previously saved OAuth1 token
|
||||
* (long-lived, survives across syncs) - no MFA needed if it's still valid.
|
||||
*/
|
||||
export async function getAuthorizedClient(): Promise<GarminConnect> {
|
||||
const saved = await getSavedOauth1Token();
|
||||
export async function getAuthorizedClient(userId: string): Promise<GarminConnect> {
|
||||
const saved = await getSavedOauth1Token(userId);
|
||||
if (!saved) {
|
||||
throw new GarminLoginRequiredError();
|
||||
}
|
||||
@@ -232,16 +230,16 @@ async function establishClientFromTicket(ticket: string): Promise<{ client: Garm
|
||||
return { client, oauth1Token: oauth1.token };
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a fresh SSO login using env credentials. If the account requires
|
||||
* MFA, returns the pending state needed to complete it via
|
||||
* `completeGarminMfaLogin` once the user supplies the emailed code.
|
||||
*/
|
||||
export async function beginGarminLogin(): Promise<
|
||||
{ client: GarminConnect; oauth1Token: IOauth1Token } | { mfaRequired: true; pendingState: GarminPendingMfa }
|
||||
export async function beginGarminLogin(
|
||||
userId: string
|
||||
): Promise<
|
||||
| { client: GarminConnect; oauth1Token: IOauth1Token }
|
||||
| { mfaRequired: true; pendingState: GarminPendingMfa }
|
||||
> {
|
||||
const { username, password } = getCredentials();
|
||||
const result = await loginAndGetTicket(username, password);
|
||||
const creds = await getGarminCredentials(userId);
|
||||
if (!creds) throw new GarminCredentialsMissingError();
|
||||
|
||||
const result = await loginAndGetTicket(creds.email, creds.password);
|
||||
if ("mfaRequired" in result) return result;
|
||||
return establishClientFromTicket(result.ticket);
|
||||
}
|
||||
@@ -254,14 +252,7 @@ export async function completeGarminMfaLogin(
|
||||
return establishClientFromTicket(ticket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all recent running activities (mapped), regardless of `since` -
|
||||
* callers should upsert all of them so previously-synced activities get
|
||||
* backfilled with newly added metric fields, but can use `since` to decide
|
||||
* which ones are "new" for reporting purposes.
|
||||
*/
|
||||
export async function fetchRunningActivities(client: GarminConnect): Promise<RunningActivityInput[]> {
|
||||
const activities = await client.getActivities(0, FETCH_LIMIT);
|
||||
|
||||
return activities.filter(isRunningActivity).map(mapActivity);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getDb } from "@/lib/db";
|
||||
export type AiAnalysisTargetType = "running" | "strength" | "dashboard";
|
||||
|
||||
export type AiAnalysisInput = {
|
||||
userId: string;
|
||||
targetType: AiAnalysisTargetType;
|
||||
targetId: ObjectId;
|
||||
summary: string;
|
||||
@@ -40,6 +41,8 @@ export function serializeAnalysis(analysis: AiAnalysis): SerializedAiAnalysis {
|
||||
|
||||
const COLLECTION = "ai_analyses";
|
||||
|
||||
const DASHBOARD_TARGET_ID = new ObjectId("000000000000000000000001");
|
||||
|
||||
async function getCollection() {
|
||||
const db = await getDb();
|
||||
return db.collection<AiAnalysis>(COLLECTION);
|
||||
@@ -53,32 +56,34 @@ export async function saveAiAnalysis(input: AiAnalysisInput): Promise<AiAnalysis
|
||||
}
|
||||
|
||||
export async function getLatestAnalysisForTarget(
|
||||
userId: string,
|
||||
targetType: AiAnalysisTargetType,
|
||||
targetId: ObjectId
|
||||
): Promise<AiAnalysis | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne({ targetType, targetId }, { sort: { createdAt: -1 } });
|
||||
return collection.findOne({ userId, 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> {
|
||||
export async function getDashboardAnalysis(userId: string): Promise<AiAnalysis | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne(
|
||||
{ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID },
|
||||
{ userId, targetType: "dashboard", targetId: DASHBOARD_TARGET_ID },
|
||||
{ sort: { createdAt: -1 } }
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveDashboardAnalysis(
|
||||
userId: string,
|
||||
summary: string,
|
||||
tips: string[],
|
||||
model: string
|
||||
): Promise<AiAnalysis> {
|
||||
return saveAiAnalysis({ targetType: "dashboard", targetId: DASHBOARD_TARGET_ID, summary, tips, model });
|
||||
return saveAiAnalysis({
|
||||
userId,
|
||||
targetType: "dashboard",
|
||||
targetId: DASHBOARD_TARGET_ID,
|
||||
summary,
|
||||
tips,
|
||||
model,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,37 +4,61 @@ import type { GarminPendingMfa } from "@/lib/garmin/sso";
|
||||
|
||||
const AUTH_COLLECTION = "garmin_auth";
|
||||
const PENDING_COLLECTION = "garmin_login_pending";
|
||||
const CREDENTIALS_COLLECTION = "garmin_credentials";
|
||||
|
||||
type GarminAuthDoc = { _id: "tokens"; oauth1Token: IOauth1Token; updatedAt: Date };
|
||||
type GarminPendingDoc = { _id: "pending"; state: GarminPendingMfa; createdAt: Date };
|
||||
type GarminAuthDoc = { _id: string; oauth1Token: IOauth1Token; updatedAt: Date };
|
||||
type GarminPendingDoc = { _id: string; state: GarminPendingMfa; createdAt: Date };
|
||||
type GarminCredentialsDoc = { _id: string; email: string; password: string };
|
||||
|
||||
export async function getSavedOauth1Token(): Promise<IOauth1Token | null> {
|
||||
export async function getSavedOauth1Token(userId: string): Promise<IOauth1Token | null> {
|
||||
const db = await getDb();
|
||||
const doc = await db.collection<GarminAuthDoc>(AUTH_COLLECTION).findOne({ _id: "tokens" });
|
||||
const doc = await db.collection<GarminAuthDoc>(AUTH_COLLECTION).findOne({ _id: userId });
|
||||
return doc?.oauth1Token ?? null;
|
||||
}
|
||||
|
||||
export async function saveOauth1Token(oauth1Token: IOauth1Token): Promise<void> {
|
||||
export async function saveOauth1Token(userId: string, oauth1Token: IOauth1Token): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db
|
||||
.collection<GarminAuthDoc>(AUTH_COLLECTION)
|
||||
.updateOne({ _id: "tokens" }, { $set: { oauth1Token, updatedAt: new Date() } }, { upsert: true });
|
||||
.updateOne({ _id: userId }, { $set: { oauth1Token, updatedAt: new Date() } }, { upsert: true });
|
||||
}
|
||||
|
||||
export async function savePendingMfaState(state: GarminPendingMfa): Promise<void> {
|
||||
export async function savePendingMfaState(userId: string, state: GarminPendingMfa): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db
|
||||
.collection<GarminPendingDoc>(PENDING_COLLECTION)
|
||||
.updateOne({ _id: "pending" }, { $set: { state, createdAt: new Date() } }, { upsert: true });
|
||||
.updateOne({ _id: userId }, { $set: { state, createdAt: new Date() } }, { upsert: true });
|
||||
}
|
||||
|
||||
export async function getPendingMfaState(): Promise<GarminPendingMfa | null> {
|
||||
export async function getPendingMfaState(userId: string): Promise<GarminPendingMfa | null> {
|
||||
const db = await getDb();
|
||||
const doc = await db.collection<GarminPendingDoc>(PENDING_COLLECTION).findOne({ _id: "pending" });
|
||||
const doc = await db.collection<GarminPendingDoc>(PENDING_COLLECTION).findOne({ _id: userId });
|
||||
return doc?.state ?? null;
|
||||
}
|
||||
|
||||
export async function clearPendingMfaState(): Promise<void> {
|
||||
export async function clearPendingMfaState(userId: string): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db.collection<GarminPendingDoc>(PENDING_COLLECTION).deleteOne({ _id: "pending" });
|
||||
await db.collection<GarminPendingDoc>(PENDING_COLLECTION).deleteOne({ _id: userId });
|
||||
}
|
||||
|
||||
export async function getGarminCredentials(
|
||||
userId: string
|
||||
): Promise<{ email: string; password: string } | null> {
|
||||
const db = await getDb();
|
||||
const doc = await db
|
||||
.collection<GarminCredentialsDoc>(CREDENTIALS_COLLECTION)
|
||||
.findOne({ _id: userId });
|
||||
if (!doc) return null;
|
||||
return { email: doc.email, password: doc.password };
|
||||
}
|
||||
|
||||
export async function saveGarminCredentials(
|
||||
userId: string,
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db
|
||||
.collection<GarminCredentialsDoc>(CREDENTIALS_COLLECTION)
|
||||
.updateOne({ _id: userId }, { $set: { email, password } }, { upsert: true });
|
||||
}
|
||||
|
||||
@@ -40,10 +40,12 @@ export type RunMetrics = {
|
||||
cadenceSpm?: number[];
|
||||
gctMs?: number[];
|
||||
gcbLeftPct?: number[];
|
||||
paceSec?: number[];
|
||||
};
|
||||
|
||||
export type RunningActivity = RunningActivityInput & {
|
||||
_id: ObjectId;
|
||||
userId: string;
|
||||
createdAt: Date;
|
||||
routePoints?: RoutePoint[];
|
||||
elevationProfile?: number[];
|
||||
@@ -56,60 +58,73 @@ 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 });
|
||||
await collection.createIndex({ userId: 1, garminActivityId: 1 }, { unique: true });
|
||||
return collection;
|
||||
}
|
||||
|
||||
export async function upsertRunningActivity(activity: RunningActivityInput): Promise<void> {
|
||||
export async function upsertRunningActivity(
|
||||
userId: string,
|
||||
activity: RunningActivityInput
|
||||
): Promise<void> {
|
||||
const collection = await getCollection();
|
||||
await collection.updateOne(
|
||||
{ garminActivityId: activity.garminActivityId },
|
||||
{ userId, garminActivityId: activity.garminActivityId },
|
||||
{
|
||||
$set: activity,
|
||||
$set: { ...activity, userId },
|
||||
$setOnInsert: { createdAt: new Date() },
|
||||
},
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function listRunningActivities(): Promise<RunningActivity[]> {
|
||||
export async function listRunningActivities(userId: string): Promise<RunningActivity[]> {
|
||||
const collection = await getCollection();
|
||||
return collection.find().sort({ startTime: -1 }).toArray();
|
||||
return collection.find({ userId }).sort({ startTime: -1 }).toArray();
|
||||
}
|
||||
|
||||
export async function getRunningActivity(id: string): Promise<RunningActivity | null> {
|
||||
export async function getRunningActivity(
|
||||
userId: string,
|
||||
id: string
|
||||
): Promise<RunningActivity | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne({ _id: new ObjectId(id) });
|
||||
return collection.findOne({ _id: new ObjectId(id), userId });
|
||||
}
|
||||
|
||||
export async function setRunningActivityMetrics(
|
||||
userId: string,
|
||||
garminActivityId: number,
|
||||
metrics: RunMetrics
|
||||
): Promise<void> {
|
||||
const collection = await getCollection();
|
||||
await collection.updateOne({ garminActivityId }, { $set: { runMetrics: metrics } });
|
||||
await collection.updateOne({ userId, garminActivityId }, { $set: { runMetrics: metrics } });
|
||||
}
|
||||
|
||||
export async function setRunningActivityRoutePoints(
|
||||
userId: string,
|
||||
garminActivityId: number,
|
||||
points: RoutePoint[],
|
||||
elevationProfile: number[]
|
||||
): Promise<void> {
|
||||
const collection = await getCollection();
|
||||
await collection.updateOne({ garminActivityId }, { $set: { routePoints: points, elevationProfile } });
|
||||
await collection.updateOne(
|
||||
{ userId, garminActivityId },
|
||||
{ $set: { routePoints: points, elevationProfile } }
|
||||
);
|
||||
}
|
||||
|
||||
type SyncState = { _id: "garmin"; lastSyncAt: Date };
|
||||
type SyncState = { _id: string; lastSyncAt: Date };
|
||||
|
||||
export async function getLastSyncAt(): Promise<Date | null> {
|
||||
export async function getLastSyncAt(userId: string): Promise<Date | null> {
|
||||
const db = await getDb();
|
||||
const state = await db.collection<SyncState>(SYNC_STATE_COLLECTION).findOne({ _id: "garmin" });
|
||||
const state = await db
|
||||
.collection<SyncState>(SYNC_STATE_COLLECTION)
|
||||
.findOne({ _id: userId });
|
||||
return state?.lastSyncAt ?? null;
|
||||
}
|
||||
|
||||
export async function setLastSyncAt(date: Date): Promise<void> {
|
||||
export async function setLastSyncAt(userId: string, date: Date): Promise<void> {
|
||||
const db = await getDb();
|
||||
await db
|
||||
.collection<SyncState>(SYNC_STATE_COLLECTION)
|
||||
.updateOne({ _id: "garmin" }, { $set: { lastSyncAt: date } }, { upsert: true });
|
||||
.updateOne({ _id: userId }, { $set: { lastSyncAt: date } }, { upsert: true });
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export type StrengthWorkoutInput = z.infer<typeof strengthWorkoutSchema>;
|
||||
|
||||
export type StrengthWorkout = StrengthWorkoutInput & {
|
||||
_id: ObjectId;
|
||||
userId: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
@@ -37,30 +38,34 @@ const COLLECTION = "strength_workouts";
|
||||
async function getCollection() {
|
||||
const db = await getDb();
|
||||
const collection = db.collection<StrengthWorkout>(COLLECTION);
|
||||
await collection.createIndex({ sourceKey: 1 }, { unique: true });
|
||||
await collection.createIndex({ userId: 1, sourceKey: 1 }, { unique: true });
|
||||
return collection;
|
||||
}
|
||||
|
||||
export async function upsertStrengthWorkout(
|
||||
userId: string,
|
||||
workout: StrengthWorkoutInput
|
||||
): Promise<void> {
|
||||
const collection = await getCollection();
|
||||
await collection.updateOne(
|
||||
{ sourceKey: workout.sourceKey },
|
||||
{ userId, sourceKey: workout.sourceKey },
|
||||
{
|
||||
$set: workout,
|
||||
$set: { ...workout, userId },
|
||||
$setOnInsert: { createdAt: new Date() },
|
||||
},
|
||||
{ upsert: true }
|
||||
);
|
||||
}
|
||||
|
||||
export async function listStrengthWorkouts(): Promise<StrengthWorkout[]> {
|
||||
export async function listStrengthWorkouts(userId: string): Promise<StrengthWorkout[]> {
|
||||
const collection = await getCollection();
|
||||
return collection.find().sort({ date: -1 }).toArray();
|
||||
return collection.find({ userId }).sort({ date: -1 }).toArray();
|
||||
}
|
||||
|
||||
export async function getStrengthWorkout(id: string): Promise<StrengthWorkout | null> {
|
||||
export async function getStrengthWorkout(
|
||||
userId: string,
|
||||
id: string
|
||||
): Promise<StrengthWorkout | null> {
|
||||
const collection = await getCollection();
|
||||
return collection.findOne({ _id: new ObjectId(id) });
|
||||
return collection.findOne({ _id: new ObjectId(id), userId });
|
||||
}
|
||||
|
||||
8
lib/session.ts
Normal file
8
lib/session.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { auth } from "@/auth";
|
||||
|
||||
export async function getCurrentUserId(): Promise<string> {
|
||||
const session = await auth();
|
||||
const id = session?.user?.id;
|
||||
if (!id) throw new Error("Użytkownik nie jest zalogowany.");
|
||||
return id;
|
||||
}
|
||||
Reference in New Issue
Block a user