Skip to content

Commit 844a789

Browse files
fix: allow deletion of cancelled tournaments with match demos (#103)
Co-authored-by: Flegma <Flegma@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent ed38d00 commit 844a789

7 files changed

Lines changed: 174 additions & 9 deletions

File tree

hasura/metadata/actions.graphql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ type Mutation {
5454
): SuccessOutput
5555
}
5656

57+
type Mutation {
58+
deleteTournament(
59+
tournament_id: uuid!
60+
): SuccessOutput
61+
}
62+
5763
type Mutation {
5864
deleteServerItem(
5965
node_id: String!

hasura/metadata/actions.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ actions:
6767
forward_client_headers: true
6868
permissions:
6969
- role: administrator
70+
- name: deleteTournament
71+
definition:
72+
kind: synchronous
73+
handler: '{{HASURA_GRAPHQL_ACTIONS_HOOK}}'
74+
forward_client_headers: true
75+
permissions:
76+
- role: user
77+
comment: Delete a tournament and clean up demo files
7078
- name: deleteServerItem
7179
definition:
7280
kind: synchronous

hasura/metadata/databases/default/tables/public_tournaments.yaml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -358,10 +358,3 @@ update_permissions:
358358
is_organizer:
359359
_eq: true
360360
comment: ""
361-
delete_permissions:
362-
- role: user
363-
permission:
364-
filter:
365-
is_organizer:
366-
_eq: true
367-
comment: ""

hasura/triggers/tournament_teams.sql

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ BEGIN
3232
SELECT status
3333
INTO tournament_status
3434
FROM tournaments
35-
WHERE id = NEW.tournament_id;
35+
WHERE id = OLD.tournament_id;
3636

37-
IF tournament_status = 'Cancelled' OR tournament_status = 'CancelledMinTeams' OR tournament_status = 'Finished' THEN
37+
-- If tournament doesn't exist (cascade delete), allow the team removal
38+
IF tournament_status IS NOT NULL AND tournament_status IN ('Cancelled', 'CancelledMinTeams', 'Finished') THEN
3839
RAISE EXCEPTION 'Cannot leave an active tournament' USING ERRCODE = '22000';
3940
END IF;
4041

src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { K8sModule } from "./k8s/k8s.module";
4343
import { FileManagerModule } from "./file-manager/file-manager.module";
4444
import { BrandingModule } from "./branding/branding.module";
4545
import { FixturesModule } from "./fixtures/fixtures.module";
46+
import { TournamentsModule } from "./tournaments/tournaments.module";
4647

4748
@Module({
4849
imports: [
@@ -126,6 +127,7 @@ import { FixturesModule } from "./fixtures/fixtures.module";
126127
FileManagerModule,
127128
BrandingModule,
128129
FixturesModule,
130+
TournamentsModule,
129131
],
130132
providers: [loggerFactory()],
131133
controllers: [AppController, QuickConnectController],
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { Controller, Logger } from "@nestjs/common";
2+
import { HasuraAction } from "../hasura/hasura.controller";
3+
import { HasuraService } from "../hasura/hasura.service";
4+
import { S3Service } from "../s3/s3.service";
5+
import { User } from "../auth/types/User";
6+
7+
@Controller("tournaments")
8+
export class TournamentsController {
9+
constructor(
10+
private readonly logger: Logger,
11+
private readonly hasura: HasuraService,
12+
private readonly s3: S3Service,
13+
) {}
14+
15+
@HasuraAction()
16+
public async deleteTournament(data: {
17+
user: User;
18+
tournament_id: string;
19+
}) {
20+
const { tournament_id } = data;
21+
this.logger.log(`[${tournament_id}] deleting tournament`);
22+
23+
// Query with user context for authorization checks
24+
const { tournaments_by_pk } = await this.hasura.query(
25+
{
26+
tournaments_by_pk: {
27+
__args: {
28+
id: tournament_id,
29+
},
30+
id: true,
31+
status: true,
32+
is_organizer: true,
33+
},
34+
},
35+
data.user.steam_id,
36+
);
37+
38+
if (!tournaments_by_pk) {
39+
throw Error("tournament not found");
40+
}
41+
42+
if (!tournaments_by_pk.is_organizer) {
43+
throw Error("not the tournament organizer");
44+
}
45+
46+
if (tournaments_by_pk.status === "Live") {
47+
throw Error("cannot delete a live tournament");
48+
}
49+
50+
// Query as admin to access demo file paths
51+
const { tournaments_by_pk: tournament } = await this.hasura.query({
52+
tournaments_by_pk: {
53+
__args: {
54+
id: tournament_id,
55+
},
56+
stages: {
57+
brackets: {
58+
match: {
59+
id: true,
60+
match_maps: {
61+
demos: {
62+
id: true,
63+
file: true,
64+
},
65+
},
66+
},
67+
},
68+
},
69+
},
70+
});
71+
72+
const demos: Array<{ id: string; file: string }> = [];
73+
const matchIds: string[] = [];
74+
75+
for (const stage of tournament.stages || []) {
76+
for (const bracket of stage.brackets || []) {
77+
if (!bracket.match) {
78+
continue;
79+
}
80+
matchIds.push(bracket.match.id);
81+
for (const matchMap of bracket.match.match_maps || []) {
82+
for (const demo of matchMap.demos || []) {
83+
demos.push(demo);
84+
}
85+
}
86+
}
87+
}
88+
89+
for (const demo of demos) {
90+
try {
91+
await this.s3.remove(demo.file);
92+
await this.hasura.mutation({
93+
delete_match_map_demos_by_pk: {
94+
__args: {
95+
id: demo.id,
96+
},
97+
__typename: true,
98+
},
99+
});
100+
} catch (error) {
101+
this.logger.error(
102+
`[${tournament_id}] failed to clean up demo ${demo.id}`,
103+
error,
104+
);
105+
}
106+
}
107+
108+
for (const matchId of matchIds) {
109+
try {
110+
await this.hasura.mutation({
111+
delete_matches_by_pk: {
112+
__args: {
113+
id: matchId,
114+
},
115+
__typename: true,
116+
},
117+
});
118+
} catch (error) {
119+
this.logger.error(
120+
`[${tournament_id}] failed to delete match ${matchId}`,
121+
error,
122+
);
123+
}
124+
}
125+
126+
await this.hasura.mutation({
127+
delete_tournaments_by_pk: {
128+
__args: {
129+
id: tournament_id,
130+
},
131+
__typename: true,
132+
},
133+
});
134+
135+
this.logger.log(
136+
`[${tournament_id}] tournament deleted, cleaned up ${demos.length} demo files across ${matchIds.length} matches`,
137+
);
138+
139+
return {
140+
success: true,
141+
};
142+
}
143+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from "@nestjs/common";
2+
import { TournamentsController } from "./tournaments.controller";
3+
import { HasuraModule } from "../hasura/hasura.module";
4+
import { S3Module } from "../s3/s3.module";
5+
import { loggerFactory } from "../utilities/LoggerFactory";
6+
7+
@Module({
8+
imports: [HasuraModule, S3Module],
9+
controllers: [TournamentsController],
10+
providers: [loggerFactory()],
11+
})
12+
export class TournamentsModule {}

0 commit comments

Comments
 (0)