@moneypot/hub 1.19.10 → 1.19.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/db-migrate.js +12 -0
- package/dist/src/db/transaction.d.ts +10 -3
- package/dist/src/db/transaction.js +62 -31
- package/dist/src/index.js +12 -3
- package/dist/src/migrations.d.ts +2 -1
- package/dist/src/migrations.js +22 -33
- package/dist/src/services/jwt-service.d.ts +2 -2
- package/dist/src/test/index.js +12 -5
- package/package.json +1 -1
package/dist/cli/db-migrate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "path";
|
|
3
|
+
import { Client } from "pg";
|
|
3
4
|
function printUsage() {
|
|
4
5
|
console.error("\nUsage:");
|
|
5
6
|
console.error(" db-migrate [userDatabaseMigrationsPath]");
|
|
@@ -22,13 +23,21 @@ async function main() {
|
|
|
22
23
|
}
|
|
23
24
|
await import("dotenv/config");
|
|
24
25
|
const { runMigrations } = await import("../src/index.js");
|
|
26
|
+
const connectionString = process.env.SUPERUSER_DATABASE_URL;
|
|
27
|
+
if (!connectionString) {
|
|
28
|
+
console.error("❌ SUPERUSER_DATABASE_URL environment variable is not set");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
25
31
|
console.log("Running database migrations...");
|
|
26
32
|
console.log("Hub core migrations: ✓");
|
|
27
33
|
if (userDatabaseMigrationsPath) {
|
|
28
34
|
console.log(`User migrations: ${userDatabaseMigrationsPath}`);
|
|
29
35
|
}
|
|
36
|
+
const pgClient = new Client({ connectionString });
|
|
30
37
|
try {
|
|
38
|
+
await pgClient.connect();
|
|
31
39
|
await runMigrations({
|
|
40
|
+
pgClient,
|
|
32
41
|
userDatabaseMigrationsPath,
|
|
33
42
|
});
|
|
34
43
|
console.log("✅ Database migrations completed successfully");
|
|
@@ -39,5 +48,8 @@ async function main() {
|
|
|
39
48
|
console.error(error);
|
|
40
49
|
process.exit(1);
|
|
41
50
|
}
|
|
51
|
+
finally {
|
|
52
|
+
await pgClient.end();
|
|
53
|
+
}
|
|
42
54
|
}
|
|
43
55
|
main();
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import * as pg from "pg";
|
|
2
2
|
declare const PgClientInTransactionBrand: unique symbol;
|
|
3
|
-
export
|
|
3
|
+
export declare class RollbackFailedError extends Error {
|
|
4
|
+
readonly originalError: unknown;
|
|
5
|
+
readonly rollbackError: unknown;
|
|
6
|
+
constructor(originalError: unknown, rollbackError: unknown);
|
|
7
|
+
}
|
|
8
|
+
export type PgClientInTransaction = pg.ClientBase & {
|
|
4
9
|
readonly [PgClientInTransactionBrand]: true;
|
|
5
10
|
};
|
|
6
|
-
export declare function isInTransaction(pgClient: pg.
|
|
7
|
-
export declare function assertInTransaction(pgClient: pg.
|
|
11
|
+
export declare function isInTransaction(pgClient: pg.ClientBase): pgClient is PgClientInTransaction;
|
|
12
|
+
export declare function assertInTransaction(pgClient: pg.ClientBase): asserts pgClient is PgClientInTransaction;
|
|
8
13
|
export declare function getIsolationLevel(pgClient: PgClientInTransaction): IsolationLevel | null;
|
|
9
14
|
export declare const IsolationLevel: {
|
|
10
15
|
readonly READ_COMMITTED: "READ COMMITTED";
|
|
@@ -12,6 +17,8 @@ export declare const IsolationLevel: {
|
|
|
12
17
|
readonly SERIALIZABLE: "SERIALIZABLE";
|
|
13
18
|
};
|
|
14
19
|
export type IsolationLevel = (typeof IsolationLevel)[keyof typeof IsolationLevel];
|
|
20
|
+
export declare function withPgClientTransaction<T>(pgClient: pg.ClientBase, callback: (pgClient: PgClientInTransaction) => Promise<T>): Promise<T>;
|
|
21
|
+
export declare function withPgClientTransaction<T>(pgClient: pg.ClientBase, isolationLevel: IsolationLevel, callback: (pgClient: PgClientInTransaction) => Promise<T>): Promise<T>;
|
|
15
22
|
export declare function withPgPoolTransaction<T>(pool: pg.Pool, callback: (pgClient: PgClientInTransaction) => Promise<T>, retryCount?: number, maxRetries?: number): Promise<T>;
|
|
16
23
|
export declare function withPgPoolTransaction<T>(pool: pg.Pool, isolationLevel: IsolationLevel, callback: (pgClient: PgClientInTransaction) => Promise<T>, retryCount?: number, maxRetries?: number): Promise<T>;
|
|
17
24
|
export {};
|
|
@@ -2,6 +2,16 @@ import * as pg from "pg";
|
|
|
2
2
|
import { logger } from "../logger.js";
|
|
3
3
|
import { setTimeout } from "node:timers/promises";
|
|
4
4
|
const PgClientInTransactionBrand = Symbol("PgClientInTransaction");
|
|
5
|
+
export class RollbackFailedError extends Error {
|
|
6
|
+
originalError;
|
|
7
|
+
rollbackError;
|
|
8
|
+
constructor(originalError, rollbackError) {
|
|
9
|
+
super("Transaction rollback failed");
|
|
10
|
+
this.originalError = originalError;
|
|
11
|
+
this.rollbackError = rollbackError;
|
|
12
|
+
this.name = "RollbackFailedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
5
15
|
const PG_ERROR_CODE = {
|
|
6
16
|
deadlock: "40P01",
|
|
7
17
|
serializationFailure: "40001",
|
|
@@ -23,25 +33,20 @@ export const IsolationLevel = {
|
|
|
23
33
|
REPEATABLE_READ: "REPEATABLE READ",
|
|
24
34
|
SERIALIZABLE: "SERIALIZABLE",
|
|
25
35
|
};
|
|
26
|
-
export async function
|
|
36
|
+
export async function withPgClientTransaction(pgClient, callbackOrIsolationLevel, maybeCallback) {
|
|
27
37
|
let callback;
|
|
28
38
|
let isolationLevel = IsolationLevel.READ_COMMITTED;
|
|
29
|
-
let retryCount = 0;
|
|
30
39
|
if (typeof callbackOrIsolationLevel === "function") {
|
|
31
40
|
callback = callbackOrIsolationLevel;
|
|
32
|
-
if (typeof callbackOrRetryCount === "number") {
|
|
33
|
-
retryCount = callbackOrRetryCount;
|
|
34
|
-
maxRetries = retryCountOrMaxRetries;
|
|
35
|
-
}
|
|
36
41
|
}
|
|
37
42
|
else {
|
|
38
43
|
isolationLevel = callbackOrIsolationLevel;
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
if (typeof maybeCallback !== "function") {
|
|
45
|
+
throw new Error("withPgClientTransaction requires a callback function");
|
|
46
|
+
}
|
|
47
|
+
callback = maybeCallback;
|
|
41
48
|
}
|
|
42
|
-
let pgClient = null;
|
|
43
49
|
try {
|
|
44
|
-
pgClient = await pool.connect();
|
|
45
50
|
if (isolationLevel === IsolationLevel.READ_COMMITTED) {
|
|
46
51
|
await pgClient.query("BEGIN");
|
|
47
52
|
}
|
|
@@ -55,39 +60,65 @@ export async function withPgPoolTransaction(pool, callbackOrIsolationLevel, call
|
|
|
55
60
|
return result;
|
|
56
61
|
}
|
|
57
62
|
catch (error) {
|
|
58
|
-
if (pgClient) {
|
|
63
|
+
if (SIDECAR.has(pgClient)) {
|
|
59
64
|
try {
|
|
60
65
|
await pgClient.query("ROLLBACK");
|
|
61
66
|
}
|
|
62
67
|
catch (rollbackError) {
|
|
63
68
|
logger.error(error, "Original error");
|
|
64
69
|
logger.error(rollbackError, "Rollback failed");
|
|
65
|
-
|
|
66
|
-
pgClient.release(true);
|
|
67
|
-
pgClient = null;
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
if (retryCount < maxRetries &&
|
|
71
|
-
error instanceof pg.DatabaseError &&
|
|
72
|
-
(error.code === PG_ERROR_CODE.deadlock ||
|
|
73
|
-
error.code === PG_ERROR_CODE.serializationFailure)) {
|
|
74
|
-
const backoffMs = Math.min(100 * Math.pow(2, retryCount), 2000);
|
|
75
|
-
logger.warn(`Retrying transaction in ${Math.floor(backoffMs)}ms (attempt ${retryCount + 2}/${maxRetries + 1}) due to pg error code ${error.code}: ${error.message}`);
|
|
76
|
-
await setTimeout(backoffMs);
|
|
77
|
-
if (isolationLevel === IsolationLevel.READ_COMMITTED) {
|
|
78
|
-
return withPgPoolTransaction(pool, callback, retryCount + 1, maxRetries);
|
|
79
|
-
}
|
|
80
|
-
else {
|
|
81
|
-
return withPgPoolTransaction(pool, isolationLevel, callback, retryCount + 1, maxRetries);
|
|
82
|
-
}
|
|
70
|
+
throw new RollbackFailedError(error, rollbackError);
|
|
83
71
|
}
|
|
84
72
|
}
|
|
85
73
|
throw error;
|
|
86
74
|
}
|
|
87
75
|
finally {
|
|
88
|
-
|
|
89
|
-
|
|
76
|
+
SIDECAR.delete(pgClient);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function withPgPoolTransaction(pool, callbackOrIsolationLevel, callbackOrRetryCount, retryCountOrMaxRetries = 0, maxRetries = 3) {
|
|
80
|
+
let callback;
|
|
81
|
+
let isolationLevel = IsolationLevel.READ_COMMITTED;
|
|
82
|
+
let retryCount = 0;
|
|
83
|
+
if (typeof callbackOrIsolationLevel === "function") {
|
|
84
|
+
callback = callbackOrIsolationLevel;
|
|
85
|
+
if (typeof callbackOrRetryCount === "number") {
|
|
86
|
+
retryCount = callbackOrRetryCount;
|
|
87
|
+
maxRetries = retryCountOrMaxRetries;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
isolationLevel = callbackOrIsolationLevel;
|
|
92
|
+
if (typeof callbackOrRetryCount !== "function") {
|
|
93
|
+
throw new Error("withPgPoolTransaction requires a callback function");
|
|
94
|
+
}
|
|
95
|
+
callback = callbackOrRetryCount;
|
|
96
|
+
retryCount = retryCountOrMaxRetries;
|
|
97
|
+
}
|
|
98
|
+
let pgClient = await pool.connect();
|
|
99
|
+
try {
|
|
100
|
+
return await withPgClientTransaction(pgClient, isolationLevel, callback);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof RollbackFailedError) {
|
|
104
|
+
pgClient.release(true);
|
|
105
|
+
pgClient = null;
|
|
106
|
+
throw error.originalError;
|
|
107
|
+
}
|
|
108
|
+
if (retryCount < maxRetries &&
|
|
109
|
+
error instanceof pg.DatabaseError &&
|
|
110
|
+
(error.code === PG_ERROR_CODE.deadlock ||
|
|
111
|
+
error.code === PG_ERROR_CODE.serializationFailure)) {
|
|
112
|
+
const backoffMs = Math.min(100 * Math.pow(2, retryCount), 2000);
|
|
113
|
+
logger.warn(`Retrying transaction in ${Math.floor(backoffMs)}ms (attempt ${retryCount + 2}/${maxRetries + 1}) due to pg error code ${error.code}: ${error.message}`);
|
|
90
114
|
pgClient.release();
|
|
115
|
+
pgClient = null;
|
|
116
|
+
await setTimeout(backoffMs);
|
|
117
|
+
return withPgPoolTransaction(pool, isolationLevel, callback, retryCount + 1, maxRetries);
|
|
91
118
|
}
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
pgClient?.release();
|
|
92
123
|
}
|
|
93
124
|
}
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NODE_ENV, PORT, SUPERUSER_DATABASE_URL, DATABASE_URL, } from "./config.js";
|
|
2
2
|
import { logger } from "./logger.js";
|
|
3
|
+
import { getPgClient } from "./db/index.js";
|
|
3
4
|
import { createHubServer } from "./server/index.js";
|
|
4
5
|
export { HubGameConfigPlugin, } from "./plugins/hub-game-config-plugin.js";
|
|
5
6
|
export { validateRisk, } from "./risk-policy.js";
|
|
@@ -21,9 +22,17 @@ function validateOptions(options) {
|
|
|
21
22
|
}
|
|
22
23
|
export async function startAndListen(options) {
|
|
23
24
|
validateOptions(options);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const pgClient = getPgClient(options.superuserDatabaseUrl ?? SUPERUSER_DATABASE_URL);
|
|
26
|
+
await pgClient.connect();
|
|
27
|
+
try {
|
|
28
|
+
await runMigrations({
|
|
29
|
+
pgClient,
|
|
30
|
+
userDatabaseMigrationsPath: options.userDatabaseMigrationsPath,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
await pgClient.end();
|
|
35
|
+
}
|
|
27
36
|
const hubServer = createHubServer({
|
|
28
37
|
superuserDatabaseUrl: options.superuserDatabaseUrl ?? SUPERUSER_DATABASE_URL,
|
|
29
38
|
postgraphileDatabaseUrl: options.postgraphileDatabaseUrl ?? DATABASE_URL,
|
package/dist/src/migrations.d.ts
CHANGED
package/dist/src/migrations.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import PgUpgradeSchema, { DatabaseAheadError, } from "@moneypot/pg-upgrade-schema";
|
|
2
|
-
import * as db from "./db/index.js";
|
|
3
|
-
import * as config from "./config.js";
|
|
4
2
|
import { join } from "path";
|
|
5
3
|
import { logger } from "./logger.js";
|
|
4
|
+
import { existsSync, statSync } from "fs";
|
|
6
5
|
export async function runMigrations(options) {
|
|
6
|
+
const { pgClient } = options;
|
|
7
7
|
if (options.userDatabaseMigrationsPath) {
|
|
8
|
-
const { existsSync, statSync } = await import("fs");
|
|
9
8
|
if (!existsSync(options.userDatabaseMigrationsPath)) {
|
|
10
9
|
throw new Error(`userDatabaseMigrationsPath does not exist: ${options.userDatabaseMigrationsPath}`);
|
|
11
10
|
}
|
|
@@ -14,38 +13,28 @@ export async function runMigrations(options) {
|
|
|
14
13
|
throw new Error(`userDatabaseMigrationsPath is not a directory: ${options.userDatabaseMigrationsPath}`);
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
|
-
const superuserDatabaseUrl = options.superuserDatabaseUrl ??
|
|
18
|
-
process.env.SUPERUSER_DATABASE_URL ??
|
|
19
|
-
config.SUPERUSER_DATABASE_URL;
|
|
20
|
-
const pgClient = db.getPgClient(superuserDatabaseUrl);
|
|
21
|
-
await pgClient.connect();
|
|
22
16
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
throw e;
|
|
38
|
-
}
|
|
39
|
-
if (options.userDatabaseMigrationsPath) {
|
|
40
|
-
await PgUpgradeSchema.default({
|
|
41
|
-
pgClient,
|
|
42
|
-
dirname: options.userDatabaseMigrationsPath,
|
|
43
|
-
schemaName: "hub_user_versions",
|
|
44
|
-
silent: process.env.NODE_ENV === "test",
|
|
45
|
-
});
|
|
17
|
+
await PgUpgradeSchema.default({
|
|
18
|
+
pgClient,
|
|
19
|
+
dirname: join(import.meta.dirname, "pg-versions"),
|
|
20
|
+
schemaName: "hub_core_versions",
|
|
21
|
+
silent: process.env.NODE_ENV === "test",
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
logger.error(e, "Error upgrading core schema");
|
|
26
|
+
if (e instanceof DatabaseAheadError) {
|
|
27
|
+
logger.error(`${"⚠️".repeat(10)}\n@moneypot/hub database was reset to prepare for a production release and you must reset your database to continue. Please see <https://www.npmjs.com/package/@moneypot/hub#change-log> for more info.`);
|
|
28
|
+
process.exit(1);
|
|
46
29
|
}
|
|
30
|
+
throw e;
|
|
47
31
|
}
|
|
48
|
-
|
|
49
|
-
await
|
|
32
|
+
if (options.userDatabaseMigrationsPath) {
|
|
33
|
+
await PgUpgradeSchema.default({
|
|
34
|
+
pgClient,
|
|
35
|
+
dirname: options.userDatabaseMigrationsPath,
|
|
36
|
+
schemaName: "hub_user_versions",
|
|
37
|
+
silent: process.env.NODE_ENV === "test",
|
|
38
|
+
});
|
|
50
39
|
}
|
|
51
40
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ClientBase } from "pg";
|
|
2
2
|
import { GraphQLClient } from "graphql-request";
|
|
3
3
|
import { Result } from "../util.js";
|
|
4
4
|
import { QueryExecutor } from "../db/index.js";
|
|
5
|
-
export declare function verifyJwtFromDbCacheAndEnsureNotAlreadyUsed(pgClient:
|
|
5
|
+
export declare function verifyJwtFromDbCacheAndEnsureNotAlreadyUsed(pgClient: ClientBase, { casinoId, jwt, }: {
|
|
6
6
|
casinoId: string;
|
|
7
7
|
jwt: string;
|
|
8
8
|
}): Promise<Result<{
|
package/dist/src/test/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Pool } from "pg";
|
|
1
|
+
import { Client, Pool } from "pg";
|
|
2
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
4
|
import { DbHashKind, } from "../db/types.js";
|
|
@@ -35,10 +35,17 @@ export async function startTestServer({ plugins = [...defaultPlugins], userDatab
|
|
|
35
35
|
process.env.LOG_LEVEL = process.env.LOG_LEVEL ?? "error";
|
|
36
36
|
process.env.HASHCHAINSERVER_URL = "mock-server";
|
|
37
37
|
process.env.HASHCHAINSERVER_APPLICATION_SECRET = "test-secret";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
const pgClient = new Client({ connectionString });
|
|
39
|
+
await pgClient.connect();
|
|
40
|
+
try {
|
|
41
|
+
await runMigrations({
|
|
42
|
+
pgClient,
|
|
43
|
+
userDatabaseMigrationsPath,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await pgClient.end();
|
|
48
|
+
}
|
|
42
49
|
const hubServer = createHubServer({
|
|
43
50
|
port: 0,
|
|
44
51
|
plugins,
|