Update PostgreSQL store to use the same upgrade system as the cache, Resolve #105: correct first word change.

This commit is contained in:
Gareth Latty
2020-02-11 17:59:07 +00:00
parent 4f0ef9a85a
commit 365e235dd2
4 changed files with 156 additions and 170 deletions
+1 -2
View File
@@ -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
View File
@@ -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;
});
}
}
+15
View File
@@ -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.