Commit 3e6f23a4 authored by nanahira's avatar nanahira

common things

parent c9131dce
Pipeline #19540 failed with stages
in 2 minutes and 20 seconds
......@@ -17,6 +17,7 @@
"@nestjs/typeorm": "^9.0.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"lodash": "^4.17.21",
"nestjs-mycard": "^3.0.2",
"nicot": "^1.0.26",
"pg": "^8.8.0",
......@@ -33,6 +34,7 @@
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.15",
"@types/jest": "28.1.8",
"@types/lodash": "^4.14.191",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
......@@ -2017,6 +2019,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
......@@ -10568,6 +10576,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"@types/lodash": {
"version": "4.14.191",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz",
"integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==",
"dev": true
},
"@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
......
......@@ -33,6 +33,7 @@ import { MatchModule } from './match/match.module';
database: config.get('DB_NAME'),
supportBigNumbers: true,
bigNumberStrings: false,
//logging: true,
}),
}),
TournamentModule,
......
import { Entity, ManyToOne } from 'typeorm';
import { EnumColumn, IdBase, IntColumn, NotChangeable, NotColumn } from 'nicot';
import {
Entity,
Index,
ManyToOne,
OneToMany,
SelectQueryBuilder,
} from 'typeorm';
import {
applyQueryProperty,
BoolColumn,
EnumColumn,
IdBase,
IntColumn,
NotChangeable,
NotColumn,
} from 'nicot';
import { TournamentIdColumn } from '../../utility/decorators/tournament-id-column';
import { Tournament } from '../../tournament/entities/Tournament.entity';
import { Participant } from '../../participant/entities/participant.entity';
import { ApiProperty, getSchemaPath } from '@nestjs/swagger';
enum MatchStatus {
export enum MatchStatus {
Pending = 'Pending',
Running = 'Running',
Finished = 'Finished',
Abandoned = 'Abandoned',
}
@Entity()
......@@ -20,6 +36,7 @@ export class Match extends IdBase() {
@ManyToOne(() => Tournament, (tournament) => tournament.matches)
tournament: Tournament;
@Index()
@NotChangeable()
@IntColumn('smallint', {
unsigned: true,
......@@ -28,6 +45,10 @@ export class Match extends IdBase() {
})
round: number;
@NotChangeable()
@BoolColumn({ default: false, description: '是否为季军赛。' })
isThirdPlaceMatch: boolean;
@EnumColumn(MatchStatus, {
default: MatchStatus.Pending,
description: '比赛状态',
......@@ -41,7 +62,7 @@ export class Match extends IdBase() {
description: '玩家 1 ID',
})
player1Id: number;
@IntColumn('mediumint', { description: '玩家 1 分数', required: false })
@IntColumn('smallint', { description: '玩家 1 分数', required: false })
player1Score: number;
@NotColumn()
......@@ -54,7 +75,7 @@ export class Match extends IdBase() {
description: '玩家 2 ID',
})
player2Id: number;
@IntColumn('mediumint', { description: '玩家 2 分数', required: false })
@IntColumn('smallint', { description: '玩家 2 分数', required: false })
player2Score: number;
@NotColumn()
......@@ -71,6 +92,14 @@ export class Match extends IdBase() {
@ManyToOne(() => Participant, (participant) => participant.wonMatches)
winner: Participant;
loserId() {
return this.player1Id === this.winnerId ? this.player2Id : this.player1Id;
}
loser() {
return this.player1Id === this.winnerId ? this.player2 : this.player1;
}
@NotChangeable()
@IntColumn('bigint', {
unsigned: true,
......@@ -79,10 +108,49 @@ export class Match extends IdBase() {
})
childMatchId: number;
setChildMatch(match: Match) {
this.childMatchId = match.id;
return this;
}
@NotColumn()
@ManyToOne(() => Match, (match) => match.parentMatches)
childMatch: Match;
@NotColumn()
@ManyToOne(() => Match, (match) => match.childMatch)
@OneToMany(() => Match, (match) => match.childMatch)
parentMatches: Match[];
participated(id: number) {
return this.player1Id === id || this.player2Id === id;
}
clone() {
const match = new Match();
Object.assign(match, this);
return match;
}
buildTree(matches: Match[]) {
this.parentMatches = matches
.filter((match) => match.childMatchId === this.id)
.map((match) => match.clone().buildTree(matches));
return this;
}
applyQuery(qb: SelectQueryBuilder<Match>, entityName: string) {
super.applyQuery(qb, entityName);
applyQueryProperty(
this,
qb,
entityName,
'round',
'player1Id',
'player2Id',
'player1Score',
'player2Score',
'winnerId',
'childMatchId',
'tournamentId',
);
}
}
......@@ -27,4 +27,13 @@ export class MatchController {
) {
return this.matchService.getMatches(dto, user);
}
@factory.update()
update(
@factory.idParam() id: number,
@factory.updateParam() dto: UpdateMatchDto,
@PutMycardUser() user: MycardUser,
) {
return this.matchService.updateMatch(id, dto, user);
}
}
import { Injectable } from '@nestjs/common';
import { CrudService, Inner } from 'nicot';
import { Match } from './entities/match.entity';
import { BlankReturnMessageDto, CrudService, Inner } from 'nicot';
import { Match, MatchStatus } from './entities/match.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard';
import { Tournament } from '../tournament/entities/Tournament.entity';
import {
Tournament,
TournamentStatus,
} from '../tournament/entities/Tournament.entity';
import { Participant } from '../participant/entities/participant.entity';
import { TournamentService } from '../tournament/tournament.service';
import { MoreThan } from 'typeorm';
@Injectable()
export class MatchService extends CrudService(Match, {
relations: [Inner('tournament'), 'player1', 'player2', 'winner'],
}) {
constructor(@InjectRepository(Match) repo) {
constructor(
@InjectRepository(Match) repo,
private tournamentService: TournamentService,
) {
super(repo);
}
......@@ -25,4 +33,67 @@ export class MatchService extends CrudService(Match, {
Tournament.extraQueryForUser(user, qb, 'tournament'),
);
}
async updateMatch(id: number, dto: Partial<Match>, user: MycardUser) {
const match = await this.repo.findOne({
where: { id },
relations: ['tournament'],
select: {
id: true,
status: true,
round: true,
tournamentId: true,
player1Id: true,
player2Id: true,
tournament: {
id: true,
status: true,
creator: true,
collaborators: true,
},
},
});
if (!match) {
throw new BlankReturnMessageDto(404, '对局不存在。').toException();
}
match.tournament.checkPermission(user);
if (dto.winnerId && !match.participated(dto.winnerId)) {
throw new BlankReturnMessageDto(
400,
'对局胜者不在对局中。',
).toException();
}
if (match.status === MatchStatus.Pending) {
throw new BlankReturnMessageDto(
400,
'对局尚未开始,无法修改。',
).toException();
}
if (match.tournament.status === TournamentStatus.Finished) {
throw new BlankReturnMessageDto(
400,
'比赛已结束,无法修改。',
).toException();
}
if (dto.winnerId !== undefined) {
dto.status = MatchStatus.Finished;
}
if (match.status === MatchStatus.Finished && dto.winnerId !== undefined) {
// clean all other matches in greater rounds
await this.repo.update(
{ round: MoreThan(match.round), tournamentId: match.tournamentId },
{
winnerId: null,
status: MatchStatus.Pending,
player1Id: null,
player2Id: null,
player1Score: null,
player2Score: null,
},
);
}
const result = await this.update(id, dto);
await this.tournamentService.afterMatchUpdate(match.tournamentId);
return result;
}
}
......@@ -7,15 +7,29 @@ import {
NotColumn,
} from 'nicot';
import { TournamentIdColumn } from '../../utility/decorators/tournament-id-column';
import {
Tournament,
TournamentVisibility,
} from '../../tournament/entities/Tournament.entity';
import { Tournament } from '../../tournament/entities/Tournament.entity';
import { MycardUser } from 'nestjs-mycard';
import { Match } from '../../match/entities/match.entity';
import { ApiProperty } from '@nestjs/swagger';
@Entity()
export class ParticipantScore {
@ApiProperty({ description: '排名' })
rank: number;
@ApiProperty({ description: '得分' })
score: number;
@ApiProperty({ description: '胜场' })
win: number;
@ApiProperty({ description: '平场' })
draw: number;
@ApiProperty({ description: '负场' })
lose: number;
@ApiProperty({ description: '轮空' })
bye: number;
@ApiProperty({ description: '平局分' })
tieBreaker: number;
}
@Entity({ orderBy: { id: 'ASC' } })
export class Participant extends NamedBase {
@BoolColumn({ default: false, description: '是否已经退赛' })
quit: boolean;
......@@ -44,6 +58,9 @@ export class Participant extends NamedBase {
@ApiProperty({ type: [Match], description: '参与的比赛。' })
matches: Match[];
@NotColumn()
score: ParticipantScore;
getMatches() {
return this.matches1.concat(this.matches2);
}
......
import { Tournament } from '../tournament/entities/Tournament.entity';
import { Match, MatchStatus } from '../match/entities/match.entity';
import { Repository } from 'typeorm';
import {
Participant,
ParticipantScore,
} from '../participant/entities/participant.entity';
export class TournamentRuleBase {
constructor(
protected tournament: Tournament,
protected repo?: Repository<Match>,
) {}
async saveMatch(matches: Match[]) {
matches.forEach((m) => (m.tournamentId = this.tournament.id));
return this.repo.save(matches);
}
currentRoundCount() {
return Math.max(
...this.tournament.matches
.filter((m) => m.status === MatchStatus.Finished)
.map((m) => m.round),
);
}
nextRoundCount() {
return this.currentRoundCount() + 1;
}
async initialize() {}
nextRound(): Partial<Match[]> {
return [];
}
specificMatches(status: MatchStatus) {
return this.tournament.matches.filter((m) => m.status === status);
}
participantScore(participant: Participant): Partial<ParticipantScore> {
const finishedMatches = this.specificMatches(MatchStatus.Finished);
return {
win: finishedMatches.filter((m) => m.winnerId === participant.id).length,
lose: finishedMatches.filter(
(m) =>
m.winnerId &&
m.winnerId !== participant.id &&
m.participated(participant.id),
).length,
draw: finishedMatches.filter(
(m) => !m.winnerId && m.participated(participant.id),
).length,
};
}
}
import { TournamentRule } from '../tournament/entities/Tournament.entity';
import { SingleElimination } from './rules/single-elimination';
import { TournamentRuleBase } from './base';
export const TournamentRules = new Map<
TournamentRule,
typeof TournamentRuleBase
>();
TournamentRules.set(<TournamentRule>'SingleElimination', SingleElimination);
import { TournamentRuleBase } from '../base';
import { Match, MatchStatus } from '../../match/entities/match.entity';
import {
Participant,
ParticipantScore,
} from '../../participant/entities/participant.entity';
import * as path from 'path';
export class SingleElimination extends TournamentRuleBase {
totalRoundCount() {
return Math.ceil(Math.log2(this.tournament.participants.length));
}
async initialize() {
const roundCount = this.totalRoundCount();
const matchStack: Record<number, Match[]> = {};
for (let i = roundCount; i > 0; i--) {
let matches: Match[];
if (i === roundCount) {
matches = [new Match()];
} else {
const nextRoundMatches = matchStack[i + 1];
matches = nextRoundMatches.flatMap((m) => [
new Match().setChildMatch(m),
new Match().setChildMatch(m),
]);
if (i === 1) {
// add participants to first round
const { participants } = this.tournament;
const neededMatchesCount =
participants.length - nextRoundMatches.length * 2;
matches = matches.slice(0, neededMatchesCount);
const participantsToAdd = [...participants].reverse();
for (const match of matches) {
match.player1Id = participantsToAdd.pop().id;
match.player2Id = participantsToAdd.pop().id;
match.status = MatchStatus.Running;
}
}
}
matches.forEach((m) => (m.round = i));
matches = await this.saveMatch(matches);
matchStack[i] = matches;
}
if (
this.tournament.participants.length >= 4 &&
this.tournament.ruleSettings.hasThirdPlaceMatch
) {
// add third place match
const thirdPlaceMatch = new Match();
thirdPlaceMatch.isThirdPlaceMatch = true;
thirdPlaceMatch.round = roundCount;
await this.repo.save(thirdPlaceMatch);
}
}
nextRound() {
const finishedMatches = this.specificMatches(MatchStatus.Finished);
const survivedParticipants = this.tournament.participants
.filter(
(p) =>
!finishedMatches.some(
(m) => m.participated(p.id) && m.winnerId !== p.id,
),
)
.reverse();
const nextRoundCount = this.nextRoundCount();
const matches = this.specificMatches(MatchStatus.Pending).filter(
(m) => m.round === nextRoundCount,
);
for (const match of matches) {
match.player1Id = survivedParticipants.pop().id;
match.player2Id = survivedParticipants.pop().id;
match.status = MatchStatus.Running;
}
if (
nextRoundCount === this.totalRoundCount() &&
this.tournament.ruleSettings.hasThirdPlaceMatch
) {
const thirdPlaceMatch = this.tournament.matches.find(
(m) => m.isThirdPlaceMatch && m.status === MatchStatus.Pending,
);
const losers = finishedMatches
.filter((m) => m.round === nextRoundCount - 1)
.map((m) => m.loserId());
if (thirdPlaceMatch && losers.length >= 2) {
thirdPlaceMatch.player1Id = losers[0];
thirdPlaceMatch.player2Id = losers[1];
thirdPlaceMatch.status = MatchStatus.Running;
matches.push(thirdPlaceMatch);
}
}
return matches;
}
participantScore(participant: Participant): Partial<ParticipantScore> {
const matches = this.specificMatches(MatchStatus.Finished);
const scores = matches
.filter((m) => m.participated(participant.id))
.map((m) => {
if (m.winnerId !== participant.id) {
return 0;
}
let point = Math.pow(2, m.round);
if (m.isThirdPlaceMatch) {
point *= 0.25;
}
return point;
});
const standardRoundCount = Math.log2(this.tournament.participants.length);
return {
...super.participantScore(participant),
bye:
standardRoundCount - Math.ceil(standardRoundCount) &&
!matches.some((m) => m.participated(participant.id) && m.round === 1)
? 1
: 0,
score: scores.reduce((a, b) => a + b, 0),
tieBreaker: 0,
};
}
}
import { Column, Entity, Index, OneToMany, SelectQueryBuilder } from 'typeorm';
import {
Column,
Entity,
Index,
OneToMany,
Repository,
SelectQueryBuilder,
} from 'typeorm';
import {
applyQueryProperty,
BlankReturnMessageDto,
......@@ -11,10 +18,15 @@ import {
} from 'nicot';
import { MycardUser } from 'nestjs-mycard';
import { DescBase } from '../../utility/NamedBase.entity';
import { Participant } from '../../participant/entities/participant.entity';
import { IsArray, IsInt, IsPositive } from 'class-validator';
import {
Participant,
ParticipantScore,
} from '../../participant/entities/participant.entity';
import { IsArray, IsInt, IsOptional, IsPositive } from 'class-validator';
import { ApiProperty, PartialType } from '@nestjs/swagger';
import { Match } from '../../match/entities/match.entity';
import { TournamentRules } from '../../tournament-rules/rule-map';
import _ from 'lodash';
export enum TournamentRule {
SingleElimination = 'SingleElimination',
......@@ -46,7 +58,7 @@ export class RuleSettings {
@ApiProperty({ description: '瑞士轮轮空分。' })
byeScore: number;
@ApiProperty({ description: '淘汰赛中是否季军塞' })
@ApiProperty({ description: '淘汰赛中是否季军塞' })
hasThirdPlaceMatch: boolean;
}
......@@ -59,6 +71,11 @@ export class Tournament extends DescBase {
})
rule: TournamentRule;
getRuleHandler(repo?: Repository<Match>) {
const ruleClass = TournamentRules.get(this.rule);
return new ruleClass(this, repo);
}
@NotChangeable()
@Column('jsonb', { comment: '规则设定', default: {} })
@ApiProperty({ type: PartialType(RuleSettings), required: false })
......@@ -88,9 +105,10 @@ export class Tournament extends DescBase {
@Index()
@IsArray()
@IsOptional()
@IsPositive({ each: true })
@IsInt({ each: true })
@ApiProperty({ type: [Number], description: '协作者 MC ID' })
@ApiProperty({ type: [Number], description: '协作者 MC ID', required: false })
@Column('int', { array: true, default: [], comment: '协作者 MC ID' })
collaborators: number[];
......@@ -107,6 +125,10 @@ export class Tournament extends DescBase {
@OneToMany(() => Match, (match) => match.tournament)
matches: Match[];
@NotColumn()
@ApiProperty({ description: '对阵图树' })
matchTree: Match;
async beforeCreate() {
this.createdAt = new Date();
}
......@@ -154,4 +176,38 @@ export class Tournament extends DescBase {
}
return this;
}
calculateScore() {
const rule = this.getRuleHandler();
this.participants.forEach((p) => {
p.score = new ParticipantScore();
Object.assign(p.score, rule.participantScore(p));
});
_.sortBy(
this.participants,
(p) => -p.score.score,
(p) => -p.score.tieBreaker,
);
this.participants.forEach((p, i) => {
p.score.rank = i + 1;
});
}
calculateTree() {
const finalMatch = _.maxBy(
this.matches.filter((m) => !m.isThirdPlaceMatch),
(m) => m.round,
);
this.matchTree = finalMatch.clone().buildTree(this.matches);
}
analytics() {
if (!this.participants || this.status === TournamentStatus.Ready) {
return;
}
this.calculateScore();
if (this.rule === TournamentRule.SingleElimination) {
this.calculateTree();
}
}
}
import { Controller } from '@nestjs/common';
import { Controller, Post } from '@nestjs/common';
import { TournamentService } from './tournament.service';
import { RestfulFactory } from 'nicot';
import { BlankReturnMessageDto, RestfulFactory } from 'nicot';
import { Tournament } from './entities/Tournament.entity';
import { MycardUser, PutMycardUser } from 'nestjs-mycard';
import { ApiHeader, ApiTags } from '@nestjs/swagger';
import {
ApiCreatedResponse,
ApiHeader,
ApiOperation,
ApiParam,
ApiTags,
} from '@nestjs/swagger';
const factory = new RestfulFactory(Tournament);
class FindTournamentDto extends factory.findAllDto {}
......@@ -55,4 +61,37 @@ export class TournamentController {
) {
return this.tournamentService.deleteTournament(id, user);
}
@Post(':id/start')
@ApiOperation({ summary: 'Start a tournament' })
@ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiCreatedResponse({ type: BlankReturnMessageDto })
async start(
@factory.idParam() id: number,
@PutMycardUser() user: MycardUser,
) {
return this.tournamentService.startTournament(id, user);
}
@Post(':id/reset')
@ApiOperation({ summary: 'Reset a tournament' })
@ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiCreatedResponse({ type: BlankReturnMessageDto })
async reset(
@factory.idParam() id: number,
@PutMycardUser() user: MycardUser,
) {
return this.tournamentService.resetTournament(id, user);
}
@Post(':id/finish')
@ApiOperation({ summary: 'Finish a tournament' })
@ApiParam({ name: 'id', description: 'Tournament ID' })
@ApiCreatedResponse({ type: BlankReturnMessageDto })
async finish(
@factory.idParam() id: number,
@PutMycardUser() user: MycardUser,
) {
return this.tournamentService.endTournament(id, user);
}
}
......@@ -3,8 +3,8 @@ import { BlankReturnMessageDto, CrudService } from 'nicot';
import { Tournament, TournamentStatus } from './entities/Tournament.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { MycardUser } from 'nestjs-mycard';
import { Match } from '../match/entities/match.entity';
import { Repository } from 'typeorm';
import { Match, MatchStatus } from '../match/entities/match.entity';
import { In, Repository } from 'typeorm';
@Injectable()
export class TournamentService extends CrudService(Tournament, {
......@@ -23,10 +23,12 @@ export class TournamentService extends CrudService(Tournament, {
super(repo);
}
getTournament(id: number, user: MycardUser) {
return this.findOne(id, (qb) =>
async getTournament(id: number, user: MycardUser) {
const result = await this.findOne(id, (qb) =>
Tournament.extraQueryForUser(user, qb, this.entityAliasName),
);
result.data?.analytics();
return result;
}
getTournaments(dto: Partial<Tournament>, user: MycardUser) {
......@@ -58,10 +60,12 @@ export class TournamentService extends CrudService(Tournament, {
id: number,
user: MycardUser,
extraGets: (keyof Tournament)[] = [],
relations: string[] = [],
) {
const tournament = await this.repo.findOne({
where: { id },
select: ['id', 'creator', 'collaborators', ...extraGets],
relations,
});
if (!tournament) {
throw new BlankReturnMessageDto(404, '未找到该比赛。').toException();
......@@ -96,17 +100,22 @@ export class TournamentService extends CrudService(Tournament, {
}
async startTournament(id: number, user: MycardUser) {
const tournament = await this.checkPermissionOfTournament(id, user, [
'status',
]);
const tournament = await this.checkPermissionOfTournament(
id,
user,
['status', 'rule', 'ruleSettings'],
['participants'],
);
if (tournament.status !== TournamentStatus.Ready) {
throw new BlankReturnMessageDto(
403,
'比赛已经开始,不能重复开始。',
).toException();
}
await this.repo.update(id, { status: TournamentStatus.Running });
// TODO: generate matches
await this.repo.manager.transaction(async (edb) => {
await tournament.getRuleHandler(edb.getRepository(Match)).initialize();
await edb.update(Tournament, id, { status: TournamentStatus.Running });
});
return new BlankReturnMessageDto(200, 'success');
}
......@@ -120,8 +129,53 @@ export class TournamentService extends CrudService(Tournament, {
'比赛还未开始,不能结束。',
).toException();
}
if (
await this.matchRepo.exist({
where: {
tournamentId: id,
status: In([MatchStatus.Running, MatchStatus.Pending]),
},
take: 1,
})
) {
throw new BlankReturnMessageDto(
403,
'比赛还有未完成的对局,不能结束。',
).toException();
}
await this.repo.update(id, { status: TournamentStatus.Finished });
// TODO: calculate results
return new BlankReturnMessageDto(200, 'success');
}
async afterMatchUpdate(id: number) {
if (
!(await this.matchRepo.exist({
where: { tournamentId: id, status: MatchStatus.Running },
take: 1,
})) &&
(await this.matchRepo.exist({
where: { tournamentId: id, status: MatchStatus.Pending },
take: 1,
}))
) {
const tournament = await this.repo.findOne({
where: { id },
select: ['id', 'status', 'rule', 'ruleSettings'],
relations: ['participants', 'matches'],
});
await this.repo.manager.transaction(async (edb) => {
const repo = edb.getRepository(Match);
const updates = tournament.getRuleHandler().nextRound();
await Promise.all(
updates.map((update) =>
repo.update(update.id, {
player1Id: update.player1Id,
player2Id: update.player2Id,
status: update.status,
}),
),
);
});
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment