diff --git a/src/aimedb/db.ts b/src/aimedb/db.ts index dcb5365..c4b54a4 100644 --- a/src/aimedb/db.ts +++ b/src/aimedb/db.ts @@ -2,7 +2,7 @@ import sql from "sql-bricks-postgres"; import { CardRepository, Repositories } from "./repo"; import { AimeId, generateExtId } from "../model"; -import { Transaction, generateId } from "../sql"; +import { Transaction } from "../sql"; class CardRepositoryImpl implements CardRepository { constructor(private readonly _txn: Transaction) {} @@ -34,8 +34,8 @@ class CardRepositoryImpl implements CardRepository { } async register(luid: string, now: Date): Promise { - const playerId = generateId(); - const cardId = generateId(); + const playerId = this._txn.generateId(); + const cardId = this._txn.generateId(); const aimeId = generateExtId() as AimeId; const playerSql = sql.insert("aime_player", { diff --git a/src/idz/db/backgrounds.ts b/src/idz/db/backgrounds.ts index c48e77e..16d8866 100644 --- a/src/idz/db/backgrounds.ts +++ b/src/idz/db/backgrounds.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { BackgroundCode } from "../model/base"; import { Profile } from "../model/profile"; import { FlagRepository } from "../repo"; -import { Id, Transaction, generateId } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlBackgroundsRepository implements FlagRepository { @@ -38,7 +39,7 @@ export class SqlBackgroundsRepository } const saveSql = sql.insert("idz_background_unlock", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, background_no: flag, }); diff --git a/src/idz/db/car.ts b/src/idz/db/car.ts index fe8b856..3b8930a 100644 --- a/src/idz/db/car.ts +++ b/src/idz/db/car.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { Car, CarSelector } from "../model/car"; import { Profile } from "../model/profile"; import { CarRepository } from "../repo"; -import { Id, Row, Transaction, generateId } from "../../sql"; +import { Id } from "../../model"; +import { Row, Transaction } from "../../sql"; function _extractRow(row: Row): Car { return { @@ -69,7 +70,7 @@ export class SqlCarRepository implements CarRepository { async saveCar(profileId: Id, car: Car): Promise { const saveSql = sql .insert("idz_car", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, selector: car.selector, field_00: car.field_00, diff --git a/src/idz/db/chara.ts b/src/idz/db/chara.ts index a3ec589..ca4722f 100644 --- a/src/idz/db/chara.ts +++ b/src/idz/db/chara.ts @@ -4,7 +4,8 @@ import { BackgroundCode, TitleCode } from "../model/base"; import { Chara, Gender } from "../model/chara"; import { Profile } from "../model/profile"; import { FacetRepository } from "../repo"; -import { Id, Row, Transaction } from "../../sql"; +import { Id } from "../../model"; +import { Row, Transaction } from "../../sql"; export function _extractChara(row: Row): Chara { return { diff --git a/src/idz/db/coursePlays.ts b/src/idz/db/coursePlays.ts index c630d98..1cdab6c 100644 --- a/src/idz/db/coursePlays.ts +++ b/src/idz/db/coursePlays.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { CourseNo } from "../model/base"; import { Profile } from "../model/profile"; import { CoursePlaysRepository } from "../repo"; -import { Id, Transaction, generateId } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlCoursePlaysRepository implements CoursePlaysRepository { constructor(private readonly _txn: Transaction) {} @@ -34,7 +35,7 @@ export class SqlCoursePlaysRepository implements CoursePlaysRepository { for (const [k, v] of plays) { const saveSql = sql .insert("idz_course_plays", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, course_no: k, count: v, diff --git a/src/idz/db/missions.ts b/src/idz/db/missions.ts index e082c41..fea4182 100644 --- a/src/idz/db/missions.ts +++ b/src/idz/db/missions.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { MissionGrid, MissionState } from "../model/mission"; import { Profile } from "../model/profile"; import { FacetRepository } from "../repo"; -import { Id, Transaction, generateId } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlMissionsRepository implements FacetRepository { constructor(private readonly _txn: Transaction) {} @@ -59,7 +60,7 @@ export class SqlMissionsRepository implements FacetRepository { const saveSql = sql .insert("idz_solo_mission_state", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, grid_no: i, cell_no: j, diff --git a/src/idz/db/profile.ts b/src/idz/db/profile.ts index 0848638..ee409fb 100644 --- a/src/idz/db/profile.ts +++ b/src/idz/db/profile.ts @@ -2,8 +2,8 @@ import sql from "sql-bricks-postgres"; import { Profile } from "../model/profile"; import { ProfileRepository } from "../repo"; -import { AimeId } from "../../model"; -import { Id, Row, Transaction, generateId } from "../../sql"; +import { AimeId, Id } from "../../model"; +import { Row, Transaction } from "../../sql"; export function _extractProfile(row: Row): Profile { return { @@ -45,7 +45,7 @@ export class SqlProfileRepository implements ProfileRepository { return undefined; } - return BigInt(row.id) as Id; + return row.id as Id; } async load(id: Id): Promise { @@ -91,7 +91,7 @@ export class SqlProfileRepository implements ProfileRepository { throw new Error("Aime ID not found"); } - const id = generateId(); + const id = this._txn.generateId(); const playerId = row.id; const createSql = sql.insert("idz_profile", { @@ -109,6 +109,6 @@ export class SqlProfileRepository implements ProfileRepository { await this._txn.modify(createSql); - return id as Id; + return id; } } diff --git a/src/idz/db/settings.ts b/src/idz/db/settings.ts index 7ef210e..bab0bff 100644 --- a/src/idz/db/settings.ts +++ b/src/idz/db/settings.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { Settings } from "../model/settings"; import { Profile } from "../model/profile"; import { FacetRepository } from "../repo"; -import { Id, Transaction } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlSettingsRepository implements FacetRepository { constructor(private readonly _txn: Transaction) {} diff --git a/src/idz/db/story.ts b/src/idz/db/story.ts index b585ac9..d799509 100644 --- a/src/idz/db/story.ts +++ b/src/idz/db/story.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { Profile } from "../model/profile"; import { Story, StoryRow, StoryCell } from "../model/story"; import { FacetRepository } from "../repo"; -import { Id, Transaction, generateId } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlStoryRepository implements FacetRepository { constructor(private readonly _txn: Transaction) {} @@ -81,7 +82,7 @@ export class SqlStoryRepository implements FacetRepository { const cellSql = sql .insert("idz_story_cell_state", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, row_no: i, col_no: j, diff --git a/src/idz/db/team.ts b/src/idz/db/team.ts index c579d71..368d83b 100644 --- a/src/idz/db/team.ts +++ b/src/idz/db/team.ts @@ -3,8 +3,8 @@ import sql from "sql-bricks-postgres"; import { ExtId } from "../model/base"; import { Team } from "../model/team"; import { TeamSpec, TeamRepository } from "../repo"; -import { generateExtId } from "../../model"; -import { Id, Transaction, generateId } from "../../sql"; +import { Id, generateExtId } from "../../model"; +import { Transaction } from "../../sql"; export class SqlTeamRepository implements TeamRepository { constructor(private readonly _txn: Transaction) {} @@ -21,7 +21,7 @@ export class SqlTeamRepository implements TeamRepository { throw new Error(`Team not found for ExtID ${extId}`); } - return BigInt(row.id) as Id; + return row.id as Id; } async load(id: Id): Promise { @@ -57,7 +57,7 @@ export class SqlTeamRepository implements TeamRepository { } async create(team: TeamSpec): Promise<[Id, ExtId]> { - const id = generateId() as Id; + const id = this._txn.generateId(); const extId = generateExtId() as ExtId; const createSql = sql.insert("idz_team", { diff --git a/src/idz/db/teamAuto.ts b/src/idz/db/teamAuto.ts index 3945da4..5aed74b 100644 --- a/src/idz/db/teamAuto.ts +++ b/src/idz/db/teamAuto.ts @@ -2,7 +2,8 @@ import sql from "sql-bricks-postgres"; import { Team, TeamAuto } from "../model/team"; import { TeamAutoRepository } from "../repo"; -import { Id, Transaction } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlTeamAutoRepository implements TeamAutoRepository { constructor(private readonly _txn: Transaction) {} @@ -22,7 +23,7 @@ export class SqlTeamAutoRepository implements TeamAutoRepository { serialNo: parseInt(row.serial_no), nameIdx: parseInt(row.name_idx), }, - BigInt(row.id) as Id, + row.id as Id, ] ); } diff --git a/src/idz/db/teamMember.ts b/src/idz/db/teamMember.ts index 4bc8f92..45e2005 100644 --- a/src/idz/db/teamMember.ts +++ b/src/idz/db/teamMember.ts @@ -5,7 +5,8 @@ import { Team, TeamMember } from "../model/team"; import { TeamMemberRepository } from "../repo"; import { _extractProfile } from "./profile"; import { _extractChara } from "./chara"; -import { Id, Transaction } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlTeamMemberRepository implements TeamMemberRepository { constructor(private readonly _txn: Transaction) {} @@ -22,7 +23,7 @@ export class SqlTeamMemberRepository implements TeamMemberRepository { return undefined; } - return BigInt(row.team_id) as Id; + return row.team_id as Id; } async findLeader(teamId: Id): Promise | undefined> { @@ -38,7 +39,7 @@ export class SqlTeamMemberRepository implements TeamMemberRepository { return undefined; } - return BigInt(row.id) as Id; + return row.id as Id; } async loadRoster(teamId: Id): Promise { @@ -65,7 +66,7 @@ export class SqlTeamMemberRepository implements TeamMemberRepository { profileId: Id, timestamp: Date ): Promise { - // Lock the team record to avoid race conditions. This way + // Lock the team record to avoid race conditions. const lockSql = sql .select("id") diff --git a/src/idz/db/teamReservation.ts b/src/idz/db/teamReservation.ts index 0e414b3..b52ad88 100644 --- a/src/idz/db/teamReservation.ts +++ b/src/idz/db/teamReservation.ts @@ -2,8 +2,8 @@ import sql from "sql-bricks-postgres"; import { Team } from "../model/team"; import { TeamReservationRepository } from "../repo"; -import { AimeId } from "../../model"; -import { Id, Transaction } from "../../sql"; +import { AimeId, Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlTeamReservationRepository implements TeamReservationRepository { diff --git a/src/idz/db/tickets.ts b/src/idz/db/tickets.ts index cf3aa65..616c0f4 100644 --- a/src/idz/db/tickets.ts +++ b/src/idz/db/tickets.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { Profile } from "../model/profile"; import { Tickets } from "../model/tickets"; import { FacetRepository } from "../repo"; -import { Id, Transaction } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; // TODO free continue diff --git a/src/idz/db/timeAttack.ts b/src/idz/db/timeAttack.ts index b9e9f17..ae377cd 100644 --- a/src/idz/db/timeAttack.ts +++ b/src/idz/db/timeAttack.ts @@ -5,7 +5,8 @@ import { CarSelector } from "../model/car"; import { Profile } from "../model/profile"; import { TimeAttackScore } from "../model/timeAttack"; import { TimeAttackRepository, TopTenResult } from "../repo"; -import { Id, Row, Transaction, generateId } from "../../sql"; +import { Id } from "../../model"; +import { Row, Transaction } from "../../sql"; function _extractRow(row: Row): TimeAttackScore { return { @@ -56,7 +57,7 @@ export class SqlTimeAttackRepository implements TimeAttackRepository { async save(profileId: Id, score: TimeAttackScore): Promise { const logSql = sql.insert("idz_ta_result", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, route_no: score.routeNo, total_time: score.totalTime, @@ -79,7 +80,7 @@ export class SqlTimeAttackRepository implements TimeAttackRepository { if (row === undefined) { const insertSql = sql.insert("idz_ta_best", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, route_no: score.routeNo, total_time: score.totalTime, diff --git a/src/idz/db/titles.ts b/src/idz/db/titles.ts index 5bc9550..7a79071 100644 --- a/src/idz/db/titles.ts +++ b/src/idz/db/titles.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { TitleCode } from "../model/base"; import { Profile } from "../model/profile"; import { FlagRepository } from "../repo"; -import { Id, Transaction, generateId } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlTitlesRepository implements FlagRepository { constructor(private readonly _txn: Transaction) {} @@ -33,7 +34,7 @@ export class SqlTitlesRepository implements FlagRepository { } const saveSql = sql.insert("idz_title_unlock", { - id: generateId(), + id: this._txn.generateId(), profile_id: profileId, title_no: flag, }); diff --git a/src/idz/db/unlocks.ts b/src/idz/db/unlocks.ts index d3e30f2..9e7f66c 100644 --- a/src/idz/db/unlocks.ts +++ b/src/idz/db/unlocks.ts @@ -3,7 +3,8 @@ import sql from "sql-bricks-postgres"; import { Profile } from "../model/profile"; import { Unlocks } from "../model/unlocks"; import { FacetRepository } from "../repo"; -import { Id, Transaction } from "../../sql"; +import { Id } from "../../model"; +import { Transaction } from "../../sql"; export class SqlUnlocksRepository implements FacetRepository { constructor(private readonly _txn: Transaction) {} diff --git a/src/idz/handler/_team.ts b/src/idz/handler/_team.ts index 07685e1..e74a64e 100644 --- a/src/idz/handler/_team.ts +++ b/src/idz/handler/_team.ts @@ -1,6 +1,6 @@ import { Team } from "../model/team"; import { Repositories } from "../repo"; -import { Id } from "../../sql"; +import { Id } from "../../model"; // Bleh. This factorization is kind of messy. diff --git a/src/idz/repo.ts b/src/idz/repo.ts index d1577b2..213a6a5 100644 --- a/src/idz/repo.ts +++ b/src/idz/repo.ts @@ -1,10 +1,7 @@ import { Subtract } from "utility-types"; import * as Model from "./model"; -import { AimeId } from "../model"; -import { Id } from "../sql"; - -// Id<> is a layer break here... need to find a better way to deal with this. +import { AimeId, Id } from "../model"; export type TeamSpec = Subtract< Model.Team, diff --git a/src/model.ts b/src/model.ts index 98ddf2a..4484e1f 100644 --- a/src/model.ts +++ b/src/model.ts @@ -1,5 +1,22 @@ import { randomBytes } from "crypto"; +/** + * An internal database id. + * + * We don't have any say over the protocols that we are implementing, and we + * would also like our internal data store to adhere to a consistent set of + * design principles. This type defines a fully opaque primary key data type + * for database records which should not be exposed to external clients under + * normal circumstances. Clients outside the SQL DB (or other persistent data + * storage) driver should make no assumptions about its structure or actual + * underlying data type. + * + * Database entities presented to external clients must be identified using + * alternative external identifiers in formats that are acceptable to those + * external systems. + */ +export type Id = string & { __type: T }; + export type AimeId = number & { __aimeId: null }; /** Generate a random 32-bit ID for use in external protocol messages */ diff --git a/src/sql/api.ts b/src/sql/api.ts index e0b3dfe..bc540e1 100644 --- a/src/sql/api.ts +++ b/src/sql/api.ts @@ -1,12 +1,23 @@ import * as sql from "sql-bricks-postgres"; -export type Id = bigint & { __id: T }; +import { Id } from "../model"; export interface Row { [key: string]: string; } export interface Transaction { + /** + * Generate a new random primary key. + * + * On SQLite this is a random 63-bit integer (the high bit is always zero + * so that all IDs are positive, mostly for aesthetic reasons although this + * may also usefully reserve a namespace for automated testing). + * + * On Postgres this might generate UUIDv4s instead. + */ + generateId(): Id; + modify(stmt: sql.Statement): Promise; fetchRow(stmt: sql.SelectStatement): Promise; diff --git a/src/sql/index.ts b/src/sql/index.ts index 5beb3b9..d9c9c91 100644 --- a/src/sql/index.ts +++ b/src/sql/index.ts @@ -1,3 +1,2 @@ export * from "./api"; export * from "./sqlite"; -export * from "./util"; diff --git a/src/sql/sqlite.ts b/src/sql/sqlite.ts index d19b78b..5cab2b9 100644 --- a/src/sql/sqlite.ts +++ b/src/sql/sqlite.ts @@ -1,7 +1,9 @@ import Database from "better-sqlite3"; +import { randomBytes } from "crypto"; import * as sql from "sql-bricks-postgres"; import { DataSource, Row, Transaction } from "./api"; +import { Id } from "../model"; type MixedRow = { [key: string]: any; @@ -52,6 +54,17 @@ function _postprocess(obj: MixedRow): Row { class SqliteTransaction implements Transaction { constructor(private readonly _db: Database.Database) {} + generateId(): Id { + const buf = randomBytes(8); + + buf[0] &= 0x7f; // Force number to be non-negative + + const val = buf.readBigUInt64BE(0); + const str = val.toString(); + + return str as Id; + } + modify(stmt: sql.Statement): Promise { const params = _preprocess(stmt); diff --git a/src/sql/util.ts b/src/sql/util.ts deleted file mode 100644 index 45c01bf..0000000 --- a/src/sql/util.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { randomBytes } from "crypto"; - -export function generateId(): bigint { - const buf = randomBytes(8); - - buf[0] &= 0x7f; // Force number to be non-negative - - // Let's not depend on Node v12 for the sake of 3 LoC just yet. - - const hi = buf.readUInt32BE(0); - const lo = buf.readUInt32BE(4); - - return (BigInt(hi) << 32n) | BigInt(lo); -}