chunithm: Initial implementation code drop

No cleaner development history than this exists. It's difficult to
methodically design something when you don't yet know what it is
you're designing.
This commit is contained in:
Tau
2019-11-06 17:29:07 -05:00
parent beb9144e9b
commit d1d63e1ef9
169 changed files with 3903 additions and 242 deletions
+66
View File
@@ -0,0 +1,66 @@
import { SqlUserActivityRepository } from "./userActivity";
import { SqlUserCharacterRepository } from "./userCharacter";
import { SqlUserDataRepository } from "./userData";
import { SqlUserDataExRepository } from "./userDataEx";
import { SqlUserGameOptionRepository } from "./userGameOption";
import { SqlUserGameOptionExRepository } from "./userGameOptionEx";
import { SqlUserItemRepository } from "./userItem";
import { SqlUserMapRepository } from "./userMap";
import { SqlUserMusicRepository } from "./userMusic";
import { SqlUserPlaylogRepository } from "./userPlaylog";
import { Repositories } from "../repo";
import { UserActivityRepository } from "../repo/userActivity";
import { UserCharacterRepository } from "../repo/userCharacter";
import { UserDataRepository } from "../repo/userData";
import { UserDataExRepository } from "../repo/userDataEx";
import { UserGameOptionRepository } from "../repo/userGameOption";
import { UserGameOptionExRepository } from "../repo/userGameOptionEx";
import { UserItemRepository } from "../repo/userItem";
import { UserMapRepository } from "../repo/userMap";
import { UserMusicRepository } from "../repo/userMusic";
import { UserPlaylogRepository } from "../repo/userPlaylog";
import { Transaction } from "../../sql";
export class SqlRepositories implements Repositories {
constructor(private readonly _txn: Transaction) {}
userActivity(): UserActivityRepository {
return new SqlUserActivityRepository(this._txn);
}
userCharacter(): UserCharacterRepository {
return new SqlUserCharacterRepository(this._txn);
}
userData(): UserDataRepository {
return new SqlUserDataRepository(this._txn);
}
userDataEx(): UserDataExRepository {
return new SqlUserDataExRepository(this._txn);
}
userGameOption(): UserGameOptionRepository {
return new SqlUserGameOptionRepository(this._txn);
}
userGameOptionEx(): UserGameOptionExRepository {
return new SqlUserGameOptionExRepository(this._txn);
}
userItem(): UserItemRepository {
return new SqlUserItemRepository(this._txn);
}
userMap(): UserMapRepository {
return new SqlUserMapRepository(this._txn);
}
userMusic(): UserMusicRepository {
return new SqlUserMusicRepository(this._txn);
}
userPlaylog(): UserPlaylogRepository {
return new SqlUserPlaylogRepository(this._txn);
}
}
+55
View File
@@ -0,0 +1,55 @@
import logger from "debug";
import { NextFunction, Router, Request, Response } from "express";
import { SqlRepositories } from "./_factory";
import { RpcHandler, RpcWrapper } from "../rpc";
import { DataSource } from "../../sql";
const debug = logger("app:chuni:handler");
export default function createSqlWrapper(db: DataSource): RpcWrapper {
// We return a piece of middleware that delegates to an Express router
const router = Router();
// The ES6 class syntax is nice, but it lacks an `operator()` syntax, so we
// have to do this the confusing and messy way. Declare an inner function and
// then add "method" fields to the resulting function object.
function self(req: Request, res: Response, next: NextFunction) {
// Forward all requests to our embedded Express router
return router(req, res, next);
}
self.rpc = function<Q, R>(path: string, handler: RpcHandler<Q, R>) {
// Declare the RPC as a POST endpoint on the Express router as follows:
router.post(path, async function(req, res) {
// When you receive an HTTP request on this path...
try {
// return the JSON response returned by...
res.json(
// ... the provided handler, executing within an SQL transaction.
await db.transaction(txn =>
handler(new SqlRepositories(txn), req.body)
)
);
} catch (e) {
// .. unless something goes wrong of course
if (debug.enabled) {
debug("Error processing request: %s", e.stack);
}
res.sendStatus(500);
}
});
// Allow .rpc() calls to be chained, since that's what Express' native
// .post() etc methods do.
return self;
};
// Done defining our Express middleware, so here it is:
return self;
}
+50
View File
@@ -0,0 +1,50 @@
import sql from "sql-bricks-postgres";
import { UserActivityItem } from "../model/userActivity";
import { UserDataItem } from "../model/userData";
import { UserActivityRepository } from "../repo/userActivity";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
kind: T.number,
activityId: T.number, // This is just called "id" on the wire
sortNumber: T.number,
param1: T.number,
param2: T.number,
param3: T.number,
param4: T.number,
});
export class SqlUserActivityRepository implements UserActivityRepository {
constructor(private readonly _txn: Transaction) {}
async load(
profileId: Id<UserDataItem>,
kind: number
): Promise<UserActivityItem[]> {
const stmt = sql
.select("*")
.from("cm_user_activity")
.where("profile_id", profileId)
.where("kind", kind);
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(profileId: Id<UserDataItem>, obj: UserActivityItem): Promise<void> {
const stmt = sql
.insert("cm_user_activity", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "kind", "activity_id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+75
View File
@@ -0,0 +1,75 @@
import sql from "sql-bricks-postgres";
import { UserCharacterItem } from "../model/userCharacter";
import { UserDataItem } from "../model/userData";
import { Page } from "../repo";
import { UserCharacterRepository } from "../repo/userCharacter";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
characterId: T.number,
playCount: T.number,
level: T.number,
skillId: T.number,
friendshipExp: T.number,
isValid: T.boolean,
isNewMark: T.boolean,
param1: T.number,
param2: T.number,
});
export class SqlUserCharacterRepository implements UserCharacterRepository {
constructor(private readonly _txn: Transaction) {}
async load(
profileId: Id<UserDataItem>,
page?: Page | undefined
): Promise<UserCharacterItem[]> {
const stmt = sql
.select("c.*")
.from("cm_user_character c")
.where("c.profile_id", profileId);
if (page) {
stmt.limit(page.limit).offset(page.offset);
}
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
async loadOne(
profileId: Id<UserDataItem>,
characterId: number
): Promise<UserCharacterItem> {
const stmt = sql
.select("c.*")
.from("cm_user_character c")
.where("c.profile_id", profileId)
.where("c.character_id", characterId);
const row = await this._txn.fetchRow(stmt);
if (row === undefined) {
throw new Error("Database corrupted: selected character not found");
}
return readRow(row);
}
save(profileId: Id<UserDataItem>, obj: UserCharacterItem): Promise<void> {
const stmt = sql
.insert("cm_user_character", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "character_id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+137
View File
@@ -0,0 +1,137 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserDataRepository } from "../repo/userData";
import { AimeId, Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow } = createSqlMapper({
accessCode: T.string,
userName: T.string,
isWebJoin: T.boolean,
webLimitDate: T.Date,
level: T.number,
reincarnationNum: T.number,
exp: T.number,
point: T.number,
totalPoint: T.bigint,
playCount: T.number,
multiPlayCount: T.number,
multiWinCount: T.number,
requestResCount: T.number,
acceptResCount: T.number,
successResCount: T.number,
playerRating: T.number,
highestRating: T.number,
nameplateId: T.number,
frameId: T.number,
characterId: T.number,
trophyId: T.number,
playedTutorialBit: T.number,
firstTutorialCancelNum: T.number,
masterTutorialCancelNum: T.number,
totalRepertoireCount: T.number,
totalMapNum: T.number,
totalHiScore: T.bigint,
totalBasicHighScore: T.bigint,
totalAdvancedHighScore: T.bigint,
totalExpertHighScore: T.bigint,
totalMasterHighScore: T.bigint,
eventWatchedDate: T.Date,
friendCount: T.number,
isMaimai: T.boolean,
firstGameId: T.string,
firstRomVersion: T.string,
firstDataVersion: T.string,
firstPlayDate: T.Date,
lastGameId: T.string,
lastRomVersion: T.string,
lastDataVersion: T.string,
lastPlayDate: T.Date,
lastPlaceId: T.number,
lastPlaceName: T.string,
lastRegionId: T.string,
lastRegionName: T.string,
lastAllNetId: T.string,
lastClientId: T.string,
});
export class SqlUserDataRepository implements UserDataRepository {
constructor(private readonly _txn: Transaction) {}
async tryLookup(aimeId: AimeId): Promise<Id<UserDataItem> | undefined> {
const stmt = sql
.select("d.id")
.from("cm_user_data d")
.join("aime_player p", { "d.player_id": "p.id" })
.where("p.ext_id", aimeId);
const row = await this._txn.fetchRow(stmt);
return row && (row.id as Id<UserDataItem>);
}
async lookup(aimeId: AimeId): Promise<Id<UserDataItem>> {
const id = await this.tryLookup(aimeId);
if (id === undefined) {
throw new Error("AiMe ID not found");
}
return id;
}
async load(dataId: Id<UserDataItem>): Promise<UserDataItem> {
const stmt = sql
.select("*")
.from("cm_user_data")
.where("id", dataId);
const row = await this._txn.fetchRow(stmt);
if (row === undefined) {
throw new Error("UserData record not found");
}
return readRow(row);
}
async save(aimeId: AimeId, obj: UserDataItem): Promise<Id<UserDataItem>> {
const lookupStmt = sql
.select("p.id as player_id", "d.id as data_id")
.from("aime_player p")
.leftJoin("cm_user_data d", { "p.id": "d.player_id" })
.where("p.ext_id", aimeId);
const row = await this._txn.fetchRow(lookupStmt);
if (row === undefined) {
throw new Error("AiMe ID not found");
}
const playerId = row.player_id;
const existingId = row.data_id as Id<UserDataItem>;
if (existingId !== null) {
const stmt = sql
.update("cm_user_data", writeRow(obj))
.where("id", existingId);
await this._txn.modify(stmt);
return existingId;
} else {
const id = this._txn.generateId();
const insertStmt = sql.insert("cm_user_data", {
id: id,
player_id: playerId,
...writeRow(obj),
});
await this._txn.modify(insertStmt);
return id as Id<UserDataItem>;
}
}
}
+76
View File
@@ -0,0 +1,76 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserDataExItem } from "../model/userDataEx";
import { UserDataExRepository } from "../repo/userDataEx";
import { AimeId, Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
compatibleCmVersion: T.string,
medal: T.number,
mapIconId: T.number,
voiceId: T.number,
ext1: T.number,
ext2: T.number,
ext3: T.number,
ext4: T.number,
ext5: T.number,
ext6: T.number,
ext7: T.number,
ext8: T.number,
ext9: T.number,
ext10: T.number,
ext11: T.number,
ext12: T.number,
ext13: T.number,
ext14: T.number,
ext15: T.number,
ext16: T.number,
ext17: T.number,
ext18: T.number,
ext19: T.number,
ext20: T.number,
extStr1: T.string,
extStr2: T.string,
extStr3: T.string,
extStr4: T.string,
extStr5: T.string,
extLong1: T.bigint,
extLong2: T.bigint,
extLong3: T.bigint,
extLong4: T.bigint,
extLong5: T.bigint,
});
export class SqlUserDataExRepository implements UserDataExRepository {
constructor(private readonly _txn: Transaction) {}
async load(dataId: Id<UserDataItem>): Promise<UserDataExItem> {
const stmt = sql
.select("*")
.from("cm_user_data_ex")
.where("id", dataId);
const row = await this._txn.fetchRow(stmt);
if (row === undefined) {
throw new Error("UserDataEx record not found");
}
return readRow(row);
}
async save(profileId: Id<UserDataItem>, obj: UserDataExItem): Promise<void> {
const stmt = sql
.insert("cm_user_data_ex", {
id: profileId,
...writeRow(obj),
})
.onConflict("id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+64
View File
@@ -0,0 +1,64 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserGameOptionItem } from "../model/userGameOption";
import { UserGameOptionRepository } from "../repo/userGameOption";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
bgInfo: T.number,
fieldColor: T.number,
guideSound: T.number,
soundEffect: T.number,
guideLine: T.number,
speed: T.number,
optionSet: T.number,
matching: T.number,
judgePos: T.number,
rating: T.number,
judgeJustice: T.number,
judgeAttack: T.number,
headphone: T.number,
playerLevel: T.number,
successTap: T.number,
successExTap: T.number,
successSlideHold: T.number,
successAir: T.number,
successFlick: T.number,
successSkill: T.number,
successTapTimbre: T.number,
privacy: T.number,
});
export class SqlUserGameOptionRepository implements UserGameOptionRepository {
constructor(private readonly _txn: Transaction) {}
async load(dataId: Id<UserDataItem>): Promise<UserGameOptionItem> {
const stmt = sql
.select("*")
.from("cm_user_game_option")
.where("id", dataId);
const row = await this._txn.fetchRow(stmt);
if (row === undefined) {
throw new Error("UserGameOption record not found");
}
return readRow(row);
}
save(profileId: Id<UserDataItem>, obj: UserGameOptionItem): Promise<void> {
const stmt = sql
.insert("cm_user_game_option", {
id: profileId,
...writeRow(obj),
})
.onConflict("id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+63
View File
@@ -0,0 +1,63 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserGameOptionExItem } from "../model/userGameOptionEx";
import { UserGameOptionExRepository } from "../repo/userGameOptionEx";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
ext1: T.number,
ext2: T.number,
ext3: T.number,
ext4: T.number,
ext5: T.number,
ext6: T.number,
ext7: T.number,
ext8: T.number,
ext9: T.number,
ext10: T.number,
ext11: T.number,
ext12: T.number,
ext13: T.number,
ext14: T.number,
ext15: T.number,
ext16: T.number,
ext17: T.number,
ext18: T.number,
ext19: T.number,
ext20: T.number,
});
export class SqlUserGameOptionExRepository
implements UserGameOptionExRepository {
constructor(private readonly _txn: Transaction) {}
async load(dataId: Id<UserDataItem>): Promise<UserGameOptionExItem> {
const stmt = sql
.select("*")
.from("cm_user_game_option_ex")
.where("id", dataId);
const row = await this._txn.fetchRow(stmt);
if (row === undefined) {
throw new Error("UserGameOptionEx record not found");
}
return readRow(row);
}
save(profileId: Id<UserDataItem>, obj: UserGameOptionExItem): Promise<void> {
const stmt = sql
.insert("cm_user_game_option_ex", {
id: profileId,
...writeRow(obj),
})
.onConflict("id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+53
View File
@@ -0,0 +1,53 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserItemItem } from "../model/userItem";
import { Page } from "../repo";
import { UserItemRepository } from "../repo/userItem";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
itemKind: T.number,
itemId: T.number,
stock: T.number,
isValid: T.boolean,
});
export class SqlUserItemRepository implements UserItemRepository {
constructor(private readonly _txn: Transaction) {}
async load(
profileId: Id<UserDataItem>,
itemKind: number,
page?: Page
): Promise<UserItemItem[]> {
const stmt = sql
.select("*")
.from("cm_user_item")
.where("profile_id", profileId)
.where("item_kind", itemKind);
if (page) {
stmt.limit(page.limit).offset(page.offset);
}
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(profileId: Id<UserDataItem>, obj: UserItemItem): Promise<void> {
const stmt = sql
.insert("cm_user_item", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "item_kind", "item_id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+56
View File
@@ -0,0 +1,56 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserMapItem } from "../model/userMap";
import { Page } from "../repo";
import { UserMapRepository } from "../repo/userMap";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
mapId: T.number,
position: T.number,
isClear: T.boolean,
areaId: T.number,
routeNumber: T.number,
eventId: T.number,
rate: T.number,
statusCount: T.number,
isValid: T.boolean,
});
export class SqlUserMapRepository implements UserMapRepository {
constructor(private readonly _txn: Transaction) {}
async load(
profileId: Id<UserDataItem>,
page?: Page
): Promise<UserMapItem[]> {
const stmt = sql
.select("*")
.from("cm_user_map")
.where("profile_id", profileId);
if (page) {
stmt.limit(page.limit).offset(page.offset);
}
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(profileId: Id<UserDataItem>, obj: UserMapItem): Promise<void> {
const stmt = sql
.insert("cm_user_map", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "map_id")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+63
View File
@@ -0,0 +1,63 @@
import sql from "sql-bricks-postgres";
import { UserDataItem } from "../model/userData";
import { UserMusicDetailItem } from "../model/userMusic";
import { Page } from "../repo";
import { UserMusicRepository } from "../repo/userMusic";
import { Id } from "../../model";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { readRow, writeRow, colNames } = createSqlMapper({
musicId: T.number,
level: T.number,
playCount: T.number,
scoreMax: T.number,
resRequestCount: T.number,
resAcceptCount: T.number,
resSuccessCount: T.number,
missCount: T.number,
maxComboCount: T.number,
isFullCombo: T.boolean,
isAllJustice: T.boolean,
isSuccess: T.boolean,
fullChain: T.number,
maxChain: T.number,
scoreRank: T.number,
isLock: T.boolean,
});
export class SqlUserMusicRepository implements UserMusicRepository {
constructor(private readonly _txn: Transaction) {}
async load(
profileId: Id<UserDataItem>,
page?: Page
): Promise<UserMusicDetailItem[]> {
const stmt = sql
.select("*")
.from("cm_user_music")
.where("profile_id", profileId);
if (page) {
stmt.limit(page.limit).offset(page.offset);
}
const rows = await this._txn.fetchRows(stmt);
return rows.map(readRow);
}
save(profileId: Id<UserDataItem>, obj: UserMusicDetailItem): Promise<void> {
const stmt = sql
.insert("cm_user_music", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
})
.onConflict("profile_id", "music_id", "level")
.doUpdate(colNames);
return this._txn.modify(stmt);
}
}
+75
View File
@@ -0,0 +1,75 @@
import sql from "sql-bricks-postgres";
import { Id } from "../../model";
import { UserDataItem } from "../model/userData";
import { UserPlaylogItem } from "../model/userPlaylog";
import { UserPlaylogRepository } from "../repo/userPlaylog";
import { Transaction } from "../../sql";
import { T, createSqlMapper } from "../../sql/util";
const { writeRow } = createSqlMapper({
orderId: T.number,
sortNumber: T.number,
placeId: T.number,
playDate: T.Date,
userPlayDate: T.Date,
musicId: T.number,
level: T.number,
customId: T.number,
playedUserId1: T.number,
playedUserId2: T.number,
playedUserId3: T.number,
playedUserName1: T.string,
playedUserName2: T.string,
playedUserName3: T.string,
playedMusicLevel1: T.number,
playedMusicLevel2: T.number,
playedMusicLevel3: T.number,
playedCustom1: T.number,
playedCustom2: T.number,
playedCustom3: T.number,
track: T.number,
score: T.number,
rank: T.number,
maxCombo: T.number,
maxChain: T.number,
rateTap: T.number,
rateHold: T.number,
rateSlide: T.number,
rateAir: T.number,
rateFlick: T.number,
judgeGuilty: T.number,
judgeAttack: T.number,
judgeJustice: T.number,
judgeCritical: T.number,
eventId: T.number,
playerRating: T.number,
isNewRecord: T.boolean,
isFullCombo: T.boolean,
fullChainKind: T.number,
isAllJustice: T.boolean,
isContinue: T.boolean,
isFreeToPlay: T.boolean,
characterId: T.number,
skillId: T.number,
playKind: T.number,
isClear: T.boolean,
skillLevel: T.number,
skillEffect: T.number,
placeName: T.string,
isMaimai: T.boolean,
});
export class SqlUserPlaylogRepository implements UserPlaylogRepository {
constructor(private readonly _txn: Transaction) {}
save(profileId: Id<UserDataItem>, obj: UserPlaylogItem): Promise<void> {
const stmt = sql.insert("cm_user_playlog", {
id: this._txn.generateId(),
profile_id: profileId,
...writeRow(obj),
});
return this._txn.modify(stmt);
}
}