feat(models): add canonical tournament, participant, result, surface-elo models
Adds CRUD modules for the canonical tables created in commit 775b905.
Each module mirrors existing app/models conventions (database() from
~/database/context, schema from ~/database/schema, mock-based tests).
Key implementation notes:
- participant.ts exports use "Canonical" prefix (CanonicalParticipant,
createCanonicalParticipant, etc.) to avoid collision with existing
season-participant.ts exports
- All four models include comprehensive unit tests following the
audit-log.test.ts pattern
- Tests use mocked db responses (no real database access)
- Upsert functions use onConflictDoUpdate for appropriate unique constraints
Part of Phase 1b of canonical tournament layer migration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
775b905069
commit
5a47300110
9 changed files with 818 additions and 0 deletions
100
app/models/__tests__/canonical-surface-elo.test.ts
Normal file
100
app/models/__tests__/canonical-surface-elo.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
upsertCanonicalSurfaceElo,
|
||||
getCanonicalSurfaceElo,
|
||||
} from "../canonical-surface-elo";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const PARTICIPANT_ID = "participant-1";
|
||||
|
||||
const SAMPLE_ELO = {
|
||||
id: "elo-1",
|
||||
participantId: PARTICIPANT_ID,
|
||||
worldRanking: 1,
|
||||
eloHard: 2400,
|
||||
eloClay: 2350,
|
||||
eloGrass: 2300,
|
||||
updatedAt: new Date("2026-01-28T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeUpsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("upsertCanonicalSurfaceElo", () => {
|
||||
it("inserts or updates surface elo data", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeUpsertDb(SAMPLE_ELO) as never);
|
||||
|
||||
const result = await upsertCanonicalSurfaceElo({
|
||||
participantId: PARTICIPANT_ID,
|
||||
worldRanking: 1,
|
||||
eloHard: 2400,
|
||||
eloClay: 2350,
|
||||
eloGrass: 2300,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_ELO);
|
||||
});
|
||||
|
||||
it("uses onConflictDoUpdate on participantId", async () => {
|
||||
const mockDb = makeUpsertDb(SAMPLE_ELO);
|
||||
vi.mocked(database).mockReturnValue(mockDb as never);
|
||||
|
||||
await upsertCanonicalSurfaceElo({
|
||||
participantId: PARTICIPANT_ID,
|
||||
worldRanking: 1,
|
||||
});
|
||||
|
||||
const onConflictCall = (mockDb.insert as ReturnType<typeof vi.fn>)
|
||||
.mock.results[0].value.values.mock.results[0].value.onConflictDoUpdate;
|
||||
|
||||
expect(onConflictCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCanonicalSurfaceElo", () => {
|
||||
it("returns elo data when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_ELO]) as never);
|
||||
|
||||
const result = await getCanonicalSurfaceElo(PARTICIPANT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_ELO);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getCanonicalSurfaceElo("nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
152
app/models/__tests__/participant.test.ts
Normal file
152
app/models/__tests__/participant.test.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
createCanonicalParticipant,
|
||||
getCanonicalParticipantById,
|
||||
findCanonicalParticipantsBySport,
|
||||
findCanonicalParticipantBySportName,
|
||||
upsertCanonicalParticipantBySportName,
|
||||
} from "../participant";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const SPORT_ID = "sport-1";
|
||||
const PARTICIPANT_ID = "participant-1";
|
||||
|
||||
const SAMPLE_PARTICIPANT = {
|
||||
id: PARTICIPANT_ID,
|
||||
sportId: SPORT_ID,
|
||||
name: "Novak Djokovic",
|
||||
externalKey: "djokovic-n",
|
||||
metadata: { atp_id: "12345" },
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeInsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
orderBy: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createCanonicalParticipant", () => {
|
||||
it("inserts a participant and returns it", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeInsertDb(SAMPLE_PARTICIPANT) as never);
|
||||
|
||||
const result = await createCanonicalParticipant({
|
||||
sportId: SPORT_ID,
|
||||
name: "Novak Djokovic",
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCanonicalParticipantById", () => {
|
||||
it("returns the participant when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_PARTICIPANT]) as never);
|
||||
|
||||
const result = await getCanonicalParticipantById(PARTICIPANT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getCanonicalParticipantById("nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCanonicalParticipantsBySport", () => {
|
||||
it("returns participants ordered by name", async () => {
|
||||
const participants = [SAMPLE_PARTICIPANT];
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb(participants) as never);
|
||||
|
||||
const result = await findCanonicalParticipantsBySport(SPORT_ID);
|
||||
|
||||
expect(result).toEqual(participants);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findCanonicalParticipantBySportName", () => {
|
||||
it("returns the participant when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_PARTICIPANT]) as never);
|
||||
|
||||
const result = await findCanonicalParticipantBySportName(SPORT_ID, "Novak Djokovic");
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await findCanonicalParticipantBySportName(SPORT_ID, "Roger Federer");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("upsertCanonicalParticipantBySportName", () => {
|
||||
it("returns existing participant if found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_PARTICIPANT]) as never);
|
||||
|
||||
const result = await upsertCanonicalParticipantBySportName(SPORT_ID, "Novak Djokovic");
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("creates new participant if not found", async () => {
|
||||
let callCount = 0;
|
||||
vi.mocked(database).mockImplementation(() => {
|
||||
callCount++;
|
||||
return (callCount === 1 ? makeSelectDb([]) : makeInsertDb(SAMPLE_PARTICIPANT)) as never;
|
||||
});
|
||||
|
||||
const result = await upsertCanonicalParticipantBySportName(SPORT_ID, "Novak Djokovic");
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
|
||||
it("passes extra fields when creating", async () => {
|
||||
let callCount = 0;
|
||||
vi.mocked(database).mockImplementation(() => {
|
||||
callCount++;
|
||||
return (callCount === 1 ? makeSelectDb([]) : makeInsertDb(SAMPLE_PARTICIPANT)) as never;
|
||||
});
|
||||
|
||||
const result = await upsertCanonicalParticipantBySportName(
|
||||
SPORT_ID,
|
||||
"Novak Djokovic",
|
||||
{ externalKey: "djokovic-n" }
|
||||
);
|
||||
|
||||
expect(result).toEqual(SAMPLE_PARTICIPANT);
|
||||
});
|
||||
});
|
||||
114
app/models/__tests__/tournament-result.test.ts
Normal file
114
app/models/__tests__/tournament-result.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
upsertTournamentResult,
|
||||
getTournamentResults,
|
||||
getTournamentResultByParticipant,
|
||||
} from "../tournament-result";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const TOURNAMENT_ID = "tournament-1";
|
||||
const PARTICIPANT_ID = "participant-1";
|
||||
|
||||
const SAMPLE_RESULT = {
|
||||
id: "result-1",
|
||||
tournamentId: TOURNAMENT_ID,
|
||||
participantId: PARTICIPANT_ID,
|
||||
placement: 1,
|
||||
rawScore: "7500.00",
|
||||
createdAt: new Date("2026-01-28T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-28T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeUpsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
onConflictDoUpdate: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
orderBy: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("upsertTournamentResult", () => {
|
||||
it("inserts or updates a result", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeUpsertDb(SAMPLE_RESULT) as never);
|
||||
|
||||
const result = await upsertTournamentResult({
|
||||
tournamentId: TOURNAMENT_ID,
|
||||
participantId: PARTICIPANT_ID,
|
||||
placement: 1,
|
||||
rawScore: "7500.00",
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_RESULT);
|
||||
});
|
||||
|
||||
it("uses onConflictDoUpdate on tournamentId and participantId", async () => {
|
||||
const mockDb = makeUpsertDb(SAMPLE_RESULT);
|
||||
vi.mocked(database).mockReturnValue(mockDb as never);
|
||||
|
||||
await upsertTournamentResult({
|
||||
tournamentId: TOURNAMENT_ID,
|
||||
participantId: PARTICIPANT_ID,
|
||||
placement: 1,
|
||||
});
|
||||
|
||||
const onConflictCall = (mockDb.insert as ReturnType<typeof vi.fn>)
|
||||
.mock.results[0].value.values.mock.results[0].value.onConflictDoUpdate;
|
||||
|
||||
expect(onConflictCall).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTournamentResults", () => {
|
||||
it("returns results ordered by placement", async () => {
|
||||
const results = [SAMPLE_RESULT];
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb(results) as never);
|
||||
|
||||
const result = await getTournamentResults(TOURNAMENT_ID);
|
||||
|
||||
expect(result).toEqual(results);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTournamentResultByParticipant", () => {
|
||||
it("returns the result when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_RESULT]) as never);
|
||||
|
||||
const result = await getTournamentResultByParticipant(TOURNAMENT_ID, PARTICIPANT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_RESULT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getTournamentResultByParticipant(TOURNAMENT_ID, "nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
174
app/models/__tests__/tournament.test.ts
Normal file
174
app/models/__tests__/tournament.test.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
vi.mock("~/database/context", () => ({
|
||||
database: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
createTournament,
|
||||
getTournamentById,
|
||||
findTournamentsBySport,
|
||||
findTournamentBySportNameYear,
|
||||
upsertTournament,
|
||||
updateTournamentStatus,
|
||||
} from "../tournament";
|
||||
import { database } from "~/database/context";
|
||||
|
||||
const SPORT_ID = "sport-1";
|
||||
const TOURNAMENT_ID = "tournament-1";
|
||||
|
||||
const SAMPLE_TOURNAMENT = {
|
||||
id: TOURNAMENT_ID,
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
startsAt: new Date("2026-01-15T00:00:00Z"),
|
||||
endsAt: new Date("2026-01-28T00:00:00Z"),
|
||||
surface: "hard",
|
||||
location: "Melbourne",
|
||||
status: "scheduled" as const,
|
||||
externalKey: "ao-2026",
|
||||
createdAt: new Date("2026-01-01T00:00:00Z"),
|
||||
updatedAt: new Date("2026-01-01T00:00:00Z"),
|
||||
};
|
||||
|
||||
function makeInsertDb(returnValue: object) {
|
||||
return {
|
||||
insert: vi.fn().mockReturnValue({
|
||||
values: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelectDb(rows: object[]) {
|
||||
return {
|
||||
select: vi.fn().mockReturnValue({
|
||||
from: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
limit: vi.fn().mockResolvedValue(rows),
|
||||
orderBy: vi.fn().mockResolvedValue(rows),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function makeUpdateDb(returnValue: object) {
|
||||
return {
|
||||
update: vi.fn().mockReturnValue({
|
||||
set: vi.fn().mockReturnValue({
|
||||
where: vi.fn().mockReturnValue({
|
||||
returning: vi.fn().mockResolvedValue([returnValue]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("createTournament", () => {
|
||||
it("inserts a tournament and returns it", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeInsertDb(SAMPLE_TOURNAMENT) as never);
|
||||
|
||||
const result = await createTournament({
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTournamentById", () => {
|
||||
it("returns the tournament when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
||||
|
||||
const result = await getTournamentById(TOURNAMENT_ID);
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await getTournamentById("nonexistent");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("findTournamentsBySport", () => {
|
||||
it("returns tournaments ordered by year and date", async () => {
|
||||
const tournaments = [SAMPLE_TOURNAMENT];
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb(tournaments) as never);
|
||||
|
||||
const result = await findTournamentsBySport(SPORT_ID);
|
||||
|
||||
expect(result).toEqual(tournaments);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findTournamentBySportNameYear", () => {
|
||||
it("returns the tournament when found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
||||
|
||||
const result = await findTournamentBySportNameYear(SPORT_ID, "Australian Open", 2026);
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
|
||||
it("returns null when not found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([]) as never);
|
||||
|
||||
const result = await findTournamentBySportNameYear(SPORT_ID, "Wimbledon", 2026);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("upsertTournament", () => {
|
||||
it("returns existing tournament if found", async () => {
|
||||
vi.mocked(database).mockReturnValue(makeSelectDb([SAMPLE_TOURNAMENT]) as never);
|
||||
|
||||
const result = await upsertTournament({
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
|
||||
it("creates new tournament if not found", async () => {
|
||||
let callCount = 0;
|
||||
vi.mocked(database).mockImplementation(() => {
|
||||
callCount++;
|
||||
return (callCount === 1 ? makeSelectDb([]) : makeInsertDb(SAMPLE_TOURNAMENT)) as never;
|
||||
});
|
||||
|
||||
const result = await upsertTournament({
|
||||
sportId: SPORT_ID,
|
||||
name: "Australian Open",
|
||||
year: 2026,
|
||||
});
|
||||
|
||||
expect(result).toEqual(SAMPLE_TOURNAMENT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateTournamentStatus", () => {
|
||||
it("updates status and returns the tournament", async () => {
|
||||
const updated = { ...SAMPLE_TOURNAMENT, status: "in_progress" as const };
|
||||
vi.mocked(database).mockReturnValue(makeUpdateDb(updated) as never);
|
||||
|
||||
const result = await updateTournamentStatus(TOURNAMENT_ID, "in_progress");
|
||||
|
||||
expect(result.status).toBe("in_progress");
|
||||
});
|
||||
});
|
||||
43
app/models/canonical-surface-elo.ts
Normal file
43
app/models/canonical-surface-elo.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { eq } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type CanonicalSurfaceElo = typeof schema.participantSurfaceElos.$inferSelect;
|
||||
export type NewCanonicalSurfaceElo = typeof schema.participantSurfaceElos.$inferInsert;
|
||||
|
||||
export async function upsertCanonicalSurfaceElo(
|
||||
data: NewCanonicalSurfaceElo
|
||||
): Promise<CanonicalSurfaceElo> {
|
||||
if (!data.participantId) {
|
||||
throw new Error("participantId is required");
|
||||
}
|
||||
|
||||
const db = database();
|
||||
const [result] = await db
|
||||
.insert(schema.participantSurfaceElos)
|
||||
.values(data)
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.participantSurfaceElos.participantId],
|
||||
set: {
|
||||
worldRanking: data.worldRanking,
|
||||
eloHard: data.eloHard,
|
||||
eloClay: data.eloClay,
|
||||
eloGrass: data.eloGrass,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getCanonicalSurfaceElo(
|
||||
participantId: string
|
||||
): Promise<CanonicalSurfaceElo | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.participantSurfaceElos)
|
||||
.where(eq(schema.participantSurfaceElos.participantId, participantId))
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
|
@ -18,3 +18,7 @@ export * from "./draft-timer";
|
|||
export * from "./draft-utils";
|
||||
export * from "./watchlist";
|
||||
export * from "./audit-log";
|
||||
export * from "./tournament";
|
||||
export * from "./participant";
|
||||
export * from "./tournament-result";
|
||||
export * from "./canonical-surface-elo";
|
||||
|
|
|
|||
76
app/models/participant.ts
Normal file
76
app/models/participant.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
import { eq, and, asc } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type CanonicalParticipant = typeof schema.participants.$inferSelect;
|
||||
export type NewCanonicalParticipant = typeof schema.participants.$inferInsert;
|
||||
|
||||
export async function createCanonicalParticipant(
|
||||
data: NewCanonicalParticipant
|
||||
): Promise<CanonicalParticipant> {
|
||||
const db = database();
|
||||
const [participant] = await db
|
||||
.insert(schema.participants)
|
||||
.values(data)
|
||||
.returning();
|
||||
return participant;
|
||||
}
|
||||
|
||||
export async function getCanonicalParticipantById(
|
||||
id: string
|
||||
): Promise<CanonicalParticipant | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.id, id))
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findCanonicalParticipantsBySport(
|
||||
sportId: string
|
||||
): Promise<CanonicalParticipant[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(eq(schema.participants.sportId, sportId))
|
||||
.orderBy(asc(schema.participants.name));
|
||||
}
|
||||
|
||||
export async function findCanonicalParticipantBySportName(
|
||||
sportId: string,
|
||||
name: string
|
||||
): Promise<CanonicalParticipant | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.participants)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.participants.sportId, sportId),
|
||||
eq(schema.participants.name, name)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function upsertCanonicalParticipantBySportName(
|
||||
sportId: string,
|
||||
name: string,
|
||||
extra?: Partial<NewCanonicalParticipant>
|
||||
): Promise<CanonicalParticipant> {
|
||||
const existing = await findCanonicalParticipantBySportName(sportId, name);
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return await createCanonicalParticipant({
|
||||
sportId,
|
||||
name,
|
||||
...extra,
|
||||
});
|
||||
}
|
||||
61
app/models/tournament-result.ts
Normal file
61
app/models/tournament-result.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { eq, and, asc } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type TournamentResult = typeof schema.tournamentResults.$inferSelect;
|
||||
export type NewTournamentResult = typeof schema.tournamentResults.$inferInsert;
|
||||
|
||||
export async function upsertTournamentResult(
|
||||
data: NewTournamentResult
|
||||
): Promise<TournamentResult> {
|
||||
if (!data.tournamentId || !data.participantId) {
|
||||
throw new Error("tournamentId and participantId are required");
|
||||
}
|
||||
|
||||
const db = database();
|
||||
const [result] = await db
|
||||
.insert(schema.tournamentResults)
|
||||
.values(data)
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.tournamentResults.tournamentId,
|
||||
schema.tournamentResults.participantId,
|
||||
],
|
||||
set: {
|
||||
placement: data.placement,
|
||||
rawScore: data.rawScore,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.returning();
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getTournamentResults(
|
||||
tournamentId: string
|
||||
): Promise<TournamentResult[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.select()
|
||||
.from(schema.tournamentResults)
|
||||
.where(eq(schema.tournamentResults.tournamentId, tournamentId))
|
||||
.orderBy(asc(schema.tournamentResults.placement));
|
||||
}
|
||||
|
||||
export async function getTournamentResultByParticipant(
|
||||
tournamentId: string,
|
||||
participantId: string
|
||||
): Promise<TournamentResult | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.tournamentResults)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.tournamentResults.tournamentId, tournamentId),
|
||||
eq(schema.tournamentResults.participantId, participantId)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
94
app/models/tournament.ts
Normal file
94
app/models/tournament.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { eq, and, asc } from "drizzle-orm";
|
||||
import { database } from "~/database/context";
|
||||
import * as schema from "~/database/schema";
|
||||
|
||||
export type Tournament = typeof schema.tournaments.$inferSelect;
|
||||
export type NewTournament = typeof schema.tournaments.$inferInsert;
|
||||
export type TournamentStatus = Tournament["status"];
|
||||
|
||||
export async function createTournament(
|
||||
data: NewTournament
|
||||
): Promise<Tournament> {
|
||||
const db = database();
|
||||
const [tournament] = await db
|
||||
.insert(schema.tournaments)
|
||||
.values(data)
|
||||
.returning();
|
||||
return tournament;
|
||||
}
|
||||
|
||||
export async function getTournamentById(
|
||||
id: string
|
||||
): Promise<Tournament | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.tournaments)
|
||||
.where(eq(schema.tournaments.id, id))
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function findTournamentsBySport(
|
||||
sportId: string
|
||||
): Promise<Tournament[]> {
|
||||
const db = database();
|
||||
return await db
|
||||
.select()
|
||||
.from(schema.tournaments)
|
||||
.where(eq(schema.tournaments.sportId, sportId))
|
||||
.orderBy(asc(schema.tournaments.year), asc(schema.tournaments.startsAt));
|
||||
}
|
||||
|
||||
export async function findTournamentBySportNameYear(
|
||||
sportId: string,
|
||||
name: string,
|
||||
year: number
|
||||
): Promise<Tournament | null> {
|
||||
const db = database();
|
||||
const results = await db
|
||||
.select()
|
||||
.from(schema.tournaments)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.tournaments.sportId, sportId),
|
||||
eq(schema.tournaments.name, name),
|
||||
eq(schema.tournaments.year, year)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
return results[0] ?? null;
|
||||
}
|
||||
|
||||
export async function upsertTournament(
|
||||
data: NewTournament
|
||||
): Promise<Tournament> {
|
||||
if (!data.sportId || !data.name || !data.year) {
|
||||
throw new Error("sportId, name, and year are required for upsert");
|
||||
}
|
||||
|
||||
const existing = await findTournamentBySportNameYear(
|
||||
data.sportId,
|
||||
data.name,
|
||||
data.year
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return await createTournament(data);
|
||||
}
|
||||
|
||||
export async function updateTournamentStatus(
|
||||
id: string,
|
||||
status: TournamentStatus
|
||||
): Promise<Tournament> {
|
||||
const db = database();
|
||||
const [tournament] = await db
|
||||
.update(schema.tournaments)
|
||||
.set({ status, updatedAt: new Date() })
|
||||
.where(eq(schema.tournaments.id, id))
|
||||
.returning();
|
||||
return tournament;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue