diff --git a/app/features/tournament/TournamentRepository.searchByName.test.ts b/app/features/tournament/TournamentRepository.searchByName.test.ts new file mode 100644 index 000000000..3c3450fff --- /dev/null +++ b/app/features/tournament/TournamentRepository.searchByName.test.ts @@ -0,0 +1,88 @@ +import { add, sub } from "date-fns"; +import { beforeEach, describe, expect, test } from "vitest"; +import * as TournamentFactory from "~/db/seed/factories/TournamentFactory"; +import * as UserFactory from "~/db/seed/factories/UserFactory"; +import { dateToDatabaseTimestamp } from "~/utils/dates"; +import * as TournamentRepository from "./TournamentRepository.server"; + +const QUERY = "In The Zone"; + +let author: { id: number }; + +const createTournament = (name: string, startsAt: Date) => + TournamentFactory.create({ + authorId: author.id, + name, + startTimes: [dateToDatabaseTimestamp(startsAt)], + }); + +const search = async (limit = 10) => + (await TournamentRepository.searchByName({ query: QUERY, limit })).map( + (tournament) => tournament.name, + ); + +describe("TournamentRepository.searchByName", () => { + beforeEach(async () => { + author = await UserFactory.create(); + }); + + test("sorts a tournament that is happening right now first", async () => { + await createTournament(`${QUERY} Tomorrow`, add(new Date(), { days: 1 })); + await createTournament(`${QUERY} Started`, sub(new Date(), { hours: 3 })); + + expect(await search()).toEqual([`${QUERY} Started`, `${QUERY} Tomorrow`]); + }); + + test("sorts the next tournament up before ones that already happened", async () => { + await createTournament(`${QUERY} Yesterday`, sub(new Date(), { days: 2 })); + await createTournament( + `${QUERY} Next Month`, + add(new Date(), { days: 30 }), + ); + + expect(await search()).toEqual([ + `${QUERY} Next Month`, + `${QUERY} Yesterday`, + ]); + }); + + test("sorts the rest by their distance from now", async () => { + await createTournament( + `${QUERY} In 3 Weeks`, + add(new Date(), { weeks: 3 }), + ); + await createTournament( + `${QUERY} 2 Weeks Ago`, + sub(new Date(), { weeks: 2 }), + ); + await createTournament( + `${QUERY} In 2 Weeks`, + add(new Date(), { weeks: 2 }), + ); + await createTournament( + `${QUERY} 3 Weeks Ago`, + sub(new Date(), { weeks: 3 }), + ); + + expect(await search()).toEqual([ + `${QUERY} In 2 Weeks`, + `${QUERY} 2 Weeks Ago`, + `${QUERY} In 3 Weeks`, + `${QUERY} 3 Weeks Ago`, + ]); + }); + + test("keeps the next tournament up in a result set the limit cuts short", async () => { + await createTournament(`${QUERY} Next Year`, add(new Date(), { years: 1 })); + await createTournament(`${QUERY} Last Week`, sub(new Date(), { weeks: 1 })); + await createTournament( + `${QUERY} Last Month`, + sub(new Date(), { days: 30 }), + ); + + expect(await search(2)).toEqual([ + `${QUERY} Next Year`, + `${QUERY} Last Week`, + ]); + }); +}); diff --git a/app/features/tournament/TournamentRepository.server.ts b/app/features/tournament/TournamentRepository.server.ts index 7c8b571ba..c09888e1a 100644 --- a/app/features/tournament/TournamentRepository.server.ts +++ b/app/features/tournament/TournamentRepository.server.ts @@ -1547,6 +1547,14 @@ export function finalizeWithoutSummary(tournamentId: number) { .execute(); } +/** How close to its start time a tournament counts as happening right now. */ +const TOURNAMENT_ONGOING_WINDOW_IN_SECONDS = 24 * 60 * 60; + +/** + * Searches tournaments whose calendar event name contains the query, hidden events excluded. + * + * Ordered so that the tournaments most likely being looked for come first + */ export async function searchByName({ query, limit, @@ -1558,6 +1566,12 @@ export async function searchByName({ minStartTime?: Date; maxStartTime?: Date; }) { + const now = databaseTimestampNow(); + const distanceFromNow = sql`abs("CalendarEventDate"."startsAt" - ${now})`; + // window function so that the next tournament up is the next one of all the matches, + // not only of the ones that happen to fit in the limit + const nextUpStartsAt = sql`min(case when "CalendarEventDate"."startsAt" - ${now} >= ${TOURNAMENT_ONGOING_WINDOW_IN_SECONDS} then "CalendarEventDate"."startsAt" end) over ()`; + let sqlQuery = db .selectFrom("Tournament") .innerJoin("CalendarEvent", "Tournament.id", "CalendarEvent.tournamentId") @@ -1574,7 +1588,15 @@ export async function searchByName({ ]) .where("CalendarEvent.name", "like", `%${query}%`) .where("CalendarEvent.hidden", "=", 0) - .orderBy("CalendarEventDate.startsAt", "desc") + .orderBy( + sql`case + when ${distanceFromNow} < ${TOURNAMENT_ONGOING_WINDOW_IN_SECONDS} then 0 + when "CalendarEventDate"."startsAt" = ${nextUpStartsAt} then 1 + else 2 + end`, + ) + .orderBy(distanceFromNow) + .orderBy("Tournament.id") .limit(limit); if (minStartTime) {