Additional assorted layering cleanups

This commit is contained in:
Tau
2019-11-06 17:29:07 -05:00
parent 6a4ac795b2
commit 82bd9ad7bb
22 changed files with 14 additions and 15 deletions
+50
View File
@@ -0,0 +1,50 @@
import sql from "sql-bricks-postgres";
import { BackgroundCode } from "../model/base";
import { Profile } from "../model/profile";
import { FlagRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlBackgroundsRepository
implements FlagRepository<BackgroundCode> {
constructor(private readonly _txn: Transaction) {}
async loadAll(id: Id<Profile>): Promise<Set<BackgroundCode>> {
const loadSql = sql
.select("bg.background_no")
.from("idz_background_unlock bg")
.join("idz_profile p", { "bg.profile_id": "p.id" })
.where("p.id", id);
const rows = await this._txn.fetchRows(loadSql);
const result = new Set<BackgroundCode>();
for (const row of rows) {
result.add(parseInt(row.background_no) as BackgroundCode);
}
return result;
}
async saveAll(
profileId: Id<Profile>,
flags: Set<BackgroundCode>
): Promise<void> {
const existing = await this.loadAll(profileId);
for (const flag of flags) {
if (existing.has(flag)) {
continue;
}
const saveSql = sql.insert("idz_background_unlock", {
id: this._txn.generateId(),
profile_id: profileId,
background_no: flag,
});
await this._txn.modify(saveSql);
}
}
}
+138
View File
@@ -0,0 +1,138 @@
import sql from "sql-bricks-postgres";
import { Car, CarSelector } from "../model/car";
import { Profile } from "../model/profile";
import { CarRepository } from "../repo";
import { Id } from "../../model";
import { Row, Transaction } from "../../sql";
function _extractRow(row: Row): Car {
return {
selector: parseInt(row.selector) as CarSelector,
field_00: parseInt(row.field_00),
field_02: parseInt(row.field_02),
field_04: row.field_04.split(",").map((x: string) => parseInt(x)),
field_46: parseInt(row.field_46),
field_48: parseInt(row.field_48),
field_4a: parseInt(row.field_4a),
field_4c: parseInt(row.field_4c),
field_50_lo: parseInt(row.field_50_lo),
field_50_hi: parseInt(row.field_50_hi),
field_58: parseInt(row.field_58),
field_5a: parseInt(row.field_5a),
field_5b: parseInt(row.field_5b),
field_5c: parseInt(row.field_5c),
field_5e: parseInt(row.field_5e),
};
}
export class SqlCarRepository implements CarRepository {
constructor(private readonly _txn: Transaction) {}
async countCars(profileId: Id<Profile>): Promise<number> {
const countSql = sql
.select("count(*) result")
.from("idz_car c")
.where("c.profile_id", profileId);
const row = await this._txn.fetchRow(countSql);
return parseInt(row!.result);
}
async loadAllCars(profileId: Id<Profile>): Promise<Car[]> {
const loadSql = sql
.select("c.*")
.from("idz_car c")
.where("c.profile_id", profileId);
const rows = await this._txn.fetchRows(loadSql);
return rows.map(_extractRow);
}
async loadSelectedCar(profileId: Id<Profile>): Promise<Car> {
const loadSql = sql
.select("c.*")
.from("idz_car c")
.join("idz_car_selection s", { "c.id": "s.car_id" })
.where("s.id", profileId);
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error(`Car selection not found, profileId=${profileId}`);
}
return _extractRow(row);
}
async saveCar(profileId: Id<Profile>, car: Car): Promise<void> {
const saveSql = sql
.insert("idz_car", {
id: this._txn.generateId(),
profile_id: profileId,
selector: car.selector,
field_00: car.field_00,
field_02: car.field_02,
field_04: car.field_04.join(","),
field_46: car.field_46,
field_48: car.field_48,
field_4a: car.field_4a,
field_4c: car.field_4c,
field_50_lo: car.field_50_lo,
field_50_hi: car.field_50_hi,
field_58: car.field_58,
field_5a: car.field_5a,
field_5b: car.field_5b,
field_5c: car.field_5c,
field_5e: car.field_5e,
})
.onConflict("profile_id", "selector")
.doUpdate([
"field_00",
"field_02",
"field_04",
"field_46",
"field_48",
"field_4a",
"field_4c",
"field_50_lo",
"field_50_hi",
"field_58",
"field_5a",
"field_5b",
"field_5c",
"field_5e",
]);
await this._txn.modify(saveSql);
}
async saveSelection(
profileId: Id<Profile>,
selector: CarSelector
): Promise<void> {
const findSql = sql
.select("c.id")
.from("idz_car c")
.where("c.profile_id", profileId)
.where("c.selector", selector);
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
throw new Error("Selected car not found");
}
const saveSql = sql
.insert("idz_car_selection", {
id: profileId,
car_id: row.id,
})
.onConflict("id")
.doUpdate(["car_id"]);
await this._txn.modify(saveSql);
}
}
+73
View File
@@ -0,0 +1,73 @@
import sql from "sql-bricks-postgres";
import { BackgroundCode, TitleCode } from "../model/base";
import { Chara, Gender } from "../model/chara";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { Id } from "../../model";
import { Row, Transaction } from "../../sql";
export function _extractChara(row: Row): Chara {
return {
gender: row.gender as Gender,
field_02: parseInt(row.field_02),
field_04: parseInt(row.field_04),
field_06: parseInt(row.field_06),
field_08: parseInt(row.field_08),
field_0a: parseInt(row.field_0a),
field_0c: parseInt(row.field_0c),
field_0e: parseInt(row.field_0e),
title: parseInt(row.title) as TitleCode,
background: parseInt(row.background) as BackgroundCode,
};
}
export class SqlCharaRepository implements FacetRepository<Chara> {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<Profile>): Promise<Chara> {
const loadSql = sql
.select("c.*")
.from("idz_chara c")
.where("c.id", profileId);
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error(`Chara not found: profileId=${profileId}`);
}
return _extractChara(row);
}
async save(profileId: Id<Profile>, chara: Chara): Promise<void> {
const saveSql = sql
.insert("idz_chara", {
id: profileId,
gender: chara.gender,
field_02: chara.field_02,
field_04: chara.field_04,
field_06: chara.field_06,
field_08: chara.field_08,
field_0a: chara.field_0a,
field_0c: chara.field_0c,
field_0e: chara.field_0e,
title: chara.title,
background: chara.background,
})
.onConflict("id")
.doUpdate([
"field_02",
"field_04",
"field_06",
"field_08",
"field_0a",
"field_0c",
"field_0e",
"title",
"background",
]);
await this._txn.modify(saveSql);
}
}
+49
View File
@@ -0,0 +1,49 @@
import sql from "sql-bricks-postgres";
import { CourseNo } from "../model/base";
import { Profile } from "../model/profile";
import { CoursePlaysRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlCoursePlaysRepository implements CoursePlaysRepository {
constructor(private readonly _txn: Transaction) {}
async loadAll(profileId: Id<Profile>): Promise<Map<CourseNo, number>> {
const loadSql = sql
.select("cp.course_no", "cp.count")
.from("idz_course_plays cp")
.where("cp.profile_id", profileId);
const rows = await this._txn.fetchRows(loadSql);
const result = new Map<CourseNo, number>();
for (const row of rows) {
const courseNo = parseInt(row.course_no) as CourseNo;
const count = parseInt(row.count);
result.set(courseNo, count);
}
return result;
}
async saveAll(
profileId: Id<Profile>,
plays: Map<CourseNo, number>
): Promise<void> {
for (const [k, v] of plays) {
const saveSql = sql
.insert("idz_course_plays", {
id: this._txn.generateId(),
profile_id: profileId,
course_no: k,
count: v,
})
.onConflict("profile_id", "course_no")
.doUpdate(["count"]);
await this._txn.modify(saveSql);
}
}
}
+87
View File
@@ -0,0 +1,87 @@
import { SqlBackgroundsRepository } from "./backgrounds";
import { SqlCarRepository } from "./car";
import { SqlCharaRepository } from "./chara";
import { SqlCoursePlaysRepository } from "./coursePlays";
import { SqlMissionsRepository } from "./missions";
import { SqlProfileRepository } from "./profile";
import { SqlSettingsRepository } from "./settings";
import { SqlStoryRepository } from "./story";
import { SqlTeamRepository } from "./team";
import { SqlTeamAutoRepository } from "./teamAuto";
import { SqlTeamMemberRepository } from "./teamMember";
import { SqlTeamReservationRepository } from "./teamReservation";
import { SqlTicketsRepository } from "./tickets";
import { SqlTimeAttackRepository } from "./timeAttack";
import { SqlTitlesRepository } from "./titles";
import { SqlUnlocksRepository } from "./unlocks";
import * as Model from "../model";
import * as Repo from "../repo";
import { Transaction } from "../../sql";
export class SqlRepositories implements Repo.Repositories {
constructor(private readonly _txn: Transaction) {}
backgrounds(): Repo.FlagRepository<Model.BackgroundCode> {
return new SqlBackgroundsRepository(this._txn);
}
car(): Repo.CarRepository {
return new SqlCarRepository(this._txn);
}
chara(): Repo.FacetRepository<Model.Chara> {
return new SqlCharaRepository(this._txn);
}
coursePlays(): Repo.CoursePlaysRepository {
return new SqlCoursePlaysRepository(this._txn);
}
missions(): Repo.FacetRepository<Model.MissionState> {
return new SqlMissionsRepository(this._txn);
}
profile(): Repo.ProfileRepository {
return new SqlProfileRepository(this._txn);
}
settings(): Repo.FacetRepository<Model.Settings> {
return new SqlSettingsRepository(this._txn);
}
story(): Repo.FacetRepository<Model.Story> {
return new SqlStoryRepository(this._txn);
}
teams(): Repo.TeamRepository {
return new SqlTeamRepository(this._txn);
}
teamAuto(): Repo.TeamAutoRepository {
return new SqlTeamAutoRepository(this._txn);
}
teamMembers(): Repo.TeamMemberRepository {
return new SqlTeamMemberRepository(this._txn);
}
teamReservations(): Repo.TeamReservationRepository {
return new SqlTeamReservationRepository(this._txn);
}
tickets(): Repo.FacetRepository<Model.Tickets> {
return new SqlTicketsRepository(this._txn);
}
timeAttack(): Repo.TimeAttackRepository {
return new SqlTimeAttackRepository(this._txn);
}
titles(): Repo.FlagRepository<Model.TitleCode> {
return new SqlTitlesRepository(this._txn);
}
unlocks(): Repo.FacetRepository<Model.Unlocks> {
return new SqlUnlocksRepository(this._txn);
}
}
+76
View File
@@ -0,0 +1,76 @@
import sql from "sql-bricks-postgres";
import { MissionGrid, MissionState } from "../model/mission";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlMissionsRepository implements FacetRepository<MissionState> {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<Profile>): Promise<MissionState> {
const result: MissionState = {
solo: new Array<MissionGrid>(),
team: new Array<MissionGrid>(),
};
for (let i = 0; i < 5; i++) {
const soloGrid: MissionGrid = { cells: new Array<number>() };
const teamGrid: MissionGrid = { cells: new Array<number>() };
for (let j = 0; j < 9; j++) {
soloGrid.cells.push(0);
teamGrid.cells.push(0);
}
result.solo.push(soloGrid);
result.team.push(teamGrid);
}
const loadSoloSql = sql
.select("sm.*")
.from("idz_solo_mission_state sm")
.where("sm.profile_id", profileId);
const rows = await this._txn.fetchRows(loadSoloSql);
for (const row of rows) {
const gridNo = parseInt(row.grid_no);
const cellNo = parseInt(row.cell_no);
const value = parseInt(row.value);
result.solo[gridNo].cells[cellNo] = value;
}
return result;
}
async save(profileId: Id<Profile>, mission: MissionState): Promise<void> {
const existing = await this.load(profileId);
for (let i = 0; i < mission.solo.length; i++) {
const exGrid = existing.solo[i].cells;
const grid = mission.solo[i].cells;
for (let j = 0; j < grid.length; j++) {
if (grid[j] === exGrid[j]) {
continue; // Most if not all cells are unaffected on save
}
const saveSql = sql
.insert("idz_solo_mission_state", {
id: this._txn.generateId(),
profile_id: profileId,
grid_no: i,
cell_no: j,
value: grid[j],
})
.onConflict("profile_id", "grid_no", "cell_no")
.doUpdate(["value"]);
await this._txn.modify(saveSql);
}
}
}
}
+114
View File
@@ -0,0 +1,114 @@
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { ProfileRepository } from "../repo";
import { AimeId, Id } from "../../model";
import { Row, Transaction } from "../../sql";
export function _extractProfile(row: Row): Profile {
return {
aimeId: parseInt(row.aime_id) as AimeId,
name: row.name,
lv: parseInt(row.lv),
exp: parseInt(row.exp),
fame: parseInt(row.fame),
dpoint: parseInt(row.dpoint),
mileage: parseInt(row.mileage),
accessTime: new Date(row.access_time),
registerTime: new Date(row.register_time),
};
}
export class SqlProfileRepository implements ProfileRepository {
constructor(private readonly _txn: Transaction) {}
async find(aimeId: AimeId): Promise<Id<Profile>> {
const profileId = await this.peek(aimeId);
if (profileId === undefined) {
throw new Error(`Profile not found for Aime ID ${aimeId}`);
}
return profileId;
}
async peek(aimeId: AimeId): Promise<Id<Profile> | undefined> {
const lookupSql = sql
.select("p.id")
.from("idz_profile p")
.join("aime_player r", { "p.player_id": "r.id" })
.where("r.ext_id", aimeId);
const row = await this._txn.fetchRow(lookupSql);
if (row === undefined) {
return undefined;
}
return row.id as Id<Profile>;
}
async load(id: Id<Profile>): Promise<Profile> {
const loadSql = sql
.select("p.*", "r.ext_id as aime_id")
.from("idz_profile p")
.join("aime_player r", { "p.player_id": "r.id" })
.where("p.id", id);
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error(`Profile not found, id=${id}`);
}
return _extractProfile(row);
}
async save(id: Id<Profile>, profile: Profile): Promise<void> {
const saveSql = sql
.update("idz_profile", {
lv: profile.lv,
exp: profile.exp,
fame: profile.fame,
dpoint: profile.dpoint,
mileage: profile.mileage,
access_time: profile.accessTime.toISOString(),
})
.where("id", id);
await this._txn.modify(saveSql);
}
async create(profile: Profile): Promise<Id<Profile>> {
const findSql = sql
.select("r.id")
.from("aime_player r")
.where("r.ext_id", profile.aimeId);
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
throw new Error("Aime ID not found");
}
const id = this._txn.generateId<Profile>();
const playerId = row.id;
const createSql = sql.insert("idz_profile", {
id: id,
player_id: playerId,
name: profile.name,
lv: profile.lv,
exp: profile.exp,
fame: profile.fame,
dpoint: profile.dpoint,
mileage: profile.mileage,
register_time: profile.registerTime.toISOString(),
access_time: profile.accessTime.toISOString(),
});
await this._txn.modify(createSql);
return id;
}
}
+46
View File
@@ -0,0 +1,46 @@
import sql from "sql-bricks-postgres";
import { Settings } from "../model/settings";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlSettingsRepository implements FacetRepository<Settings> {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<Profile>): Promise<Settings> {
const loadSql = sql
.select("s.*")
.from("idz_settings s")
.where("s.id", profileId);
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error(`Settings not found, profileId=${profileId}`);
}
return {
music: parseInt(row.music),
pack: parseInt(row.pack),
paperCup: parseInt(row.paper_cup),
gauges: parseInt(row.gauges),
};
}
async save(profileId: Id<Profile>, settings: Settings): Promise<void> {
const saveSql = sql
.insert("idz_settings", {
id: profileId,
music: settings.music,
pack: settings.pack,
paper_cup: settings.paperCup,
gauges: settings.gauges,
})
.onConflict("id")
.doUpdate(["music", "pack", "paper_cup", "gauges"]);
await this._txn.modify(saveSql);
}
}
+99
View File
@@ -0,0 +1,99 @@
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { Story, StoryRow, StoryCell } from "../model/story";
import { FacetRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlStoryRepository implements FacetRepository<Story> {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<Profile>): Promise<Story> {
const loadSql = sql
.select("s.*")
.from("idz_story_state s")
.where("s.id", profileId);
const header = await this._txn.fetchRow(loadSql);
// Must succeed even if nonexistent (required by save method below)
const result = {
x: header !== undefined ? parseInt(header.x) : 0,
y: header !== undefined ? parseInt(header.y) : 0,
rows: new Array<StoryRow>(),
};
for (let i = 0; i < 27; i++) {
const row: StoryRow = { cells: new Array<StoryCell>() };
for (let j = 0; j < 9; j++) {
row.cells.push({ a: 0, b: 0 });
}
result.rows.push(row);
}
const loadCellSql = sql
.select("sc.*")
.from("idz_story_cell_state sc")
.where("sc.profile_id", profileId);
const rows = await this._txn.fetchRows(loadCellSql);
for (const row of rows) {
const rowNo = parseInt(row.row_no);
const colNo = parseInt(row.col_no);
const cell = result.rows[rowNo].cells[colNo];
cell.a = parseInt(row.a);
cell.b = parseInt(row.b);
}
return result;
}
async save(profileId: Id<Profile>, story: Story): Promise<void> {
const existing = await this.load(profileId);
const headSql = sql
.insert("idz_story_state", {
id: profileId,
x: story.x,
y: story.y,
})
.onConflict("id")
.doUpdate(["x", "y"]);
await this._txn.modify(headSql);
for (let i = 0; i < story.rows.length; i++) {
const exRow = existing.rows[i];
const row = story.rows[i];
for (let j = 0; j < row.cells.length; j++) {
const exCell = exRow.cells[j];
const cell = row.cells[j];
if (cell.a === exCell.a && cell.b === exCell.b) {
continue; // Most if not all cells are unchanged on profile save.
}
const cellSql = sql
.insert("idz_story_cell_state", {
id: this._txn.generateId(),
profile_id: profileId,
row_no: i,
col_no: j,
a: cell.a,
b: cell.b,
})
.onConflict("profile_id", "row_no", "col_no")
.doUpdate(["a", "b"]);
await this._txn.modify(cellSql);
}
}
}
}
+82
View File
@@ -0,0 +1,82 @@
import sql from "sql-bricks-postgres";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { TeamSpec, TeamRepository } from "../repo";
import { Id, generateExtId } from "../../model";
import { Transaction } from "../../sql";
export class SqlTeamRepository implements TeamRepository {
constructor(private readonly _txn: Transaction) {}
async find(extId: ExtId<Team>): Promise<Id<Team>> {
const findSql = sql
.select("t.id")
.from("idz_team t")
.where("t.ext_id", extId);
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
throw new Error(`Team not found for ExtID ${extId}`);
}
return row.id as Id<Team>;
}
async load(id: Id<Team>): Promise<Team> {
const loadSql = sql
.select("t.*")
.from("idz_team t")
.where("t.id", id);
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error("Team not found");
}
return {
extId: parseInt(row.ext_id) as ExtId<Team>,
name: row.name,
nameBg: parseInt(row.name_bg),
nameFx: parseInt(row.name_fx),
registerTime: new Date(row.register_time),
};
}
async save(id: Id<Team>, team: Team): Promise<void> {
const saveSql = sql
.update("idz_team", {
name_bg: team.nameBg,
name_fx: team.nameFx,
})
.where("id", id);
await this._txn.modify(saveSql);
}
async create(team: TeamSpec): Promise<[Id<Team>, ExtId<Team>]> {
const id = this._txn.generateId<Team>();
const extId = generateExtId() as ExtId<Team>;
const createSql = sql.insert("idz_team", {
id: id,
ext_id: extId,
name: team.name,
name_bg: team.nameBg,
name_fx: team.nameFx,
register_time: team.registerTime,
});
await this._txn.modify(createSql);
return [id, extId];
}
async delete(id: Id<Team>): Promise<void> {
const deleteSql = sql.delete("idz_team").where("id", id);
await this._txn.modify(deleteSql);
}
}
+40
View File
@@ -0,0 +1,40 @@
import sql from "sql-bricks-postgres";
import { Team, TeamAuto } from "../model/team";
import { TeamAutoRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlTeamAutoRepository implements TeamAutoRepository {
constructor(private readonly _txn: Transaction) {}
async peek(): Promise<[TeamAuto, Id<Team>] | undefined> {
const peekSql = sql
.select("tt.*")
.from("idz_team_auto tt")
.orderBy("serial_no desc", "name_idx desc")
.limit(1);
const row = await this._txn.fetchRow(peekSql);
return (
row && [
{
serialNo: parseInt(row.serial_no),
nameIdx: parseInt(row.name_idx),
},
row.id as Id<Team>,
]
);
}
async push(teamId: Id<Team>, auto: TeamAuto): Promise<void> {
const pushSql = sql.insert("idz_team_auto", {
id: teamId,
serial_no: auto.serialNo,
name_idx: auto.nameIdx,
});
await this._txn.modify(pushSql);
}
}
+141
View File
@@ -0,0 +1,141 @@
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { Team, TeamMember } from "../model/team";
import { TeamMemberRepository } from "../repo";
import { _extractProfile } from "./profile";
import { _extractChara } from "./chara";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlTeamMemberRepository implements TeamMemberRepository {
constructor(private readonly _txn: Transaction) {}
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);
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
return undefined;
}
return row.team_id as Id<Team>;
}
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);
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
return undefined;
}
return row.id as Id<Profile>;
}
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);
const rows = await this._txn.fetchRows(loadSql);
return rows.map(row => ({
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.
const lockSql = sql
.select("id")
.from("idz_team")
.where("id", teamId)
.forUpdate();
await this._txn.modify(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);
const row = await this._txn.fetchRow(countSql);
if (parseInt(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"]);
await this._txn.modify(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);
await this._txn.modify(leaveSql);
}
async makeLeader(teamId: Id<Team>, profileId: Id<Profile>): Promise<void> {
const clearSql = sql
.update("idz_team_member", {
leader: false,
})
.where("team_id", teamId);
await this._txn.modify(clearSql);
const setSql = sql
.update("idz_team_member", {
leader: true,
})
.where("id", profileId)
.where("team_id", teamId);
await this._txn.modify(setSql);
}
}
+103
View File
@@ -0,0 +1,103 @@
import sql from "sql-bricks-postgres";
import { Team } from "../model/team";
import { TeamReservationRepository } from "../repo";
import { AimeId, Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlTeamReservationRepository
implements TeamReservationRepository {
constructor(private readonly _txn: Transaction) {}
private async _lockTeam(teamId: Id<Team>): Promise<void> {
const lockSql = sql
.select("t.id")
.from("idz_team t")
.where("t.id", teamId)
.forUpdate();
await this._txn.modify(lockSql);
}
async occupancyHack(teamId: Id<Team>): Promise<number> {
await this._lockTeam(teamId);
const memberSql = sql
.select("count(*) as count")
.from("idz_team_member tm")
.where("tm.team_id", teamId);
const memberRes = await this._txn.fetchRow(memberSql);
const memberCount = parseInt(memberRes!.count);
const reservSql = sql
.select("count(*) as count")
.from("idz_team_reservation tr")
.where("tr.team_id", teamId);
const reservRes = await this._txn.fetchRow(reservSql);
const reservCount = parseInt(reservRes!.count);
return memberCount + reservCount;
}
async reserveHack(
teamId: Id<Team>,
aimeId: AimeId,
timestamp: Date,
leader?: "leader"
): Promise<void> {
const lookupSql = sql
.select("r.id")
.from("aime_player r")
.where("r.ext_id", aimeId);
const row = await this._txn.fetchRow(lookupSql);
if (row === undefined) {
throw new Error(`Unknown Aime ID ${aimeId}`);
}
const playerId = row.id;
const insertSql = sql
.insert("idz_team_reservation", {
id: playerId,
team_id: teamId,
join_time: timestamp,
leader: leader === "leader",
})
.onConflict("id")
.doUpdate(["team_id"]);
await this._txn.modify(insertSql);
}
async commitHack(aimeId: AimeId): Promise<void> {
const lookupSql = sql
.select("p.id as profile_id", "tr.*")
.from("idz_profile p")
.join("aime_player r", { "p.player_id": "r.id" })
.join("idz_team_reservation tr", { "r.id": "tr.id" })
.where("r.ext_id", aimeId);
const row = await this._txn.fetchRow(lookupSql);
if (row === undefined) {
throw new Error(`Reservation not found for Aime ID ${aimeId}`);
}
const insertSql = sql.insert("idz_team_member", {
id: row.profile_id,
team_id: row.team_id,
join_time: row.join_time,
leader: row.leader,
});
await this._txn.modify(insertSql);
const cleanupSql = sql.delete("idz_team_reservation").where("id", row.id);
await this._txn.modify(cleanupSql);
}
}
+48
View File
@@ -0,0 +1,48 @@
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { Tickets } from "../model/tickets";
import { FacetRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
// TODO free continue
export class SqlTicketsRepository implements FacetRepository<Tickets> {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<Profile>): Promise<Tickets> {
const loadSql = sql
.select("fc.*")
.from("idz_free_car fc")
.where("fc.id", profileId);
const row = await this._txn.fetchRow(loadSql);
return {
freeCar: row && {
validFrom: new Date(row.valid_from),
},
};
}
async save(profileId: Id<Profile>, tickets: Tickets): Promise<void> {
const { freeCar } = tickets;
if (!freeCar) {
const delSql = sql.delete("idz_free_car").where("id", profileId);
await this._txn.modify(delSql);
} else {
const saveSql = sql
.insert("idz_free_car", {
id: profileId,
valid_from: freeCar.validFrom,
})
.onConflict("id")
.doUpdate(["valid_from"]);
await this._txn.modify(saveSql);
}
}
}
+113
View File
@@ -0,0 +1,113 @@
import sql from "sql-bricks-postgres";
import { RouteNo } from "../model/base";
import { CarSelector } from "../model/car";
import { Profile } from "../model/profile";
import { TimeAttackScore } from "../model/timeAttack";
import { TimeAttackRepository, TopTenResult } from "../repo";
import { Id } from "../../model";
import { Row, Transaction } from "../../sql";
function _extractRow(row: Row): TimeAttackScore {
return {
routeNo: parseInt(row.route_no) as RouteNo,
timestamp: new Date(row.timestamp),
flags: parseInt(row.flags),
totalTime: parseFloat(row.total_time),
sectionTimes: row.section_times.split(",").map(parseFloat),
grade: parseInt(row.grade),
carSelector: parseInt(row.car_selector) as CarSelector,
};
}
export class SqlTimeAttackRepository implements TimeAttackRepository {
constructor(private readonly _txn: Transaction) {}
async loadTopTen(
routeNo: RouteNo,
minTimestamp: Date
): Promise<TopTenResult[]> {
const loadSql = sql
.select("p.name", "ta.*")
.from("idz_ta_best ta")
.join("idz_profile p", { "ta.profile_id": "p.id" })
.where("ta.route_no", routeNo)
.where(sql.gt("ta.timestamp", minTimestamp))
.orderBy(["ta.total_time asc", "ta.timestamp asc"])
.limit(10);
const rows = await this._txn.fetchRows(loadSql);
return rows.map(row => ({
driverName: row.name,
ta: _extractRow(row),
}));
}
async loadAll(profileId: Id<Profile>): Promise<TimeAttackScore[]> {
const loadSql = sql
.select("ta.*")
.from("idz_ta_best ta")
.where("ta.profile_id", profileId);
const rows = await this._txn.fetchRows(loadSql);
return rows.map(_extractRow);
}
async save(profileId: Id<Profile>, score: TimeAttackScore): Promise<void> {
const logSql = sql.insert("idz_ta_result", {
id: this._txn.generateId(),
profile_id: profileId,
route_no: score.routeNo,
total_time: score.totalTime,
section_times: score.sectionTimes.join(","),
flags: score.flags,
grade: score.grade,
car_selector: score.carSelector,
timestamp: score.timestamp,
});
await this._txn.modify(logSql);
const existSql = sql
.select("ta.total_time")
.from("idz_ta_best ta")
.where("ta.profile_id", profileId)
.where("ta.route_no", score.routeNo);
const row = await this._txn.fetchRow(existSql);
if (row === undefined) {
const insertSql = sql.insert("idz_ta_best", {
id: this._txn.generateId(),
profile_id: profileId,
route_no: score.routeNo,
total_time: score.totalTime,
section_times: score.sectionTimes.join(","),
flags: score.flags,
grade: score.grade,
car_selector: score.carSelector,
timestamp: score.timestamp,
});
await this._txn.modify(insertSql);
} else {
if (score.totalTime < parseFloat(row.total_time)) {
const updateSql = sql
.update("idz_ta_best", {
total_time: score.totalTime,
section_times: score.sectionTimes.join(","),
flags: score.flags,
grade: score.grade,
car_selector: score.carSelector,
timestamp: score.timestamp,
})
.where("profile_id", profileId)
.where("route_no", score.routeNo);
await this._txn.modify(updateSql);
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
import sql from "sql-bricks-postgres";
import { TitleCode } from "../model/base";
import { Profile } from "../model/profile";
import { FlagRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlTitlesRepository implements FlagRepository<TitleCode> {
constructor(private readonly _txn: Transaction) {}
async loadAll(profileId: Id<Profile>): Promise<Set<TitleCode>> {
const loadSql = sql
.select("t.title_no")
.from("idz_title_unlock t")
.where("t.profile_id", profileId);
const rows = await this._txn.fetchRows(loadSql);
const result = new Set<TitleCode>();
for (const row of rows) {
result.add(parseInt(row.title_no) as TitleCode);
}
return result;
}
async saveAll(profileId: Id<Profile>, flags: Set<TitleCode>): Promise<void> {
const existing = await this.loadAll(profileId);
for (const flag of flags) {
if (existing.has(flag)) {
continue;
}
const saveSql = sql.insert("idz_title_unlock", {
id: this._txn.generateId(),
profile_id: profileId,
title_no: flag,
});
await this._txn.modify(saveSql);
}
}
}
+46
View File
@@ -0,0 +1,46 @@
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { Unlocks } from "../model/unlocks";
import { FacetRepository } from "../repo";
import { Id } from "../../model";
import { Transaction } from "../../sql";
export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<Profile>): Promise<Unlocks> {
const loadSql = sql
.select("u.*")
.from("idz_unlocks u")
.where("u.id", profileId);
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error(`Unlocks not found, profileId=${profileId}`);
}
return {
cup: parseInt(row.cup),
gauges: parseInt(row.gauges),
music: parseInt(row.music),
lastMileageReward: parseInt(row.last_mileage_reward),
};
}
async save(profileId: Id<Profile>, unlocks: Unlocks): Promise<void> {
const saveSql = sql
.insert("idz_unlocks", {
id: profileId,
cup: unlocks.cup,
gauges: unlocks.gauges,
music: unlocks.music,
last_mileage_reward: unlocks.lastMileageReward,
})
.onConflict("id")
.doUpdate(["cup", "gauges", "music", "last_mileage_reward"]);
await this._txn.modify(saveSql);
}
}