Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions webapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,22 @@ This service ports the Discord AdventureBot experience to a standalone web stack
```bash
npm install
```
3. Run the development server with automatic reloads:
3. Create or refresh the database schema and seed data from the repository dump:
```bash
npm run db:setup
```
This script creates the configured database if it does not exist, then installs the AdventureBot tables and seed rows from
`database/dump.sql`.

4. Run the development server with automatic reloads:
```bash
npm run dev
```
4. Build the production bundle:
5. Build the production bundle:
```bash
npm run build
```
5. Start the compiled server:
6. Start the compiled server:
```bash
npm start
```
Expand Down
5 changes: 3 additions & 2 deletions webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"dev": "ts-node-dev --respawn --transpile-only src/server.ts",
"build": "tsc --project tsconfig.json",
"start": "node dist/server.js",
"lint": "eslint \"src/**/*.ts\""
"lint": "eslint \"src/**/*.ts\"",
"db:setup": "npm run build && node dist/db/setup.js"
},
"engines": {
"node": ">=18"
Expand Down Expand Up @@ -37,4 +38,4 @@
"ts-node-dev": "^2.0.0",
"typescript": "5.5.4"
}
}
}
85 changes: 85 additions & 0 deletions webapp/src/db/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import fs from 'fs';
import path from 'path';
import mysql, { RowDataPacket } from 'mysql2/promise';
import { getEnv } from '../config/env.js';
import { logger } from '../logger.js';

const env = getEnv();

const loadSetupStatements = (): string[] => {
const sqlPath = path.resolve(process.cwd(), '..', 'database', 'dump.sql');
const sql = fs.readFileSync(sqlPath, 'utf8');

const cleaned = sql
.split('\n')
.map((line) => line.trim())
.filter((line) => line && !line.startsWith('--'))
.join('\n');

return cleaned
.split(';')
.map((statement) => statement.trim())
.filter((statement) => statement.length > 0);
};

const databaseExists = async (connection: mysql.Connection): Promise<boolean> => {
const [rows] = await connection.query<RowDataPacket[]>(
'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?',
[env.mysql.database]
);

return rows.length > 0;
};

const hasSchema = async (connection: mysql.Connection): Promise<boolean> => {
const [rows] = await connection.query<RowDataPacket[]>("SHOW TABLES LIKE 'difficulties'");
return rows.length > 0;
};

export const ensureDatabaseSetup = async (): Promise<void> => {
const adminConnection = await mysql.createConnection({
host: env.mysql.host,
port: env.mysql.port,
user: env.mysql.user,
password: env.mysql.password,
});

try {
const exists = await databaseExists(adminConnection);
if (!exists) {
await adminConnection.query(`CREATE DATABASE IF NOT EXISTS \`${env.mysql.database}\``);
logger.info({ database: env.mysql.database }, 'Created missing database');
}
} finally {
await adminConnection.end();
}

const connection = await mysql.createConnection({
host: env.mysql.host,
port: env.mysql.port,
user: env.mysql.user,
password: env.mysql.password,
database: env.mysql.database,
multipleStatements: false,
});

try {
const schemaPresent = await hasSchema(connection);
if (schemaPresent) {
logger.info('Database schema already present; skipping setup');
return;
}

const statements = loadSetupStatements();
for (const statement of statements) {
await connection.query(statement);
}

logger.info('Database schema and seed data installed');
} catch (error) {
logger.error({ err: error }, 'Database setup failed');
throw error;
} finally {
await connection.end();
}
};
17 changes: 14 additions & 3 deletions webapp/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { createApp } from './app.js';
import { getEnv } from './config/env.js';
import { ensureDatabaseSetup } from './db/setup.js';
import { logger } from './logger.js';

const env = getEnv();
const app = createApp();

app.listen(env.port, () => {
logger.info({ port: env.port, nodeEnv: env.nodeEnv }, 'Web app server listening');
});
const startServer = async () => {
try {
await ensureDatabaseSetup();
app.listen(env.port, () => {
logger.info({ port: env.port, nodeEnv: env.nodeEnv }, 'Web app server listening');
});
} catch (error) {
logger.error({ err: error }, 'Failed to start server');
process.exit(1);
}
};

startServer();
6 changes: 3 additions & 3 deletions webapp/src/services/difficulties.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Difficulty, DifficultyDefinition } from './types.js';
import { Difficulty, DifficultyDefinition, difficultyKeys } from './types.js';

export const difficultyOrder: Difficulty[] = ['easy', 'medium', 'hard', 'crazy_catto'];
export const difficultyOrder: Difficulty[] = [...difficultyKeys];

export const difficultyDefinitions: Record<Difficulty, DifficultyDefinition> = {
easy: {
Expand Down Expand Up @@ -63,4 +63,4 @@ export const difficultyDefinitions: Record<Difficulty, DifficultyDefinition> = {

export const getDifficultyDefinition = (key: Difficulty): DifficultyDefinition => difficultyDefinitions[key];

export const difficultyKeys = Object.keys(difficultyDefinitions) as Difficulty[];
export { difficultyKeys };
3 changes: 2 additions & 1 deletion webapp/src/services/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type Difficulty = 'easy' | 'medium' | 'hard' | 'crazy_catto';
export const difficultyKeys = ['easy', 'medium', 'hard', 'crazy_catto'] as const;
export type Difficulty = (typeof difficultyKeys)[number];

export interface DifficultyDefinition {
key: Difficulty;
Expand Down
Loading