@moneypot/hub 1.19.10 → 1.19.12

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.
@@ -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();
@@ -4,7 +4,7 @@ export declare const PORT: number;
4
4
  export declare const LOG_LEVEL: string;
5
5
  export declare const LOG_PRETTY: boolean | undefined;
6
6
  export declare const MP_GRAPHQL_URL: string;
7
- export declare const DATABASE_URL: string;
7
+ export declare const POSTGRAPHILE_DATABASE_URL: string;
8
8
  export declare const SUPERUSER_DATABASE_URL: string;
9
9
  export declare const HASHCHAINSERVER_URL: string;
10
10
  export declare const HASHCHAINSERVER_MAX_ITERATIONS: number;
@@ -69,16 +69,23 @@ export const MP_GRAPHQL_URL = getEnvVariable("MP_GRAPHQL_URL", (value) => {
69
69
  }
70
70
  return value;
71
71
  });
72
- export const DATABASE_URL = getEnvVariable("DATABASE_URL", (value) => {
72
+ export const POSTGRAPHILE_DATABASE_URL = getEnvVariable("POSTGRAPHILE_DATABASE_URL", (value) => {
73
73
  if (!value) {
74
- throw new Error(`Missing DATABASE_URL env var.`);
74
+ const legacyValue = process.env.DATABASE_URL;
75
+ if (legacyValue) {
76
+ console.warn("⚠️ DATABASE_URL is deprecated. Please rename it to POSTGRAPHILE_DATABASE_URL.");
77
+ value = legacyValue;
78
+ }
79
+ else {
80
+ throw new Error(`Missing POSTGRAPHILE_DATABASE_URL env var.`);
81
+ }
75
82
  }
76
83
  if (!URL.parse(value)) {
77
- console.warn("DATABASE_URL is not a valid URL.");
84
+ console.warn("POSTGRAPHILE_DATABASE_URL is not a valid URL.");
78
85
  }
79
86
  const databaseUrlUsername = pgConnectionString.parse(value).user;
80
87
  if (databaseUrlUsername !== "app_postgraphile") {
81
- console.warn(`DATABASE_URL username is ${databaseUrlUsername}, expected app_postgraphile`);
88
+ console.warn(`POSTGRAPHILE_DATABASE_URL username is ${databaseUrlUsername}, expected app_postgraphile`);
82
89
  }
83
90
  return value;
84
91
  });
@@ -3,7 +3,7 @@ import * as config from "./config.js";
3
3
  import { logger } from "./logger.js";
4
4
  import { DatabaseNotifier } from "./db/index.js";
5
5
  export function createServerContext(overrides) {
6
- const postgraphileDatabaseUrl = overrides?.postgraphileDatabaseUrl ?? config.DATABASE_URL;
6
+ const postgraphileDatabaseUrl = overrides?.postgraphileDatabaseUrl ?? config.POSTGRAPHILE_DATABASE_URL;
7
7
  const superuserDatabaseUrl = overrides?.superuserDatabaseUrl ?? config.SUPERUSER_DATABASE_URL;
8
8
  const context = {
9
9
  postgraphilePool: new pg.Pool({
@@ -1,10 +1,15 @@
1
1
  import * as pg from "pg";
2
2
  declare const PgClientInTransactionBrand: unique symbol;
3
- export type PgClientInTransaction = pg.PoolClient & {
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.PoolClient): pgClient is PgClientInTransaction;
7
- export declare function assertInTransaction(pgClient: pg.PoolClient): asserts pgClient is PgClientInTransaction;
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 withPgPoolTransaction(pool, callbackOrIsolationLevel, callbackOrRetryCount, retryCountOrMaxRetries = 0, maxRetries = 3) {
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
- callback = callbackOrRetryCount;
40
- retryCount = retryCountOrMaxRetries;
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
- SIDECAR.delete(pgClient);
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
- if (pgClient) {
89
- SIDECAR.delete(pgClient);
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
- import { NODE_ENV, PORT, SUPERUSER_DATABASE_URL, DATABASE_URL, } from "./config.js";
1
+ import { NODE_ENV, PORT, SUPERUSER_DATABASE_URL, POSTGRAPHILE_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,12 +22,20 @@ function validateOptions(options) {
21
22
  }
22
23
  export async function startAndListen(options) {
23
24
  validateOptions(options);
24
- await runMigrations({
25
- userDatabaseMigrationsPath: options.userDatabaseMigrationsPath,
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
- postgraphileDatabaseUrl: options.postgraphileDatabaseUrl ?? DATABASE_URL,
38
+ postgraphileDatabaseUrl: options.postgraphileDatabaseUrl ?? POSTGRAPHILE_DATABASE_URL,
30
39
  configureApp: options.configureApp,
31
40
  plugins: options.plugins,
32
41
  exportSchemaSDLPath: options.exportSchemaSDLPath,
@@ -1,4 +1,5 @@
1
+ import { type Client } from "pg";
1
2
  export declare function runMigrations(options: {
3
+ pgClient: Client;
2
4
  userDatabaseMigrationsPath?: string;
3
- superuserDatabaseUrl?: string;
4
5
  }): Promise<void>;
@@ -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
- try {
24
- await PgUpgradeSchema.default({
25
- pgClient,
26
- dirname: join(import.meta.dirname, "pg-versions"),
27
- schemaName: "hub_core_versions",
28
- silent: process.env.NODE_ENV === "test",
29
- });
30
- }
31
- catch (e) {
32
- logger.error(e, "Error upgrading core schema");
33
- if (e instanceof DatabaseAheadError) {
34
- 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.`);
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
- finally {
49
- await pgClient.end();
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 { PoolClient } from "pg";
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: PoolClient, { casinoId, jwt, }: {
5
+ export declare function verifyJwtFromDbCacheAndEnsureNotAlreadyUsed(pgClient: ClientBase, { casinoId, jwt, }: {
6
6
  casinoId: string;
7
7
  jwt: string;
8
8
  }): Promise<Result<{
@@ -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
- await runMigrations({
39
- userDatabaseMigrationsPath,
40
- superuserDatabaseUrl: connectionString,
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneypot/hub",
3
- "version": "1.19.10",
3
+ "version": "1.19.12",
4
4
  "author": "moneypot.com",
5
5
  "homepage": "https://moneypot.com/hub",
6
6
  "keywords": [