Merge branch 'phase-1b-canonical-tables' into canonical-layer-pr1
This commit is contained in:
commit
0e893b9c0d
13 changed files with 6761 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 "./draft-utils";
|
||||||
export * from "./watchlist";
|
export * from "./watchlist";
|
||||||
export * from "./audit-log";
|
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;
|
||||||
|
}
|
||||||
|
|
@ -150,6 +150,12 @@ export const simulatorTypeEnum = pgEnum("simulator_type", [
|
||||||
"llws_bracket",
|
"llws_bracket",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const tournamentStatusEnum = pgEnum("tournament_status", [
|
||||||
|
"scheduled",
|
||||||
|
"in_progress",
|
||||||
|
"completed",
|
||||||
|
]);
|
||||||
|
|
||||||
export const playoffMatchGameStatusEnum = pgEnum("playoff_match_game_status", [
|
export const playoffMatchGameStatusEnum = pgEnum("playoff_match_game_status", [
|
||||||
"scheduled",
|
"scheduled",
|
||||||
"complete",
|
"complete",
|
||||||
|
|
@ -378,6 +384,9 @@ export const seasonParticipants = pgTable("season_participants", {
|
||||||
sportsSeasonId: uuid("sports_season_id")
|
sportsSeasonId: uuid("sports_season_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||||
|
participantId: uuid("participant_id").references(() => participants.id, {
|
||||||
|
onDelete: "restrict",
|
||||||
|
}),
|
||||||
name: varchar("name", { length: 255 }).notNull(),
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
shortName: varchar("short_name", { length: 100 }),
|
shortName: varchar("short_name", { length: 100 }),
|
||||||
externalId: varchar("external_id", { length: 255 }),
|
externalId: varchar("external_id", { length: 255 }),
|
||||||
|
|
@ -389,6 +398,96 @@ export const seasonParticipants = pgTable("season_participants", {
|
||||||
uniqueIndex("participants_sports_season_name_unique").on(table.sportsSeasonId, table.name),
|
uniqueIndex("participants_sports_season_name_unique").on(table.sportsSeasonId, table.name),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export const participants = pgTable(
|
||||||
|
"participants",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
sportId: uuid("sport_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => sports.id, { onDelete: "cascade" }),
|
||||||
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
externalKey: varchar("external_key", { length: 255 }),
|
||||||
|
metadata: jsonb("metadata"),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
uniqueSportName: uniqueIndex("participants_sport_name_unique").on(
|
||||||
|
t.sportId,
|
||||||
|
t.name,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const tournaments = pgTable(
|
||||||
|
"tournaments",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
sportId: uuid("sport_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => sports.id, { onDelete: "cascade" }),
|
||||||
|
name: varchar("name", { length: 255 }).notNull(),
|
||||||
|
year: integer("year").notNull(),
|
||||||
|
startsAt: timestamp("starts_at"),
|
||||||
|
endsAt: timestamp("ends_at"),
|
||||||
|
surface: varchar("surface", { length: 50 }), // tennis only: hard | clay | grass
|
||||||
|
location: varchar("location", { length: 255 }),
|
||||||
|
status: tournamentStatusEnum("status").notNull().default("scheduled"),
|
||||||
|
externalKey: varchar("external_key", { length: 255 }),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
uniqueSportNameYear: uniqueIndex("tournaments_sport_name_year_unique").on(
|
||||||
|
t.sportId,
|
||||||
|
t.name,
|
||||||
|
t.year,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const tournamentResults = pgTable(
|
||||||
|
"tournament_results",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
tournamentId: uuid("tournament_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => tournaments.id, { onDelete: "cascade" }),
|
||||||
|
participantId: uuid("participant_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => participants.id, { onDelete: "cascade" }),
|
||||||
|
placement: integer("placement"),
|
||||||
|
rawScore: decimal("raw_score", { precision: 10, scale: 2 }),
|
||||||
|
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
uniqueTournamentParticipant: uniqueIndex(
|
||||||
|
"tournament_results_tournament_participant_unique",
|
||||||
|
).on(t.tournamentId, t.participantId),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const participantSurfaceElos = pgTable(
|
||||||
|
"participant_surface_elos",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
participantId: uuid("participant_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => participants.id, { onDelete: "cascade" }),
|
||||||
|
worldRanking: integer("world_ranking"),
|
||||||
|
eloHard: integer("elo_hard"),
|
||||||
|
eloClay: integer("elo_clay"),
|
||||||
|
eloGrass: integer("elo_grass"),
|
||||||
|
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||||
|
},
|
||||||
|
(t) => ({
|
||||||
|
uniqueParticipant: uniqueIndex("participant_surface_elos_participant_unique").on(
|
||||||
|
t.participantId,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const seasonTemplateSports = pgTable("season_template_sports", {
|
export const seasonTemplateSports = pgTable("season_template_sports", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
templateId: uuid("template_id")
|
templateId: uuid("template_id")
|
||||||
|
|
@ -435,6 +534,9 @@ export const scoringEvents = pgTable("scoring_events", {
|
||||||
sportsSeasonId: uuid("sports_season_id")
|
sportsSeasonId: uuid("sports_season_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||||||
|
tournamentId: uuid("tournament_id").references(() => tournaments.id, {
|
||||||
|
onDelete: "set null",
|
||||||
|
}),
|
||||||
name: varchar("name", { length: 255 }).notNull(), // "2024 Masters", "Super Bowl LIX", etc.
|
name: varchar("name", { length: 255 }).notNull(), // "2024 Masters", "Super Bowl LIX", etc.
|
||||||
eventDate: date("event_date"),
|
eventDate: date("event_date"),
|
||||||
eventStartsAt: timestamp("event_starts_at", { withTimezone: true }),
|
eventStartsAt: timestamp("event_starts_at", { withTimezone: true }),
|
||||||
|
|
@ -783,6 +885,8 @@ export const teamEvSnapshots = pgTable("team_ev_snapshots", {
|
||||||
// Relations
|
// Relations
|
||||||
export const sportsRelations = relations(sports, ({ many }) => ({
|
export const sportsRelations = relations(sports, ({ many }) => ({
|
||||||
sportsSeasons: many(sportsSeasons),
|
sportsSeasons: many(sportsSeasons),
|
||||||
|
tournaments: many(tournaments),
|
||||||
|
participants: many(participants),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({
|
export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({
|
||||||
|
|
@ -803,10 +907,57 @@ export const seasonParticipantsRelations = relations(seasonParticipants, ({ one,
|
||||||
fields: [seasonParticipants.sportsSeasonId],
|
fields: [seasonParticipants.sportsSeasonId],
|
||||||
references: [sportsSeasons.id],
|
references: [sportsSeasons.id],
|
||||||
}),
|
}),
|
||||||
|
canonicalParticipant: one(participants, {
|
||||||
|
fields: [seasonParticipants.participantId],
|
||||||
|
references: [participants.id],
|
||||||
|
}),
|
||||||
results: many(seasonParticipantResults),
|
results: many(seasonParticipantResults),
|
||||||
regularSeasonStandings: many(regularSeasonStandings),
|
regularSeasonStandings: many(regularSeasonStandings),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const participantsRelations = relations(participants, ({ one, many }) => ({
|
||||||
|
sport: one(sports, {
|
||||||
|
fields: [participants.sportId],
|
||||||
|
references: [sports.id],
|
||||||
|
}),
|
||||||
|
seasonParticipants: many(seasonParticipants),
|
||||||
|
tournamentResults: many(tournamentResults),
|
||||||
|
surfaceElo: one(participantSurfaceElos, {
|
||||||
|
fields: [participants.id],
|
||||||
|
references: [participantSurfaceElos.participantId],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const tournamentsRelations = relations(tournaments, ({ one, many }) => ({
|
||||||
|
sport: one(sports, {
|
||||||
|
fields: [tournaments.sportId],
|
||||||
|
references: [sports.id],
|
||||||
|
}),
|
||||||
|
scoringEvents: many(scoringEvents),
|
||||||
|
tournamentResults: many(tournamentResults),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const tournamentResultsRelations = relations(tournamentResults, ({ one }) => ({
|
||||||
|
tournament: one(tournaments, {
|
||||||
|
fields: [tournamentResults.tournamentId],
|
||||||
|
references: [tournaments.id],
|
||||||
|
}),
|
||||||
|
participant: one(participants, {
|
||||||
|
fields: [tournamentResults.participantId],
|
||||||
|
references: [participants.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const participantSurfaceElosRelations = relations(
|
||||||
|
participantSurfaceElos,
|
||||||
|
({ one }) => ({
|
||||||
|
participant: one(participants, {
|
||||||
|
fields: [participantSurfaceElos.participantId],
|
||||||
|
references: [participants.id],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
export const seasonTemplatesRelations = relations(seasonTemplates, ({ many }) => ({
|
export const seasonTemplatesRelations = relations(seasonTemplates, ({ many }) => ({
|
||||||
seasonTemplateSports: many(seasonTemplateSports),
|
seasonTemplateSports: many(seasonTemplateSports),
|
||||||
seasons: many(seasons),
|
seasons: many(seasons),
|
||||||
|
|
@ -943,6 +1094,10 @@ export const scoringEventsRelations = relations(scoringEvents, ({ one, many }) =
|
||||||
fields: [scoringEvents.sportsSeasonId],
|
fields: [scoringEvents.sportsSeasonId],
|
||||||
references: [sportsSeasons.id],
|
references: [sportsSeasons.id],
|
||||||
}),
|
}),
|
||||||
|
tournament: one(tournaments, {
|
||||||
|
fields: [scoringEvents.tournamentId],
|
||||||
|
references: [tournaments.id],
|
||||||
|
}),
|
||||||
eventResults: many(eventResults),
|
eventResults: many(eventResults),
|
||||||
playoffMatches: many(playoffMatches),
|
playoffMatches: many(playoffMatches),
|
||||||
tournamentGroups: many(tournamentGroups),
|
tournamentGroups: many(tournamentGroups),
|
||||||
|
|
|
||||||
93
drizzle/0088_cheerful_norrin_radd.sql
Normal file
93
drizzle/0088_cheerful_norrin_radd.sql
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
CREATE TYPE "public"."tournament_status" AS ENUM('scheduled', 'in_progress', 'completed');--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "participant_surface_elos" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"participant_id" uuid NOT NULL,
|
||||||
|
"world_ranking" integer,
|
||||||
|
"elo_hard" integer,
|
||||||
|
"elo_clay" integer,
|
||||||
|
"elo_grass" integer,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "participants" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"sport_id" uuid NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"external_key" varchar(255),
|
||||||
|
"metadata" jsonb,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "tournament_results" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tournament_id" uuid NOT NULL,
|
||||||
|
"participant_id" uuid NOT NULL,
|
||||||
|
"placement" integer,
|
||||||
|
"raw_score" numeric(10, 2),
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE IF NOT EXISTS "tournaments" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"sport_id" uuid NOT NULL,
|
||||||
|
"name" varchar(255) NOT NULL,
|
||||||
|
"year" integer NOT NULL,
|
||||||
|
"starts_at" timestamp,
|
||||||
|
"ends_at" timestamp,
|
||||||
|
"surface" varchar(50),
|
||||||
|
"location" varchar(255),
|
||||||
|
"status" "tournament_status" DEFAULT 'scheduled' NOT NULL,
|
||||||
|
"external_key" varchar(255),
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "scoring_events" ADD COLUMN "tournament_id" uuid;--> statement-breakpoint
|
||||||
|
ALTER TABLE "season_participants" ADD COLUMN "participant_id" uuid;--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "participant_surface_elos" ADD CONSTRAINT "participant_surface_elos_participant_id_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "participants" ADD CONSTRAINT "participants_sport_id_sports_id_fk" FOREIGN KEY ("sport_id") REFERENCES "public"."sports"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "tournament_results" ADD CONSTRAINT "tournament_results_tournament_id_tournaments_id_fk" FOREIGN KEY ("tournament_id") REFERENCES "public"."tournaments"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "tournament_results" ADD CONSTRAINT "tournament_results_participant_id_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."participants"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "tournaments" ADD CONSTRAINT "tournaments_sport_id_sports_id_fk" FOREIGN KEY ("sport_id") REFERENCES "public"."sports"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "participant_surface_elos_participant_unique" ON "participant_surface_elos" USING btree ("participant_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "participants_sport_name_unique" ON "participants" USING btree ("sport_id","name");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "tournament_results_tournament_participant_unique" ON "tournament_results" USING btree ("tournament_id","participant_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "tournaments_sport_name_year_unique" ON "tournaments" USING btree ("sport_id","name","year");--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "scoring_events" ADD CONSTRAINT "scoring_events_tournament_id_tournaments_id_fk" FOREIGN KEY ("tournament_id") REFERENCES "public"."tournaments"("id") ON DELETE set null ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
DO $$ BEGIN
|
||||||
|
ALTER TABLE "season_participants" ADD CONSTRAINT "season_participants_participant_id_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."participants"("id") ON DELETE restrict ON UPDATE no action;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
5688
drizzle/meta/0088_snapshot.json
Normal file
5688
drizzle/meta/0088_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -617,6 +617,13 @@
|
||||||
"when": 1777661299612,
|
"when": 1777661299612,
|
||||||
"tag": "0087_small_susan_delgado",
|
"tag": "0087_small_susan_delgado",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 88,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777666830394,
|
||||||
|
"tag": "0088_cheerful_norrin_radd",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue