Update PostgreSQL store to use the same upgrade system as the cache, Resolve #105: correct first word change.
This commit is contained in:
+1
-2
@@ -19,8 +19,7 @@
|
||||
"dev": "nodemon --es-module-specifier-resolution=node .",
|
||||
"debug": "nodemon --inspect --es-module-specifier-resolution=node .",
|
||||
"generate-json-validators": "bash generate-validators.sh",
|
||||
"generate-secret": "npm run prestart && node --es-module-specifier-resolution=node dist/secret.js",
|
||||
"postgres-setup": "npm run prestart && node dist/postgres-setup.js"
|
||||
"generate-secret": "npm run prestart && node --es-module-specifier-resolution=node dist/secret.js"
|
||||
},
|
||||
"files": [
|
||||
"dist/*",
|
||||
|
||||
@@ -78,8 +78,8 @@ export interface Cardcast {
|
||||
export type PlayCode = string;
|
||||
|
||||
const nextWordShouldBeCapitalizedRegex = /[.?!]\s*$/;
|
||||
const nextWordShouldBeCapitalized = (previously: string | null): boolean =>
|
||||
previously === null ||
|
||||
const nextWordShouldBeCapitalized = (previously: string): boolean =>
|
||||
previously === "" ||
|
||||
previously.match(nextWordShouldBeCapitalizedRegex) !== null;
|
||||
|
||||
/**
|
||||
@@ -94,11 +94,13 @@ function* parts(call: CCCard): Iterable<Card.Part> {
|
||||
let first = true;
|
||||
let previous: string | null = null;
|
||||
for (const text of call.text) {
|
||||
const capitalize: Slot =
|
||||
nextWordShouldBeCapitalized(previous) || first
|
||||
? { transform: "Capitalize" }
|
||||
: {};
|
||||
yield { ...capitalize, ...upper };
|
||||
if (previous !== null) {
|
||||
const capitalize: Slot =
|
||||
nextWordShouldBeCapitalized(previous) || first
|
||||
? { transform: "Capitalize" }
|
||||
: {};
|
||||
yield { ...capitalize, ...upper };
|
||||
}
|
||||
if (text !== "") {
|
||||
yield text;
|
||||
}
|
||||
|
||||
+131
-161
@@ -5,46 +5,27 @@ import * as Config from "../config";
|
||||
import { LobbyClosedError } from "../errors/lobby";
|
||||
import * as Lobby from "../lobby";
|
||||
import * as GameCode from "../lobby/game-code";
|
||||
import * as Logging from "../logging";
|
||||
import * as Store from "../store";
|
||||
import * as Timeout from "../timeout";
|
||||
import * as Token from "../user/token";
|
||||
import * as Postgres from "../util/postgres";
|
||||
|
||||
const idColumn = "id";
|
||||
const versionColumn = "version";
|
||||
class To0 extends Postgres.Upgrade<undefined, 0> {
|
||||
public readonly to = 0;
|
||||
|
||||
/**
|
||||
* A store where the data is stored in a PostgreSQL database.
|
||||
*/
|
||||
export class PostgresStore extends Store.Store {
|
||||
private static readonly version = 0;
|
||||
|
||||
public readonly config: Config.PostgreSQL;
|
||||
private readonly pool: Pg.Pool;
|
||||
private readonly cachedId: string;
|
||||
|
||||
public static async create(
|
||||
config: Config.PostgreSQL
|
||||
): Promise<PostgresStore> {
|
||||
const client = await new Pg.Client(config.connection);
|
||||
try {
|
||||
client.connect();
|
||||
const exists = await client.query(
|
||||
"SELECT EXISTS (SELECT FROM information_schema.schemata WHERE schema_name = 'massivedecks');"
|
||||
);
|
||||
if (!exists.rows[0]["exists"]) {
|
||||
await client.query("CREATE SCHEMA massivedecks;");
|
||||
await client.query(
|
||||
"CREATE TABLE massivedecks.meta ( version INTEGER NOT NULL, id UUID NOT NULL );"
|
||||
);
|
||||
await client.query(`
|
||||
public async apply(client: Pg.PoolClient): Promise<0> {
|
||||
await client.query("CREATE SCHEMA massivedecks;");
|
||||
await client.query(
|
||||
"CREATE TABLE massivedecks.meta ( version INTEGER NOT NULL, id UUID NOT NULL );"
|
||||
);
|
||||
await client.query(`
|
||||
CREATE TABLE massivedecks.lobbies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
lobby JSONB NOT NULL,
|
||||
last_access TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
await client.query(`
|
||||
await client.query(`
|
||||
CREATE TABLE massivedecks.timeouts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
lobby INTEGER NOT NULL REFERENCES massivedecks.lobbies(id) ON DELETE CASCADE,
|
||||
@@ -52,7 +33,7 @@ export class PostgresStore extends Store.Store {
|
||||
timeout JSONB NOT NULL
|
||||
);
|
||||
`);
|
||||
await client.query(`
|
||||
await client.query(`
|
||||
CREATE VIEW massivedecks.summaries AS SELECT
|
||||
id,
|
||||
lobby->'name' AS name,
|
||||
@@ -67,64 +48,68 @@ export class PostgresStore extends Store.Store {
|
||||
lobby->'password' IS NOT NULL AS password
|
||||
FROM massivedecks.lobbies WHERE (lobby->'config'->'public')::boolean = true;
|
||||
`);
|
||||
await client.query("INSERT INTO massivedecks.meta VALUES ($1, $2);", [
|
||||
PostgresStore.version,
|
||||
uuid()
|
||||
]);
|
||||
}
|
||||
const rows = await client.query(
|
||||
`SELECT version, ${idColumn} FROM massivedecks.meta`
|
||||
);
|
||||
const row = rows.rows[0];
|
||||
const version = row[versionColumn];
|
||||
if (version !== PostgresStore.version) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error(
|
||||
`Database at incorrect version (expected ` +
|
||||
`"${PostgresStore.version}" got "${version}"). Please upgrade.`
|
||||
);
|
||||
}
|
||||
return new PostgresStore(row[idColumn] as string, config);
|
||||
} catch (error) {
|
||||
Logging.logException("Database is not configured correctly.", error);
|
||||
throw error;
|
||||
} finally {
|
||||
await this.destroyClient(client);
|
||||
}
|
||||
await client.query("INSERT INTO massivedecks.meta VALUES ($1, $2);", [
|
||||
this.to,
|
||||
uuid()
|
||||
]);
|
||||
return this.to;
|
||||
}
|
||||
}
|
||||
|
||||
const upgrades: Postgres.Upgrades = version => {
|
||||
switch (version) {
|
||||
case undefined:
|
||||
return new To0(version);
|
||||
|
||||
case 0:
|
||||
return undefined;
|
||||
|
||||
default:
|
||||
throw new Error("Database is on unsupported version, can't upgrade.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A store where the data is stored in a PostgreSQL database.
|
||||
*/
|
||||
export class PostgresStore extends Store.Store {
|
||||
public readonly config: Config.PostgreSQL;
|
||||
private readonly cachedId: string;
|
||||
private readonly pg: Postgres.Postgres;
|
||||
|
||||
public static async create(
|
||||
config: Config.PostgreSQL
|
||||
): Promise<PostgresStore> {
|
||||
const pg = new Postgres.Postgres("mdcache", config.connection, upgrades);
|
||||
|
||||
await pg.ensureCurrent();
|
||||
|
||||
return await pg.withClient(async client => {
|
||||
const rows = await client.query(`SELECT id FROM massivedecks.meta`);
|
||||
|
||||
return new PostgresStore(rows.rows[0]["id"], config, pg);
|
||||
});
|
||||
}
|
||||
|
||||
private static async destroyClient(client: Pg.Client): Promise<void> {
|
||||
await client.end();
|
||||
}
|
||||
|
||||
private constructor(id: string, config: Config.PostgreSQL) {
|
||||
private constructor(
|
||||
id: string,
|
||||
config: Config.PostgreSQL,
|
||||
pg: Postgres.Postgres
|
||||
) {
|
||||
super();
|
||||
this.cachedId = id;
|
||||
this.config = config;
|
||||
this.pool = new Pg.Pool({ ...config.connection });
|
||||
this.pg = pg;
|
||||
}
|
||||
|
||||
public async id(): Promise<string> {
|
||||
return this.cachedId;
|
||||
}
|
||||
|
||||
private async lobbyQuery<Result>(
|
||||
gameCode: GameCode.GameCode,
|
||||
f: (lobbyId: GameCode.LobbyId, client: Pg.PoolClient) => Promise<Result>
|
||||
): Promise<Result> {
|
||||
const lobbyId = GameCode.decode(gameCode);
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
return await f(lobbyId, client);
|
||||
} finally {
|
||||
await client.release();
|
||||
}
|
||||
}
|
||||
|
||||
public async exists(gameCode: GameCode.GameCode): Promise<boolean> {
|
||||
return await this.lobbyQuery(
|
||||
gameCode,
|
||||
async (lobbyId, client) =>
|
||||
const lobbyId = GameCode.decode(gameCode);
|
||||
return await this.pg.withClient(
|
||||
async client =>
|
||||
(
|
||||
await client.query(
|
||||
"SELECT EXISTS (SELECT id FROM massivedecks.lobbies WHERE id = $1)",
|
||||
@@ -135,9 +120,9 @@ export class PostgresStore extends Store.Store {
|
||||
}
|
||||
|
||||
public async delete(gameCode: GameCode.GameCode): Promise<void> {
|
||||
await this.lobbyQuery(
|
||||
gameCode,
|
||||
async (lobbyId, client) =>
|
||||
const lobbyId = GameCode.decode(gameCode);
|
||||
await this.pg.withClient(
|
||||
async client =>
|
||||
await client.query("DELETE FROM massivedecks.lobbies WHERE id = $1", [
|
||||
lobbyId
|
||||
])
|
||||
@@ -145,8 +130,7 @@ export class PostgresStore extends Store.Store {
|
||||
}
|
||||
|
||||
public async garbageCollect(): Promise<number> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
return await this.pg.withClient(async client => {
|
||||
const result = await client.query(
|
||||
`
|
||||
DELETE FROM massivedecks.lobbies WHERE
|
||||
@@ -156,29 +140,30 @@ export class PostgresStore extends Store.Store {
|
||||
[`${this.config.abandonedTime} milliseconds`]
|
||||
);
|
||||
return result.rowCount;
|
||||
} finally {
|
||||
await client.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async *lobbySummaries(): AsyncIterableIterator<Lobby.Summary> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
const result = await client.query("SELECT * FROM massivedecks.summaries");
|
||||
for (const { id, name, started, ended, users, password } of result.rows) {
|
||||
yield {
|
||||
gameCode: GameCode.encode(id),
|
||||
name,
|
||||
state: !started || ended ? "SettingUp" : "Playing",
|
||||
users: {
|
||||
players: users["Player"] || 0,
|
||||
spectators: users["Spectator"] || 0
|
||||
},
|
||||
password
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
await client.release();
|
||||
return this.pg.withClientIterator(
|
||||
PostgresStore.lobbySummariesInternal.bind(this)
|
||||
);
|
||||
}
|
||||
|
||||
private static async *lobbySummariesInternal(
|
||||
client: Pg.PoolClient
|
||||
): AsyncIterableIterator<Lobby.Summary> {
|
||||
const result = await client.query("SELECT * FROM massivedecks.summaries");
|
||||
for (const { id, name, started, ended, users, password } of result.rows) {
|
||||
yield {
|
||||
gameCode: GameCode.encode(id),
|
||||
name,
|
||||
state: !started || ended ? "SettingUp" : "Playing",
|
||||
users: {
|
||||
players: users["Player"] || 0,
|
||||
spectators: users["Spectator"] || 0
|
||||
},
|
||||
password
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,8 +171,7 @@ export class PostgresStore extends Store.Store {
|
||||
creation: CreateLobby,
|
||||
secret: string
|
||||
): Promise<{ gameCode: GameCode.GameCode; token: Token.Token }> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
return await this.pg.withClient(async client => {
|
||||
const lobby = Lobby.create(creation);
|
||||
const result = await client.query(
|
||||
"INSERT INTO massivedecks.lobbies VALUES (DEFAULT, $1) RETURNING id",
|
||||
@@ -205,27 +189,26 @@ export class PostgresStore extends Store.Store {
|
||||
secret
|
||||
)
|
||||
};
|
||||
} finally {
|
||||
await client.release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async *timedOut(): AsyncIterableIterator<Timeout.TimedOut> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
const result = await client.query(
|
||||
"SELECT * FROM massivedecks.timeouts WHERE after < $1",
|
||||
[Date.now()]
|
||||
);
|
||||
for (const row of result.rows) {
|
||||
yield {
|
||||
id: row["id"],
|
||||
lobby: GameCode.encode(row["lobby"]),
|
||||
timeout: row["timeout"]
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
await client.release();
|
||||
return this.pg.withClientIterator(PostgresStore.timedOutInternal);
|
||||
}
|
||||
|
||||
private static async *timedOutInternal(
|
||||
client: Pg.PoolClient
|
||||
): AsyncIterableIterator<Timeout.TimedOut> {
|
||||
const result = await client.query(
|
||||
"SELECT * FROM massivedecks.timeouts WHERE after < $1",
|
||||
[Date.now()]
|
||||
);
|
||||
for (const row of result.rows) {
|
||||
yield {
|
||||
id: row["id"],
|
||||
lobby: GameCode.encode(row["lobby"]),
|
||||
timeout: row["timeout"]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,51 +216,38 @@ export class PostgresStore extends Store.Store {
|
||||
gameCode: string,
|
||||
write: (lobby: Lobby.Lobby) => { transaction: Store.Transaction; result: T }
|
||||
): Promise<T> {
|
||||
return await this.lobbyQuery(gameCode, async (lobbyId, client) => {
|
||||
await client.query("BEGIN;");
|
||||
let error = false;
|
||||
try {
|
||||
const get = await client.query(
|
||||
"SELECT lobby FROM massivedecks.lobbies WHERE id = $1;",
|
||||
[lobbyId]
|
||||
const lobbyId = GameCode.decode(gameCode);
|
||||
return await this.pg.inTransaction(async client => {
|
||||
const get = await client.query(
|
||||
"SELECT lobby FROM massivedecks.lobbies WHERE id = $1;",
|
||||
[lobbyId]
|
||||
);
|
||||
if (get.rowCount < 1) {
|
||||
throw new LobbyClosedError(gameCode);
|
||||
}
|
||||
const { transaction, result } = write(
|
||||
Lobby.fromJSON(get.rows[0]["lobby"] as Lobby.Lobby)
|
||||
);
|
||||
if (transaction.lobby) {
|
||||
await client.query(
|
||||
"UPDATE massivedecks.lobbies SET lobby=$2, last_access=CURRENT_TIMESTAMP WHERE id = $1;",
|
||||
[lobbyId, transaction.lobby]
|
||||
);
|
||||
if (get.rowCount < 1) {
|
||||
throw new LobbyClosedError(gameCode);
|
||||
}
|
||||
const { transaction, result } = write(
|
||||
Lobby.fromJSON(get.rows[0]["lobby"] as Lobby.Lobby)
|
||||
);
|
||||
if (transaction.lobby) {
|
||||
}
|
||||
if (transaction.timeouts !== undefined) {
|
||||
for (const timeout of transaction.timeouts) {
|
||||
await client.query(
|
||||
"UPDATE massivedecks.lobbies SET lobby=$2, last_access=CURRENT_TIMESTAMP WHERE id = $1;",
|
||||
[lobbyId, transaction.lobby]
|
||||
"INSERT INTO massivedecks.timeouts VALUES (DEFAULT, $1, $2, $3)",
|
||||
[lobbyId, Date.now() + timeout.after, timeout.timeout]
|
||||
);
|
||||
}
|
||||
if (transaction.timeouts !== undefined) {
|
||||
for (const timeout of transaction.timeouts) {
|
||||
await client.query(
|
||||
"INSERT INTO massivedecks.timeouts VALUES (DEFAULT, $1, $2, $3)",
|
||||
[lobbyId, Date.now() + timeout.after, timeout.timeout]
|
||||
);
|
||||
}
|
||||
}
|
||||
if (transaction.executedTimeout !== undefined) {
|
||||
await client.query(
|
||||
"DELETE FROM massivedecks.timeouts WHERE id = $1",
|
||||
[transaction.executedTimeout]
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
error = true;
|
||||
throw e;
|
||||
} finally {
|
||||
if (error) {
|
||||
await client.query("ROLLBACK;");
|
||||
} else {
|
||||
await client.query("COMMIT;");
|
||||
}
|
||||
}
|
||||
if (transaction.executedTimeout !== undefined) {
|
||||
await client.query("DELETE FROM massivedecks.timeouts WHERE id = $1", [
|
||||
transaction.executedTimeout
|
||||
]);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,21 @@ export class Postgres {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the given action with a client from the pool, releasing it when
|
||||
* done.
|
||||
*/
|
||||
public async *withClientIterator<Result>(
|
||||
f: (client: Pg.PoolClient) => AsyncIterableIterator<Result>
|
||||
): AsyncIterableIterator<Result> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
return await f(client);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to perform the given function in a transaction, and if there is an
|
||||
* exception roll back all the database operations performed.
|
||||
|
||||
Reference in New Issue
Block a user