Files
buzz/lib/tierlist.ts
T
dmgnr 7be3db93b4
Build and Push Image / build-backend (push) Successful in 4m48s
Build and Push Image / build-frontend (push) Successful in 6m28s
Build and Push Image / push (push) Successful in 1m17s
fix(tl): deprecate date parsing
2026-08-06 12:08:17 +07:00

201 lines
5.0 KiB
TypeScript

import { and, eq, inArray, isNull, or } from "drizzle-orm";
import { db } from "@/lib/db";
import {
characters,
tierlistBadges,
tierlistColumns,
tierlistTiers,
tierlistTypes,
tierlistVersions,
versions,
} from "@/lib/db/schema";
export type TierlistBadgeResolved = typeof tierlistBadges.$inferSelect & {
tier: string[];
};
export type TierlistResolvedConfig = {
type: typeof tierlistTypes.$inferSelect;
version: typeof tierlistVersions.$inferSelect;
tiers: (typeof tierlistTiers.$inferSelect)[];
columns: (typeof tierlistColumns.$inferSelect)[];
badges: TierlistBadgeResolved[];
chars: (typeof characters.$inferSelect)[];
};
type DbLike = Pick<typeof db, "select" | "update">;
function normalizeSnapshot(
config: TierlistResolvedConfig,
): TierlistResolvedConfig {
return {
...config,
version: {
...config.version,
snapshot: null,
},
};
}
async function resolveCharacterVersions(tx: DbLike, rootVersionId: string) {
const vers = await tx
.select({ id: versions.id, from: versions.from })
.from(versions);
const ids: string[] = [];
let cur: string | null = rootVersionId;
while (cur) {
ids.push(cur);
const found = vers.find((version) => version.id === cur);
cur = found?.from ?? null;
}
return ids;
}
async function buildLiveTierlistConfig(
tx: DbLike,
version: typeof tierlistVersions.$inferSelect,
) {
const [typeInfo] = await tx
.select()
.from(tierlistTypes)
.where(eq(tierlistTypes.id, version.type));
if (!typeInfo) return null;
const ids = await resolveCharacterVersions(tx, version.from);
const chars =
ids.length > 0
? await tx
.select()
.from(characters)
.where(inArray(characters.version, ids))
.orderBy(characters.order)
: ([] as (typeof characters.$inferSelect)[]);
const [tiers, columns, badgesList] = await Promise.all([
tx.select().from(tierlistTiers).orderBy(tierlistTiers.order),
tx.select().from(tierlistColumns).orderBy(tierlistColumns.order),
tx
.select()
.from(tierlistBadges)
.orderBy(tierlistBadges.order, tierlistBadges.id)
.where(
or(isNull(tierlistBadges.type), eq(tierlistBadges.type, version.type)),
),
]);
const badges = badgesList.map((badge) => ({
...badge,
tier: tiers
.filter((tier) => tier.badges?.includes(badge.id))
.map((tier) => tier.id),
}));
return normalizeSnapshot({
type: typeInfo,
version,
tiers,
columns,
badges,
chars,
});
}
function shouldSnapshot(config: TierlistResolvedConfig) {
const deprecatedAt = parseDate(config.version.deprecates);
const olderThanMonth =
Number.isFinite(deprecatedAt) &&
deprecatedAt <= Date.now() - 30 * 24 * 60 * 60 * 1000;
const placed = new Set(
Object.values(config.version.placements)
.flat()
.map((entry) => entry.split("#")[0]),
);
const allTiered =
config.chars.length > 0 &&
config.chars.every((char) => placed.has(char.id));
return olderThanMonth || allTiered;
}
function parseDate(value: string) {
const match = value.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
if (!match) return Number.NaN;
const [, day, month, year] = match;
const date = new Date(Number(year), Number(month) - 1, Number(day));
if (
date.getFullYear() !== Number(year) ||
date.getMonth() !== Number(month) - 1 ||
date.getDate() !== Number(day)
) {
return Number.NaN;
}
return date.getTime();
}
async function maybeSnapshotVersion(
tx: DbLike,
version: typeof tierlistVersions.$inferSelect,
config: TierlistResolvedConfig,
) {
if (version.snapshot || !shouldSnapshot(config)) return config;
await tx
.update(tierlistVersions)
.set({ snapshot: config })
.where(eq(tierlistVersions.id, version.id));
return config;
}
export async function getTierlistConfig(typeId: string, versionId: string) {
return db.transaction(async (tx) => {
const [version] = await tx
.select()
.from(tierlistVersions)
.where(
and(
eq(tierlistVersions.type, typeId),
eq(tierlistVersions.id, versionId),
),
);
if (!version) return null;
if (version.snapshot) {
return normalizeSnapshot(version.snapshot as TierlistResolvedConfig);
}
const config = await buildLiveTierlistConfig(tx, version);
if (!config) return null;
return maybeSnapshotVersion(tx, version, config);
});
}
export async function syncTierlistSnapshots(typeId: string) {
return db.transaction(async (tx) => {
const versionList = await tx
.select()
.from(tierlistVersions)
.where(eq(tierlistVersions.type, typeId))
.orderBy(tierlistVersions.order, tierlistVersions.id);
for (const version of versionList) {
if (version.snapshot) continue;
const config = await buildLiveTierlistConfig(tx, version);
if (!config || !shouldSnapshot(config)) continue;
await tx
.update(tierlistVersions)
.set({ snapshot: config })
.where(eq(tierlistVersions.id, version.id));
}
});
}