Add an SQL DB abstraction layer

Because what self-respecting Enterprise(R) project doesn't have
one of those?
This commit is contained in:
Tau
2019-11-06 17:28:30 -05:00
parent 2ccd9dc382
commit 3e6b4f4798
32 changed files with 440 additions and 541 deletions
+11 -15
View File
@@ -1,24 +1,22 @@
import { ClientBase } from "pg";
import sql from "sql-bricks";
import sql from "sql-bricks-postgres";
import { BackgroundCode } from "../model/base";
import { Profile } from "../model/profile";
import { FlagRepository } from "../repo";
import { generateId, Id } from "../../db";
import { Id, Transaction, generateId } from "../../sql";
export class SqlBackgroundsRepository
implements FlagRepository<BackgroundCode> {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("p.id", id);
const { rows } = await this._conn.query(loadSql);
const rows = await this._txn.fetchRows(loadSql);
const result = new Set<BackgroundCode>();
for (const row of rows) {
@@ -39,15 +37,13 @@ export class SqlBackgroundsRepository
continue;
}
const saveSql = sql
.insert("idz_background_unlock", {
id: generateId(),
profile_id: profileId,
background_no: flag,
})
.toParams();
const saveSql = sql.insert("idz_background_unlock", {
id: generateId(),
profile_id: profileId,
background_no: flag,
});
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
}
+16 -25
View File
@@ -1,10 +1,9 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { Car, CarSelector } from "../model/car";
import { Profile } from "../model/profile";
import { CarRepository } from "../repo";
import { generateId, Id } from "../../db";
import { Id, Transaction, generateId } from "../../sql";
function _extractRow(row: any): Car {
return {
@@ -27,29 +26,26 @@ function _extractRow(row: any): Car {
}
export class SqlCarRepository implements CarRepository {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("c.profile_id", profileId);
const { rows } = await this._conn.query(countSql);
const row = rows[0];
const row = await this._txn.fetchRow(countSql);
return parseInt(row.result, 10);
return parseInt(row!.result, 10);
}
async loadAllCars(profileId: Id<Profile>): Promise<Car[]> {
const loadSql = sql
.select("c.*")
.from("idz_car c")
.where("c.profile_id", profileId)
.toParams();
.where("c.profile_id", profileId);
const { rows } = await this._conn.query(loadSql);
const rows = await this._txn.fetchRows(loadSql);
return rows.map(_extractRow);
}
@@ -59,12 +55,11 @@ export class SqlCarRepository implements CarRepository {
.select("c.*")
.from("idz_car c")
.join("idz_car_selection s", { "c.id": "s.car_id" })
.where("s.id", profileId)
.toParams();
.where("s.id", profileId);
const { rows } = await this._conn.query(loadSql);
const row = await this._txn.fetchRow(loadSql);
return _extractRow(rows[0]);
return _extractRow(row);
}
async saveCar(profileId: Id<Profile>, car: Car): Promise<void> {
@@ -104,10 +99,9 @@ export class SqlCarRepository implements CarRepository {
"field_5b",
"field_5c",
"field_5e",
])
.toParams();
]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
async saveSelection(
@@ -118,11 +112,9 @@ export class SqlCarRepository implements CarRepository {
.select("c.id")
.from("idz_car c")
.where("c.profile_id", profileId)
.where("c.selector", selector)
.toParams();
.where("c.selector", selector);
const { rows } = await this._conn.query(findSql);
const row = rows[0];
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
throw new Error("Selected car not found");
@@ -134,9 +126,8 @@ export class SqlCarRepository implements CarRepository {
car_id: row.id,
})
.onConflict("id")
.doUpdate(["car_id"])
.toParams();
.doUpdate(["car_id"]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
+6 -10
View File
@@ -1,10 +1,9 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { Chara } from "../model/chara";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
import { Id, Transaction } from "../../sql";
export function _extractChara(row: any): Chara {
return {
@@ -22,17 +21,15 @@ export function _extractChara(row: any): Chara {
}
export class SqlCharaRepository implements FacetRepository<Chara> {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("c.id", profileId);
const { rows } = await this._conn.query(loadSql);
const row = rows[0];
const row = await this._txn.fetchRow(loadSql);
return _extractChara(row);
}
@@ -63,9 +60,8 @@ export class SqlCharaRepository implements FacetRepository<Chara> {
"field_0e",
"title",
"background",
])
.toParams();
]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
+7 -10
View File
@@ -1,22 +1,20 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { CourseNo, ExtId } from "../model/base";
import { CourseNo } from "../model/base";
import { Profile } from "../model/profile";
import { CoursePlaysRepository } from "../repo";
import { generateId, Id } from "../../db";
import { Id, Transaction, generateId } from "../../sql";
export class SqlCoursePlaysRepository implements CoursePlaysRepository {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("cp.profile_id", profileId);
const { rows } = await this._conn.query(loadSql);
const rows = await this._txn.fetchRows(loadSql);
const result = new Map();
for (const row of rows) {
@@ -39,10 +37,9 @@ export class SqlCoursePlaysRepository implements CoursePlaysRepository {
count: v,
})
.onConflict("profile_id", "course_no")
.doUpdate(["count"])
.toParams();
.doUpdate(["count"]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
}
+19 -39
View File
@@ -1,5 +1,3 @@
import { PoolClient } from "pg";
import { SqlBackgroundsRepository } from "./backgrounds";
import { SqlCarRepository } from "./car";
import { SqlCharaRepository } from "./chara";
@@ -18,90 +16,72 @@ import { SqlTitlesRepository } from "./titles";
import { SqlUnlocksRepository } from "./unlocks";
import * as Model from "../model";
import * as Repo from "../repo";
import { connect } from "../../db";
import { Transaction } from "../../sql";
class TransactionImpl implements Repo.Transaction {
constructor(private readonly _conn: PoolClient) {}
export class SqlRepositories implements Repo.Repositories {
constructor(private readonly _txn: Transaction) {}
backgrounds(): Repo.FlagRepository<Model.BackgroundCode> {
return new SqlBackgroundsRepository(this._conn);
return new SqlBackgroundsRepository(this._txn);
}
car(): Repo.CarRepository {
return new SqlCarRepository(this._conn);
return new SqlCarRepository(this._txn);
}
chara(): Repo.FacetRepository<Model.Chara> {
return new SqlCharaRepository(this._conn);
return new SqlCharaRepository(this._txn);
}
coursePlays(): Repo.CoursePlaysRepository {
return new SqlCoursePlaysRepository(this._conn);
return new SqlCoursePlaysRepository(this._txn);
}
missions(): Repo.FacetRepository<Model.MissionState> {
return new SqlMissionsRepository(this._conn);
return new SqlMissionsRepository(this._txn);
}
profile(): Repo.ProfileRepository {
return new SqlProfileRepository(this._conn);
return new SqlProfileRepository(this._txn);
}
settings(): Repo.FacetRepository<Model.Settings> {
return new SqlSettingsRepository(this._conn);
return new SqlSettingsRepository(this._txn);
}
story(): Repo.FacetRepository<Model.Story> {
return new SqlStoryRepository(this._conn);
return new SqlStoryRepository(this._txn);
}
teams(): Repo.TeamRepository {
return new SqlTeamRepository(this._conn);
return new SqlTeamRepository(this._txn);
}
teamAuto(): Repo.TeamAutoRepository {
return new SqlTeamAutoRepository(this._conn);
return new SqlTeamAutoRepository(this._txn);
}
teamMembers(): Repo.TeamMemberRepository {
return new SqlTeamMemberRepository(this._conn);
return new SqlTeamMemberRepository(this._txn);
}
teamReservations(): Repo.TeamReservationRepository {
return new SqlTeamReservationRepository(this._conn);
return new SqlTeamReservationRepository(this._txn);
}
tickets(): Repo.FacetRepository<Model.Tickets> {
return new SqlTicketsRepository(this._conn);
return new SqlTicketsRepository(this._txn);
}
timeAttack(): Repo.TimeAttackRepository {
return new SqlTimeAttackRepository(this._conn);
return new SqlTimeAttackRepository(this._txn);
}
titles(): Repo.FlagRepository<Model.TitleCode> {
return new SqlTitlesRepository(this._conn);
return new SqlTitlesRepository(this._txn);
}
unlocks(): Repo.FacetRepository<Model.Unlocks> {
return new SqlUnlocksRepository(this._conn);
}
async commit(): Promise<void> {
await this._conn.query("commit");
await this._conn.release();
}
async rollback(): Promise<void> {
await this._conn.query("rollback");
await this._conn.release();
return new SqlUnlocksRepository(this._txn);
}
}
export async function beginDbSession(): Promise<Repo.Transaction> {
const conn = await connect();
await conn.query("begin");
return new TransactionImpl(conn);
}
+6 -9
View File
@@ -1,13 +1,12 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { MissionGrid, MissionState } from "../model/mission";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { generateId, Id } from "../../db";
import { Id, Transaction, generateId } from "../../sql";
export class SqlMissionsRepository implements FacetRepository<MissionState> {
constructor(private readonly _conn: ClientBase) {}
constructor(private readonly _txn: Transaction) {}
async load(profileId: Id<Profile>): Promise<MissionState> {
const result: MissionState = {
@@ -31,10 +30,9 @@ export class SqlMissionsRepository implements FacetRepository<MissionState> {
const loadSoloSql = sql
.select("sm.*")
.from("idz_solo_mission_state sm")
.where("sm.profile_id", profileId)
.toParams();
.where("sm.profile_id", profileId);
const { rows } = await this._conn.query(loadSoloSql);
const rows = await this._txn.fetchRows(loadSoloSql);
for (const row of rows) {
result.solo[row.grid_no].cells[row.cell_no] = row.value;
@@ -64,10 +62,9 @@ export class SqlMissionsRepository implements FacetRepository<MissionState> {
value: grid[j],
})
.onConflict("profile_id", "grid_no", "cell_no")
.doUpdate(["value"])
.toParams();
.doUpdate(["value"]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
}
+25 -34
View File
@@ -1,10 +1,9 @@
import sql from "sql-bricks";
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { ProfileRepository } from "../repo";
import { generateId, Id } from "../../db";
import { AimeId } from "../../model";
import { Id, Transaction, generateId } from "../../sql";
export function _extractProfile(row: any): Profile {
return {
@@ -21,7 +20,7 @@ export function _extractProfile(row: any): Profile {
}
export class SqlProfileRepository implements ProfileRepository {
constructor(private readonly _conn: ClientBase) {}
constructor(private readonly _txn: Transaction) {}
async find(aimeId: AimeId): Promise<Id<Profile>> {
const profileId = await this.peek(aimeId);
@@ -38,11 +37,9 @@ export class SqlProfileRepository implements ProfileRepository {
.select("p.id")
.from("idz_profile p")
.join("aime_player r", { "p.player_id": "r.id" })
.where("r.ext_id", aimeId)
.toParams();
.where("r.ext_id", aimeId);
const { rows } = await this._conn.query(lookupSql);
const row = rows[0];
const row = await this._txn.fetchRow(lookupSql);
if (row === undefined) {
return undefined;
@@ -56,12 +53,11 @@ export class SqlProfileRepository implements ProfileRepository {
.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)
.toParams();
.where("p.id", id);
const { rows } = await this._conn.query(loadSql);
const row = await this._txn.fetchRow(loadSql);
return _extractProfile(rows[0]);
return _extractProfile(row);
}
async save(id: Id<Profile>, profile: Profile): Promise<void> {
@@ -74,21 +70,18 @@ export class SqlProfileRepository implements ProfileRepository {
mileage: profile.mileage,
access_time: profile.accessTime,
})
.where("id", id)
.toParams();
.where("id", id);
await this._conn.query(saveSql);
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)
.toParams();
.where("r.ext_id", profile.aimeId);
const { rows } = await this._conn.query(findSql);
const row = rows[0];
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
throw new Error("Aime ID not found");
@@ -97,22 +90,20 @@ export class SqlProfileRepository implements ProfileRepository {
const id = generateId();
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,
access_time: profile.accessTime,
})
.toParams();
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,
access_time: profile.accessTime,
});
await this._conn.query(createSql);
await this._txn.modify(createSql);
return id as Id<Profile>;
}
+10 -10
View File
@@ -1,23 +1,24 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { Settings } from "../model/settings";
import { Profile } from "../model/profile";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
import { Id, Transaction } from "../../sql";
export class SqlSettingsRepository implements FacetRepository<Settings> {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("s.id", profileId);
const { rows } = await this._conn.query(loadSql);
const row = rows[0];
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error(`Settings not found, profileId=${profileId}`);
}
return {
music: row.music,
@@ -37,9 +38,8 @@ export class SqlSettingsRepository implements FacetRepository<Settings> {
gauges: settings.gauges,
})
.onConflict("id")
.doUpdate(["music", "pack", "paper_cup", "gauges"])
.toParams();
.doUpdate(["music", "pack", "paper_cup", "gauges"]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
+10 -16
View File
@@ -1,23 +1,20 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { Story, StoryRow, StoryCell } from "../model/story";
import { FacetRepository } from "../repo";
import { generateId, Id } from "../../db";
import { Id, Transaction, generateId } from "../../sql";
export class SqlStoryRepository implements FacetRepository<Story> {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("s.id", profileId);
const headerResult = await this._conn.query(loadSql);
const header = headerResult.rows[0];
const header = await this._txn.fetchRow(loadSql);
// Must succeed even if nonexistent (required by save method below)
@@ -40,10 +37,9 @@ export class SqlStoryRepository implements FacetRepository<Story> {
const loadCellSql = sql
.select("sc.*")
.from("idz_story_cell_state sc")
.where("sc.profile_id", profileId)
.toParams();
.where("sc.profile_id", profileId);
const { rows } = await this._conn.query(loadCellSql);
const rows = await this._txn.fetchRows(loadCellSql);
for (const row of rows) {
const cell = result.rows[row.row_no].cells[row.col_no];
@@ -65,10 +61,9 @@ export class SqlStoryRepository implements FacetRepository<Story> {
y: story.y,
})
.onConflict("id")
.doUpdate(["x", "y"])
.toParams();
.doUpdate(["x", "y"]);
await this._conn.query(headSql);
await this._txn.modify(headSql);
for (let i = 0; i < story.rows.length; i++) {
const exRow = existing.rows[i];
@@ -92,10 +87,9 @@ export class SqlStoryRepository implements FacetRepository<Story> {
b: cell.b,
})
.onConflict("profile_id", "row_no", "col_no")
.doUpdate(["a", "b"])
.toParams();
.doUpdate(["a", "b"]);
await this._conn.query(cellSql);
await this._txn.modify(cellSql);
}
}
}
+22 -32
View File
@@ -1,23 +1,21 @@
import { ClientBase } from "pg";
import sql from "sql-bricks";
import sql from "sql-bricks-postgres";
import { ExtId } from "../model/base";
import { Team } from "../model/team";
import { TeamSpec, TeamRepository } from "../repo";
import { Id, generateExtId, generateId } from "../../db";
import { generateExtId } from "../../model";
import { Id, Transaction, generateId } from "../../sql";
export class SqlTeamRepository implements TeamRepository {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("t.ext_id", extId);
const { rows } = await this._conn.query(findSql);
const row = rows[0];
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
throw new Error(`Team not found for ExtID ${extId}`);
@@ -30,13 +28,11 @@ export class SqlTeamRepository implements TeamRepository {
const loadSql = sql
.select("t.*")
.from("idz_team t")
.where("t.id", id)
.toParams();
.where("t.id", id);
const { rows } = await this._conn.query(loadSql);
const row = rows[0];
const row = await this._txn.fetchRow(loadSql);
if (row == undefined) {
if (row === undefined) {
throw new Error("Team not found");
}
@@ -55,38 +51,32 @@ export class SqlTeamRepository implements TeamRepository {
name_bg: team.nameBg,
name_fx: team.nameFx,
})
.where("id", id)
.toParams();
.where("id", id);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
async create(team: TeamSpec): Promise<[Id<Team>, ExtId<Team>]> {
const id = generateId() as Id<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,
})
.toParams();
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._conn.query(createSql);
await this._txn.modify(createSql);
return [id, extId];
}
async delete(id: Id<Team>): Promise<void> {
const deleteSql = sql
.delete("idz_team")
.where("id", id)
.toParams();
const deleteSql = sql.delete("idz_team").where("id", id);
await this._conn.query(deleteSql);
await this._txn.modify(deleteSql);
}
}
+10 -15
View File
@@ -1,23 +1,20 @@
import sql from "sql-bricks-postgres";
import { ClientBase } from "pg";
import { Team, TeamAuto } from "../model/team";
import { TeamAutoRepository } from "../repo";
import { Id } from "../../db";
import { Id, Transaction } from "../../sql";
export class SqlTeamAutoRepository implements TeamAutoRepository {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.limit(1);
const { rows } = await this._conn.query(peekSql);
const row = rows[0];
const row = await this._txn.fetchRow(peekSql);
return (
row && [
@@ -31,14 +28,12 @@ export class SqlTeamAutoRepository implements TeamAutoRepository {
}
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,
})
.toParams();
const pushSql = sql.insert("idz_team_auto", {
id: teamId,
serial_no: auto.serialNo,
name_idx: auto.nameIdx,
});
await this._conn.query(pushSql);
await this._txn.modify(pushSql);
}
}
+21 -34
View File
@@ -1,25 +1,22 @@
import 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";
import { Id, Transaction } from "../../sql";
export class SqlTeamMemberRepository implements TeamMemberRepository {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("tm.id", profileId);
const { rows } = await this._conn.query(findSql);
const row = rows[0];
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
return undefined;
@@ -33,11 +30,9 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
.select("tm.id")
.from("idz_team_member tm")
.where("tm.team_id", teamId)
.where("tm.leader", true)
.toParams();
.where("tm.leader", true);
const { rows } = await this._conn.query(findSql);
const row = rows[0];
const row = await this._txn.fetchRow(findSql);
if (row === undefined) {
return undefined;
@@ -53,10 +48,9 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
.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();
.where("tm.team_id", teamId);
const { rows } = await this._conn.query(loadSql);
const rows = await this._txn.fetchRows(loadSql);
return rows.map((row: any) => ({
profile: _extractProfile(row),
@@ -77,10 +71,9 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
.select("id")
.from("idz_team")
.where("id", teamId)
.forUpdate()
.toParams();
.forUpdate();
await this._conn.query(lockSql);
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
@@ -94,13 +87,11 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
const countSql = sql
.select("count(*) as count")
.from("idz_team_member")
.where("team_id", teamId)
.toParams();
.where("team_id", teamId);
const { rows } = await this._conn.query(countSql);
const row = rows[0];
const row = await this._txn.fetchRow(countSql);
if (row.count >= 6) {
if (row!.count >= 6) {
throw new Error(`Team ${teamId} is full`);
}
@@ -114,20 +105,18 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
join_time: timestamp,
})
.onConflict("id")
.doUpdate(["team_id", "leader", "join_time"])
.toParams();
.doUpdate(["team_id", "leader", "join_time"]);
await this._conn.query(joinSql);
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)
.toParams();
.where("id", profileId);
await this._conn.query(leaveSql);
await this._txn.modify(leaveSql);
}
async makeLeader(teamId: Id<Team>, profileId: Id<Profile>): Promise<void> {
@@ -135,19 +124,17 @@ export class SqlTeamMemberRepository implements TeamMemberRepository {
.update("idz_team_member", {
leader: false,
})
.where("team_id", teamId)
.toParams();
.where("team_id", teamId);
await this._conn.query(clearSql);
await this._txn.modify(clearSql);
const setSql = sql
.update("idz_team_member", {
leader: true,
})
.where("id", profileId)
.where("team_id", teamId)
.toParams();
.where("team_id", teamId);
await this._conn.query(setSql);
await this._txn.modify(setSql);
}
}
+25 -39
View File
@@ -1,24 +1,22 @@
import sql from "sql-bricks-postgres";
import { ClientBase } from "pg";
import { Team } from "../model/team";
import { TeamReservationRepository } from "../repo";
import { Id } from "../../db";
import { AimeId } from "../../model";
import { Id, Transaction } from "../../sql";
export class SqlTeamReservationRepository
implements TeamReservationRepository {
constructor(private readonly _conn: ClientBase) {}
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()
.toParams();
.forUpdate();
await this._conn.query(lockSql);
await this._txn.modify(lockSql);
}
async occupancyHack(teamId: Id<Team>): Promise<number> {
@@ -30,20 +28,18 @@ export class SqlTeamReservationRepository
const memberSql = sql
.select("count(*) as count")
.from("idz_team_member tm")
.where("tm.team_id", teamId)
.toParams();
.where("tm.team_id", teamId);
const memberRes = await this._conn.query(memberSql);
const memberCount = parseInt(memberRes.rows[0].count, 10);
const memberRes = await this._txn.fetchRow(memberSql);
const memberCount = parseInt(memberRes!.count, 10);
const reservSql = sql
.select("count(*) as count")
.from("idz_team_reservation tr")
.where("tr.team_id", teamId)
.toParams();
.where("tr.team_id", teamId);
const reservRes = await this._conn.query(reservSql);
const reservCount = parseInt(reservRes.rows[0].count, 10);
const reservRes = await this._txn.fetchRow(reservSql);
const reservCount = parseInt(reservRes!.count, 10);
return memberCount + reservCount;
}
@@ -57,11 +53,9 @@ export class SqlTeamReservationRepository
const lookupSql = sql
.select("r.id")
.from("aime_player r")
.where("r.ext_id", aimeId)
.toParams();
.where("r.ext_id", aimeId);
const { rows } = await this._conn.query(lookupSql);
const row = rows[0];
const row = await this._txn.fetchRow(lookupSql);
if (row === undefined) {
throw new Error(`Unknown Aime ID ${aimeId}`);
@@ -77,10 +71,9 @@ export class SqlTeamReservationRepository
leader: leader === "leader",
})
.onConflict("id")
.doUpdate(["team_id"])
.toParams();
.doUpdate(["team_id"]);
await this._conn.query(insertSql);
await this._txn.modify(insertSql);
}
async commitHack(aimeId: AimeId): Promise<void> {
@@ -89,32 +82,25 @@ export class SqlTeamReservationRepository
.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)
.toParams();
.where("r.ext_id", aimeId);
const { rows } = await this._conn.query(lookupSql);
const row = rows[0];
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,
})
.toParams();
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._conn.query(insertSql);
await this._txn.modify(insertSql);
const cleanupSql = sql
.delete("idz_team_reservation")
.where("id", row.id)
.toParams();
const cleanupSql = sql.delete("idz_team_reservation").where("id", row.id);
await this._conn.query(cleanupSql);
await this._txn.modify(cleanupSql);
}
}
+8 -15
View File
@@ -1,25 +1,22 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { Profile } from "../model/profile";
import { Tickets } from "../model/tickets";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
import { Id, Transaction } from "../../sql";
// TODO free continue
export class SqlTicketsRepository implements FacetRepository<Tickets> {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("fc.id", profileId);
const { rows } = await this._conn.query(loadSql);
const row = rows[0];
const row = await this._txn.fetchRow(loadSql);
return {
freeCar: row && {
@@ -32,12 +29,9 @@ export class SqlTicketsRepository implements FacetRepository<Tickets> {
const { freeCar } = tickets;
if (!freeCar) {
const delSql = sql
.delete("idz_free_car")
.where("id", profileId)
.toParams();
const delSql = sql.delete("idz_free_car").where("id", profileId);
await this._conn.query(delSql);
await this._txn.modify(delSql);
} else {
const saveSql = sql
.insert("idz_free_car", {
@@ -45,10 +39,9 @@ export class SqlTicketsRepository implements FacetRepository<Tickets> {
valid_from: freeCar.validFrom,
})
.onConflict("id")
.doUpdate(["valid_from"])
.toParams();
.doUpdate(["valid_from"]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
}
+34 -44
View File
@@ -1,14 +1,13 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { RouteNo } from "../model/base";
import { Profile } from "../model/profile";
import { TimeAttackScore } from "../model/timeAttack";
import { TimeAttackRepository, TopTenResult } from "../repo";
import { generateId, Id } from "../../db";
import { Id, Transaction, generateId } from "../../sql";
export class SqlTimeAttackRepository implements TimeAttackRepository {
constructor(private readonly _conn: ClientBase) {}
constructor(private readonly _txn: Transaction) {}
async loadTopTen(
routeNo: RouteNo,
@@ -21,10 +20,9 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
.where("ta.route_no", routeNo)
.where(sql.gt("ta.timestamp", minTimestamp))
.orderBy(["ta.total_time asc", "ta.timestamp asc"])
.limit(10)
.toParams();
.limit(10);
const { rows } = await this._conn.query(loadSql);
const rows = await this._txn.fetchRows(loadSql);
return rows.map(row => ({
driverName: row.name,
@@ -44,10 +42,9 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
const loadSql = sql
.select("ta.*")
.from("idz_ta_best ta")
.where("ta.profile_id", profileId)
.toParams();
.where("ta.profile_id", profileId);
const { rows } = await this._conn.query(loadSql);
const rows = await this._txn.fetchRows(loadSql);
return rows.map(row => ({
routeNo: row.route_no,
@@ -61,8 +58,30 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
}
async save(profileId: Id<Profile>, score: TimeAttackScore): Promise<void> {
const logSql = sql
.insert("idz_ta_result", {
const logSql = sql.insert("idz_ta_result", {
id: generateId(),
profile_id: profileId,
route_no: score.routeNo,
total_time: score.totalTime,
section_times: score.sectionTimes,
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: generateId(),
profile_id: profileId,
route_no: score.routeNo,
@@ -72,37 +91,9 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
grade: score.grade,
car_selector: score.carSelector,
timestamp: score.timestamp,
})
.toParams();
});
await this._conn.query(logSql);
const existSql = sql
.select("ta.total_time")
.from("idz_ta_best ta")
.where("ta.profile_id", profileId)
.where("ta.route_no", score.routeNo)
.toParams();
const { rows } = await this._conn.query(existSql);
const row = rows[0];
if (row === undefined) {
const insertSql = sql
.insert("idz_ta_best", {
id: generateId(),
profile_id: profileId,
route_no: score.routeNo,
total_time: score.totalTime,
section_times: score.sectionTimes,
flags: score.flags,
grade: score.grade,
car_selector: score.carSelector,
timestamp: score.timestamp,
})
.toParams();
await this._conn.query(insertSql);
await this._txn.modify(insertSql);
} else if (score.totalTime < row.total_time) {
const updateSql = sql
.update("idz_ta_best", {
@@ -114,10 +105,9 @@ export class SqlTimeAttackRepository implements TimeAttackRepository {
timestamp: score.timestamp,
})
.where("profile_id", profileId)
.where("route_no", score.routeNo)
.toParams();
.where("route_no", score.routeNo);
await this._conn.query(updateSql);
await this._txn.modify(updateSql);
}
}
}
+12 -16
View File
@@ -1,22 +1,20 @@
import { ClientBase } from "pg";
import sql from "sql-bricks";
import sql from "sql-bricks-postgres";
import { TitleCode, ExtId } from "../model/base";
import { TitleCode } from "../model/base";
import { Profile } from "../model/profile";
import { FlagRepository } from "../repo";
import { generateId, Id } from "../../db";
import { Id, Transaction, generateId } from "../../sql";
export class SqlTitlesRepository implements FlagRepository<TitleCode> {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("t.profile_id", profileId);
const { rows } = await this._conn.query(loadSql);
const rows = await this._txn.fetchRows(loadSql);
const result = new Set<TitleCode>();
for (const row of rows) {
@@ -34,15 +32,13 @@ export class SqlTitlesRepository implements FlagRepository<TitleCode> {
continue;
}
const saveSql = sql
.insert("idz_title_unlock", {
id: generateId(),
profile_id: profileId,
title_no: flag,
})
.toParams();
const saveSql = sql.insert("idz_title_unlock", {
id: generateId(),
profile_id: profileId,
title_no: flag,
});
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
}
+10 -11
View File
@@ -1,24 +1,24 @@
import { ClientBase } from "pg";
import sql from "sql-bricks-postgres";
import { ExtId } from "../model/base";
import { Profile } from "../model/profile";
import { Unlocks } from "../model/unlocks";
import { FacetRepository } from "../repo";
import { Id } from "../../db";
import { Id, Transaction } from "../../sql";
export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
constructor(private readonly _conn: ClientBase) {}
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)
.toParams();
.where("u.id", profileId);
const { rows } = await this._conn.query(loadSql);
const row = rows[0];
const row = await this._txn.fetchRow(loadSql);
if (row === undefined) {
throw new Error(`Unlocks not found, profileId=${profileId}`);
}
return {
cup: row.cup,
@@ -38,9 +38,8 @@ export class SqlUnlocksRepository implements FacetRepository<Unlocks> {
last_mileage_reward: unlocks.lastMileageReward,
})
.onConflict("id")
.doUpdate(["cup", "gauges", "music", "last_mileage_reward"])
.toParams();
.doUpdate(["cup", "gauges", "music", "last_mileage_reward"]);
await this._conn.query(saveSql);
await this._txn.modify(saveSql);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { Team } from "../model/team";
import { Repositories } from "../repo";
import { Id } from "../../db";
import { Id } from "../../sql";
// Bleh. This factorization is kind of messy.
+5 -1
View File
@@ -43,11 +43,15 @@ import { updateUserLog } from "./updateUserLog";
import { Request } from "../request";
import { Response } from "../response";
import { Repositories } from "../repo";
import { Transaction } from "../../sql";
import { SqlRepositories } from "../db";
export async function dispatch(
w: Repositories,
txn: Transaction,
req: Request
): Promise<Response> {
const w = new SqlRepositories(txn);
switch (req.type) {
case "check_team_name_req":
return checkTeamName(w, req);
+14 -16
View File
@@ -1,30 +1,28 @@
import logger from "debug";
import { Socket } from "net";
import { beginDbSession } from "./db";
import { dispatch } from "./handler";
import { setup } from "./setup";
import { DataSource } from "../sql";
const debug = logger("app:idz:session");
export default async function idz(socket: Socket) {
const txn = await beginDbSession();
const { input, output } = setup(socket);
export default function idz(db: DataSource) {
return async function(socket: Socket) {
const { input, output } = setup(socket);
debug("Connection opened");
debug("Connection opened");
try {
for await (const req of input) {
output.write(await dispatch(txn, req));
try {
for await (const req of input) {
output.write(await db.transaction(txn => dispatch(txn, req)));
}
} catch (e) {
debug(`Error:\n${e.toString()}`);
}
await txn.commit();
} catch (e) {
debug(`Error:\n${e.toString()}`);
await txn.rollback();
}
debug("Connection closed");
debug("Connection closed");
input.end();
input.end();
};
}
+3 -7
View File
@@ -2,7 +2,9 @@ import { Subtract } from "utility-types";
import * as Model from "./model";
import { AimeId } from "../model";
import { Id } from "../db";
import { Id } from "../sql";
// Id<> is a layer break here... need to find a better way to deal with this.
export type TeamSpec = Subtract<
Model.Team,
@@ -164,9 +166,3 @@ export interface Repositories {
unlocks(): FacetRepository<Model.Unlocks>;
}
export interface Transaction extends Repositories {
commit(): Promise<void>;
rollback(): Promise<void>;
}