mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-31 16:17:07 -05:00
Step 11-13
This commit is contained in:
parent
2282e10adb
commit
84b1f735ae
40
app/db/seed/core/backdate.ts
Normal file
40
app/db/seed/core/backdate.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { type RawBuilder, sql } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
|
||||
/** Tables `backdate` can address: those with an `id` for it to key the update on. */
|
||||
type BackdatableTable = {
|
||||
[T in keyof Tables]: "id" extends keyof Tables[T] ? T : never;
|
||||
}[keyof Tables];
|
||||
|
||||
/** The only columns `backdate` may touch. */
|
||||
type TimestampColumn<T extends BackdatableTable> = Extract<
|
||||
keyof Tables[T],
|
||||
`${string}At`
|
||||
>;
|
||||
|
||||
/**
|
||||
* Moves a row's timestamps into the past. Every production write stamps *now*, so
|
||||
* a seed that needs rows looking old — an expired vote, a season's worth of matches
|
||||
* — has no way to ask for one.
|
||||
*/
|
||||
export async function backdate<T extends BackdatableTable>(
|
||||
table: T,
|
||||
id: number,
|
||||
timestamps: Partial<Record<TimestampColumn<T>, Date>>,
|
||||
) {
|
||||
const assignments: RawBuilder<unknown>[] = [];
|
||||
|
||||
for (const [column, date] of Object.entries(timestamps)) {
|
||||
assignments.push(
|
||||
sql`${sql.ref(column)} = ${dateToDatabaseTimestamp(date as Date)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (assignments.length === 0) return;
|
||||
|
||||
await sql`update ${sql.table(table)} set ${sql.join(assignments)} where "id" = ${id}`.execute(
|
||||
db,
|
||||
);
|
||||
}
|
||||
13
app/db/seed/factories/BadgeFactory.ts
Normal file
13
app/db/seed/factories/BadgeFactory.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
|
||||
import { defineFactory } from "../core/defineFactory";
|
||||
import { faker } from "../core/faker";
|
||||
|
||||
export const { createMany } = defineFactory({
|
||||
defaults: ({ seq }) => ({
|
||||
code: `badge-${seq}`,
|
||||
displayName: faker.lorem.words(2),
|
||||
hue: null,
|
||||
authorId: null,
|
||||
}),
|
||||
insert: BadgeRepository.insert,
|
||||
});
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import * as ImageRepository from "~/features/img-upload/ImageRepository.server";
|
||||
import { defineFactory } from "../core/defineFactory";
|
||||
|
||||
type Options = {
|
||||
/** Has an admin approved the image, making it show wherever it is used? */
|
||||
isValidated: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates user submitted images, unvalidated as the repository function makes them.
|
||||
* An image on its own is an orphan — the queries counting images for approval only
|
||||
|
|
@ -14,4 +19,9 @@ export const { create } = defineFactory({
|
|||
validatedAt: null,
|
||||
}),
|
||||
insert: ImageRepository.insert,
|
||||
applyOptions: async (image, { isValidated }: Options) => {
|
||||
if (!isValidated) return;
|
||||
|
||||
await ImageRepository.validateById(image.id);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
23
app/db/seed/factories/TournamentStreamerFactory.ts
Normal file
23
app/db/seed/factories/TournamentStreamerFactory.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { Tables } from "~/db/tables";
|
||||
import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { defineFactory } from "../core/defineFactory";
|
||||
|
||||
type InsertArgs = Omit<Tables["TournamentStreamer"], "id">;
|
||||
|
||||
export const { create } = defineFactory({
|
||||
defaults: ({ seq }) => ({
|
||||
twitchAccount: `streamer_${seq}`,
|
||||
userId: null,
|
||||
}),
|
||||
insert: async (args: InsertArgs) => {
|
||||
const [row] = await LiveStreamRepository.insertTournamentStreamers([args]);
|
||||
|
||||
invariant(
|
||||
row,
|
||||
`${args.twitchAccount} is already a streamer of tournament ${args.tournamentId}`,
|
||||
);
|
||||
|
||||
return row;
|
||||
},
|
||||
});
|
||||
36
app/db/seed/factories/XRankPlacementFactory.ts
Normal file
36
app/db/seed/factories/XRankPlacementFactory.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as XRankPlacementRepository from "~/features/top-search/XRankPlacementRepository.server";
|
||||
import { defineFactory } from "../core/defineFactory";
|
||||
import { faker } from "../core/faker";
|
||||
import * as SplatoonFaker from "../core/SplatoonFaker";
|
||||
|
||||
const PLACED_ON = { month: 1, year: 2024 };
|
||||
|
||||
/**
|
||||
* Creates X Rank placements. `playerSplId` is the in-game id of the player who
|
||||
* placed — the repository creates their `SplatoonPlayer` row if the id is new, so
|
||||
* repeating an id places the same player again. `playerUserId` is the site user
|
||||
* whose results they are.
|
||||
*
|
||||
* Rank counts up, since a month's leaderboard has no two players at the same rank.
|
||||
*/
|
||||
export const { create } = defineFactory({
|
||||
defaults: ({ seq }) => ({
|
||||
badges: "",
|
||||
bannerSplId: 1,
|
||||
mode: "SZ" as const,
|
||||
month: PLACED_ON.month,
|
||||
year: PLACED_ON.year,
|
||||
name: faker.internet.username(),
|
||||
nameDiscriminator: String(seq).padStart(4, "0"),
|
||||
power: faker.number.int({ min: 2000, max: 3300 }),
|
||||
rank: seq,
|
||||
region: "WEST" as const,
|
||||
title: faker.lorem.words(2),
|
||||
weaponSplId: SplatoonFaker.mainWeapon(),
|
||||
}),
|
||||
insert: async (args: XRankPlacementRepository.XRankPlacementInsertArgs) => {
|
||||
const [id] = await XRankPlacementRepository.insertMany([args]);
|
||||
|
||||
return { id };
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as BadgeFactory from "~/db/seed/factories/BadgeFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory";
|
||||
import * as XRankPlacementRepository from "~/features/top-search/XRankPlacementRepository.server";
|
||||
import { dbReset } from "~/utils/Test";
|
||||
import * as BadgeRepository from "./BadgeRepository.server";
|
||||
import { SPLATOON_3_XP_BADGE_VALUES } from "./badges-constants";
|
||||
|
|
@ -10,7 +12,10 @@ describe("syncXPBadges", () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
user = await UserFactory.create();
|
||||
await insertXPBadges();
|
||||
await BadgeFactory.createMany(SPLATOON_3_XP_BADGE_VALUES.length, (i) => ({
|
||||
code: String(SPLATOON_3_XP_BADGE_VALUES[i]),
|
||||
displayName: `${SPLATOON_3_XP_BADGE_VALUES[i]}+ XP`,
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -18,11 +23,7 @@ describe("syncXPBadges", () => {
|
|||
});
|
||||
|
||||
test("assigns badge to user with qualifying peakXp", async () => {
|
||||
await insertSplatoonPlayer({
|
||||
splId: "abc123",
|
||||
userId: user.id,
|
||||
peakXp: 3000,
|
||||
});
|
||||
await givePeakXp(user.id, 3000);
|
||||
|
||||
await BadgeRepository.syncXPBadges();
|
||||
|
||||
|
|
@ -32,11 +33,7 @@ describe("syncXPBadges", () => {
|
|||
});
|
||||
|
||||
test("assigns highest qualifying badge when peakXp exceeds threshold", async () => {
|
||||
await insertSplatoonPlayer({
|
||||
splId: "abc123",
|
||||
userId: user.id,
|
||||
peakXp: 3250,
|
||||
});
|
||||
await givePeakXp(user.id, 3250);
|
||||
|
||||
await BadgeRepository.syncXPBadges();
|
||||
|
||||
|
|
@ -48,11 +45,7 @@ describe("syncXPBadges", () => {
|
|||
});
|
||||
|
||||
test("does not assign badge when peakXp is below minimum threshold", async () => {
|
||||
await insertSplatoonPlayer({
|
||||
splId: "abc123",
|
||||
userId: user.id,
|
||||
peakXp: 2500,
|
||||
});
|
||||
await givePeakXp(user.id, 2500);
|
||||
|
||||
await BadgeRepository.syncXPBadges();
|
||||
|
||||
|
|
@ -61,40 +54,15 @@ describe("syncXPBadges", () => {
|
|||
});
|
||||
});
|
||||
|
||||
async function insertXPBadges() {
|
||||
await db
|
||||
.insertInto("Badge")
|
||||
.values(
|
||||
SPLATOON_3_XP_BADGE_VALUES.map((value) => ({
|
||||
code: String(value),
|
||||
displayName: `${value}+ XP`,
|
||||
hue: null,
|
||||
authorId: null,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
}
|
||||
/** Gives the user a linked X Rank player whose one placement is worth `power`. */
|
||||
async function givePeakXp(userId: number, power: number) {
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: `player-${userId}`,
|
||||
playerUserId: userId,
|
||||
power,
|
||||
});
|
||||
|
||||
async function insertSplatoonPlayer(args: {
|
||||
splId: string;
|
||||
userId: number | null;
|
||||
peakXp: number | null;
|
||||
}) {
|
||||
await db
|
||||
.insertInto("SplatoonPlayer")
|
||||
.values({
|
||||
splId: args.splId,
|
||||
userId: args.userId,
|
||||
peakXp:
|
||||
args.peakXp === null
|
||||
? null
|
||||
: JSON.stringify({
|
||||
overall: args.peakXp,
|
||||
tentatek: args.peakXp,
|
||||
takoroka: null,
|
||||
}),
|
||||
})
|
||||
.execute();
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
}
|
||||
|
||||
async function findBadgeByCode(code: string) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ExpressionBuilder, NotNull } from "kysely";
|
||||
import { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB } from "~/db/tables";
|
||||
import type { DB, TablesInsertable } from "~/db/tables";
|
||||
import { sortBadgesByFavorites } from "~/features/user-page/core/badge-sorting.server";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { commonUserSelect, peakXpOverallSql } from "~/utils/kysely.server";
|
||||
|
|
@ -55,6 +55,20 @@ const withOwners = (eb: ExpressionBuilder<DB, "Badge">, badgeId: number) => {
|
|||
).as("owners");
|
||||
};
|
||||
|
||||
/** Adds a badge. `authorId` is who made it, `null` for a legacy badge. */
|
||||
export function insert(
|
||||
args: Pick<
|
||||
TablesInsertable["Badge"],
|
||||
"code" | "displayName" | "hue" | "authorId"
|
||||
>,
|
||||
) {
|
||||
return db
|
||||
.insertInto("Badge")
|
||||
.values(args)
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
export async function findAll() {
|
||||
const rows = await db
|
||||
.selectFrom("Badge")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as BuildFactory from "~/db/seed/factories/BuildFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import type {
|
||||
BuildAbilitiesTuple,
|
||||
|
|
@ -57,39 +58,14 @@ const createBuild = (
|
|||
...overrides,
|
||||
});
|
||||
|
||||
const insertSplatoonPlayer = async (userId: number, splId: string) => {
|
||||
const { id } = await db
|
||||
.insertInto("SplatoonPlayer")
|
||||
.values({ splId, userId })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
return id;
|
||||
};
|
||||
|
||||
const insertXRankPlacement = async (
|
||||
playerId: number,
|
||||
weaponSplId: MainWeaponId,
|
||||
rank: number,
|
||||
) => {
|
||||
await db
|
||||
.insertInto("XRankPlacement")
|
||||
.values({
|
||||
playerId,
|
||||
weaponSplId,
|
||||
badges: "[]",
|
||||
bannerSplId: 1,
|
||||
mode: "SZ",
|
||||
month: 1,
|
||||
year: 2024,
|
||||
name: "Test Player",
|
||||
nameDiscriminator: "0000",
|
||||
power: 2500,
|
||||
rank,
|
||||
region: "WEST",
|
||||
title: "Test",
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
/** Puts the user in the top 500 with the given weapon, which builds sort by. */
|
||||
const makeTop500 = (userId: number, weaponSplId: MainWeaponId) =>
|
||||
XRankPlacementFactory.create({
|
||||
playerSplId: `player-${userId}`,
|
||||
playerUserId: userId,
|
||||
weaponSplId,
|
||||
rank: 1,
|
||||
});
|
||||
|
||||
const buildById = (id: number) =>
|
||||
db
|
||||
|
|
@ -244,8 +220,7 @@ describe("BuildRepository.insert — computeBuildData", () => {
|
|||
});
|
||||
|
||||
test("subtracts 1 when the weapon is top500 for the owner", async () => {
|
||||
const playerId = await insertSplatoonPlayer(owner.id, "owner-spl-id");
|
||||
await insertXRankPlacement(playerId, SPLATTERSHOT, 1);
|
||||
await makeTop500(owner.id, SPLATTERSHOT);
|
||||
|
||||
const { id } = await BuildRepository.insert(
|
||||
baseArgs({ weaponSplIds: [SPLATTERSHOT, SPLATTERSHOT_NOUVEAU] }),
|
||||
|
|
@ -263,8 +238,7 @@ describe("BuildRepository.insert — computeBuildData", () => {
|
|||
|
||||
test("combines top500 with the owner's PlusTier", async () => {
|
||||
const plusOwner = await UserFactory.create(null, { plusTier: 1 });
|
||||
const playerId = await insertSplatoonPlayer(plusOwner.id, "owner-spl-id");
|
||||
await insertXRankPlacement(playerId, SPLATTERSHOT, 1);
|
||||
await makeTop500(plusOwner.id, SPLATTERSHOT);
|
||||
|
||||
const { id } = await BuildRepository.insert(
|
||||
baseArgs({ ownerId: plusOwner.id }),
|
||||
|
|
@ -276,8 +250,7 @@ describe("BuildRepository.insert — computeBuildData", () => {
|
|||
});
|
||||
|
||||
test("findAllByWeaponId.weapons[].isTop500 matches the sortValue formula", async () => {
|
||||
const playerId = await insertSplatoonPlayer(owner.id, "owner-spl-id");
|
||||
await insertXRankPlacement(playerId, SPLATTERSHOT, 1);
|
||||
await makeTop500(owner.id, SPLATTERSHOT);
|
||||
|
||||
await createBuild({
|
||||
ownerId: owner.id,
|
||||
|
|
|
|||
|
|
@ -15,10 +15,15 @@ export function replaceAll(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given accounts as streamers of their tournament. Returns the ids of the
|
||||
* rows actually inserted, in insertion order — an account already streaming that
|
||||
* tournament is skipped and so has no id among them.
|
||||
*/
|
||||
export function insertTournamentStreamers(
|
||||
rows: Omit<Tables["TournamentStreamer"], "id">[],
|
||||
) {
|
||||
if (rows.length === 0) return;
|
||||
if (rows.length === 0) return Promise.resolve([]);
|
||||
|
||||
return db
|
||||
.insertInto("TournamentStreamer")
|
||||
|
|
@ -26,6 +31,7 @@ export function insertTournamentStreamers(
|
|||
.onConflict((oc) =>
|
||||
oc.columns(["twitchAccount", "tournamentId"]).doNothing(),
|
||||
)
|
||||
.returning("id")
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +1,28 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { dbReset } from "~/utils/Test";
|
||||
import * as XRankPlacementRepository from "./XRankPlacementRepository.server";
|
||||
|
||||
let placementCounter = 0;
|
||||
const SPLATTERSHOT: MainWeaponId = 40;
|
||||
const SPLATTERSHOT_NOUVEAU: MainWeaponId = 41;
|
||||
|
||||
const createUser = async (discordId: string) => {
|
||||
const result = await db
|
||||
.insertInto("User")
|
||||
.values({ discordId, discordName: discordId })
|
||||
.returning("id")
|
||||
const PLACED_ON = { month: 1, year: 2024 };
|
||||
|
||||
const peakXpOf = (playerSplId: string) =>
|
||||
db
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select("peakXp")
|
||||
.where("splId", "=", playerSplId)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return result.id;
|
||||
};
|
||||
|
||||
const createSplatoonPlayer = async (splId: string, userId?: number | null) => {
|
||||
const result = await db
|
||||
.insertInto("SplatoonPlayer")
|
||||
.values({ splId, userId: userId ?? null })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return result.id;
|
||||
};
|
||||
|
||||
const createXRankPlacement = async (args: {
|
||||
playerId: number;
|
||||
power: number;
|
||||
region?: "WEST" | "JPN";
|
||||
rank?: number;
|
||||
weaponSplId?: MainWeaponId;
|
||||
}) => {
|
||||
placementCounter++;
|
||||
|
||||
await db
|
||||
.insertInto("XRankPlacement")
|
||||
.values({
|
||||
playerId: args.playerId,
|
||||
power: args.power,
|
||||
badges: "[]",
|
||||
bannerSplId: 1,
|
||||
mode: "SZ",
|
||||
month: 1,
|
||||
year: 2024,
|
||||
name: "Test Player",
|
||||
nameDiscriminator: "0000",
|
||||
rank: args.rank ?? placementCounter,
|
||||
region: args.region ?? "WEST",
|
||||
title: "Test",
|
||||
weaponSplId: args.weaponSplId ?? (0 as MainWeaponId),
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
const findTenStarWeapons = () =>
|
||||
db.selectFrom("TenStarWeapon").selectAll().execute();
|
||||
|
||||
describe("refreshAllPeakXp", () => {
|
||||
beforeEach(async () => {
|
||||
placementCounter = 0;
|
||||
await dbReset();
|
||||
});
|
||||
|
||||
|
|
@ -66,30 +31,22 @@ describe("refreshAllPeakXp", () => {
|
|||
});
|
||||
|
||||
test("sets peakXp to max power for each player", async () => {
|
||||
const player1Id = await createSplatoonPlayer("player1");
|
||||
const player2Id = await createSplatoonPlayer("player2");
|
||||
for (const power of [2500, 2700, 2600]) {
|
||||
await XRankPlacementFactory.create({ playerSplId: "player-1", power });
|
||||
}
|
||||
|
||||
await createXRankPlacement({ playerId: player1Id, power: 2500 });
|
||||
await createXRankPlacement({ playerId: player1Id, power: 2700 });
|
||||
await createXRankPlacement({ playerId: player1Id, power: 2600 });
|
||||
|
||||
await createXRankPlacement({ playerId: player2Id, power: 3000 });
|
||||
await createXRankPlacement({ playerId: player2Id, power: 2800 });
|
||||
for (const power of [3000, 2800]) {
|
||||
await XRankPlacementFactory.create({ playerSplId: "player-2", power });
|
||||
}
|
||||
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
|
||||
const players = await db
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select(["id", "peakXp"])
|
||||
.orderBy("id", "asc")
|
||||
.execute();
|
||||
|
||||
expect(players[0].peakXp).toEqual({
|
||||
expect((await peakXpOf("player-1")).peakXp).toEqual({
|
||||
overall: 2700,
|
||||
tentatek: 2700,
|
||||
takoroka: null,
|
||||
});
|
||||
expect(players[1].peakXp).toEqual({
|
||||
expect((await peakXpOf("player-2")).peakXp).toEqual({
|
||||
overall: 3000,
|
||||
tentatek: 3000,
|
||||
takoroka: null,
|
||||
|
|
@ -97,20 +54,20 @@ describe("refreshAllPeakXp", () => {
|
|||
});
|
||||
|
||||
test("splits peakXp by division (region)", async () => {
|
||||
const playerId = await createSplatoonPlayer("player1");
|
||||
|
||||
await createXRankPlacement({ playerId, power: 2700, region: "WEST" });
|
||||
await createXRankPlacement({ playerId, power: 2900, region: "JPN" });
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
power: 2700,
|
||||
region: "WEST",
|
||||
});
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
power: 2900,
|
||||
region: "JPN",
|
||||
});
|
||||
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
|
||||
const player = await db
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select("peakXp")
|
||||
.where("id", "=", playerId)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
expect(player.peakXp).toEqual({
|
||||
expect((await peakXpOf("player-1")).peakXp).toEqual({
|
||||
overall: 2900,
|
||||
tentatek: 2700,
|
||||
takoroka: 2900,
|
||||
|
|
@ -118,24 +75,26 @@ describe("refreshAllPeakXp", () => {
|
|||
});
|
||||
|
||||
test("sets peakXp to null for player with no placements", async () => {
|
||||
const playerId = await createSplatoonPlayer("player1");
|
||||
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
power: 2500,
|
||||
...PLACED_ON,
|
||||
});
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
|
||||
const player = await db
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select("peakXp")
|
||||
.where("id", "=", playerId)
|
||||
.executeTakeFirstOrThrow();
|
||||
await XRankPlacementRepository.deleteAllByMonthYear(PLACED_ON);
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
|
||||
expect(player.peakXp).toBeNull();
|
||||
expect((await peakXpOf("player-1")).peakXp).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshTenStarWeapons", () => {
|
||||
let user: { id: number };
|
||||
|
||||
beforeEach(async () => {
|
||||
placementCounter = 0;
|
||||
await dbReset();
|
||||
user = await UserFactory.create();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -143,113 +102,97 @@ describe("refreshTenStarWeapons", () => {
|
|||
});
|
||||
|
||||
test("JPN placement qualifies regardless of rank", async () => {
|
||||
const userId = await createUser("user1");
|
||||
const playerId = await createSplatoonPlayer("player1", userId);
|
||||
|
||||
await createXRankPlacement({
|
||||
playerId,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
playerUserId: user.id,
|
||||
power: 2500,
|
||||
region: "JPN",
|
||||
rank: 450,
|
||||
weaponSplId: 40 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT,
|
||||
});
|
||||
|
||||
await XRankPlacementRepository.refreshTenStarWeapons();
|
||||
|
||||
const rows = await db.selectFrom("TenStarWeapon").selectAll().execute();
|
||||
const rows = await findTenStarWeapons();
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].userId).toBe(userId);
|
||||
expect(rows[0].weaponSplId).toBe(40);
|
||||
expect(rows[0].userId).toBe(user.id);
|
||||
expect(rows[0].weaponSplId).toBe(SPLATTERSHOT);
|
||||
});
|
||||
|
||||
test("WEST placement with rank <= 100 qualifies", async () => {
|
||||
const userId = await createUser("user1");
|
||||
const playerId = await createSplatoonPlayer("player1", userId);
|
||||
|
||||
await createXRankPlacement({
|
||||
playerId,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
playerUserId: user.id,
|
||||
power: 3000,
|
||||
region: "WEST",
|
||||
rank: 50,
|
||||
weaponSplId: 40 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT,
|
||||
});
|
||||
|
||||
await XRankPlacementRepository.refreshTenStarWeapons();
|
||||
|
||||
const rows = await db.selectFrom("TenStarWeapon").selectAll().execute();
|
||||
const rows = await findTenStarWeapons();
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].weaponSplId).toBe(40);
|
||||
expect(rows[0].weaponSplId).toBe(SPLATTERSHOT);
|
||||
});
|
||||
|
||||
test("WEST placement with rank > 100 does not qualify", async () => {
|
||||
const userId = await createUser("user1");
|
||||
const playerId = await createSplatoonPlayer("player1", userId);
|
||||
|
||||
await createXRankPlacement({
|
||||
playerId,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
playerUserId: user.id,
|
||||
power: 2500,
|
||||
region: "WEST",
|
||||
rank: 101,
|
||||
weaponSplId: 40 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT,
|
||||
});
|
||||
|
||||
await XRankPlacementRepository.refreshTenStarWeapons();
|
||||
|
||||
const rows = await db.selectFrom("TenStarWeapon").selectAll().execute();
|
||||
|
||||
expect(rows).toHaveLength(0);
|
||||
expect(await findTenStarWeapons()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("unlinked players are excluded", async () => {
|
||||
const playerId = await createSplatoonPlayer("player1");
|
||||
|
||||
await createXRankPlacement({
|
||||
playerId,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
power: 2500,
|
||||
region: "JPN",
|
||||
rank: 1,
|
||||
weaponSplId: 40 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT,
|
||||
});
|
||||
|
||||
await XRankPlacementRepository.refreshTenStarWeapons();
|
||||
|
||||
const rows = await db.selectFrom("TenStarWeapon").selectAll().execute();
|
||||
|
||||
expect(rows).toHaveLength(0);
|
||||
expect(await findTenStarWeapons()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("duplicate weapon placements produce one row", async () => {
|
||||
const userId = await createUser("user1");
|
||||
const playerId = await createSplatoonPlayer("player1", userId);
|
||||
|
||||
await createXRankPlacement({
|
||||
playerId,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
playerUserId: user.id,
|
||||
power: 2500,
|
||||
region: "JPN",
|
||||
rank: 100,
|
||||
weaponSplId: 40 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT,
|
||||
});
|
||||
await createXRankPlacement({
|
||||
playerId,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
playerUserId: user.id,
|
||||
power: 2700,
|
||||
region: "JPN",
|
||||
rank: 50,
|
||||
weaponSplId: 40 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT,
|
||||
});
|
||||
|
||||
await XRankPlacementRepository.refreshTenStarWeapons();
|
||||
|
||||
const rows = await db.selectFrom("TenStarWeapon").selectAll().execute();
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(await findTenStarWeapons()).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("refreshTenStarWeapons with userId", () => {
|
||||
beforeEach(async () => {
|
||||
placementCounter = 0;
|
||||
await dbReset();
|
||||
});
|
||||
|
||||
|
|
@ -258,49 +201,37 @@ describe("refreshTenStarWeapons with userId", () => {
|
|||
});
|
||||
|
||||
test("only affects the target user", async () => {
|
||||
const user1Id = await createUser("user1");
|
||||
const user2Id = await createUser("user2");
|
||||
const player1Id = await createSplatoonPlayer("player1", user1Id);
|
||||
const player2Id = await createSplatoonPlayer("player2", user2Id);
|
||||
const [user1, user2] = await UserFactory.createMany(2);
|
||||
|
||||
await createXRankPlacement({
|
||||
playerId: player1Id,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-1",
|
||||
playerUserId: user1.id,
|
||||
power: 2500,
|
||||
region: "JPN",
|
||||
rank: 100,
|
||||
weaponSplId: 40 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT,
|
||||
});
|
||||
await createXRankPlacement({
|
||||
playerId: player2Id,
|
||||
await XRankPlacementFactory.create({
|
||||
playerSplId: "player-2",
|
||||
playerUserId: user2.id,
|
||||
power: 2500,
|
||||
region: "JPN",
|
||||
rank: 200,
|
||||
weaponSplId: 50 as MainWeaponId,
|
||||
weaponSplId: SPLATTERSHOT_NOUVEAU,
|
||||
});
|
||||
|
||||
await XRankPlacementRepository.refreshTenStarWeapons();
|
||||
|
||||
const beforeRows = await db
|
||||
.selectFrom("TenStarWeapon")
|
||||
.selectAll()
|
||||
.execute();
|
||||
expect(beforeRows).toHaveLength(2);
|
||||
expect(await findTenStarWeapons()).toHaveLength(2);
|
||||
|
||||
await db
|
||||
.updateTable("SplatoonPlayer")
|
||||
.set({ userId: null })
|
||||
.where("id", "=", player1Id)
|
||||
.execute();
|
||||
await XRankPlacementRepository.unlinkPlayerByUserId(user1.id);
|
||||
|
||||
await XRankPlacementRepository.refreshTenStarWeapons(user1Id);
|
||||
await XRankPlacementRepository.refreshTenStarWeapons(user1.id);
|
||||
|
||||
const afterRows = await db
|
||||
.selectFrom("TenStarWeapon")
|
||||
.selectAll()
|
||||
.execute();
|
||||
const afterRows = await findTenStarWeapons();
|
||||
|
||||
expect(afterRows).toHaveLength(1);
|
||||
expect(afterRows[0].userId).toBe(user2Id);
|
||||
expect(afterRows[0].weaponSplId).toBe(50);
|
||||
expect(afterRows[0].userId).toBe(user2.id);
|
||||
expect(afterRows[0].weaponSplId).toBe(SPLATTERSHOT_NOUVEAU);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,70 @@ import { modesShort } from "~/modules/in-game-lists/modes";
|
|||
import type { MainWeaponId } from "~/modules/in-game-lists/types";
|
||||
import { peakXpOverallSql } from "~/utils/kysely.server";
|
||||
|
||||
export type XRankPlacementInsertArgs = Omit<
|
||||
Tables["XRankPlacement"],
|
||||
"id" | "playerId"
|
||||
> & {
|
||||
/** In-game id of the player the placement belongs to. */
|
||||
playerSplId: string;
|
||||
/**
|
||||
* Site user whose results these are, for a source that already knows the pairing.
|
||||
* A player claiming their own results afterwards goes through
|
||||
* `AdminRepository.linkUserAndPlayer`, which also refreshes what the link derives.
|
||||
*/
|
||||
playerUserId?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds the given placements, creating a `SplatoonPlayer` row for every in-game id
|
||||
* not seen before. Returns the placement ids in insertion order.
|
||||
*/
|
||||
export function insertMany(placements: XRankPlacementInsertArgs[]) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const ids: number[] = [];
|
||||
|
||||
for (const { playerSplId, playerUserId, ...placement } of placements) {
|
||||
await trx
|
||||
.insertInto("SplatoonPlayer")
|
||||
.values({ splId: playerSplId, userId: playerUserId ?? null })
|
||||
.onConflict((oc) =>
|
||||
playerUserId
|
||||
? oc.column("splId").doUpdateSet({ userId: playerUserId })
|
||||
: oc.column("splId").doNothing(),
|
||||
)
|
||||
.execute();
|
||||
|
||||
const inserted = await trx
|
||||
.insertInto("XRankPlacement")
|
||||
.values({
|
||||
...placement,
|
||||
playerId: (eb) =>
|
||||
eb
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select("SplatoonPlayer.id")
|
||||
.where("splId", "=", playerSplId),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
ids.push(inserted.id);
|
||||
}
|
||||
|
||||
return ids;
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes every placement of the given month, before its results are read in anew. */
|
||||
export function deleteAllByMonthYear(
|
||||
args: Pick<Tables["XRankPlacement"], "month" | "year">,
|
||||
) {
|
||||
return db
|
||||
.deleteFrom("XRankPlacement")
|
||||
.where("month", "=", args.month)
|
||||
.where("year", "=", args.year)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export function unlinkPlayerByUserId(userId: number) {
|
||||
return db
|
||||
.updateTable("SplatoonPlayer")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import * as ImageFactory from "~/db/seed/factories/ImageFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import * as XRankPlacementFactory from "~/db/seed/factories/XRankPlacementFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import { dbReset, withNoUser, withUserId } from "~/utils/Test";
|
||||
import * as UserCardRepository from "./UserCardRepository.server";
|
||||
|
|
@ -8,35 +10,17 @@ import type { UserCardData } from "./user-card-types";
|
|||
let owner: { id: number };
|
||||
let other: { id: number };
|
||||
|
||||
const insertVerifiedXp = async (
|
||||
const insertVerifiedXp = (
|
||||
userId: number,
|
||||
power: number,
|
||||
region: "WEST" | "JPN" = "WEST",
|
||||
) => {
|
||||
const player = await db
|
||||
.insertInto("SplatoonPlayer")
|
||||
.values({ splId: `spl-${userId}`, userId })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
await db
|
||||
.insertInto("XRankPlacement")
|
||||
.values({
|
||||
playerId: player.id,
|
||||
weaponSplId: 0,
|
||||
badges: "[]",
|
||||
bannerSplId: 1,
|
||||
mode: "SZ",
|
||||
month: 1,
|
||||
year: 2024,
|
||||
name: "Test Player",
|
||||
nameDiscriminator: "0000",
|
||||
power,
|
||||
rank: 1,
|
||||
region,
|
||||
title: "Test",
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
) =>
|
||||
XRankPlacementFactory.create({
|
||||
playerSplId: `player-${userId}`,
|
||||
playerUserId: userId,
|
||||
power,
|
||||
region,
|
||||
});
|
||||
|
||||
const findXpStat = (card: UserCardData | undefined) =>
|
||||
card?.stats.find((stat) => stat.type === "XP");
|
||||
|
|
@ -254,15 +238,10 @@ describe("UserCardRepository.findAllByUserIds", () => {
|
|||
});
|
||||
|
||||
it("produces a URL banner when an uploaded banner image is set", async () => {
|
||||
const image = await db
|
||||
.insertInto("UnvalidatedUserSubmittedImage")
|
||||
.values({
|
||||
url: "banner.webp",
|
||||
submitterUserId: owner.id,
|
||||
validatedAt: 1,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
const image = await ImageFactory.create(
|
||||
{ submitterUserId: owner.id },
|
||||
{ isValidated: true },
|
||||
);
|
||||
|
||||
await withUserId(owner.id, () =>
|
||||
UserCardRepository.updateOwnCard({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { sub } from "date-fns";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
vi.mock("~/features/chat/ChatSystemMessage.server", () => ({
|
||||
|
|
@ -6,66 +7,51 @@ vi.mock("~/features/chat/ChatSystemMessage.server", () => ({
|
|||
setMetadata: vi.fn(),
|
||||
}));
|
||||
|
||||
import { backdate } from "~/db/seed/core/backdate";
|
||||
import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory";
|
||||
import * as SQMatchFactory from "~/db/seed/factories/SQMatchFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import { FULL_GROUP_SIZE } from "~/features/sendouq/q-constants";
|
||||
import { dbReset } from "~/utils/Test";
|
||||
import * as GroupMatchContinueVoteRepository from "~/features/sendouq-match/GroupMatchContinueVoteRepository.server";
|
||||
import { dbReset, withUserId } from "~/utils/Test";
|
||||
import { CloseExpiredContinueVotesRoutine } from "./closeExpiredContinueVotes";
|
||||
|
||||
let alphaUserIds: number[];
|
||||
let bravoUserIds: number[];
|
||||
|
||||
const insertGroup = async ({
|
||||
matchmade,
|
||||
userIds,
|
||||
const setupMatch = async ({
|
||||
isMatchmade,
|
||||
confirmedAt,
|
||||
}: {
|
||||
matchmade: 0 | 1;
|
||||
userIds: readonly number[];
|
||||
isMatchmade: boolean;
|
||||
confirmedAt: Date;
|
||||
}) => {
|
||||
const group = await db
|
||||
.insertInto("Group")
|
||||
.values({
|
||||
chatCode: `chat-${Math.random().toString(36).slice(2, 10)}`,
|
||||
inviteCode: `inv-${Math.random().toString(36).slice(2, 10)}`,
|
||||
status: "INACTIVE",
|
||||
matchmade,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
const alphaGroup = await createGroup(alphaUserIds, isMatchmade);
|
||||
const bravoGroup = await createGroup(bravoUserIds, isMatchmade);
|
||||
|
||||
await db
|
||||
.insertInto("GroupMember")
|
||||
.values(
|
||||
userIds.map((userId, i) => ({
|
||||
groupId: group.id,
|
||||
userId,
|
||||
role: i === 0 ? ("OWNER" as const) : ("REGULAR" as const),
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
const match = await SQMatchFactory.create(
|
||||
{ alphaGroupId: alphaGroup.id, bravoGroupId: bravoGroup.id },
|
||||
{ isConcluded: true },
|
||||
);
|
||||
await backdate("GroupMatch", match.id, { confirmedAt });
|
||||
|
||||
return group.id;
|
||||
return { alphaGroupId: alphaGroup.id, bravoGroupId: bravoGroup.id };
|
||||
};
|
||||
|
||||
const insertMatch = async ({
|
||||
alphaGroupId,
|
||||
bravoGroupId,
|
||||
confirmedAtSeconds,
|
||||
}: {
|
||||
alphaGroupId: number;
|
||||
bravoGroupId: number;
|
||||
confirmedAtSeconds: number;
|
||||
}) => {
|
||||
await db
|
||||
.insertInto("GroupMatch")
|
||||
.values({
|
||||
alphaGroupId,
|
||||
bravoGroupId,
|
||||
chatCode: "test-match-chat",
|
||||
confirmedAt: confirmedAtSeconds,
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
const createGroup = ([owner, ...members]: number[], isMatchmade: boolean) =>
|
||||
SQGroupFactory.create(
|
||||
{ userId: owner, additionalMemberUserIds: members },
|
||||
{ isMatchmade },
|
||||
);
|
||||
|
||||
const castContinueVote = (groupId: number, userId: number) =>
|
||||
withUserId(userId, () =>
|
||||
GroupMatchContinueVoteRepository.castOwnVote({
|
||||
groupId,
|
||||
isContinuing: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const fetchVotes = (groupId: number) =>
|
||||
db
|
||||
|
|
@ -89,29 +75,13 @@ describe("CloseExpiredContinueVotesRoutine", () => {
|
|||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const nowSeconds = () => Math.floor(Date.now() / 1000);
|
||||
|
||||
test("flips all non-NO members to NO for matchmade groups whose match confirmed over 1h ago", async () => {
|
||||
const alphaGroupId = await insertGroup({
|
||||
matchmade: 1,
|
||||
userIds: alphaUserIds,
|
||||
const { alphaGroupId, bravoGroupId } = await setupMatch({
|
||||
isMatchmade: true,
|
||||
confirmedAt: sub(new Date(), { hours: 2 }),
|
||||
});
|
||||
const bravoGroupId = await insertGroup({
|
||||
matchmade: 1,
|
||||
userIds: bravoUserIds,
|
||||
});
|
||||
await insertMatch({
|
||||
alphaGroupId,
|
||||
bravoGroupId,
|
||||
confirmedAtSeconds: nowSeconds() - 60 * 60 * 2,
|
||||
});
|
||||
await db
|
||||
.insertInto("GroupMatchContinueVote")
|
||||
.values([
|
||||
{ groupId: alphaGroupId, userId: alphaUserIds[0], isContinuing: 1 },
|
||||
{ groupId: alphaGroupId, userId: alphaUserIds[1], isContinuing: 1 },
|
||||
])
|
||||
.execute();
|
||||
await castContinueVote(alphaGroupId, alphaUserIds[0]);
|
||||
await castContinueVote(alphaGroupId, alphaUserIds[1]);
|
||||
|
||||
await CloseExpiredContinueVotesRoutine.run();
|
||||
|
||||
|
|
@ -129,27 +99,11 @@ describe("CloseExpiredContinueVotesRoutine", () => {
|
|||
});
|
||||
|
||||
test("leaves matches confirmed under 1h ago untouched", async () => {
|
||||
const alphaGroupId = await insertGroup({
|
||||
matchmade: 1,
|
||||
userIds: alphaUserIds,
|
||||
const { alphaGroupId, bravoGroupId } = await setupMatch({
|
||||
isMatchmade: true,
|
||||
confirmedAt: sub(new Date(), { minutes: 30 }),
|
||||
});
|
||||
const bravoGroupId = await insertGroup({
|
||||
matchmade: 1,
|
||||
userIds: bravoUserIds,
|
||||
});
|
||||
await insertMatch({
|
||||
alphaGroupId,
|
||||
bravoGroupId,
|
||||
confirmedAtSeconds: nowSeconds() - 60 * 30,
|
||||
});
|
||||
await db
|
||||
.insertInto("GroupMatchContinueVote")
|
||||
.values({
|
||||
groupId: alphaGroupId,
|
||||
userId: alphaUserIds[0],
|
||||
isContinuing: 1,
|
||||
})
|
||||
.execute();
|
||||
await castContinueVote(alphaGroupId, alphaUserIds[0]);
|
||||
|
||||
await CloseExpiredContinueVotesRoutine.run();
|
||||
|
||||
|
|
@ -160,18 +114,9 @@ describe("CloseExpiredContinueVotesRoutine", () => {
|
|||
});
|
||||
|
||||
test("does not touch non-matchmade groups even if match confirmed long ago", async () => {
|
||||
const alphaGroupId = await insertGroup({
|
||||
matchmade: 0,
|
||||
userIds: alphaUserIds,
|
||||
});
|
||||
const bravoGroupId = await insertGroup({
|
||||
matchmade: 0,
|
||||
userIds: bravoUserIds,
|
||||
});
|
||||
await insertMatch({
|
||||
alphaGroupId,
|
||||
bravoGroupId,
|
||||
confirmedAtSeconds: nowSeconds() - 60 * 60 * 2,
|
||||
const { alphaGroupId, bravoGroupId } = await setupMatch({
|
||||
isMatchmade: false,
|
||||
confirmedAt: sub(new Date(), { hours: 2 }),
|
||||
});
|
||||
|
||||
await CloseExpiredContinueVotesRoutine.run();
|
||||
|
|
@ -181,29 +126,13 @@ describe("CloseExpiredContinueVotesRoutine", () => {
|
|||
});
|
||||
|
||||
test("skips groups whose cascade is fully resolved (every member already has a vote row)", async () => {
|
||||
const alphaGroupId = await insertGroup({
|
||||
matchmade: 1,
|
||||
userIds: alphaUserIds,
|
||||
const { alphaGroupId } = await setupMatch({
|
||||
isMatchmade: true,
|
||||
confirmedAt: sub(new Date(), { hours: 2 }),
|
||||
});
|
||||
const bravoGroupId = await insertGroup({
|
||||
matchmade: 1,
|
||||
userIds: bravoUserIds,
|
||||
});
|
||||
await insertMatch({
|
||||
alphaGroupId,
|
||||
bravoGroupId,
|
||||
confirmedAtSeconds: nowSeconds() - 60 * 60 * 2,
|
||||
});
|
||||
await db
|
||||
.insertInto("GroupMatchContinueVote")
|
||||
.values(
|
||||
alphaUserIds.map((userId) => ({
|
||||
groupId: alphaGroupId,
|
||||
userId,
|
||||
isContinuing: 1 as const,
|
||||
})),
|
||||
)
|
||||
.execute();
|
||||
for (const userId of alphaUserIds) {
|
||||
await castContinueVote(alphaGroupId, userId);
|
||||
}
|
||||
|
||||
await CloseExpiredContinueVotesRoutine.run();
|
||||
|
||||
|
|
|
|||
|
|
@ -256,8 +256,7 @@ describe("syncLiveStreams tournament streamers", () => {
|
|||
const rowsAfterFirst = await findAllTournamentStreamers();
|
||||
expect(rowsAfterFirst).toHaveLength(1);
|
||||
|
||||
// clear DB and add a different tournament — if throttle works, nothing new is inserted
|
||||
await db.deleteFrom("TournamentStreamer").execute();
|
||||
// add a different tournament — if throttle works, nothing new is inserted
|
||||
RunningTournaments.clear();
|
||||
|
||||
mockGetStreams.mockResolvedValue([
|
||||
|
|
@ -296,6 +295,7 @@ describe("syncLiveStreams tournament streamers", () => {
|
|||
await SyncLiveStreamsRoutine.run();
|
||||
|
||||
const rowsAfterSecond = await findAllTournamentStreamers();
|
||||
expect(rowsAfterSecond).toHaveLength(0);
|
||||
expect(rowsAfterSecond).toHaveLength(1);
|
||||
expect(rowsAfterSecond[0].twitchAccount).toBe("streamer_a");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { sql } from "kysely";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import * as TournamentStreamerFactory from "~/db/seed/factories/TournamentStreamerFactory";
|
||||
import * as UserFactory from "~/db/seed/factories/UserFactory";
|
||||
import { db } from "~/db/sql";
|
||||
import type { CastedMatchesInfo } from "~/db/tables-json";
|
||||
|
|
@ -360,10 +361,11 @@ async function seedStreamer(
|
|||
twitchAccount: string,
|
||||
userId: number | null = null,
|
||||
) {
|
||||
await db
|
||||
.insertInto("TournamentStreamer")
|
||||
.values({ tournamentId: TOURNAMENT_ID, twitchAccount, userId })
|
||||
.execute();
|
||||
await TournamentStreamerFactory.create({
|
||||
tournamentId: TOURNAMENT_ID,
|
||||
twitchAccount,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
function findAllVods() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { db } from "~/db/sql";
|
||||
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
|
|
@ -20,9 +20,11 @@ invariant(
|
|||
"hue must be between -360 and 360",
|
||||
);
|
||||
|
||||
await db
|
||||
.insertInto("Badge")
|
||||
.values({ code, displayName, hue: parsedHue ?? null })
|
||||
.execute();
|
||||
await BadgeRepository.insert({
|
||||
code,
|
||||
displayName,
|
||||
hue: parsedHue ?? null,
|
||||
authorId: null,
|
||||
});
|
||||
|
||||
logger.info(`Added new badge: ${displayName}`);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { db } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
|
||||
import * as BuildRepository from "~/features/builds/BuildRepository.server";
|
||||
import * as XRankPlacementRepository from "~/features/top-search/XRankPlacementRepository.server";
|
||||
|
|
@ -17,9 +15,7 @@ invariant(
|
|||
"jsonNumber must be an integer (argument 1)",
|
||||
);
|
||||
|
||||
type Placements = Array<
|
||||
Omit<Tables["XRankPlacement"], "playerId" | "id"> & { playerSplId: string }
|
||||
>;
|
||||
type Placements = XRankPlacementRepository.XRankPlacementInsertArgs[];
|
||||
|
||||
const modes = ["splatzones", "towercontrol", "rainmaker", "clamblitz"] as const;
|
||||
const modeToShort = {
|
||||
|
|
@ -35,7 +31,9 @@ void main();
|
|||
async function main() {
|
||||
const placements: Placements = [];
|
||||
|
||||
await wipeMonthYearPlacements(resolveMonthYear(jsonNumber));
|
||||
await XRankPlacementRepository.deleteAllByMonthYear(
|
||||
resolveMonthYear(jsonNumber),
|
||||
);
|
||||
for (const mode of modes) {
|
||||
for (const region of regions) {
|
||||
for (const includeWeapon of [false]) {
|
||||
|
|
@ -51,7 +49,7 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
await addPlacements(placements);
|
||||
await XRankPlacementRepository.insertMany(placements);
|
||||
await XRankPlacementRepository.refreshAllPeakXp();
|
||||
await BadgeRepository.syncXPBadges();
|
||||
await BuildRepository.recalculateAllSortValues();
|
||||
|
|
@ -139,41 +137,3 @@ function resolveMonthYear(number: number) {
|
|||
year: start.getFullYear(),
|
||||
};
|
||||
}
|
||||
|
||||
function addPlacements(placements: Placements) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
for (const { playerSplId, ...placement } of placements) {
|
||||
await trx
|
||||
.insertInto("SplatoonPlayer")
|
||||
.values({ splId: playerSplId })
|
||||
.onConflict((oc) => oc.column("splId").doNothing())
|
||||
.execute();
|
||||
|
||||
await trx
|
||||
.insertInto("XRankPlacement")
|
||||
.values({
|
||||
...placement,
|
||||
playerId: (eb) =>
|
||||
eb
|
||||
.selectFrom("SplatoonPlayer")
|
||||
.select("SplatoonPlayer.id")
|
||||
.where("splId", "=", playerSplId),
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wipeMonthYearPlacements({
|
||||
month,
|
||||
year,
|
||||
}: {
|
||||
month: number;
|
||||
year: number;
|
||||
}) {
|
||||
return db
|
||||
.deleteFrom("XRankPlacement")
|
||||
.where("month", "=", month)
|
||||
.where("year", "=", year)
|
||||
.execute();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { db } from "~/db/sql";
|
||||
import * as BadgeRepository from "~/features/badges/BadgeRepository.server";
|
||||
import { logger } from "~/utils/logger";
|
||||
|
||||
const HOMEMADE_BADGES_URL =
|
||||
|
|
@ -162,14 +163,7 @@ async function addBadge(badge: {
|
|||
displayName: string;
|
||||
authorId: number;
|
||||
}) {
|
||||
return db
|
||||
.insertInto("Badge")
|
||||
.values({
|
||||
code: badge.code,
|
||||
displayName: badge.displayName,
|
||||
authorId: badge.authorId,
|
||||
})
|
||||
.execute();
|
||||
return BadgeRepository.insert({ ...badge, hue: null });
|
||||
}
|
||||
|
||||
main();
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user