brackt/app/models/participant-season-result.ts
chrisp 0150fb7ab9
Some checks failed
🚀 Deploy / 🧪 Test (push) Failing after 2m56s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m27s
🚀 Deploy / 🐳 Build (push) Has been skipped
🚀 Deploy / 🚀 Deploy (push) Has been skipped
sync racing standings (#80)
Co-authored-by: Claude <noreply@anthropic.com>
Reviewed-on: #80
2026-06-10 04:25:48 +00:00

268 lines
7 KiB
TypeScript

import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, inArray, max, sql } from "drizzle-orm";
export interface UpsertSeasonResultData {
participantId: string;
sportsSeasonId: string;
currentPoints?: number;
currentPosition?: number;
}
export interface UpdateSeasonResultData {
currentPoints?: number;
currentPosition?: number;
}
/**
* Upsert a single participant's season result atomically using ON CONFLICT DO UPDATE.
*/
export async function upsertParticipantSeasonResult(
data: UpsertSeasonResultData,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const now = new Date();
const [row] = await db
.insert(schema.participantSeasonResults)
.values({
participantId: data.participantId,
sportsSeasonId: data.sportsSeasonId,
currentPoints: (data.currentPoints ?? 0).toString(),
currentPosition: data.currentPosition ?? null,
updatedAt: now,
})
.onConflictDoUpdate({
target: [
schema.participantSeasonResults.participantId,
schema.participantSeasonResults.sportsSeasonId,
],
set: {
currentPoints: sql`excluded.current_points`,
currentPosition: sql`excluded.current_position`,
updatedAt: sql`excluded.updated_at`,
},
})
.returning();
return row;
}
/**
* Bulk upsert season results for multiple participants in a single statement.
*/
export async function upsertSeasonResultsBulk(
results: UpsertSeasonResultData[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
if (results.length === 0) return [];
const now = new Date();
const values = results.map((data) => ({
participantId: data.participantId,
sportsSeasonId: data.sportsSeasonId,
currentPoints: (data.currentPoints ?? 0).toString(),
currentPosition: data.currentPosition ?? null,
updatedAt: now,
}));
return db
.insert(schema.participantSeasonResults)
.values(values)
.onConflictDoUpdate({
target: [
schema.participantSeasonResults.participantId,
schema.participantSeasonResults.sportsSeasonId,
],
set: {
currentPoints: sql`excluded.current_points`,
currentPosition: sql`excluded.current_position`,
updatedAt: sql`excluded.updated_at`,
},
})
.returning();
}
/**
* Get a participant's season result
*/
export async function getParticipantSeasonResult(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
return await db.query.participantSeasonResults.findFirst({
where: and(
eq(schema.participantSeasonResults.participantId, participantId),
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)
),
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
}
/**
* Get all season results for a sports season
* Returns participants ordered by current position (or points if position not set)
*/
export async function getSeasonResults(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const results = await db.query.participantSeasonResults.findMany({
where: eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId),
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
// Sort by position (nulls last), then by points descending
return results.toSorted((a, b) => {
if (a.currentPosition !== null && b.currentPosition !== null) {
return a.currentPosition - b.currentPosition;
}
if (a.currentPosition !== null) return -1;
if (b.currentPosition !== null) return 1;
// Both null positions, sort by points
const aPoints = parseFloat(a.currentPoints || "0");
const bPoints = parseFloat(b.currentPoints || "0");
return bPoints - aPoints;
});
}
/**
* Get season results for specific participants
*/
export async function getSeasonResultsForParticipants(
sportsSeasonId: string,
participantIds: string[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
if (participantIds.length === 0) return [];
return await db.query.participantSeasonResults.findMany({
where: and(
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId),
inArray(schema.participantSeasonResults.participantId, participantIds)
),
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
}
/**
* Update a participant's season result
*/
export async function updateParticipantSeasonResult(
participantId: string,
sportsSeasonId: string,
data: UpdateSeasonResultData,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const [updated] = await db
.update(schema.participantSeasonResults)
.set({
currentPoints: data.currentPoints?.toString(),
currentPosition: data.currentPosition,
updatedAt: new Date(),
})
.where(
and(
eq(schema.participantSeasonResults.participantId, participantId),
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)
)
)
.returning();
return updated;
}
/**
* Delete all season results for a sports season
*/
export async function deleteSeasonResults(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
await db
.delete(schema.participantSeasonResults)
.where(eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId));
}
/**
* Check if a participant has a season result
*/
export async function hasParticipantSeasonResult(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
): Promise<boolean> {
const db = providedDb || database();
const result = await db.query.participantSeasonResults.findFirst({
where: and(
eq(schema.participantSeasonResults.participantId, participantId),
eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)
),
});
return !!result;
}
/**
* Returns the most recent updatedAt timestamp across all season results for a sports season.
* Used to display "Last synced" time for season_standings sports (F1, IndyCar).
*/
export async function getLastSeasonResultsSyncedAt(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
): Promise<Date | null> {
const db = providedDb || database();
const result = await db
.select({ lastUpdated: max(schema.participantSeasonResults.updatedAt) })
.from(schema.participantSeasonResults)
.where(eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId));
return result[0]?.lastUpdated ?? null;
}