More consistent date columns

This commit is contained in:
Kalle 2026-07-26 15:14:24 +03:00
parent 4b48a45109
commit 4cf2003e3c
109 changed files with 536 additions and 404 deletions

View File

@ -63,7 +63,7 @@ export function EventsList({
const groupedEvents = events.reduce<Record<string, typeof events>>(
(acc, event) => {
const key = getDayKey(event.startTime);
const key = getDayKey(event.startsAt);
if (!acc[key]) {
acc[key] = [];
}
@ -79,7 +79,7 @@ export function EventsList({
<>
{dayKeys.map((dayKey) => {
const dayEvents = groupedEvents[dayKey];
const firstDate = new Date(dayEvents[0].startTime * 1000);
const firstDate = new Date(dayEvents[0].startsAt * 1000);
return (
<div key={dayKey}>
@ -89,7 +89,7 @@ export function EventsList({
key={`${event.type}-${event.id}`}
to={event.url}
imageUrl={event.logoUrl ?? undefined}
subtitle={timeFormatter.format(event.startTime)}
subtitle={timeFormatter.format(event.startsAt)}
onClick={onClick}
>
{event.scrimStatus === "booked"

View File

@ -99,7 +99,7 @@ function TournamentItem({ item }: { item: TournamentSearchItem }) {
<div className={searchSelectStyles.itemTextsContainer}>
<span>{item.name}</span>
<LocaleTime
date={item.startTime}
date={item.startsAt}
options={{
day: "numeric",
month: "numeric",

View File

@ -509,7 +509,7 @@ function ResultItem({ result }: { result: SearchResult }) {
<div className={styles.resultTexts}>
<span className={styles.resultName}>{result.name}</span>
<LocaleTime
date={result.startTime}
date={result.startsAt}
options={{ day: "numeric", month: "long", year: "numeric" }}
className={styles.resultSecondary}
/>

View File

@ -320,7 +320,7 @@ export function Layout({
imageUrl={event.logoUrl ?? undefined}
subtitle={
isHydrated ? (
formatRelativeDate(event.startTime)
formatRelativeDate(event.startsAt)
) : (
<span className="invisible">Placeholder</span>
)

View File

@ -312,7 +312,7 @@ function finalizedBracket() {
// CalendarEventDate — recent start time (within 7-day spoiler window)
sql
.prepare(
`insert into "CalendarEventDate" ("eventId", "startTime")
`insert into "CalendarEventDate" ("eventId", "startsAt")
values ($eventId, $startTime)`,
)
.run({
@ -564,7 +564,7 @@ function abDivisionsTournament() {
sql
.prepare(
`insert into "CalendarEventDate" ("eventId", "startTime")
`insert into "CalendarEventDate" ("eventId", "startsAt")
values ($eventId, $startTime)`,
)
.run({
@ -705,7 +705,7 @@ function fixAdminId() {
function makeAdminPatron() {
sql
.prepare(
`update "User" set "patronTier" = 2, "patronSince" = 1674663454 where id = 1`,
`update "User" set "patronTier" = 2, "patronStartedAt" = 1674663454 where id = 1`,
)
.run();
}
@ -1117,7 +1117,7 @@ async function lastMonthsVoting() {
year,
score: 1,
tier: idToPlusTier(id),
validAfter: dateToDatabaseTimestamp(fiveMinutesAgo),
becomesValidAt: dateToDatabaseTimestamp(fiveMinutesAgo),
votedId: id,
});
}
@ -1129,7 +1129,7 @@ async function lastMonthsVoting() {
year,
score: -1,
tier: idToPlusTier(id),
validAfter: dateToDatabaseTimestamp(fiveMinutesAgo),
becomesValidAt: dateToDatabaseTimestamp(fiveMinutesAgo),
votedId: id,
});
}
@ -1267,19 +1267,19 @@ function patrons() {
) as number[];
const givePatronStm = sql.prepare(
`update user set "patronTier" = $patronTier, "patronSince" = $patronSince where id = $id`,
`update user set "patronTier" = $patronTier, "patronStartedAt" = $patronStartedAt where id = $id`,
);
for (const id of userIds) {
givePatronStm.run({
id,
patronSince: dateToDatabaseTimestamp(faker.date.past()),
patronStartedAt: dateToDatabaseTimestamp(faker.date.past()),
patronTier: faker.helpers.arrayElement([1, 1, 2, 2, 2, 3, 3, 4]),
});
}
givePatronStm.run({
id: ADMIN_ID,
patronSince: dateToDatabaseTimestamp(faker.date.past()),
patronStartedAt: dateToDatabaseTimestamp(faker.date.past()),
patronTier: 2,
});
@ -1372,7 +1372,7 @@ function calendarEvents() {
`
insert into "CalendarEventDate" (
"eventId",
"startTime"
"startsAt"
) values (
$eventId,
$startTime
@ -1392,7 +1392,7 @@ function calendarEvents() {
`
insert into "CalendarEventDate" (
"eventId",
"startTime"
"startsAt"
) values (
$eventId,
$startTime
@ -1441,7 +1441,7 @@ async function calendarEventResults() {
`select "CalendarEvent"."id"
from "CalendarEvent"
join "CalendarEventDate" on "CalendarEventDate"."eventId" = "CalendarEvent"."id"
where "CalendarEventDate"."startTime" < $startTime`,
where "CalendarEventDate"."startsAt" < $startTime`,
)
.all({ startTime: dateToDatabaseTimestamp(new Date()) }) as any[]
).map((r) => r.id),
@ -1729,7 +1729,7 @@ function calendarEventWithToTools(
`
insert into "CalendarEventDate" (
"eventId",
"startTime"
"startsAt"
) values (
$eventId,
$startTime
@ -3306,8 +3306,8 @@ async function scrimPosts() {
// scrim with admin on the ALPHA side and N-ZAP on the BRAVO side.
const adminVsNzapAt = date(true);
const adminVsNzapPostId = await ScrimPostRepository.insert({
at: adminVsNzapAt,
rangeEnd: null,
startsAt: adminVsNzapAt,
rangeEndsAt: null,
isScheduledForFuture: true,
teamId: null,
text: null,
@ -3329,8 +3329,8 @@ async function scrimPosts() {
const atTime = date();
const hasRangeEnd = Math.random() > 0.5;
await ScrimPostRepository.insert({
at: atTime,
rangeEnd: hasRangeEnd
startsAt: atTime,
rangeEndsAt: hasRangeEnd
? dateToDatabaseTimestamp(
add(databaseTimestampToDate(atTime), {
hours: faker.helpers.rangeToNumber({ min: 1, max: 3 }),
@ -3355,7 +3355,7 @@ async function scrimPosts() {
const adminPostAtTime = date(true); // admin's scrim is always at least 1 hour in the future
const adminPostId = await ScrimPostRepository.insert({
at: adminPostAtTime,
startsAt: adminPostAtTime,
isScheduledForFuture: true,
text:
faker.number.float(1) > 0.5
@ -3777,7 +3777,7 @@ function splatoonRotations() {
sql
.prepare(
`
insert into "SplatoonRotation" ("type", "mode", "stageId1", "stageId2", "startTime", "endTime")
insert into "SplatoonRotation" ("type", "mode", "stageId1", "stageId2", "startsAt", "endsAt")
values ($type, $mode, $stageId1, $stageId2, $startTime, $endTime)
`,
)

View File

@ -226,7 +226,7 @@ export interface CalendarEventBadge {
export interface CalendarEventDate {
eventId: number;
id: GeneratedAlways<number>;
startTime: number;
startsAt: number;
}
export interface CalendarEventResultPlayer {
@ -329,7 +329,7 @@ export interface LFGPost {
plusTierVisibility: number | null;
languages: string | null;
updatedAt: Generated<number>;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface MapPoolMap {
@ -362,7 +362,7 @@ export interface PlayerResult {
export interface PlusSuggestion {
authorId: number;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
id: GeneratedAlways<number>;
month: number;
suggestedId: number;
@ -382,7 +382,8 @@ export interface PlusVote {
month: number;
score: number;
tier: number;
validAfter: number;
/** When the vote stops being secret and starts counting towards the results i.e. the end of the voting range. */
becomesValidAt: number;
votedId: number;
year: number;
}
@ -417,6 +418,7 @@ export interface Skill {
season: number;
tournamentId: number | null;
userId: number | null;
/** Can be null because we did not always save this. */
createdAt: number | null;
}
@ -521,7 +523,7 @@ export interface TournamentMatchPickBanEvent {
matchId: number;
authorId: number | null;
number: number;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface TournamentMatchGameResult {
@ -585,8 +587,7 @@ export interface TournamentStage {
settings: JSONColumnType<StageSettings>;
tournamentId: number;
type: (typeof TOURNAMENT_STAGE_TYPES)[number];
// not Generated<> because SQLite doesn't allow altering tables to add columns with default values :(
createdAt: number | null;
createdAt: Generated<number>;
}
/** Tournament sub post, shown in a list of subs available for teams to pick from. */
@ -681,7 +682,7 @@ export interface TournamentAuditLog {
/** References {@link TournamentTeamHistory.id} so the team name stays resolvable after the team is hard-deleted. */
tournamentTeamHistoryId: number | null;
metadata: JSONColumnTypeNullable<TournamentAuditLogMetadata>;
createdAt: number;
createdAt: Generated<number>;
}
export interface TournamentOrganization {
@ -769,7 +770,8 @@ export interface UnvalidatedVideo {
title: string;
type: string;
validatedAt: number | null;
youtubeDate: number;
/** When the video was published on YouTube. Day precision only, stored as noon UTC of that day. */
youtubePublishedAt: number;
youtubeId: string;
}
@ -806,9 +808,9 @@ export interface User {
languages: string | null;
motionSens: number | null;
pronouns: JSONColumnTypeNullable<Pronouns>;
patronSince: number | null;
patronStartedAt: number | null;
patronTier: number | null;
patronTill: number | null;
patronExpiresAt: number | null;
showDiscordUniqueName: Generated<DBBoolean>;
stickSens: number | null;
twitch: string | null;
@ -886,7 +888,7 @@ export interface UserFriendCode {
friendCode: string;
userId: number;
submitterUserId: number;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface UserWidget {
@ -899,7 +901,7 @@ export interface ApiToken {
userId: number;
token: string;
type: Generated<ApiTokenType>;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface LiveStream {
@ -922,7 +924,7 @@ export interface ExternalStream {
name: string;
url: string;
avatarImgId: number | null;
startTime: number;
startsAt: number;
createdAt: Generated<number>;
}
@ -943,7 +945,7 @@ export interface BanLog {
banned: number | null;
bannedReason: string | null;
bannedByUserId: number;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface ModNote {
@ -951,7 +953,7 @@ export interface ModNote {
userId: number;
authorId: number;
text: string;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
isDeleted: Generated<DBBoolean>;
}
@ -974,7 +976,8 @@ export interface Video {
type: "SCRIM" | "TOURNAMENT" | "MATCHMAKING" | "CAST" | "SENDOUQ";
/** Never `null` in practice, the view filters unvalidated rows out. */
validatedAt: number | null;
youtubeDate: number;
/** When the video was published on YouTube. Day precision only, stored as noon UTC of that day. */
youtubePublishedAt: number;
youtubeId: string;
}
@ -1014,9 +1017,9 @@ export interface XRankPlacement {
export interface ScrimPost {
id: GeneratedAlways<number>;
/** When is the scrim scheduled to happen */
at: number;
/** Optional end of time range indicating team accepts scrims starting between at and rangeEnd */
rangeEnd: number | null;
startsAt: number;
/** Optional end of time range indicating team accepts scrims starting between startsAt and rangeEndsAt */
rangeEndsAt: number | null;
/** Highest LUTI div accepted */
maxDiv: number | null;
/** Lowest LUTI div accepted */
@ -1043,7 +1046,7 @@ export interface ScrimPost {
maps: "SZ" | "ALL" | "RANKED" | null;
/** If set, specifies the maps of a tournament to play */
mapsTournamentId: number | null;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
updatedAt: Generated<number>;
}
@ -1080,10 +1083,10 @@ export interface ScrimPostRequest {
scrimPostId: number;
teamId: number | null;
message: string | null;
/** Specific time selected by requester (required when post has rangeEnd) */
at: number | null;
/** Specific time selected by requester (required when post has rangeEndsAt) */
startsAt: number | null;
isAccepted: Generated<DBBoolean>;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface ScrimPostRequestUser {
@ -1097,7 +1100,7 @@ export interface Association {
id: GeneratedAlways<number>;
name: string;
inviteCode: string;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface AssociationMember {
@ -1111,7 +1114,7 @@ export interface Notification {
type: NotificationValue["type"];
meta: JSONColumnTypeNullable<Record<string, number | string>>;
pictureUrl: string | null;
createdAt: GeneratedAlways<number>;
createdAt: Generated<number>;
}
export interface NotificationUser {
@ -1133,8 +1136,8 @@ export interface SplatoonRotation {
mode: string;
stageId1: number;
stageId2: number;
startTime: number;
endTime: number;
startsAt: number;
endsAt: number;
}
export type Tables = { [P in keyof DB]: Selectable<DB[P]> };

View File

@ -291,15 +291,15 @@ export async function linkUserAndPlayer({
export function forcePatron(args: {
id: number;
patronTier: Tables["User"]["patronTier"];
patronSince: Date;
patronTill: Date;
patronStartedAt: Date;
patronExpiresAt: Date;
}) {
return db
.updateTable("User")
.set({
patronTier: args.patronTier,
patronSince: dateToDatabaseTimestamp(args.patronSince),
patronTill: dateToDatabaseTimestamp(args.patronTill),
patronStartedAt: dateToDatabaseTimestamp(args.patronStartedAt),
patronExpiresAt: dateToDatabaseTimestamp(args.patronExpiresAt),
})
.where("User.id", "=", args.id)
.execute();

View File

@ -12,7 +12,7 @@ const RETENTION_SECONDS = 24 * 60 * 60;
export function insert(
args: Pick<
TablesInsertable["ExternalStream"],
"name" | "url" | "avatarImgId" | "startTime"
"name" | "url" | "avatarImgId" | "startsAt"
>,
) {
return db.insertInto("ExternalStream").values(args).execute();
@ -36,12 +36,12 @@ export function all() {
"ExternalStream.id",
"ExternalStream.name",
"ExternalStream.url",
"ExternalStream.startTime",
"ExternalStream.startsAt",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"avatarUrl",
),
])
.orderBy("ExternalStream.startTime", "asc")
.orderBy("ExternalStream.startsAt", "asc")
.execute();
}
@ -58,13 +58,13 @@ export function forSidebar() {
"ExternalStream.id",
"ExternalStream.name",
"ExternalStream.url",
"ExternalStream.startTime",
"ExternalStream.startsAt",
concatUserSubmittedImagePrefix(eb.ref("UserSubmittedImage.url")).as(
"avatarUrl",
),
])
.where(
"ExternalStream.startTime",
"ExternalStream.startsAt",
">=",
databaseTimestampNow() - SIDEBAR_VISIBLE_SECONDS,
)
@ -75,6 +75,6 @@ export function forSidebar() {
export function deleteOld() {
return db
.deleteFrom("ExternalStream")
.where("startTime", "<", databaseTimestampNow() - RETENTION_SECONDS)
.where("startsAt", "<", databaseTimestampNow() - RETENTION_SECONDS)
.executeTakeFirst();
}

View File

@ -74,9 +74,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
await AdminRepository.forcePatron({
id: data.user,
patronSince: new Date(),
patronStartedAt: new Date(),
patronTier: data.patronTier,
patronTill: new Date(data.patronTill),
patronExpiresAt: new Date(data.patronExpiresAt),
});
message = "Patron status updated";
@ -207,7 +207,7 @@ export const adminActionSchema = z.union([
_action: _action("FORCE_PATRON"),
user: z.preprocess(actualNumber, z.number().positive()),
patronTier: z.preprocess(actualNumber, z.number()),
patronTill: z.string(),
patronExpiresAt: z.string(),
}),
z.object({
_action: _action("VIDEO_ADDER"),

View File

@ -27,7 +27,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
name: data.name,
url: data.url,
avatarImgId: data.avatar,
startTime: dateToDatabaseTimestamp(data.startTime),
startsAt: dateToDatabaseTimestamp(data.startTime),
});
break;
}

View File

@ -68,7 +68,7 @@ function ExternalStreamList() {
</Link>
<span className="text-xs text-lighter">
<LocaleTime
date={stream.startTime}
date={stream.startsAt}
inline
options={{
month: "numeric",

View File

@ -35,7 +35,7 @@ const voteArgs = ({
authorId,
month,
tier: 1,
validAfter: dateToDatabaseTimestamp(new Date("2021-12-11T00:00:00.000Z")),
becomesValidAt: dateToDatabaseTimestamp(new Date("2021-12-11T00:00:00.000Z")),
year,
});

View File

@ -370,7 +370,7 @@ function ForcePatron() {
<div className="flex-same-size">
<label>Patron till</label>
<input name="patronTill" type="date" />
<input name="patronExpiresAt" type="date" />
</div>
</div>
<div className="stack horizontal md">

View File

@ -88,7 +88,7 @@ function adjustSeedDatesToCurrent(variation: SeedVariation) {
REG_OPEN_TOURNAMENT_IDS.includes(tournamentId);
sql
.prepare(`UPDATE "CalendarEventDate" SET startTime = ? WHERE eventId = ?`)
.prepare(`UPDATE "CalendarEventDate" SET startsAt = ? WHERE eventId = ?`)
.run(isRegOpen ? halfAnHourFromNow : oneHourAgo, id);
}
@ -103,17 +103,17 @@ function adjustSeedDatesToCurrent(variation: SeedVariation) {
const scrimTimeOffset = now - SEED_REFERENCE_TIMESTAMP;
sql
.prepare(
`UPDATE "ScrimPost" SET "at" = "at" + ?, "createdAt" = "createdAt" + ?`,
`UPDATE "ScrimPost" SET "startsAt" = "startsAt" + ?, "createdAt" = "createdAt" + ?`,
)
.run(scrimTimeOffset, scrimTimeOffset);
sql
.prepare(
`UPDATE "ScrimPost" SET "rangeEnd" = "rangeEnd" + ? WHERE "rangeEnd" IS NOT NULL`,
`UPDATE "ScrimPost" SET "rangeEndsAt" = "rangeEndsAt" + ? WHERE "rangeEndsAt" IS NOT NULL`,
)
.run(scrimTimeOffset);
sql
.prepare(
`UPDATE "ScrimPostRequest" SET "at" = "at" + ? WHERE "at" IS NOT NULL`,
`UPDATE "ScrimPostRequest" SET "startsAt" = "startsAt" + ? WHERE "startsAt" IS NOT NULL`,
)
.run(scrimTimeOffset);
}

View File

@ -24,7 +24,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const result: GetCalendarWeekResponse = events.map((event) => ({
name: event.name,
startTime: databaseTimestampToDate(event.startTime).toISOString(),
startTime: databaseTimestampToDate(event.startsAt).toISOString(),
tournamentId: event.tournamentId,
tournamentUrl: event.tournamentId
? `https://sendou.ink/to/${event.tournamentId}/brackets`
@ -48,19 +48,15 @@ function fetchEventsOfWeek(args: { week: number; year: number }) {
.select([
"Tournament.id as tournamentId",
"CalendarEvent.name",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
])
.where(
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
">=",
dateToDatabaseTimestamp(startTime),
)
.where(
"CalendarEventDate.startTime",
"<=",
dateToDatabaseTimestamp(endTime),
)
.where("CalendarEventDate.startsAt", "<=", dateToDatabaseTimestamp(endTime))
.where("CalendarEvent.hidden", "=", 0)
.orderBy("CalendarEventDate.startTime", "asc")
.orderBy("CalendarEventDate.startsAt", "asc")
.execute();
}

View File

@ -26,7 +26,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
.select(({ eb, exists, selectFrom }) => [
"CalendarEvent.name",
"CalendarEvent.organizationId",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
"Tournament.settings",
exists(
selectFrom("TournamentResult")
@ -61,7 +61,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const result: GetTournamentResponse = {
name: tournament.name,
startTime: databaseTimestampToDate(tournament.startTime).toISOString(),
startTime: databaseTimestampToDate(tournament.startsAt).toISOString(),
url: `https://sendou.ink/to/${id}/brackets`,
logoUrl: tournament.logoUrl,
teams: {

View File

@ -46,8 +46,8 @@ describe("Permission logic consistency between allApiTokens and checkUserHasApiA
await AdminRepository.forcePatron({
id: 1,
patronTier: 2,
patronSince: new Date(),
patronTill: add(new Date(), { months: 3 }),
patronStartedAt: new Date(),
patronExpiresAt: add(new Date(), { months: 3 }),
});
await ApiRepository.generateToken(1, "read");
@ -64,8 +64,8 @@ describe("Permission logic consistency between allApiTokens and checkUserHasApiA
await AdminRepository.forcePatron({
id: 1,
patronTier: 1,
patronSince: new Date(),
patronTill: add(new Date(), { months: 3 }),
patronStartedAt: new Date(),
patronExpiresAt: add(new Date(), { months: 3 }),
});
await ApiRepository.generateToken(1, "read");

View File

@ -149,7 +149,7 @@ const withTeamsCount = (
.where((eb) =>
eb.or([
eb("TournamentTeamCheckIn.checkedInAt", "is not", null),
eb("CalendarEventDate.startTime", ">", databaseTimestampNow()),
eb("CalendarEventDate.startsAt", ">", databaseTimestampNow()),
]),
)
.select(({ fn }) => [fn.countAll<number>().as("teamsCount")]);
@ -176,10 +176,10 @@ function findAllBetweenTwoTimestampsQuery({
"Tournament.tier",
"CalendarEvent.name",
"CalendarEvent.tags",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
// events get grouped to their closest :00 or :30 so for example users can't make their event start at :59 to make it show at the top
sql<number>`(("CalendarEventDate"."startTime" + 900) / 1800) * 1800`.as(
"normalizedStartTime",
sql<number>`(("CalendarEventDate"."startsAt" + 900) / 1800) * 1800`.as(
"normalizedStartsAt",
),
withOrganization(eb).as("organization"),
withTeamsCount(eb).as("teamsCount"),
@ -205,15 +205,11 @@ function findAllBetweenTwoTimestampsQuery({
])
.where("CalendarEvent.hidden", "=", 0)
.where(
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
">=",
dateToDatabaseTimestamp(startTime),
)
.where(
"CalendarEventDate.startTime",
"<=",
dateToDatabaseTimestamp(endTime),
)
.where("CalendarEventDate.startsAt", "<=", dateToDatabaseTimestamp(endTime))
.$narrowType<{ teamsCount: NotNull }>()
.execute();
}
@ -224,14 +220,14 @@ function findAllBetweenTwoTimestampsMapped(
at: number;
events: Array<CalendarEvent>;
}> {
const mapped: Array<CalendarEvent & { startTime: number }> = rows.map(
const mapped: Array<CalendarEvent & { startsAt: number }> = rows.map(
(row) => {
const tags = row.tags
? (row.tags.split(",") as CalendarEvent["tags"])
: [];
const isPastEvent =
databaseTimestampToDate(row.startTime) < sub(new Date(), { days: 1 });
databaseTimestampToDate(row.startsAt) < sub(new Date(), { days: 1 });
const tentativeTier =
row.tier === null &&
row.organizationId !== null &&
@ -241,7 +237,7 @@ function findAllBetweenTwoTimestampsMapped(
: null;
return {
at: databaseTimestampToJavascriptTimestamp(row.startTime),
at: databaseTimestampToJavascriptTimestamp(row.startsAt),
type: "calendar",
id: row.eventId,
url: row.tournamentId
@ -265,11 +261,11 @@ function findAllBetweenTwoTimestampsMapped(
: null,
badges: row.badges,
logoUrl: row.logoUrl,
startTime: row.normalizedStartTime,
startsAt: row.normalizedStartsAt,
isRanked: row.tournamentSettings
? tournamentIsRanked({
isSetAsRanked: row.tournamentSettings.isRanked,
startTime: databaseTimestampToDate(row.startTime),
startsAt: databaseTimestampToDate(row.startsAt),
minMembersPerTeam: row.tournamentSettings.minMembersPerTeam ?? 4,
isTest: row.tournamentSettings.isTest ?? false,
})
@ -280,7 +276,7 @@ function findAllBetweenTwoTimestampsMapped(
},
);
const grouped = R.groupBy(mapped, (row) => row.startTime);
const grouped = R.groupBy(mapped, (row) => row.startsAt);
const dates = Object.keys(grouped)
.map((dbTimestamp) => ({
at: databaseTimestampToDate(Number(dbTimestamp)).getTime(),
@ -327,7 +323,7 @@ export async function findById(
"CalendarEvent.avatarImgId",
"Tournament.mapPickingStyle",
"User.id as authorId",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
"CalendarEventDate.eventId",
"User.username",
"User.discordId",
@ -338,7 +334,7 @@ export async function findById(
),
])
.where("CalendarEvent.id", "=", id)
.orderBy("CalendarEventDate.startTime", "asc")
.orderBy("CalendarEventDate.startsAt", "asc")
.execute();
if (!firstRow) return null;
@ -346,8 +342,8 @@ export async function findById(
return {
...firstRow,
tags: tagsArray(firstRow),
startTimes: [firstRow, ...rest].map((row) => row.startTime),
startTime: undefined,
startTimes: [firstRow, ...rest].map((row) => row.startsAt),
startsAt: undefined,
};
}
@ -363,7 +359,7 @@ export async function findRecentTournamentsByAuthorId(authorId: number) {
.select([
"CalendarEvent.id",
"CalendarEvent.name",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
])
.where("CalendarEvent.authorId", "=", authorId)
.orderBy("CalendarEvent.id", "desc")
@ -425,7 +421,7 @@ type CreateArgs = Pick<
| "bracketUrl"
| "organizationId"
> & {
startTimes: Array<Tables["CalendarEventDate"]["startTime"]>;
startTimes: Array<Tables["CalendarEventDate"]["startsAt"]>;
badges: Array<Tables["CalendarEventBadge"]["badgeId"]>;
mapPoolMaps?: Array<Pick<Tables["MapPoolMap"], "mode" | "stageId">>;
isFullTournament: boolean;
@ -750,7 +746,7 @@ function createDatesInTrx({
}) {
return trx
.insertInto("CalendarEventDate")
.values(startTimes.map((startTime) => ({ startTime, eventId })))
.values(startTimes.map((startsAt) => ({ startsAt, eventId })))
.execute();
}

View File

@ -44,7 +44,7 @@ export interface CalendarEvent extends CommonEvent {
export interface ShowcaseCalendarEvent extends CommonEvent {
type: "showcase";
startTime: number;
startsAt: number;
/** Id of the organization the event belongs to, if any */
organizationId: number | null;
/** Tournament is hidden from the public (test tournament) */

View File

@ -36,7 +36,7 @@ export function TournamentCard({
const isHostedOnSendouInk = typeof tournament.isRanked === "boolean";
const startDate = isShowcase
? databaseTimestampToDate(tournament.startTime)
? databaseTimestampToDate(tournament.startsAt)
: null;
return (

View File

@ -29,19 +29,19 @@ export const loader = async () => {
const registered = tournamentsData.participatingFor
.map(tournamentToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
.sort((a, b) => a.startsAt - b.startsAt);
const hosting = tournamentsData.organizingFor
.map(tournamentToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
.sort((a, b) => a.startsAt - b.startsAt);
const scrims = scrimsData
.map(scrimToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
.sort((a, b) => a.startsAt - b.startsAt);
const saved = savedTournaments
.map(tournamentToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
.sort((a, b) => a.startsAt - b.startsAt);
const userOrganizationIds = new Set(userOrganizations.map((org) => org.id));
const organization = upcomingTournaments
@ -52,7 +52,7 @@ export const loader = async () => {
userOrganizationIds.has(tournament.organizationId),
)
.map(tournamentToSidebarEvent)
.sort((a, b) => a.startTime - b.startTime);
.sort((a, b) => a.startsAt - b.startsAt);
return { registered, hosting, scrims, saved, organization };
};

View File

@ -142,7 +142,7 @@ function useDefaultValues() {
const regClosesAt: RegClosesAtOption = tournamentCtx?.settings.regClosesAt
? datesToRegClosesAt({
startTime: databaseTimestampToDate(tournamentCtx.startTime),
startTime: databaseTimestampToDate(tournamentCtx.startsAt),
regClosesAt: databaseTimestampToDate(
tournamentCtx.settings.regClosesAt,
),
@ -243,7 +243,7 @@ function TemplateTournamentForm() {
<option value="">Select a template</option>
{recentTournaments.map((event) => (
<option key={event.id} value={event.id}>
{event.name} ({formatter.format(event.startTime) ?? ""})
{event.name} ({formatter.format(event.startsAt) ?? ""})
</option>
))}
</select>

View File

@ -39,7 +39,7 @@ export function getLiveTournamentStreams(): SidebarStream[] {
imageUrl: tournament.ctx.logoUrl,
url: tournamentStreamsPage(tournament.ctx.id),
subtitle: deriveCurrentRound(tournament),
startsAt: dateToDatabaseTimestamp(tournament.ctx.startTime),
startsAt: dateToDatabaseTimestamp(tournament.ctx.startsAt),
tier: tournament.ctx.tier,
membersPerTeam: tournament.minMembersPerTeam,
});

View File

@ -105,7 +105,7 @@ function withLfgJoins<QB extends SelectQueryBuilder<any, any, any>>(qb: QB) {
...commonUserSelect(eb),
"CalendarEvent.name as tournamentName",
"TournamentTeam.tournamentId",
"CalendarEventDate.startTime as tournamentStartTime",
"CalendarEventDate.startsAt as tournamentStartTime",
sql<
number | null
>`(SELECT COUNT(*) FROM "TournamentTeamMember" "ttm" WHERE "ttm"."tournamentTeamId" = "TournamentTeam"."id")`.as(

View File

@ -44,7 +44,7 @@ export function SplatoonRotations() {
const nowUnixLive = useNowUnix(data.now);
const allInThePast = data.rotations.every(
(rotation) => rotation.endTime <= nowUnixLive,
(rotation) => rotation.endsAt <= nowUnixLive,
);
if (allInThePast) return null;
@ -63,8 +63,8 @@ export function SplatoonRotations() {
if (activeFilter !== "ALL" && rotation.mode !== activeFilter) continue;
const isCurrent =
rotation.startTime <= nowUnixLive && rotation.endTime > nowUnixLive;
const isNext = rotation.startTime > nowUnixLive;
rotation.startsAt <= nowUnixLive && rotation.endsAt > nowUnixLive;
const isNext = rotation.startsAt > nowUnixLive;
if (!isCurrent && !isNext) continue;
@ -173,8 +173,8 @@ function RotationCard({
const progress = current
? rotationProgress(
now,
databaseTimestampToDate(current.startTime),
databaseTimestampToDate(current.endTime),
databaseTimestampToDate(current.startsAt),
databaseTimestampToDate(current.endsAt),
)
: null;
const displayRotation = current ?? next;
@ -195,7 +195,7 @@ function RotationCard({
style={{ width: `${progress * 100}%` }}
/>
<span className={styles.rotationCardProgressText}>
{formatDistanceToNow(current.endTime)}
{formatDistanceToNow(current.endsAt)}
</span>
</div>
) : null}
@ -208,7 +208,7 @@ function RotationCard({
>
<span className={styles.rotationCardProgressText}>
<NextLabel
startTime={databaseTimestampToDate(next.startTime)}
startTime={databaseTimestampToDate(next.startsAt)}
now={now}
/>
</span>
@ -233,11 +233,11 @@ function RotationCard({
<div className={styles.rotationCardNext}>
{shownNext ? (
<div className={styles.rotationCardNextInfo}>
{current && shownNext.startTime === current.endTime ? (
{current && shownNext.startsAt === current.endsAt ? (
t("front:rotations.nextLabel")
) : (
<NextLabel
startTime={databaseTimestampToDate(shownNext.startTime)}
startTime={databaseTimestampToDate(shownNext.startsAt)}
now={now}
compact
/>

View File

@ -190,7 +190,7 @@ function deleteExtraResults(tournaments: ShowcaseCalendarEvent[]) {
(tournament) =>
tournament.firstPlacers.length === 0 &&
!tournament.isFinalized &&
tournament.startTime > threeDaysAgo,
tournament.startsAt > threeDaysAgo,
);
const rankedResults = tournaments
@ -214,7 +214,7 @@ function deleteExtraResults(tournaments: ShowcaseCalendarEvent[]) {
return {
results: [...rankedResultsToKeep, ...nonRankedResultsToKeep].sort(
(a, b) => b.startTime - a.startTime,
(a, b) => b.startsAt - a.startsAt,
),
upcoming: nonResults,
};
@ -225,8 +225,8 @@ function resolveShowcaseTournaments(
): ShowcaseCalendarEvent[] {
const happeningDuringNextWeek = tournaments.filter(
(tournament) =>
tournament.startTime > databaseTimestampSixHoursAgo() &&
tournament.startTime < databaseTimestampWeekFromNow(),
tournament.startsAt > databaseTimestampSixHoursAgo() &&
tournament.startsAt < databaseTimestampWeekFromNow(),
);
const sorted = happeningDuringNextWeek.sort(
(a, b) => b.teamsCount - a.teamsCount,
@ -238,7 +238,7 @@ function resolveShowcaseTournaments(
.filter((tournament) => !tournament.isRanked)
.slice(0, 6 - ranked.length);
return [...ranked, ...nonRanked].sort((a, b) => a.startTime - b.startTime);
return [...ranked, ...nonRanked].sort((a, b) => a.startsAt - b.startsAt);
}
async function tournamentsToParticipationInfoMap(
@ -302,7 +302,7 @@ function mapTournamentFromDB(
authorId: tournament.authorId,
organizationId: tournament.organizationId,
name: tournament.name,
startTime: tournament.startTime,
startsAt: tournament.startsAt,
teamsCount: tournament.teamsCount,
logoUrl: tournament.logoUrl,
organization: tournament.organization
@ -313,7 +313,7 @@ function mapTournamentFromDB(
: null,
isRanked: tournamentIsRanked({
isSetAsRanked: tournament.settings.isRanked,
startTime: databaseTimestampToDate(tournament.startTime),
startsAt: databaseTimestampToDate(tournament.startsAt),
minMembersPerTeam: tournament.settings.minMembersPerTeam ?? 4,
isTest: tournament.settings.isTest ?? false,
}),

View File

@ -169,7 +169,7 @@ export async function seasonProgressionByUserId({
)
.select(({ fn }) => [
fn.max("Skill.ordinal").as("ordinal"),
sql<string>`date(coalesce("Skill"."createdAt", "GroupMatch"."createdAt", "CalendarEventDate"."startTime"), 'unixepoch')`.as(
sql<string>`date(coalesce("Skill"."createdAt", "GroupMatch"."createdAt", "CalendarEventDate"."startsAt"), 'unixepoch')`.as(
"date",
),
])

View File

@ -37,7 +37,7 @@ export async function allPlusTiersFromLatestVoting() {
const latestVoting = await db
.selectFrom("PlusVote")
.select(["PlusVote.year", "PlusVote.month"])
.where("PlusVote.validAfter", "<", sql<number>`strftime('%s', 'now')`)
.where("PlusVote.becomesValidAt", "<", sql<number>`strftime('%s', 'now')`)
.orderBy("PlusVote.year", "desc")
.orderBy("PlusVote.month", "desc")
.limit(1)
@ -201,7 +201,13 @@ export async function hasVoted(args: {
export type UpsertManyPlusVotesArgs = Pick<
TablesInsertable["PlusVote"],
"month" | "year" | "tier" | "authorId" | "votedId" | "score" | "validAfter"
| "month"
| "year"
| "tier"
| "authorId"
| "votedId"
| "score"
| "becomesValidAt"
>[];
export function upsertMany(votes: UpsertManyPlusVotesArgs) {
const firstVote = votes[0];

View File

@ -59,7 +59,7 @@ export const action: ActionFunction = async ({ request }) => {
month,
year,
tier: user.plusTier!, // no clue why i couldn't make narrowing the type down above work
validAfter: dateToDatabaseTimestamp(votingRange.endDate),
becomesValidAt: dateToDatabaseTimestamp(votingRange.endDate),
})),
);

View File

@ -16,17 +16,17 @@ const WINDOW = {
};
function insertPost({
at,
rangeEnd = null,
startsAt,
rangeEndsAt = null,
users,
}: {
at: Date;
rangeEnd?: Date | null;
startsAt: Date;
rangeEndsAt?: Date | null;
users: Array<{ userId: number; isOwner: 0 | 1 }>;
}) {
return ScrimPostRepository.insert({
at: dbTs(at),
rangeEnd: rangeEnd ? dbTs(rangeEnd) : null,
startsAt: dbTs(startsAt),
rangeEndsAt: rangeEndsAt ? dbTs(rangeEndsAt) : null,
maxDiv: null,
minDiv: null,
teamId: null,
@ -51,7 +51,7 @@ describe("findPendingOverlapsForUsers", () => {
test("returns a specific-time pending post in window with its member ids", async () => {
const postId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [
{ userId: 1, isOwner: 1 },
{ userId: 2, isOwner: 0 },
@ -71,8 +71,8 @@ describe("findPendingOverlapsForUsers", () => {
test("returns a ranged post whose interval overlaps the window even if its start is outside", async () => {
const postId = await insertPost({
at: sub(BOOKED_AT, { hours: 2 }),
rangeEnd: BOOKED_AT,
startsAt: sub(BOOKED_AT, { hours: 2 }),
rangeEndsAt: BOOKED_AT,
users: [{ userId: 1, isOwner: 1 }],
});
@ -87,8 +87,8 @@ describe("findPendingOverlapsForUsers", () => {
test("does not return a ranged post whose interval does not overlap the window", async () => {
await insertPost({
at: sub(BOOKED_AT, { hours: 5 }),
rangeEnd: sub(BOOKED_AT, { hours: 3 }),
startsAt: sub(BOOKED_AT, { hours: 5 }),
rangeEndsAt: sub(BOOKED_AT, { hours: 3 }),
users: [{ userId: 1, isOwner: 1 }],
});
@ -103,7 +103,7 @@ describe("findPendingOverlapsForUsers", () => {
test("excludes the just-booked post even when it overlaps", async () => {
const postId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 1, isOwner: 1 }],
});
@ -118,7 +118,7 @@ describe("findPendingOverlapsForUsers", () => {
test("does not return posts that involve none of the given users", async () => {
await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 3, isOwner: 1 }],
});
@ -133,14 +133,14 @@ describe("findPendingOverlapsForUsers", () => {
test("excludes already-accepted (booked) posts", async () => {
const postId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 1, isOwner: 1 }],
});
await ScrimPostRepository.insertRequest({
scrimPostId: postId,
teamId: null,
message: null,
at: dbTs(BOOKED_AT),
startsAt: dbTs(BOOKED_AT),
users: [{ userId: 3, isOwner: 1 }],
});
const post = await ScrimPostRepository.findById(postId);
@ -159,14 +159,14 @@ describe("findPendingOverlapsForUsers", () => {
test("returns pending request ids whose effective time falls in the window", async () => {
const postId = await insertPost({
at: add(BOOKED_AT, { hours: 3 }),
startsAt: add(BOOKED_AT, { hours: 3 }),
users: [{ userId: 3, isOwner: 1 }],
});
await ScrimPostRepository.insertRequest({
scrimPostId: postId,
teamId: null,
message: null,
at: dbTs(BOOKED_AT),
startsAt: dbTs(BOOKED_AT),
users: [{ userId: 1, isOwner: 1 }],
});
const post = await ScrimPostRepository.findById(postId);
@ -185,14 +185,14 @@ describe("findPendingOverlapsForUsers", () => {
test("does not return pending requests whose effective time is outside the window", async () => {
const postId = await insertPost({
at: add(BOOKED_AT, { hours: 3 }),
startsAt: add(BOOKED_AT, { hours: 3 }),
users: [{ userId: 3, isOwner: 1 }],
});
await ScrimPostRepository.insertRequest({
scrimPostId: postId,
teamId: null,
message: null,
at: dbTs(add(BOOKED_AT, { hours: 3 })),
startsAt: dbTs(add(BOOKED_AT, { hours: 3 })),
users: [{ userId: 1, isOwner: 1 }],
});
@ -218,14 +218,14 @@ describe("findUserScrims", () => {
test("passed-over requester does not see the scrim booked between the post and another team", async () => {
const postId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 3, isOwner: 1 }],
});
await ScrimPostRepository.insertRequest({
scrimPostId: postId,
teamId: null,
message: null,
at: null,
startsAt: null,
users: [
{ userId: 1, isOwner: 1 },
{ userId: 2, isOwner: 0 },
@ -235,7 +235,7 @@ describe("findUserScrims", () => {
scrimPostId: postId,
teamId: null,
message: null,
at: null,
startsAt: null,
users: [
{ userId: 4, isOwner: 1 },
{ userId: 5, isOwner: 0 },
@ -256,21 +256,21 @@ describe("findUserScrims", () => {
test("post owner sees the accepted request's side as the opponent", async () => {
const postId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 3, isOwner: 1 }],
});
await ScrimPostRepository.insertRequest({
scrimPostId: postId,
teamId: null,
message: null,
at: null,
startsAt: null,
users: [{ userId: 1, isOwner: 1 }],
});
await ScrimPostRepository.insertRequest({
scrimPostId: postId,
teamId: null,
message: null,
at: null,
startsAt: null,
users: [{ userId: 4, isOwner: 1 }],
});
@ -309,13 +309,13 @@ describe("insertRequest", () => {
scrimPostId,
teamId,
message: null,
at: null,
startsAt: null,
users: [{ userId, isOwner: 1 }],
});
test("throws if the team already has a request for the post", async () => {
const postId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 1, isOwner: 1 }],
});
const team = await TeamRepository.create({
@ -340,11 +340,11 @@ describe("insertRequest", () => {
test("allows the team to request another post", async () => {
const postId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 1, isOwner: 1 }],
});
const otherPostId = await insertPost({
at: BOOKED_AT,
startsAt: BOOKED_AT,
users: [{ userId: 4, isOwner: 1 }],
});
const team = await TeamRepository.create({

View File

@ -24,8 +24,8 @@ import { getPostRequestCensor, parseLutiDiv } from "./scrims-utils";
type InsertArgs = Pick<
TablesInsertable["ScrimPost"],
| "at"
| "rangeEnd"
| "startsAt"
| "rangeEndsAt"
| "maxDiv"
| "minDiv"
| "teamId"
@ -49,8 +49,8 @@ export function insert(args: InsertArgs) {
const newPost = await trx
.insertInto("ScrimPost")
.values({
at: args.at,
rangeEnd: args.rangeEnd,
startsAt: args.startsAt,
rangeEndsAt: args.rangeEndsAt,
maxDiv: args.maxDiv,
minDiv: args.minDiv,
teamId: args.teamId,
@ -76,7 +76,7 @@ export function insert(args: InsertArgs) {
type InsertRequestArgs = Pick<
Insertable<Tables["ScrimPostRequest"]>,
"scrimPostId" | "teamId" | "message" | "at"
"scrimPostId" | "teamId" | "message" | "startsAt"
> & {
users: Array<
Pick<Insertable<Tables["ScrimPostRequestUser"]>, "userId" | "isOwner">
@ -113,7 +113,7 @@ export function insertRequest(args: InsertRequestArgs) {
scrimPostId: args.scrimPostId,
teamId: args.teamId,
message: args.message,
at: args.at,
startsAt: args.startsAt,
})
.returning("id")
.executeTakeFirstOrThrow();
@ -146,8 +146,8 @@ const baseFindQuery = db
)
.select((eb) => [
"ScrimPost.id",
"ScrimPost.at",
"ScrimPost.rangeEnd",
"ScrimPost.startsAt",
"ScrimPost.rangeEndsAt",
"ScrimPost.createdAt",
"ScrimPost.visibility",
"ScrimPost.maxDiv",
@ -197,7 +197,7 @@ const baseFindQuery = db
"ScrimPostRequest.isAccepted",
"ScrimPostRequest.createdAt",
"ScrimPostRequest.message",
"ScrimPostRequest.at",
"ScrimPostRequest.startsAt",
jsonBuildObject({
name: innerEb.ref("Team.name"),
customUrl: innerEb.ref("Team.customUrl"),
@ -229,8 +229,8 @@ function findMany() {
const min = sub(new Date(), { hours: 3 });
return baseFindQuery
.orderBy("at", "asc")
.where("ScrimPost.at", ">=", dateToDatabaseTimestamp(min))
.orderBy("startsAt", "asc")
.where("ScrimPost.startsAt", ">=", dateToDatabaseTimestamp(min))
.execute();
}
@ -277,8 +277,8 @@ const mapDBRowToScrimPost = (
const result = {
id: row.id,
at: row.at,
rangeEnd: row.rangeEnd,
startsAt: row.startsAt,
rangeEndsAt: row.rangeEndsAt,
createdAt: row.createdAt,
visibility: row.visibility,
text: row.text,
@ -309,7 +309,7 @@ const mapDBRowToScrimPost = (
isAccepted: Boolean(request.isAccepted),
createdAt: request.createdAt,
message: request.message,
at: request.at,
startsAt: request.startsAt,
team: request.team.name
? {
name: request.team.name,
@ -347,8 +347,8 @@ const mapDBRowToScrimPost = (
return {
...result,
at: Scrim.getStartTime(result),
rangeEnd: null,
startsAt: Scrim.getStartTime(result),
rangeEndsAt: null,
};
};
@ -447,8 +447,8 @@ export async function findAcceptedScrimsBetweenTwoTimestamps({
excludeRecentlyCreated: Date;
}) {
const rows = await baseFindQuery
.where("ScrimPost.at", ">=", dateToDatabaseTimestamp(startTime))
.where("ScrimPost.at", "<", dateToDatabaseTimestamp(endTime))
.where("ScrimPost.startsAt", ">=", dateToDatabaseTimestamp(startTime))
.where("ScrimPost.startsAt", "<", dateToDatabaseTimestamp(endTime))
.where("ScrimPost.canceledAt", "is", null)
.where(
"ScrimPost.createdAt",
@ -481,7 +481,7 @@ export async function findPendingOverlapsForUsers({
endTime: number;
excludePostId: number;
}): Promise<{
posts: Array<{ id: number; at: number; memberIds: number[] }>;
posts: Array<{ id: number; startsAt: number; memberIds: number[] }>;
requestIds: number[];
}> {
if (userIds.length === 0) {
@ -492,7 +492,7 @@ export async function findPendingOverlapsForUsers({
const rows = await baseFindQuery
.where("ScrimPost.canceledAt", "is", null)
.where("ScrimPost.at", ">=", now)
.where("ScrimPost.startsAt", ">=", now)
.where((eb) =>
eb.or([
eb.exists(
@ -520,7 +520,8 @@ export async function findPendingOverlapsForUsers({
const userIdSet = new Set(userIds);
const posts: Array<{ id: number; at: number; memberIds: number[] }> = [];
const posts: Array<{ id: number; startsAt: number; memberIds: number[] }> =
[];
const requestIds: number[] = [];
for (const post of rows
@ -530,18 +531,19 @@ export async function findPendingOverlapsForUsers({
const postInvolvesUser = post.users.some((u) => userIdSet.has(u.id));
const postIntervalOverlaps =
post.at <= endTime && (post.rangeEnd ?? post.at) >= startTime;
post.startsAt <= endTime &&
(post.rangeEndsAt ?? post.startsAt) >= startTime;
if (postInvolvesUser && postIntervalOverlaps) {
posts.push({
id: post.id,
at: post.at,
startsAt: post.startsAt,
memberIds: post.users.map((u) => u.id),
});
}
for (const request of post.requests) {
if (request.isAccepted) continue;
const effectiveAt = request.at ?? post.at;
const effectiveAt = request.startsAt ?? post.startsAt;
const requestInvolvesUser = request.users.some((u) =>
userIdSet.has(u.id),
);
@ -560,7 +562,7 @@ export async function findPendingOverlapsForUsers({
export type SidebarScrim = {
id: number;
at: number;
startsAt: number;
opponentName: string | null;
opponentAvatarUrl: string | null;
status: "booked" | "looking" | "requestPending";
@ -571,7 +573,7 @@ export async function findUserScrims(userId: number): Promise<SidebarScrim[]> {
const rows = await baseFindQuery
.where("ScrimPost.canceledAt", "is", null)
.where("ScrimPost.at", ">=", now)
.where("ScrimPost.startsAt", ">=", now)
.where((eb) =>
eb.or([
eb.exists(
@ -595,7 +597,7 @@ export async function findUserScrims(userId: number): Promise<SidebarScrim[]> {
),
]),
)
.orderBy("ScrimPost.at", "asc")
.orderBy("ScrimPost.startsAt", "asc")
.execute();
return rows
@ -610,7 +612,7 @@ export async function findUserScrims(userId: number): Promise<SidebarScrim[]> {
if (!isAccepted) {
return {
id: post.id,
at: post.at,
startsAt: post.startsAt,
opponentName: null,
opponentAvatarUrl: null,
status: userIsInPost
@ -627,7 +629,7 @@ export async function findUserScrims(userId: number): Promise<SidebarScrim[]> {
return {
id: post.id,
at: post.at,
startsAt: post.startsAt,
opponentName: opponentTeam?.name ?? null,
opponentAvatarUrl:
opponentTeam?.avatarUrl ?? opponentOwner?.discordAvatar ?? null,

View File

@ -57,8 +57,8 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const resolvedDivs = data.divs ? resolveDivs(data.divs) : null;
await ScrimPostRepository.insert({
at: dateToDatabaseTimestamp(data.at),
rangeEnd: rangeEndDate ? dateToDatabaseTimestamp(rangeEndDate) : null,
startsAt: dateToDatabaseTimestamp(data.at),
rangeEndsAt: rangeEndDate ? dateToDatabaseTimestamp(rangeEndDate) : null,
maxDiv: resolvedDivs?.[0] ? serializeLutiDiv(resolvedDivs[0]) : null,
minDiv: resolvedDivs?.[1] ? serializeLutiDiv(resolvedDivs[1]) : null,
text: data.postText,

View File

@ -74,17 +74,17 @@ export const action = async ({ request }: ActionFunctionArgs) => {
errorToastIfFalsy(canSeePost, "Post not found");
}
if (post.rangeEnd && !data.at) {
if (post.rangeEndsAt && !data.at) {
return actionError<typeof newRequestSchema>({
msg: "Please select a time for the scrim",
field: "at",
});
}
if (post.rangeEnd && data.at) {
if (post.rangeEndsAt && data.at) {
const validTimeOptions = generateTimeOptions(
databaseTimestampToDate(post.at),
databaseTimestampToDate(post.rangeEnd),
databaseTimestampToDate(post.startsAt),
databaseTimestampToDate(post.rangeEndsAt),
);
const requestTime = data.at.getTime();
@ -101,7 +101,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
scrimPostId: data.scrimPostId,
teamId: data.from.mode === "TEAM" ? data.from.teamId : null,
message: data.message,
at: data.at ? dateToDatabaseTimestamp(data.at) : null,
startsAt: data.at ? dateToDatabaseTimestamp(data.at) : null,
users: (
await usersListForPost({ authorId: user.id, from: data.from })
).map((userId) => ({
@ -154,15 +154,18 @@ export const action = async ({ request }: ActionFunctionArgs) => {
ChatSystemMessage.setMetadata({
chatCode: fullPost.chatCode,
header: datePlaceholder(
databaseTimestampToDate(request.at ?? post.at),
databaseTimestampToDate(request.startsAt ?? post.startsAt),
),
subtitle: "Scrim",
url: scrimPage(post.id),
imageUrl: `${navIconUrl("scrims")}.avif`,
participantUserIds: Scrim.participantIdsListFromAccepted(fullPost),
expiresAt: add(databaseTimestampToDate(request.at ?? post.at), {
hours: 3,
}),
expiresAt: add(
databaseTimestampToDate(request.startsAt ?? post.startsAt),
{
hours: 3,
},
),
});
}
@ -218,7 +221,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
defaultSeenUserIds: [user.id],
notification: {
type: "SCRIM_AUTO_DELETED",
meta: { at: removed.at },
meta: { at: removed.startsAt },
},
});
}

View File

@ -57,8 +57,8 @@ export function ScrimPostCard({
const isPickup = !post.team?.name;
const teamName = post.team?.name ?? owner.username;
const flexTimeDisplay = post.rangeEnd
? formatFlexTimeDisplay(post.at, post.rangeEnd)
const flexTimeDisplay = post.rangeEndsAt
? formatFlexTimeDisplay(post.startsAt, post.rangeEndsAt)
: null;
useEffect(() => {
@ -109,7 +109,7 @@ export function ScrimPostCard({
<ScrimInfoItem label="Start">
<ScrimStartTimeDisplay
isScheduledForFuture={post.isScheduledForFuture}
startTimestamp={post.at}
startTimestamp={post.startsAt}
createdAtTimestamp={post.createdAt}
canceled={post.canceled}
/>
@ -486,13 +486,13 @@ function ScrimActionButtons({
<div className="text-lighter">{userRequest.message}</div>
</div>
) : null}
{userRequest.at ? (
{userRequest.startsAt ? (
<div>
<div className="text-sm font-semi-bold mb-1">
{t("scrims:requestModal.at.label")}
</div>
<LocaleTime
date={userRequest.at}
date={userRequest.startsAt}
options={{
hour: "numeric",
minute: "2-digit",
@ -576,8 +576,8 @@ export function ScrimRequestCard({
const isPickup = !request.team?.name;
const teamName = request.team?.name ?? owner.username;
const confirmedTime = request.at
? databaseTimestampToDate(request.at)
const confirmedTime = request.startsAt
? databaseTimestampToDate(request.startsAt)
: databaseTimestampToDate(postStartTime);
return (

View File

@ -78,7 +78,7 @@ function ScrimMatchBannerTopRow() {
const acceptedRequest = data.post.requests.find((r) => r.isAccepted);
const scheduledAt = databaseTimestampToDate(
acceptedRequest?.at ?? data.post.at,
acceptedRequest?.startsAt ?? data.post.startsAt,
);
return (

View File

@ -19,7 +19,7 @@ export function ScrimMatchHeader() {
const canCancel =
allowedToCancel &&
!isCanceled &&
databaseTimestampToDate(data.post.at) > new Date();
databaseTimestampToDate(data.post.startsAt) > new Date();
const acceptedRequest = data.post.requests.find((r) => r.isAccepted);
const viewerSide = data.mapByMap.viewerSide;

View File

@ -29,10 +29,10 @@ export function ScrimRequestModal({
minute: "numeric",
});
const timeOptions = post.rangeEnd
const timeOptions = post.rangeEndsAt
? generateTimeOptions(
databaseTimestampToDate(post.at),
databaseTimestampToDate(post.rangeEnd),
databaseTimestampToDate(post.startsAt),
databaseTimestampToDate(post.rangeEndsAt),
).map((timestamp) => ({
value: String(timestamp),
label: timeFormatter.format(new Date(timestamp)) ?? "",
@ -55,7 +55,7 @@ export function ScrimRequestModal({
) as unknown as number[],
},
message: "",
at: post.rangeEnd && timeOptions[0] ? timeOptions[0].value : null,
at: post.rangeEndsAt && timeOptions[0] ? timeOptions[0].value : null,
}}
>
{({ FormField }) => (
@ -74,7 +74,7 @@ export function ScrimRequestModal({
<WithFormField usersTeams={data.teams} {...props} />
)}
</FormField>
{post.rangeEnd ? (
{post.rangeEndsAt ? (
<FormField name="at" options={timeOptions} />
) : null}
<FormField name="message" />

View File

@ -108,14 +108,14 @@ describe("sideDisplayName", () => {
describe("applyFilters", () => {
function createPostForFilters(
at: Date,
rangeEnd?: Date,
startsAt: Date,
rangeEndsAt?: Date,
divs?: { min: string; max: string },
): ScrimPost {
return {
id: 1,
at: dateToDatabaseTimestamp(at),
rangeEnd: rangeEnd ? dateToDatabaseTimestamp(rangeEnd) : null,
startsAt: dateToDatabaseTimestamp(startsAt),
rangeEndsAt: rangeEndsAt ? dateToDatabaseTimestamp(rangeEndsAt) : null,
divs: divs ? { min: divs.min as any, max: divs.max as any } : null,
users: [],
requests: [],

View File

@ -43,12 +43,12 @@ export function participantIdsListFromAccepted(post: ScrimPost) {
/**
* Returns the actual start time of the scrim.
* When the post has a time range (rangeEnd is set), returns the accepted request's specific time if available.
* When the post has a time range (rangeEndsAt is set), returns the accepted request's specific time if available.
* Otherwise returns the post's start time.
*/
export function getStartTime(post: ScrimPost): number {
const acceptedRequest = post.requests.find((r) => r.isAccepted);
return acceptedRequest?.at ?? post.at;
return acceptedRequest?.startsAt ?? post.startsAt;
}
/**
@ -91,14 +91,14 @@ export function applyFilters(post: ScrimPost, filters: ScrimFilters): boolean {
}
}
const timeFilters = isWeekend(databaseTimestampToDate(post.at))
const timeFilters = isWeekend(databaseTimestampToDate(post.startsAt))
? filters.weekendTimes
: filters.weekdayTimes;
if (timeFilters) {
const startDate = databaseTimestampToDate(post.at);
const endDate = post.rangeEnd
? databaseTimestampToDate(post.rangeEnd)
const startDate = databaseTimestampToDate(post.startsAt);
const endDate = post.rangeEndsAt
? databaseTimestampToDate(post.rangeEndsAt)
: startDate;
const startTimeString = format(startDate, "HH:mm");

View File

@ -199,7 +199,7 @@ function ScrimsDaySeparatedCards({
autoScrollToPostId: number | null;
}) {
const postsByDay = R.groupBy(posts, (post) =>
format(databaseTimestampToDate(post.at), "yyyy-MM-dd"),
format(databaseTimestampToDate(post.startsAt), "yyyy-MM-dd"),
);
return (
@ -251,7 +251,7 @@ function ScrimsDaySection({
<div className="stack xxs">
<h2 className="text-sm">
<LocaleTime
date={posts[0].at}
date={posts[0].startsAt}
options={{
day: "numeric",
month: "numeric",
@ -368,7 +368,7 @@ function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) {
const user = useUser();
const postsByDay = R.groupBy(posts, (post) =>
format(databaseTimestampToDate(post.at), "yyyy-MM-dd"),
format(databaseTimestampToDate(post.startsAt), "yyyy-MM-dd"),
);
return (
@ -380,7 +380,7 @@ function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) {
<div key={day} className="stack md">
<h2 className="text-sm">
<LocaleTime
date={posts![0].at}
date={posts![0].startsAt}
options={{
day: "numeric",
month: "numeric",
@ -410,7 +410,7 @@ function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) {
<ScrimRequestCard
key={request.id}
request={request}
postStartTime={post.at}
postStartTime={post.startsAt}
canAccept={Boolean(
user &&
post.permissions.MANAGE_REQUESTS.includes(
@ -438,7 +438,7 @@ function ScrimsDaySeparatedOwnedCards({ posts }: { posts: ScrimPost[] }) {
function ScrimsDaySeparatedBookedCards({ posts }: { posts: ScrimPost[] }) {
const postsByDay = R.groupBy(posts, (post) =>
format(databaseTimestampToDate(post.at), "yyyy-MM-dd"),
format(databaseTimestampToDate(post.startsAt), "yyyy-MM-dd"),
);
return (
@ -450,7 +450,7 @@ function ScrimsDaySeparatedBookedCards({ posts }: { posts: ScrimPost[] }) {
<div key={day} className="stack md">
<h2 className="text-sm">
<LocaleTime
date={posts![0].at}
date={posts![0].startsAt}
options={{
day: "numeric",
month: "numeric",
@ -470,7 +470,7 @@ function ScrimsDaySeparatedBookedCards({ posts }: { posts: ScrimPost[] }) {
{acceptedRequest ? (
<ScrimRequestCard
request={acceptedRequest}
postStartTime={post.at}
postStartTime={post.startsAt}
canAccept={false}
showFooter={false}
/>

View File

@ -8,8 +8,8 @@ export type ScrimSide = "ALPHA" | "BRAVO";
export interface ScrimPost {
id: number;
at: number;
rangeEnd: number | null;
startsAt: number;
rangeEndsAt: number | null;
createdAt: number;
visibility: AssociationVisibility | null;
text: string | null;
@ -53,7 +53,7 @@ export interface ScrimPostRequest {
users: Array<ScrimPostUser>;
team: ScrimPostTeam | null;
message: string | null;
at: number | null;
startsAt: number | null;
permissions: {
CANCEL: number[];
};

View File

@ -98,7 +98,7 @@ async function searchByType({
id: t.id,
name: t.name,
logoUrl: t.logoUrl,
startTime: t.startTime,
startsAt: t.startsAt,
}));
}
}

View File

@ -233,7 +233,7 @@ const tournamentResultsSubQuery = (
"TournamentResult.setResults",
"TournamentResult.tournamentId",
"TournamentResult.tournamentTeamId",
"CalendarEventDate.startTime as tournamentStartTime",
"CalendarEventDate.startsAt as tournamentStartTime",
"CalendarEvent.name as tournamentName",
tournamentLogoWithDefault(eb).as("logoUrl"),
])

View File

@ -43,7 +43,7 @@ export type SidebarEvent = {
name: string;
url: string;
logoUrl: string | null;
startTime: number;
startsAt: number;
type: "tournament" | "scrim";
scrimStatus?: "booked" | "looking" | "requestPending";
};
@ -116,7 +116,7 @@ export async function resolveSidebarData(userId: number | null) {
const scrimEvents: SidebarEvent[] = scrimsData.map(scrimToSidebarEvent);
const events = [...tournamentEvents, ...savedEvents, ...scrimEvents]
.sort((a, b) => a.startTime - b.startTime)
.sort((a, b) => a.startsAt - b.startsAt)
.slice(0, MAX_EVENTS_VISIBLE);
const friends = resolveFriends(friendsWithActivity);
@ -170,7 +170,7 @@ async function combinedStreams(): Promise<SidebarStream[]> {
imageUrl: externalStream.avatarUrl ?? BLANK_IMAGE_URL,
url: externalStream.url,
subtitle: "",
startsAt: externalStream.startTime,
startsAt: externalStream.startsAt,
tier: null,
},
score: StreamRanking.EXTERNAL_STREAM_SCORE,
@ -245,8 +245,8 @@ async function combinedStreams(): Promise<SidebarStream[]> {
for (const event of upcomingTournaments) {
const effectiveTier = event.tier ?? event.tentativeTier;
if (effectiveTier === null) continue;
if (event.startTime < nowTimestamp) continue;
if (event.startTime > threeDaysFromNow) continue;
if (event.startsAt < nowTimestamp) continue;
if (event.startsAt > threeDaysFromNow) continue;
if (event.hidden) continue;
const membersPerTeam = event.minMembersPerTeam ?? 4;
@ -258,7 +258,7 @@ async function combinedStreams(): Promise<SidebarStream[]> {
imageUrl: event.logoUrl ?? BLANK_IMAGE_URL,
url: event.url,
subtitle: "",
startsAt: event.startTime,
startsAt: event.startsAt,
tier: (event.tier as TournamentTierNumber) ?? null,
membersPerTeam,
tentativeTier: event.tentativeTier ?? undefined,
@ -397,7 +397,7 @@ export function tournamentToSidebarEvent(
name: t.name,
url: t.url,
logoUrl: t.logoUrl,
startTime: t.startTime,
startsAt: t.startsAt,
type: "tournament" as const,
};
}
@ -415,7 +415,7 @@ export function scrimToSidebarEvent(s: SidebarScrim): SidebarEvent {
? `${href("/scrims")}?pendingRequestPostId=${s.id}`
: href("/scrims"),
logoUrl: s.opponentAvatarUrl ?? SCRIMS_ICON_URL,
startTime: s.at,
startsAt: s.startsAt,
type: "scrim" as const,
scrimStatus: s.status,
};

View File

@ -36,9 +36,9 @@ function queryAll() {
"SplatoonRotation.mode",
"SplatoonRotation.stageId1",
"SplatoonRotation.stageId2",
"SplatoonRotation.startTime",
"SplatoonRotation.endTime",
"SplatoonRotation.startsAt",
"SplatoonRotation.endsAt",
])
.orderBy("SplatoonRotation.startTime", "asc")
.orderBy("SplatoonRotation.startsAt", "asc")
.execute();
}

View File

@ -106,8 +106,8 @@ export async function fetchRotations(): Promise<
mode,
stageId1,
stageId2,
startTime: Math.floor(new Date(node.startTime).getTime() / 1000),
endTime: Math.floor(new Date(node.endTime).getTime() / 1000),
startsAt: Math.floor(new Date(node.startTime).getTime() / 1000),
endsAt: Math.floor(new Date(node.endTime).getTime() / 1000),
});
}
}
@ -127,8 +127,8 @@ export async function fetchRotations(): Promise<
mode,
stageId1,
stageId2,
startTime: Math.floor(new Date(node.startTime).getTime() / 1000),
endTime: Math.floor(new Date(node.endTime).getTime() / 1000),
startsAt: Math.floor(new Date(node.startTime).getTime() / 1000),
endsAt: Math.floor(new Date(node.endTime).getTime() / 1000),
});
}

View File

@ -261,7 +261,7 @@ export async function findResultsById(teamId: number) {
"results.participantCount",
"results.tournamentTeamId",
"CalendarEvent.name as tournamentName",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
"Tournament.tier",
tournamentLogoOrNull(eb).as("logoUrl"),
jsonArrayFrom(
@ -285,7 +285,7 @@ export async function findResultsById(teamId: number) {
.select((eb) => commonUserSelect(eb)),
).as("participants"),
])
.orderBy("CalendarEventDate.startTime", "desc")
.orderBy("CalendarEventDate.startsAt", "desc")
.execute();
const members = await allMembersById(teamId);

View File

@ -44,7 +44,7 @@ export function TeamResultsTable({ results }: TeamResultsTableProps) {
</td>
<td className="whitespace-nowrap">
<LocaleTime
date={result.startTime}
date={result.startsAt}
options={{
day: "numeric",
month: "numeric",

View File

@ -5,7 +5,7 @@ describe("subsOfResult()", () => {
it("returns empty array if all participants are current members", () => {
const result = {
participants: [{ id: 1 }, { id: 2 }],
startTime: 1000,
startsAt: 1000,
};
const members = [
{ userId: 1, createdAt: 500, leftAt: null },
@ -18,7 +18,7 @@ describe("subsOfResult()", () => {
it("returns participant not in members as sub", () => {
const result = {
participants: [{ id: 1 }, { id: 2 }],
startTime: 1000,
startsAt: 1000,
};
const members = [{ userId: 1, createdAt: 500, leftAt: null }];
const subs = subsOfResult(result, members);
@ -28,7 +28,7 @@ describe("subsOfResult()", () => {
it("returns participant as sub if they left before result startTime", () => {
const result = {
participants: [{ id: 1 }, { id: 2 }],
startTime: 1000,
startsAt: 1000,
};
const members = [
{ userId: 1, createdAt: 500, leftAt: 900 },
@ -41,7 +41,7 @@ describe("subsOfResult()", () => {
it("does not return participant as sub if they were a member during result", () => {
const result = {
participants: [{ id: 1 }, { id: 2 }],
startTime: 1000,
startsAt: 1000,
};
const members = [
{ userId: 1, createdAt: 500, leftAt: 2000 },
@ -54,7 +54,7 @@ describe("subsOfResult()", () => {
it("returns multiple subs correctly", () => {
const result = {
participants: [{ id: 1 }, { id: 2 }, { id: 3 }],
startTime: 1000,
startsAt: 1000,
};
const members = [
{ userId: 1, createdAt: 500, leftAt: 900 },
@ -67,7 +67,7 @@ describe("subsOfResult()", () => {
it("returns empty array if no participants", () => {
const result = {
participants: [],
startTime: 1000,
startsAt: 1000,
};
const members = [{ userId: 1, createdAt: 500, leftAt: null }];
const subs = subsOfResult(result, members);

View File

@ -101,7 +101,7 @@ export function getMemberRoleType(member: {
* - They are not a past member who was part of the team during the result's start time.
*/
export function subsOfResult<T extends { id: number }>(
result: { participants: Array<T>; startTime: number },
result: { participants: Array<T>; startsAt: number },
members: Array<Pick<Tables["TeamMember"], "userId" | "createdAt" | "leftAt">>,
) {
const currentMembers = members.filter((member) => !member.leftAt);
@ -113,9 +113,9 @@ export function subsOfResult<T extends { id: number }>(
pastMembers.some(
(member) =>
member.userId === cur.id &&
member.createdAt < result.startTime &&
member.createdAt < result.startsAt &&
member.leftAt &&
member.leftAt > result.startTime,
member.leftAt > result.startsAt,
)
) {
return acc;

View File

@ -162,7 +162,6 @@ export function insertBracket(args: {
type: stageInput.type,
settings: JSON.stringify(stageInput.settings),
number: kyselySql<number>`(select coalesce(max("number"), 0) + 1 from "TournamentStage" where "tournamentId" = ${args.tournamentId})`,
createdAt: databaseTimestampNow(),
})
.returning(["id"])
.executeTakeFirstOrThrow();

View File

@ -70,7 +70,7 @@ export function findTournamentsNeedingVodSync() {
.where(({ or, and, eb }) =>
or([
and([
eb("CalendarEventDate.startTime", ">", oneDayAgo),
eb("CalendarEventDate.startsAt", ">", oneDayAgo),
eb("Tournament.vodsSyncCount", "=", 0),
]),
and([
@ -122,7 +122,7 @@ export function deleteObsolete() {
"CalendarEvent.id",
)
.select("TournamentMatch.id")
.where("CalendarEventDate.startTime", "<", cutoff),
.where("CalendarEventDate.startsAt", "<", cutoff),
)
.executeTakeFirst();
}

View File

@ -218,7 +218,7 @@ function resolveFinalizationSeason(tournament: Tournament) {
// league divisions might be running for many weeks
const attributionDate = tournament.isLeagueDivision
? new Date()
: tournament.ctx.startTime;
: tournament.ctx.startsAt;
const season = Seasons.current(attributionDate);
if (!season) return undefined;

View File

@ -11,7 +11,7 @@ export function useBracketSpoilerCensor() {
const withinSpoilerWindow =
tournament.ctx.isFinalized &&
differenceInDays(new Date(), tournament.ctx.startTime) <
differenceInDays(new Date(), tournament.ctx.startsAt) <
TOURNAMENT.VOD_VISIBILITY_DAYS;
const censored = withinSpoilerWindow && isCensored(tournament.ctx.id);

View File

@ -144,7 +144,7 @@ function mostRecentStartTime(tournament: Tournament) {
.filter((b) => b.startTime)
.map((b) => databaseTimestampToDate(b.startTime!));
const allStartTimes = [tournament.ctx.startTime, ...bracketStartTimes];
const allStartTimes = [tournament.ctx.startsAt, ...bracketStartTimes];
return allStartTimes
.filter((t) => t <= new Date())

View File

@ -66,7 +66,7 @@ export class Tournament {
? // after the start the teams who did not check-in are irrelevant
teamsInSeedOrder.filter((team) => team.checkIns.length > 0)
: teamsInSeedOrder,
startTime: databaseTimestampToDate(ctx.startTime),
startsAt: databaseTimestampToDate(ctx.startsAt),
};
this.initBrackets(data);
@ -477,7 +477,7 @@ export class Tournament {
get ranked() {
return tournamentIsRanked({
isSetAsRanked: this.ctx.settings.isRanked,
startTime: this.ctx.startTime,
startsAt: this.ctx.startsAt,
minMembersPerTeam: this.minMembersPerTeam,
isTest: this.isTest,
});
@ -726,7 +726,7 @@ export class Tournament {
return (
!this.ctx.settings.regClosesAt ||
this.ctx.settings.regClosesAt ===
dateToDatabaseTimestamp(this.ctx.startTime) ||
dateToDatabaseTimestamp(this.ctx.startsAt) ||
this.registrationOpen
);
}
@ -753,7 +753,7 @@ export class Tournament {
/** Has the regular check-in (check-in for the whole tournament) ended? */
get regularCheckInHasEnded() {
return this.ctx.startTime < new Date();
return this.ctx.startsAt < new Date();
}
/** Has the regular check-in (check-in for the whole tournament) started? Note it is also considered started if it has ended. */
@ -763,21 +763,21 @@ export class Tournament {
/** Date when the regular check-in is scheduled to start. */
get regularCheckInStartsAt() {
const result = new Date(this.ctx.startTime);
const result = new Date(this.ctx.startsAt);
result.setMinutes(result.getMinutes() - 60);
return result;
}
/** Date when the regular check-in is scheduled to start. */
get regularCheckInEndsAt() {
return this.ctx.startTime;
return this.ctx.startsAt;
}
/** Date when the tournament registration is scheduled to end. This can be set by the organizer. */
get registrationClosesAt() {
return this.ctx.settings.regClosesAt
? databaseTimestampToDate(this.ctx.settings.regClosesAt)
: this.ctx.startTime;
: this.ctx.startsAt;
}
/** Is the tournament registration open at this time? */
@ -789,11 +789,11 @@ export class Tournament {
/** Can participants submit/undo their own weapon reports right now?
* Always open while the tournament is running; once finalized it stays open only for tournaments
* whose startTime is inside the current-season-plus-adjacent-off-season window. */
* whose start time is inside the current-season-plus-adjacent-off-season window. */
get weaponReportingOpen() {
if (!this.ctx.isFinalized) return true;
return tournamentInWeaponReportingWindow({
tournamentStartTime: this.ctx.startTime,
tournamentStartTime: this.ctx.startsAt,
});
}

View File

@ -6248,7 +6248,7 @@ export const LOW_INK_DECEMBER_2024 = (): TournamentData => ({
mapPickingStyle: "TO",
hasRules: true,
name: "Low Ink December 2024",
startTime: 1734199200,
startsAt: 1734199200,
organization: {
id: 3,
name: "Inkling Performance Labs",

View File

@ -1752,7 +1752,7 @@ export const SWIM_OR_SINK_167 = (
mapPickingStyle: "TO",
hasRules: true,
name: "Swim or Sink 167",
startTime: 1730941200,
startsAt: 1730941200,
organization: {
id: 3,
name: "Inkling Performance Labs",

View File

@ -303,7 +303,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
parentTournamentId: null,
parentTournamentName: null,
name: "Zones Weekly 38",
startTime: 1734685200,
startsAt: 1734685200,
isFinalized: 0,
organization: null,
logoUrl: "tournament-logo-hfX5gzVyrt5QCV8fiQA4n-1716906622859.webp",

View File

@ -1250,7 +1250,7 @@ export const PADDLING_POOL_257 = () =>
name: "Paddling Pool 257",
hasRules: false,
logoUrl: "/test.avif",
startTime: 1709748000,
startsAt: 1709748000,
author: {
id: 860,
username: "공주 Alice",
@ -8033,7 +8033,7 @@ export const PADDLING_POOL_255 = () =>
name: "Paddling Pool 255",
hasRules: false,
logoUrl: "/test.avif",
startTime: 1708538400,
startsAt: 1708538400,
author: {
id: 860,
username: "공주 Alice",
@ -15193,7 +15193,7 @@ export const IN_THE_ZONE_32 = ({
name: "In The Zone 32",
hasRules: false,
logoUrl: "/test.avif",
startTime: 1707588000,
startsAt: 1707588000,
author: {
id: 274,
username: "Sendou",

View File

@ -69,7 +69,7 @@ export const testTournament = ({
hasRules: false,
logoUrl: "/test.avif",
discordUrl: null,
startTime: 1705858842,
startsAt: 1705858842,
isFinalized: 0,
name: "test",
castTwitchAccounts: [],

View File

@ -60,7 +60,7 @@ async function standingsWithSetParticipation(tournament: Tournament) {
);
invariant(results.length > 0, "No results found");
const season = Seasons.current(tournament.ctx.startTime)?.nth;
const season = Seasons.current(tournament.ctx.startsAt)?.nth;
const seedingSkillCountsFor = tournament.skillCountsFor;

View File

@ -82,7 +82,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
id: tournamentId,
name: tournament.ctx.name,
logoUrl: tournament.ctx.logoUrl,
startTime: tournament.ctx.startTime,
startTime: tournament.ctx.startsAt,
},
});
}
@ -211,7 +211,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
id: tournamentId,
name: tournament.ctx.name,
logoUrl: tournament.ctx.logoUrl,
startTime: tournament.ctx.startTime,
startTime: tournament.ctx.startsAt,
},
});
}

View File

@ -767,7 +767,7 @@ export const action: ActionFunction = async ({ params, request }) => {
tournamentMatchId: matchId,
mapIndex: data.mapIndex,
weaponSplId: data.weaponSplId,
createdAt: dateToDatabaseTimestamp(tournament.ctx.startTime),
createdAt: dateToDatabaseTimestamp(tournament.ctx.startsAt),
});
break;

View File

@ -199,7 +199,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
? true
: !chatAccessible({
expiresAfterDays: tournament.isLeagueDivision ? 30 : 7,
comparedTo: tournament.ctx.startTime,
comparedTo: tournament.ctx.startsAt,
});
const visibleChatCode =

View File

@ -231,7 +231,7 @@ const findEventsBaseQuery = (organizationId: number) =>
"CalendarEvent.id as eventId",
"CalendarEvent.name",
"CalendarEvent.tournamentId",
eb.fn.min("CalendarEventDate.startTime").as("startTime"),
eb.fn.min("CalendarEventDate.startsAt").as("startsAt"),
tournamentLogoWithDefault(eb).as("logoUrl"),
jsonArrayFrom(
eb
@ -347,16 +347,16 @@ export async function findEventsByMonth({
const events = await findEventsBaseQuery(organizationId)
.where(
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
">=",
dateToDatabaseTimestamp(firstDayOfTheMonth),
)
.where(
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
"<=",
dateToDatabaseTimestamp(lastDayOfTheMonth),
)
.orderBy("CalendarEventDate.startTime", "asc")
.orderBy("CalendarEventDate.startsAt", "asc")
.execute();
return events.map(mapEvent);
@ -387,7 +387,7 @@ const findSeriesEventsBaseQuery = ({
),
),
)
.orderBy("CalendarEventDate.startTime", "desc");
.orderBy("CalendarEventDate.startsAt", "desc");
export async function findPaginatedEventsBySeries({
organizationId,
@ -458,8 +458,8 @@ export async function countActiveParticipants({
)
.select(({ fn }) => fn.count<number>("tmgrp.userId").distinct().as("count"))
.where("ce.organizationId", "=", organizationId)
.where("ced.startTime", ">=", startTime)
.where("ced.startTime", "<", endTime)
.where("ced.startsAt", ">=", startTime)
.where("ced.startsAt", "<", endTime)
.where("ttci.checkedInAt", "is not", null)
.where("ttci.isCheckOut", "=", 0)
.executeTakeFirst();
@ -580,12 +580,12 @@ export function update({
"Tournament.id as tournamentId",
"CalendarEvent.name",
"Tournament.tier",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
])
.where("Tournament.isFinalized", "=", 1)
.where("CalendarEvent.organizationId", "=", id)
.where("CalendarEvent.hidden", "=", 0)
.orderBy("CalendarEventDate.startTime", "asc")
.orderBy("CalendarEventDate.startsAt", "asc")
.execute();
for (const s of insertedSeries) {

View File

@ -44,7 +44,7 @@ export function EventCalendar({
))}
{dates.map((date, i) => {
const daysEvents = events.filter((event) => {
const startTimeDate = databaseTimestampToDate(event.startTime);
const startTimeDate = databaseTimestampToDate(event.startsAt);
return (
isHydrated &&

View File

@ -137,6 +137,6 @@ async function seriesStuff({
: null,
eventsCount: events.length,
logoUrl: events[0].logoUrl,
established: events.at(-1)!.startTime,
established: events.at(-1)!.startsAt,
};
}

View File

@ -485,11 +485,11 @@ function EventsList({
const events = filteredByMonth
? data.events.filter(
(event) =>
databaseTimestampToDate(event.startTime).getMonth() === data.month,
databaseTimestampToDate(event.startsAt).getMonth() === data.month,
)
: data.events;
const pastEvents = events.filter((event) => event.startTime < now);
const upcomingEvents = events.filter((event) => event.startTime >= now);
const pastEvents = events.filter((event) => event.startsAt < now);
const upcomingEvents = events.filter((event) => event.startsAt >= now);
return (
<div className="w-full stack xs">
@ -540,7 +540,7 @@ function EventInfo({
<div>
<div>{event.name}</div>
<LocaleTime
date={event.startTime}
date={event.startsAt}
options={{
day: "numeric",
month: "numeric",

View File

@ -46,7 +46,7 @@ export async function seedOrgEventWithParticipants({
await db
.insertInto("CalendarEventDate")
.values({ eventId: event.id, startTime })
.values({ eventId: event.id, startsAt: startTime })
.execute();
const team = await db

View File

@ -5,7 +5,7 @@ import { db } from "~/db/sql";
import type { DB, Tables } from "~/db/tables";
import type { TournamentAuditLogMetadata } from "~/db/tables-json";
import { actorId } from "~/features/auth/core/user.server";
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { commonUserSelect } from "~/utils/kysely.server";
export const AUDIT_LOG_PAGE_SIZE = 30;
@ -56,7 +56,6 @@ export async function insert(trx: Transaction<DB>, args: InsertArgs) {
subjectUserId: args.subjectUserId ?? null,
tournamentTeamHistoryId,
metadata: args.metadata ? JSON.stringify(args.metadata) : null,
createdAt: databaseTimestampNow(),
})
.execute();
}

View File

@ -67,7 +67,7 @@ export async function findById(id: number) {
.as("parentTournamentName"),
"Tournament.tier",
"CalendarEvent.name",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
"Tournament.isFinalized",
jsonObjectFrom(
eb
@ -549,7 +549,7 @@ export function forShowcase() {
"CalendarEvent.authorId",
"CalendarEvent.name",
"CalendarEvent.organizationId",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
"CalendarEvent.hidden",
eb
.selectFrom("TournamentTeam")
@ -567,7 +567,7 @@ export function forShowcase() {
.where((eb) =>
eb.or([
eb("TournamentTeamCheckIn.checkedInAt", "is not", null),
eb("CalendarEventDate.startTime", ">", databaseTimestampNow()),
eb("CalendarEventDate.startsAt", ">", databaseTimestampNow()),
]),
)
.select(({ fn }) => [
@ -639,8 +639,8 @@ export function forShowcase() {
.select(({ fn }) => [fn.countAll<number>().as("count")])
.as("vodCount"),
])
.where("CalendarEventDate.startTime", ">", databaseTimestampWeekAgo())
.orderBy("CalendarEventDate.startTime", "asc")
.where("CalendarEventDate.startsAt", ">", databaseTimestampWeekAgo())
.orderBy("CalendarEventDate.startsAt", "asc")
.$narrowType<{ teamsCount: NotNull }>()
.execute();
}
@ -670,15 +670,11 @@ export function findAllBetweenTwoTimestamps({
.innerJoin("Tournament", "CalendarEvent.tournamentId", "Tournament.id")
.select(["Tournament.id as tournamentId"])
.where(
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
">",
dateToDatabaseTimestamp(startTime),
)
.where(
"CalendarEventDate.startTime",
"<=",
dateToDatabaseTimestamp(endTime),
)
.where("CalendarEventDate.startsAt", "<=", dateToDatabaseTimestamp(endTime))
.where("CalendarEvent.hidden", "=", 0)
.execute();
}
@ -1304,17 +1300,17 @@ export async function searchByName({
.select((eb) => [
"Tournament.id",
"CalendarEvent.name",
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
tournamentLogoWithDefault(eb).as("logoUrl"),
])
.where("CalendarEvent.name", "like", `%${query}%`)
.where("CalendarEvent.hidden", "=", 0)
.orderBy("CalendarEventDate.startTime", "desc")
.orderBy("CalendarEventDate.startsAt", "desc")
.limit(limit);
if (minStartTime) {
sqlQuery = sqlQuery.where(
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
">=",
dateToDatabaseTimestamp(minStartTime),
);
@ -1322,7 +1318,7 @@ export async function searchByName({
if (maxStartTime) {
sqlQuery = sqlQuery.where(
"CalendarEventDate.startTime",
"CalendarEventDate.startsAt",
"<=",
dateToDatabaseTimestamp(maxStartTime),
);
@ -1398,8 +1394,8 @@ export async function findRunningTournamentIds() {
)
.select("Tournament.id")
.where("Tournament.isFinalized", "=", 0)
.where("CalendarEventDate.startTime", "<", dateToDatabaseTimestamp(now))
.where("CalendarEventDate.startTime", ">", dateToDatabaseTimestamp(cutoff))
.where("CalendarEventDate.startsAt", "<", dateToDatabaseTimestamp(now))
.where("CalendarEventDate.startsAt", ">", dateToDatabaseTimestamp(cutoff))
.where((eb) =>
eb.exists(
eb

View File

@ -52,7 +52,7 @@ const regOpenTournamentTeamsByJoinedUserId = (userId: number) =>
.where(
sql`coalesce(
"Tournament"."settings" ->> 'regClosesAt',
"CalendarEventDate"."startTime"
"CalendarEventDate"."startsAt"
)`,
">",
databaseTimestampNow(),
@ -375,7 +375,7 @@ async function registrationClosedNow(
.select(
sql<number>`coalesce(
"Tournament"."settings" ->> 'regClosesAt',
min("CalendarEventDate"."startTime")
min("CalendarEventDate"."startsAt")
)`.as("regClosesAt"),
)
.where("Tournament.id", "=", tournamentId)

View File

@ -24,7 +24,7 @@ export function TournamentHeader({ tournament }: { tournament: Tournament }) {
const startTimes = R.uniqueBy(
[
tournament.ctx.startTime,
tournament.ctx.startsAt,
...tournament.ctx.settings.bracketProgression
.filter((b) => b.startTime)
.map((b) => databaseTimestampToDate(b.startTime!)),

View File

@ -37,7 +37,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const friendCodeVisibilityDays = tournament.ctx.parentTournamentId ? 120 : 30;
const tournamentStartedRecently = isAfter(
databaseTimestampToDate(tournament.ctx.startTime),
databaseTimestampToDate(tournament.ctx.startsAt),
subDays(new Date(), friendCodeVisibilityDays),
);
const isTournamentAdmin =
@ -73,7 +73,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const showVods =
tournament.ctx.isFinalized &&
isAfter(
databaseTimestampToDate(tournament.ctx.startTime),
databaseTimestampToDate(tournament.ctx.startsAt),
subDays(new Date(), TOURNAMENT.VOD_VISIBILITY_DAYS),
);

View File

@ -86,7 +86,7 @@ function useFacts(
const showsEstimatedTier = !tournament.ctx.tier && !tournament.hasStarted;
const rankedSeason = Seasons.current(tournament.ctx.startTime);
const rankedSeason = Seasons.current(tournament.ctx.startsAt);
return [
{

View File

@ -300,12 +300,12 @@ function RegistrationProgress({
const regClosesBeforeStart =
tournament.registrationClosesAt.getTime() !==
tournament.ctx.startTime.getTime();
tournament.ctx.startsAt.getTime();
const registrationClosesAtString =
registrationClosesFormatter.format(
tournament.isLeagueSignup
? tournament.ctx.startTime
? tournament.ctx.startsAt
: tournament.registrationClosesAt,
) ?? "";

View File

@ -34,7 +34,7 @@ export default function TournamentResultsPage() {
const { isCensored, reveal } = useSpoilerFree();
const withinSpoilerWindow =
differenceInDays(new Date(), tournament.ctx.startTime) <
differenceInDays(new Date(), tournament.ctx.startsAt) <
TOURNAMENT.VOD_VISIBILITY_DAYS;
const censored = withinSpoilerWindow && isCensored(tournament.ctx.id);

View File

@ -139,18 +139,18 @@ export function validateCounterPickMapPool(
export function tournamentIsRanked({
isSetAsRanked,
startTime,
startsAt,
minMembersPerTeam,
isTest,
}: {
isSetAsRanked?: boolean;
startTime: Date;
startsAt: Date;
minMembersPerTeam: number;
isTest: boolean;
}) {
if (isTest) return false;
const seasonIsActive = Boolean(Seasons.current(startTime));
const seasonIsActive = Boolean(Seasons.current(startsAt));
if (!seasonIsActive) return false;
// 1v1, 2v2 and 3v3 are always considered "gimmicky"
@ -160,7 +160,7 @@ export function tournamentIsRanked({
}
/**
* Whether a tournament's startTime falls inside the active weapon-reporting window
* Whether a tournament's start time falls inside the active weapon-reporting window
* for late (post-finalization) reporting.
*
* - In-season: window is `(previousSeason.ends, now]` current season plus the off-season immediately before it.

View File

@ -472,7 +472,7 @@ export function findAllPatrons() {
.select(["User.id", "User.discordId", "User.username", "User.patronTier"])
.where("User.patronTier", "is not", null)
.orderBy("User.patronTier", "desc")
.orderBy("User.patronSince", "asc")
.orderBy("User.patronStartedAt", "asc")
.execute();
}
@ -507,9 +507,9 @@ export async function findChatUsersByUserIds(userIds: number[]) {
const withMaxEventStartTime = (eb: ExpressionBuilder<DB, "CalendarEvent">) => {
return eb
.selectFrom("CalendarEventDate")
.select(({ fn }) => [fn.max("CalendarEventDate.startTime").as("startTime")])
.select(({ fn }) => [fn.max("CalendarEventDate.startsAt").as("startsAt")])
.whereRef("CalendarEventDate.eventId", "=", "CalendarEvent.id")
.as("startTime");
.as("startsAt");
};
const baseCalendarEventResultsQuery = (userId: number) =>
@ -669,8 +669,8 @@ export function findResultsByUserId(
let query = calendarEventResultsQuery
.unionAll(tournamentResultsQuery)
.orderBy("startTime", "desc")
.$narrowType<{ startTime: NotNull }>();
.orderBy("startsAt", "desc")
.$narrowType<{ startsAt: NotNull }>();
if (limit !== undefined) {
query = query.limit(limit);
@ -973,14 +973,14 @@ export async function inGameNameByUserId(userId: number) {
)?.inGameName;
}
export async function patronSinceByUserId(userId: number) {
export async function patronStartedAtByUserId(userId: number) {
return (
await db
.selectFrom("User")
.select("User.patronSince")
.select("User.patronStartedAt")
.where("id", "=", userId)
.executeTakeFirst()
)?.patronSince;
)?.patronStartedAt;
}
export async function joinOrderByUserId(userId: number) {
@ -1248,7 +1248,7 @@ export function updateOwnBuildSorting(buildSorting: BuildSort[] | null) {
}
export type UpdatePatronDataArgs = Array<
Pick<Tables["User"], "discordId" | "patronTier" | "patronSince">
Pick<Tables["User"], "discordId" | "patronTier" | "patronStartedAt">
>;
export function updatePatronData(users: UpdatePatronDataArgs) {
return db.transaction().execute(async (trx) => {
@ -1256,13 +1256,13 @@ export function updatePatronData(users: UpdatePatronDataArgs) {
.updateTable("User")
.set({
patronTier: null,
patronSince: null,
patronTill: null,
patronStartedAt: null,
patronExpiresAt: null,
})
.where((eb) =>
eb.or([
eb("patronTill", "<", dateToDatabaseTimestamp(new Date())),
eb("patronTill", "is", null),
eb("patronExpiresAt", "<", dateToDatabaseTimestamp(new Date())),
eb("patronExpiresAt", "is", null),
]),
)
.execute();
@ -1272,8 +1272,8 @@ export function updatePatronData(users: UpdatePatronDataArgs) {
.updateTable("User")
.set({
patronTier: user.patronTier,
patronSince: user.patronSince,
patronTill: null,
patronStartedAt: user.patronStartedAt,
patronExpiresAt: null,
})
.where("User.discordId", "=", user.discordId)
.execute();

View File

@ -154,8 +154,8 @@ describe("UserRepository", () => {
await AdminRepository.forcePatron({
id,
patronTier: 1,
patronSince: now,
patronTill: oneYearFromNow,
patronStartedAt: now,
patronExpiresAt: oneYearFromNow,
});
const user = await UserRepository.findLeanById(id);
@ -178,8 +178,8 @@ describe("UserRepository", () => {
await AdminRepository.forcePatron({
id,
patronTier: 2,
patronSince: now,
patronTill: oneYearFromNow,
patronStartedAt: now,
patronExpiresAt: oneYearFromNow,
});
const user = await UserRepository.findLeanById(id);
@ -305,8 +305,8 @@ describe("UserRepository", () => {
await AdminRepository.forcePatron({
id,
patronTier: 2,
patronSince: now,
patronTill: oneYearFromNow,
patronStartedAt: now,
patronExpiresAt: oneYearFromNow,
});
await AdminRepository.makeArtistByUserId(id);

View File

@ -120,7 +120,7 @@ export function UserResultsTable({
</td>
<td className="whitespace-nowrap">
<LocaleTime
date={result.startTime}
date={result.startsAt}
options={{
day: "numeric",
month: "numeric",

View File

@ -62,7 +62,7 @@ export function Widget({
user: Pick<Tables["User"], "id" | "discordId" | "customUrl">;
}) {
const { t } = useTranslation(["user", "badges", "team", "org", "lfg"]);
const { formatter: patronSinceFormatter } = useDateTimeFormat({
const { formatter: patronStartedAtFormatter } = useDateTimeFormat({
day: "numeric",
month: "numeric",
year: "numeric",
@ -184,7 +184,9 @@ export function Widget({
case "patron-since":
if (!widget.data) return null;
return (
<BigValue value={patronSinceFormatter.format(widget.data) ?? ""} />
<BigValue
value={patronStartedAtFormatter.format(widget.data) ?? ""}
/>
);
case "join-date":
if (!widget.data) return null;
@ -418,7 +420,7 @@ function HighlightedResults({
) : null}
</div>
<LocaleTime
date={result.startTime}
date={result.startsAt}
options={{
day: "numeric",
month: "numeric",

View File

@ -189,7 +189,7 @@ export const WIDGET_LOADERS = {
};
},
"patron-since": async (userId: number) => {
return UserRepository.patronSinceByUserId(userId);
return UserRepository.patronStartedAtByUserId(userId);
},
"join-date": async (userId: number) => {
return UserRepository.joinOrderByUserId(userId);

View File

@ -95,7 +95,7 @@ export async function findVods({
}
const result = await query
.groupBy("Video.id")
.orderBy("Video.youtubeDate", "desc")
.orderBy("Video.youtubePublishedAt", "desc")
.limit(limit)
.offset(offset)
.execute();
@ -163,7 +163,7 @@ export async function findVodById(id: Tables["Video"]["id"]) {
.select([
"id",
"title",
"youtubeDate",
"youtubePublishedAt",
"youtubeId",
"type",
"submitterUserId",
@ -259,7 +259,7 @@ export async function insert(
const video = {
title: args.title,
type: args.type,
youtubeDate: dayMonthYearToDatabaseTimestamp(args.date),
youtubePublishedAt: dayMonthYearToDatabaseTimestamp(args.date),
eventId: args.eventId ?? null,
youtubeId,
submitterUserId: args.submitterUserId,

View File

@ -97,7 +97,7 @@ export default function VodPage() {
<div className="stack horizontal sm items-center">
<PovUser pov={data.vod.pov} />
<LocaleTime
date={data.vod.youtubeDate}
date={data.vod.youtubePublishedAt}
options={{
day: "numeric",
month: "numeric",

View File

@ -15,7 +15,7 @@ export interface Vod {
| string;
title: Tables["Video"]["title"];
type: Tables["Video"]["type"];
youtubeDate: Tables["Video"]["youtubeDate"];
youtubePublishedAt: Tables["Video"]["youtubePublishedAt"];
youtubeId: Tables["Video"]["youtubeId"];
matches: Array<VodMatch>;
submitterUserId: Tables["Video"]["submitterUserId"];
@ -28,7 +28,7 @@ export type VodMatch = Pick<
weapons: Array<MainWeaponId>;
};
export type ListVod = Omit<Vod, "youtubeDate" | "matches"> & {
export type ListVod = Omit<Vod, "youtubePublishedAt" | "matches"> & {
weapons: Array<MainWeaponId>;
type: Tables["Video"]["type"];
};

View File

@ -223,7 +223,7 @@ describe("generateYoutubeTimestamps", () => {
});
describe("vodToVideoBeingAdded", () => {
// youtubeDate is stored as noon UTC of the chosen day (see
// youtubePublishedAt is stored as noon UTC of the chosen day (see
// dayMonthYearToDatabaseTimestamp), so reading the day/month/year back out
// must also use UTC. Force a timezone east of UTC+12 where local time has
// already rolled over to the next day, making the bug deterministic.
@ -242,7 +242,7 @@ describe("vodToVideoBeingAdded", () => {
title: "Test VOD",
type: "TOURNAMENT",
youtubeId: "dQw4w9WgXcQ",
youtubeDate: dayMonthYearToDatabaseTimestamp(date),
youtubePublishedAt: dayMonthYearToDatabaseTimestamp(date),
submitterUserId: 1,
matches: [],
};

View File

@ -5,7 +5,7 @@ import { HOURS_MINUTES_SECONDS_REGEX } from "./vods-schemas";
import type { VideoBeingAdded, Vod } from "./vods-types";
export function vodToVideoBeingAdded(vod: Vod): VideoBeingAdded {
const dateObj = databaseTimestampToDate(vod.youtubeDate);
const dateObj = databaseTimestampToDate(vod.youtubePublishedAt);
return {
title: vod.title,

View File

@ -41,7 +41,7 @@ export async function updatePatreonData(): Promise<void> {
).map((discordId) => ({
discordId,
patronTier: 4,
patronSince: dateToDatabaseTimestamp(new Date()),
patronStartedAt: dateToDatabaseTimestamp(new Date()),
})),
];
@ -130,7 +130,7 @@ function parsePatronData({
patronsWithIds.push({
patreonId: patron.relationships.user.data.id,
patronSince: dateToDatabaseTimestamp(
patronStartedAt: dateToDatabaseTimestamp(
new Date(patron.attributes.pledge_relationship_start ?? Date.now()),
),
patronTier: idToTierNumber(tier),
@ -162,7 +162,7 @@ function parsePatronData({
}
result.patrons.push({
patronSince: patronData.patronSince,
patronStartedAt: patronData.patronStartedAt,
discordId,
patronTier: patronData.patronTier,
});

Binary file not shown.

View File

@ -25,14 +25,18 @@ Counter-examples that look like booleans but aren't: `User.banned` (`1` = permab
## Generated vs. GeneratedAlways
- `Generated<T>` — column has a DB default, so it's optional on insert but you may still pass a value (`createdAt`, every boolean flag).
- `GeneratedAlways<T>` — never insertable or updatable: primary keys, computed columns, `createdAt` columns whose value should never be supplied by app code.
- `GeneratedAlways<T>` — never insertable or updatable: primary keys and computed columns (`User.username`, the `UserSearch` FTS columns).
- Plain `T` — required on every insert.
Pick by what the DB allows, not by what app code happens to do today: if the column has a default it is `Generated<T>`, even when nothing currently overrides it.
## Timestamps
Unix seconds as `number`, named `*At`. Never ISO strings, never milliseconds.
Columns with `default (strftime('%s', 'now'))` are `Generated<number>`; use `GeneratedAlways<number>` when app code should never set them. Nullable `*At` columns double as markers (`deletedAt`, `validatedAt`, `leftAt`) — document that meaning in JSDoc.
Name every instant with an `*At` suffix, including future ones: `startsAt`, `endsAt`, `expiresAt`, `becomesValidAt`. Not `startTime`, `youtubeDate` or a bare `at`. Domain objects and repository return shapes that carry a column's value keep the column's name; form fields, query range bounds and external API payloads are free to use their own names.
Every `createdAt` gets `default (strftime('%s', 'now'))` and is typed `Generated<number>`. The two nullable ones (`User`, `Skill`) predate the column existing and have no backfillable value — JSDoc says so on each. Other nullable `*At` columns double as markers (`deletedAt`, `validatedAt`, `leftAt`) — document that meaning in JSDoc too.
Convert with `~/utils/dates`: `dateToDatabaseTimestamp`, `databaseTimestampToDate`, `databaseTimestampNow`.
@ -63,7 +67,7 @@ Non-obvious columns get a JSDoc comment: what `null` means, what a magic number
- SQLite can't add `not null` to an existing column. To tighten one: add `"<col>_new" integer not null default 0`, `update ... set "<col>_new" = coalesce("<col>", 0)`, drop the old column, rename the new one. See `migrations/162-boolean-columns-not-null.js`.
- `drop column` fails if the column is indexed, part of a constraint, or referenced by a trigger, view, generated column or partial index. Check `sqlite_master` first.
- Only when that isn't enough, rebuild the table: `pragma foreign_keys = OFF`, create `X_new`, copy, drop, rename, recreate every index, then `pragma foreign_key_check` inside the transaction. See `migrations/161-tournament-match-nullable-opponents.js`.
- New columns can't be added with a non-constant default, so `createdAt` added later ends up nullable (see `TournamentStage.createdAt`).
- New columns can't be added with a non-constant default, so a `createdAt` added later starts out nullable. Backfill it and rebuild the table in the same migration rather than leaving the nullability behind — see the `TournamentStage` rebuild in `migrations/162-boolean-columns-not-null.js`.
## After changing the schema

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More