Unify weapon report data model

This commit is contained in:
Kalle 2026-05-02 16:57:29 +03:00
parent c012022720
commit 6764340ea1
18 changed files with 96 additions and 121 deletions

View File

@ -129,7 +129,6 @@ export function WeaponReporter({
</div>
<SendouButton
variant="primary"
size="small"
isDisabled={selectedWeapon === null || isSubmitting}
onPress={() => {
if (selectedWeapon === null) return;

View File

@ -3079,7 +3079,7 @@ async function playedMatches() {
if (faker.number.float(1) > 0.9) continue;
const users = [...groupAlphaMembers, ...groupBravoMembers];
const mapsWithUsers = users.flatMap((u) =>
finishedMatch.mapList.map((m) => ({ map: m, user: u })),
finishedMatch.mapList.map((_, mapIndex) => ({ mapIndex, user: u })),
);
await ReportedWeaponRepository.createMany(
@ -3097,7 +3097,8 @@ async function playedMatches() {
};
return {
groupMatchMapId: mu.map.id,
groupMatchId: match.id,
mapIndex: mu.mapIndex,
userId: mu.user,
weaponSplId: weapon(),
};

View File

@ -453,14 +453,10 @@ export interface PlusVotingResult {
passedVoting: DBBoolean;
}
// xxx: unify SendouQ side to also key on (matchId, mapIndex). Replace
// groupMatchMapId with groupMatchId, make mapIndex non-null, and have a
// single CHECK that exactly one of groupMatchId / tournamentMatchId is set.
// Backfill (groupMatchId, mapIndex) from GroupMatchMap.matchId/index.
export interface ReportedWeapon {
groupMatchMapId: number | null;
groupMatchId: number | null;
tournamentMatchId: number | null;
mapIndex: number | null;
mapIndex: number;
userId: number;
weaponSplId: MainWeaponId;
}

View File

@ -410,12 +410,7 @@ export async function seasonPopularUsersWeapon(
.with("q1", (db) =>
db
.selectFrom("ReportedWeapon")
.innerJoin(
"GroupMatchMap",
"ReportedWeapon.groupMatchMapId",
"GroupMatchMap.id",
)
.innerJoin("GroupMatch", "GroupMatchMap.matchId", "GroupMatch.id")
.innerJoin("GroupMatch", "ReportedWeapon.groupMatchId", "GroupMatch.id")
.select(({ fn }) => [
"ReportedWeapon.userId",
"ReportedWeapon.weaponSplId",

View File

@ -15,19 +15,24 @@ export function createMany(
}
export async function upsertOne({
groupMatchMapId,
groupMatchId,
mapIndex,
userId,
weaponSplId,
}: TablesInsertable["ReportedWeapon"] & { groupMatchMapId: number }) {
}: TablesInsertable["ReportedWeapon"] & {
groupMatchId: number;
mapIndex: number;
}) {
await db
.deleteFrom("ReportedWeapon")
.where("groupMatchMapId", "=", groupMatchMapId)
.where("groupMatchId", "=", groupMatchId)
.where("mapIndex", "=", mapIndex)
.where("userId", "=", userId)
.execute();
await db
.insertInto("ReportedWeapon")
.values({ groupMatchMapId, userId, weaponSplId })
.values({ groupMatchId, mapIndex, userId, weaponSplId })
.execute();
}
@ -38,23 +43,11 @@ export async function replaceByMatchId(
) {
const executor = trx ?? db;
const groupMatchMaps = await executor
.selectFrom("GroupMatchMap")
.select("id")
.where("matchId", "=", matchId)
await executor
.deleteFrom("ReportedWeapon")
.where("groupMatchId", "=", matchId)
.execute();
if (groupMatchMaps.length > 0) {
await executor
.deleteFrom("ReportedWeapon")
.where(
"groupMatchMapId",
"in",
groupMatchMaps.map((m) => m.id),
)
.execute();
}
if (weapons.length > 0) {
await executor.insertInto("ReportedWeapon").values(weapons).execute();
}
@ -69,18 +62,10 @@ export async function deleteByUserMapIndex({
userId: number;
mapIndex: number;
}) {
const groupMatchMap = await db
.selectFrom("GroupMatchMap")
.select("id")
.where("matchId", "=", matchId)
.where("index", "=", mapIndex)
.executeTakeFirst();
if (!groupMatchMap) return;
await db
.deleteFrom("ReportedWeapon")
.where("groupMatchMapId", "=", groupMatchMap.id)
.where("groupMatchId", "=", matchId)
.where("mapIndex", "=", mapIndex)
.where("userId", "=", userId)
.execute();
}
@ -88,21 +73,16 @@ export async function deleteByUserMapIndex({
export async function findByMatchId(matchId: number) {
const rows = await db
.selectFrom("ReportedWeapon")
.innerJoin(
"GroupMatchMap",
"GroupMatchMap.id",
"ReportedWeapon.groupMatchMapId",
)
.select([
"ReportedWeapon.groupMatchMapId",
"ReportedWeapon.groupMatchId",
"ReportedWeapon.mapIndex",
"ReportedWeapon.weaponSplId",
"ReportedWeapon.userId",
"GroupMatchMap.index as mapIndex",
])
.where("GroupMatchMap.matchId", "=", matchId)
.orderBy("GroupMatchMap.index", "asc")
.where("ReportedWeapon.groupMatchId", "=", matchId)
.orderBy("ReportedWeapon.mapIndex", "asc")
.orderBy("ReportedWeapon.userId", "asc")
.$narrowType<{ groupMatchMapId: NotNull }>()
.$narrowType<{ groupMatchId: NotNull }>()
.execute();
if (rows.length === 0) return null;
@ -186,12 +166,7 @@ export async function seasonReportedWeaponsByUserId({
const sendouqWeapons = db
.selectFrom("ReportedWeapon")
.innerJoin(
"GroupMatchMap",
"GroupMatchMap.id",
"ReportedWeapon.groupMatchMapId",
)
.innerJoin("GroupMatch", "GroupMatch.id", "GroupMatchMap.matchId")
.innerJoin("GroupMatch", "GroupMatch.id", "ReportedWeapon.groupMatchId")
.select(({ fn }) => [
"ReportedWeapon.weaponSplId",
fn.countAll<number>().as("count"),

View File

@ -101,13 +101,8 @@ const fetchSkills = async (matchId: number) => {
const fetchReportedWeapons = async (matchId: number) => {
return db
.selectFrom("ReportedWeapon")
.innerJoin(
"GroupMatchMap",
"GroupMatchMap.id",
"ReportedWeapon.groupMatchMapId",
)
.selectAll("ReportedWeapon")
.where("GroupMatchMap.matchId", "=", matchId)
.where("ReportedWeapon.groupMatchId", "=", matchId)
.execute();
};
@ -266,20 +261,13 @@ describe("reportScore", () => {
const bravoGroupId = await createGroup([5, 6, 7, 8]);
const match = await createMatch(alphaGroupId, bravoGroupId);
const groupMatchMaps = await db
.selectFrom("GroupMatchMap")
.select(["id", "index"])
.where("matchId", "=", match.id)
.orderBy("index", "asc")
.execute();
const result = await SQMatchRepository.reportScore({
matchId: match.id,
reportedByUserId: 1,
winners: ["ALPHA", "ALPHA", "BRAVO", "ALPHA"],
weapons: [
{
groupMatchMapId: groupMatchMaps[0].id,
groupMatchId: match.id,
weaponSplId: 40,
userId: 1,
mapIndex: 0,

View File

@ -261,9 +261,14 @@ const groupMatchResultsSubQuery = (eb: ExpressionBuilder<DB, "Skill">) => {
.selectFrom("ReportedWeapon")
.select(["ReportedWeapon.userId", "ReportedWeapon.weaponSplId"])
.whereRef(
"ReportedWeapon.groupMatchMapId",
"ReportedWeapon.groupMatchId",
"=",
"GroupMatchMap.id",
"GroupMatchMap.matchId",
)
.whereRef(
"ReportedWeapon.mapIndex",
"=",
"GroupMatchMap.index",
),
).as("weapons"),
])
@ -614,7 +619,7 @@ export type CancelMatchResult =
| { status: "DUPLICATE"; shouldRefreshCaches: false };
type WeaponInput = {
groupMatchMapId: number;
groupMatchId: number;
weaponSplId: MainWeaponId;
userId: number;
mapIndex: number;
@ -714,7 +719,8 @@ export async function reportScore({
newReportedMapsCount: winners.length,
});
const weaponsForDb = mergedWeapons.map((w) => ({
groupMatchMapId: w.groupMatchMapId,
groupMatchId: w.groupMatchId,
mapIndex: w.mapIndex,
userId: w.userId,
weaponSplId: w.weaponSplId,
}));

View File

@ -222,7 +222,8 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
);
await ReportedWeaponRepository.upsertOne({
groupMatchMapId: data.groupMatchMapId,
groupMatchId: matchId,
mapIndex: data.mapIndex,
userId: user.id,
weaponSplId: data.weaponSplId,
});

View File

@ -280,13 +280,12 @@ function WeaponReportSection({
onSubmit={(weaponSplId) => {
addRecentlyReportedWeapon(weaponSplId);
const mapIndex = pastReported.length;
const map = completedMaps[mapIndex];
if (!map) return;
if (!completedMaps[mapIndex]) return;
weaponFetcher.submit(
{
_action: "REPORT_WEAPON",
weaponSplId: String(weaponSplId),
groupMatchMapId: String(map.id),
mapIndex: String(mapIndex),
},
{ method: "post" },
);
@ -469,12 +468,11 @@ function InProgressTab({
onSubmit: (weaponSplId) => {
addRecentlyReportedWeapon(weaponSplId);
const mapIndex = weaponPastReported.length;
const map = data.match.mapList[mapIndex];
weaponFetcher.submit(
{
_action: "REPORT_WEAPON",
weaponSplId: String(weaponSplId),
groupMatchMapId: String(map.id),
mapIndex: String(mapIndex),
},
{ method: "post" },
);

View File

@ -151,7 +151,7 @@ describe("resolveTimelineMaps()", () => {
members: [member({ id: 2 })],
},
}),
[{ groupMatchMapId: 1, userId: 1, weaponSplId: 40 }] as never,
[{ mapIndex: 0, userId: 1, weaponSplId: 40 }] as never,
);
expect(result[0].weapons).toEqual({

View File

@ -39,17 +39,18 @@ export function resolveTimelineMaps(
reportedWeapons: SendouQMatchLoaderData["reportedWeapons"],
): TimelineMap[] {
return match.mapList
.filter((m) => m.winnerGroupId !== null)
.map((map) => {
.map((map, mapIndex) => ({ map, mapIndex }))
.filter(({ map }) => map.winnerGroupId !== null)
.map(({ map, mapIndex }) => {
const alphaWeapons = match.groupAlpha.members.map((member) => {
const w = reportedWeapons?.find(
(rw) => rw.groupMatchMapId === map.id && rw.userId === member.id,
(rw) => rw.mapIndex === mapIndex && rw.userId === member.id,
);
return w ? w.weaponSplId : null;
});
const bravoWeapons = match.groupBravo.members.map((member) => {
const w = reportedWeapons?.find(
(rw) => rw.groupMatchMapId === map.id && rw.userId === member.id,
(rw) => rw.mapIndex === mapIndex && rw.userId === member.id,
);
return w ? w.weaponSplId : null;
});

View File

@ -5,7 +5,7 @@ import { mergeReportedWeapons } from "./reported-weapons.server";
describe("mergeReportedWeapons()", () => {
const newWeapons = [
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 0,
userId: 1,
weaponSplId: 0 as MainWeaponId,
@ -23,7 +23,7 @@ describe("mergeReportedWeapons()", () => {
newWeapons,
oldWeapons: [
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 0,
userId: 1,
weaponSplId: 1 as MainWeaponId,
@ -39,7 +39,7 @@ describe("mergeReportedWeapons()", () => {
newWeapons,
oldWeapons: [
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 0,
userId: 2,
weaponSplId: 0 as MainWeaponId,
@ -49,7 +49,7 @@ describe("mergeReportedWeapons()", () => {
expect(result).toEqual([
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 0,
userId: 2,
weaponSplId: 0 as MainWeaponId,
@ -63,13 +63,13 @@ describe("mergeReportedWeapons()", () => {
newWeapons,
oldWeapons: [
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 0,
userId: 1,
weaponSplId: 1 as MainWeaponId,
},
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 0,
userId: 2,
weaponSplId: 0 as MainWeaponId,
@ -80,7 +80,7 @@ describe("mergeReportedWeapons()", () => {
expect(result).toEqual([
...newWeapons,
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 0,
userId: 2,
weaponSplId: 0 as MainWeaponId,
@ -93,7 +93,7 @@ describe("mergeReportedWeapons()", () => {
newWeapons,
oldWeapons: [
{
groupMatchMapId: 1,
groupMatchId: 1,
mapIndex: 1,
userId: 1,
weaponSplId: 0 as MainWeaponId,

View File

@ -3,7 +3,7 @@ import type { MainWeaponId } from "~/modules/in-game-lists/types";
export type ReportedWeaponForMerging = {
weaponSplId?: MainWeaponId;
mapIndex: number;
groupMatchMapId: number;
groupMatchId: number;
userId: number;
};
type ReportedWeapon = ReportedWeaponForMerging & { weaponSplId: MainWeaponId };
@ -22,7 +22,8 @@ export function mergeReportedWeapons({
for (const oldWeapon of oldWeapons) {
const replacement = newWeapons.find(
(newWeapon) =>
newWeapon.groupMatchMapId === oldWeapon.groupMatchMapId &&
newWeapon.groupMatchId === oldWeapon.groupMatchId &&
newWeapon.mapIndex === oldWeapon.mapIndex &&
newWeapon.userId === oldWeapon.userId,
);
@ -38,7 +39,8 @@ export function mergeReportedWeapons({
if (
!result.some(
(oldWeapon) =>
newWeapon.groupMatchMapId === oldWeapon.groupMatchMapId &&
newWeapon.groupMatchId === oldWeapon.groupMatchId &&
newWeapon.mapIndex === oldWeapon.mapIndex &&
newWeapon.userId === oldWeapon.userId,
)
) {

View File

@ -19,7 +19,7 @@ export const matchSchema = z.union([
z.object({
_action: _action("REPORT_WEAPON"),
weaponSplId,
groupMatchMapId: id,
mapIndex: z.coerce.number().int().nonnegative(),
}),
z.object({
_action: _action("ADD_PRIVATE_USER_NOTE"),

View File

@ -23,11 +23,13 @@ const stm = sql.prepare(/* sql */ `
) as "weaponUserGroupId"
from "GroupMember"
left join "Group" on "Group"."id" = "GroupMember"."groupId"
inner join "GroupMatch" on
"GroupMatch"."alphaGroupId" = "Group"."id"
inner join "GroupMatch" on
"GroupMatch"."alphaGroupId" = "Group"."id"
or "GroupMatch"."bravoGroupId" = "Group"."id"
left join "GroupMatchMap" on "GroupMatchMap"."matchId" = "GroupMatch"."id"
inner join "ReportedWeapon" on "ReportedWeapon"."groupMatchMapId" = "GroupMatchMap"."id"
inner join "ReportedWeapon"
on "ReportedWeapon"."groupMatchId" = "GroupMatch"."id"
and "ReportedWeapon"."mapIndex" = "GroupMatchMap"."index"
where
"GroupMember"."userId" = @userId
and "GroupMatch"."createdAt" between @starts and @ends

Binary file not shown.

View File

@ -138,21 +138,17 @@ export function up(db) {
db.prepare(
/* sql */ `
create table "ReportedWeapon_new" (
"groupMatchMapId" integer,
"groupMatchId" integer,
"tournamentMatchId" integer,
"mapIndex" integer,
"mapIndex" integer not null,
"weaponSplId" integer not null,
"userId" integer not null,
foreign key ("groupMatchMapId") references "GroupMatchMap"("id") on delete restrict,
foreign key ("groupMatchId") references "GroupMatch"("id") on delete cascade,
foreign key ("tournamentMatchId") references "TournamentMatch"("id") on delete cascade,
foreign key ("userId") references "User"("id") on delete restrict,
unique("groupMatchMapId", "userId") on conflict rollback,
unique("groupMatchId", "mapIndex", "userId") on conflict rollback,
unique("tournamentMatchId", "mapIndex", "userId") on conflict rollback,
check (
("groupMatchMapId" is not null and "tournamentMatchId" is null and "mapIndex" is null)
or
("groupMatchMapId" is null and "tournamentMatchId" is not null and "mapIndex" is not null)
)
check (("groupMatchId" is not null) <> ("tournamentMatchId" is not null))
) strict
`,
).run();
@ -160,10 +156,13 @@ export function up(db) {
db.prepare(
/* sql */ `
insert into "ReportedWeapon_new" (
"groupMatchMapId", "tournamentMatchId", "mapIndex", "weaponSplId", "userId"
"groupMatchId", "tournamentMatchId", "mapIndex", "weaponSplId", "userId"
)
select "groupMatchMapId", null, null, "weaponSplId", "userId"
select
"GroupMatchMap"."matchId", null, "GroupMatchMap"."index",
"ReportedWeapon"."weaponSplId", "ReportedWeapon"."userId"
from "ReportedWeapon"
inner join "GroupMatchMap" on "GroupMatchMap"."id" = "ReportedWeapon"."groupMatchMapId"
`,
).run();
@ -173,7 +172,7 @@ export function up(db) {
).run();
db.prepare(
/* sql */ `create index reported_weapon_group_match_map_id on "ReportedWeapon"("groupMatchMapId")`,
/* sql */ `create index reported_weapon_group_match_id on "ReportedWeapon"("groupMatchId")`,
).run();
db.prepare(
/* sql */ `create index reported_weapon_tournament_match_id on "ReportedWeapon"("tournamentMatchId")`,

View File

@ -22,7 +22,7 @@ async function main() {
.where("User.discordId", "=", discordId)
.executeTakeFirstOrThrow();
const groupMatchMaps = await db
const playedMaps = await db
.selectFrom("GroupMember")
.innerJoin("Group", "Group.id", "GroupMember.groupId")
.innerJoin("GroupMatch", (join) =>
@ -34,7 +34,7 @@ async function main() {
),
)
.innerJoin("GroupMatchMap", "GroupMatchMap.matchId", "GroupMatch.id")
.select("GroupMatchMap.id")
.select(["GroupMatchMap.matchId", "GroupMatchMap.index"])
.where(
"GroupMatch.createdAt",
">",
@ -49,16 +49,28 @@ async function main() {
.where("GroupMatchMap.winnerGroupId", "is not", null)
.execute();
const groupMatchMapIds = groupMatchMaps.map((gmm) => gmm.id);
if (playedMaps.length === 0) {
logger.info(`No reported weapons to delete for user ${discordId}`);
return;
}
await db
.deleteFrom("ReportedWeapon")
.where("userId", "=", user.id)
.where("ReportedWeapon.groupMatchMapId", "in", groupMatchMapIds)
.where((eb) =>
eb.or(
playedMaps.map((m) =>
eb.and([
eb("ReportedWeapon.groupMatchId", "=", m.matchId),
eb("ReportedWeapon.mapIndex", "=", m.index),
]),
),
),
)
.execute();
logger.info(
`Deleted ${groupMatchMapIds.length} reported weapons for user ${discordId}`,
`Deleted reported weapons across ${playedMaps.length} maps for user ${discordId}`,
);
}