mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 09:50:43 +00:00
add database layer
This commit is contained in:
Generated
+109
@@ -58,6 +58,7 @@
|
||||
"@types/jquery": "^3.5.31",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.5.11",
|
||||
@@ -4701,6 +4702,18 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.11.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.11.tgz",
|
||||
"integrity": "sha512-kGT1qKM8wJQ5qlawUrEkXgvMSXoV213KfMGXcwfDwUIfUHXqXYXOfS1nE1LINRJVVVx5wCm70XnFlMHaIcQAfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
"pg-types": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.16.tgz",
|
||||
@@ -12997,6 +13010,52 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-numeric": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz",
|
||||
"integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.1.tgz",
|
||||
"integrity": "sha512-gjTHWGYWsEgy9MsY0Gp6ZJxV24IjDqdpTW7Eh0x+WfJLFsm/TJx1MzL6T0D88mBvkpxotCQ6TwW6N+Kko7lhgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
|
||||
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"pg-numeric": "1.0.2",
|
||||
"postgres-array": "~3.0.1",
|
||||
"postgres-bytea": "~3.0.0",
|
||||
"postgres-date": "~2.1.0",
|
||||
"postgres-interval": "^3.0.0",
|
||||
"postgres-range": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/phin": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz",
|
||||
@@ -13382,6 +13441,56 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
|
||||
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
|
||||
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"obuf": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
|
||||
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
|
||||
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-range": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz",
|
||||
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"@types/jquery": "^3.5.31",
|
||||
"@types/mocha": "^10.0.7",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/pg": "^8.11.11",
|
||||
"@types/sinon": "^17.0.3",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/ws": "^8.5.11",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Pool, PoolConfig } from "pg";
|
||||
|
||||
export interface SessionData {
|
||||
discord_id: string;
|
||||
session_id: string;
|
||||
metadata?: Record<string, any>;
|
||||
created_at?: Date;
|
||||
last_active?: Date;
|
||||
}
|
||||
|
||||
export class Database {
|
||||
private pool: Pool;
|
||||
|
||||
constructor(pool: Pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates or updates a session
|
||||
*/
|
||||
async upsertSession(data: {
|
||||
discord_id: string;
|
||||
session_id: string;
|
||||
metadata?: Record<string, any>;
|
||||
}): Promise<SessionData> {
|
||||
const { discord_id, session_id, metadata = {} } = data;
|
||||
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
`INSERT INTO player_sessions (discord_id, session_id, metadata)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (session_id)
|
||||
DO UPDATE SET
|
||||
last_active = CURRENT_TIMESTAMP,
|
||||
metadata = player_sessions.metadata || $3::jsonb
|
||||
RETURNING *`,
|
||||
[discord_id, session_id, metadata],
|
||||
);
|
||||
return result.rows[0];
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create/update session: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a session by its ID
|
||||
*/
|
||||
async getSession(sessionId: string): Promise<SessionData | null> {
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
"SELECT * FROM player_sessions WHERE session_id = $1",
|
||||
[sessionId],
|
||||
);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch session: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all sessions for a Discord user
|
||||
*/
|
||||
async getUserSessions(discordId: string): Promise<SessionData[]> {
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
"SELECT * FROM player_sessions WHERE discord_id = $1 ORDER BY last_active DESC",
|
||||
[discordId],
|
||||
);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch user sessions: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a session by its ID
|
||||
*/
|
||||
async deleteSession(sessionId: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.pool.query(
|
||||
"DELETE FROM player_sessions WHERE session_id = $1 RETURNING *",
|
||||
[sessionId],
|
||||
);
|
||||
return result.rows.length > 0;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete session: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Pool, PoolClient } from "pg";
|
||||
import dotenv from "dotenv";
|
||||
import { schemas } from "./Schema";
|
||||
dotenv.config();
|
||||
|
||||
// Environment variable interface for type safety
|
||||
interface DBConfig {
|
||||
user: string;
|
||||
host: string;
|
||||
database: string;
|
||||
password: string;
|
||||
port: number;
|
||||
ssl?: {
|
||||
rejectUnauthorized: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
// Create the config from environment variables
|
||||
const createDBConfig = (): DBConfig => {
|
||||
const config: DBConfig = {
|
||||
user: process.env.DB_USER || "",
|
||||
host: process.env.DB_HOST || "",
|
||||
database: process.env.DB_NAME || "",
|
||||
password: process.env.DB_PASSWORD || "",
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
};
|
||||
|
||||
// Add SSL if enabled
|
||||
if (process.env.DB_SSL === "true") {
|
||||
config.ssl = { rejectUnauthorized: false };
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const pool = new Pool(createDBConfig());
|
||||
|
||||
// Error handling for the pool
|
||||
pool.on("error", (err: Error) => {
|
||||
console.error("Unexpected error on idle client", err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
// Initialize database
|
||||
export const initDB = async (): Promise<void> => {
|
||||
let client: PoolClient | null = null;
|
||||
try {
|
||||
client = await pool.connect();
|
||||
console.log("Connected to database, initializing schemas...");
|
||||
|
||||
// Execute all schema creation queries
|
||||
await client.query(schemas.playerSessions);
|
||||
|
||||
console.log("Database initialization completed");
|
||||
} catch (error) {
|
||||
console.error("Error initializing database:", error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (client) {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get a client from the pool
|
||||
export const getClient = async (): Promise<PoolClient> => {
|
||||
const client = await pool.connect();
|
||||
return client;
|
||||
};
|
||||
|
||||
// Query helper with automatic client release
|
||||
export const query = async (text: string, params?: any[]) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
return await client.query(text, params);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
// Transaction helper
|
||||
export const transaction = async <T>(
|
||||
callback: (client: PoolClient) => Promise<T>,
|
||||
): Promise<T> => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const result = await callback(client);
|
||||
await client.query("COMMIT");
|
||||
return result;
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
// Health check function
|
||||
export const checkConnection = async (): Promise<boolean> => {
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query("SELECT 1");
|
||||
return true;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Database connection check failed:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Clean up function for graceful shutdown
|
||||
export const closePool = async (): Promise<void> => {
|
||||
await pool.end();
|
||||
};
|
||||
|
||||
export default pool;
|
||||
@@ -0,0 +1,22 @@
|
||||
// src/db/schema.ts
|
||||
export const TABLES = {
|
||||
PLAYER_SESSIONS: "player_sessions",
|
||||
} as const;
|
||||
|
||||
export const schemas = {
|
||||
playerSessions: `
|
||||
CREATE TABLE IF NOT EXISTS ${TABLES.PLAYER_SESSIONS} (
|
||||
id SERIAL PRIMARY KEY,
|
||||
discord_id TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
last_active TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
metadata JSONB DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_discord_id
|
||||
ON ${TABLES.PLAYER_SESSIONS}(discord_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_session_id
|
||||
ON ${TABLES.PLAYER_SESSIONS}(session_id);
|
||||
`,
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface PlayerSession {
|
||||
discord_id: string;
|
||||
session_id: string;
|
||||
created_at?: Date;
|
||||
last_active?: Date;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
Reference in New Issue
Block a user