idz: Add TeamMemberRepository

This commit is contained in:
Tau 2019-05-26 20:08:22 -04:00
parent c6a67bd647
commit be48619a4a
3 changed files with 181 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import { SqlProfileRepository } from "./profile";
import { SqlSettingsRepository } from "./settings";
import { SqlStoryRepository } from "./story";
import { SqlTeamRepository } from "./team";
import { SqlTeamMemberRepository } from "./teamMember";
import { SqlTicketsRepository } from "./tickets";
import { SqlTimeAttackRepository } from "./timeAttack";
import { SqlTitlesRepository } from "./titles";
@ -56,6 +57,10 @@ class TransactionImpl implements Repo.Transaction {
return new SqlTeamRepository(this._conn);
}
teamMembers(): Repo.TeamMemberRepository {
return new SqlTeamMemberRepository(this._conn);
}
tickets(): Repo.FacetRepository<Model.Tickets> {
return new SqlTicketsRepository(this._conn);
}

153
src/idz/db/teamMember.ts Normal file
View File

@ -0,0 +1,153 @@
import * as sql from "sql-bricks-postgres";
import { ClientBase } from "pg";
import { Profile } from "../model/profile";
import { Team, TeamMember } from "../model/team";
import { TeamMemberRepository } from "../repo";
import { Id, generateId } from "../../db";
import { _extractProfile } from "./profile";
import { _extractChara } from "./chara";
export class SqlTeamMemberRepository implements TeamMemberRepository {
constructor(private readonly _conn: ClientBase) {}
async findTeam(profileId: Id<Profile>): Promise<Id<Team> | undefined> {
const findSql = sql
.select("tm.team_id")
.from("idz.team_member tm")
.where("tm.id", profileId)
.toParams();
const { rows } = await this._conn.query(findSql);
const row = rows[0];
if (row === undefined) {
return undefined;
}
return row.team_id;
}
async findLeader(teamId: Id<Team>): Promise<Id<Profile> | undefined> {
const findSql = sql
.select("tm.id")
.from("idz.team_member tm")
.where("tm.team_id", teamId)
.where("tm.leader", true)
.toParams();
const { rows } = await this._conn.query(findSql);
const row = rows[0];
if (row === undefined) {
return undefined;
}
return row.id;
}
async loadRoster(teamId: Id<Team>): Promise<TeamMember[]> {
const loadSql = sql
.select("tm.*", "p.*", "c.*", "r.ext_id as aime_id")
.from("idz.team_member tm")
.join("idz.profile p", { "tm.id": "p.id" })
.join("idz.chara c", { "tm.id": "c.id" })
.join("aime.player r", { "p.player_id": "r.id" })
.where("tm.team_id", teamId)
.toParams();
const { rows } = await this._conn.query(loadSql);
return rows.map((row: any) => ({
profile: _extractProfile(row),
chara: _extractChara(row),
leader: row.leader,
joinTime: new Date(row.join_time),
}));
}
async join(
teamId: Id<Team>,
profileId: Id<Profile>,
timestamp: Date
): Promise<void> {
// Lock the team record to avoid race conditions. This way
const lockSql = sql
.select("id")
.from("idz.team")
.where("id", teamId)
.forUpdate()
.toParams();
await this._conn.query(lockSql);
// Double-check (with lock held) that there is room to join this team.
// If this fails then the error will propagate to the client and it will
// retry, and, assuming we have a race between two new registrations to
// take up the last slot in the current auto-team, hopefully succeed.
//
// There is arguably some business logic pollution here, since we have a
// hard-coded maximum team size imposed by the protocol. This is why a
// three-layered server would be better than our two-layered server.
const countSql = sql
.select("count(*) as count")
.from("idz.team_member")
.where("team_id", teamId)
.toParams();
const { rows } = await this._conn.query(countSql);
const row = rows[0];
if (row.count >= 6) {
throw new Error(`Team ${teamId} is full`);
}
// Do upsert
const joinSql = sql
.insert("idz.team_member", {
id: profileId,
team_id: teamId,
leader: false,
join_time: timestamp,
})
.onConflict("id")
.doUpdate(["team_id", "leader", "join_time"])
.toParams();
await this._conn.query(joinSql);
}
async leave(teamId: Id<Team>, profileId: Id<Profile>): Promise<void> {
const leaveSql = sql
.delete("idz.team_member")
.where("team_id", teamId)
.where("id", profileId)
.toParams();
await this._conn.query(leaveSql);
}
async makeLeader(teamId: Id<Team>, profileId: Id<Profile>): Promise<void> {
const clearSql = sql
.update("idz.team_member", {
leader: false,
})
.where("team_id", teamId)
.toParams();
await this._conn.query(clearSql);
const setSql = sql
.update("idz.team_member", {
leader: true,
})
.where("id", profileId)
.where("team_id", teamId)
.toParams();
await this._conn.query(setSql);
}
}

View File

@ -69,6 +69,27 @@ export interface TeamRepository {
delete(id: Id<Model.Team>): Promise<void>;
}
export interface TeamMemberRepository {
findTeam(profileId: Id<Model.Profile>): Promise<Id<Model.Team> | undefined>;
findLeader(teamId: Id<Model.Team>): Promise<Id<Model.Profile> | undefined>;
loadRoster(id: Id<Model.Team>): Promise<Model.TeamMember[]>;
join(
teamId: Id<Model.Team>,
profileId: Id<Model.Profile>,
timestamp: Date
): Promise<void>;
leave(teamId: Id<Model.Team>, profileId: Id<Model.Profile>): Promise<void>;
makeLeader(
team: Id<Model.Team>,
profileId: Id<Model.Profile>
): Promise<void>;
}
// TODO extend and factorize
export interface TopTenResult {
driverName: string;
@ -110,6 +131,8 @@ export interface Repositories {
teams(): TeamRepository;
teamMembers(): TeamMemberRepository;
tickets(): FacetRepository<Model.Tickets>;
timeAttack(): TimeAttackRepository;