diff --git a/README.md b/README.md
index 15486c6..f994ed4 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@
- Download |
+ Download |
Features |
Installation |
Contributing
@@ -103,7 +103,7 @@ Before building from source, ensure you have:
### From Releases (Recommended)
-Download the latest release for your platform from the [**Releases**](https://github.com/Yashh56/RelWave/releases) page:
+Download the latest release for your platform from the [**Releases**](https://github.com/Relwave/relwave-app/releases) page:
| Platform | File Type | Description |
|----------|----------|-------------|
@@ -116,7 +116,7 @@ Download the latest release for your platform from the [**Releases**](https://gi
1. Clone the repository:
```bash
-git clone https://github.com/Yashh56/RelWave.git
+git clone https://github.com/Relwave/relwave-app.git
cd RelWave
```
diff --git a/bridge/__tests__/discoveryService.test.ts b/bridge/__tests__/discoveryService.test.ts
new file mode 100644
index 0000000..e122c6c
--- /dev/null
+++ b/bridge/__tests__/discoveryService.test.ts
@@ -0,0 +1,360 @@
+import { describe, expect, test, beforeEach } from "@jest/globals";
+import { DiscoveryService, DiscoveredDatabase } from "../src/services/discoveryService";
+
+describe("DiscoveryService", () => {
+ let service: DiscoveryService;
+
+ beforeEach(() => {
+ service = new DiscoveryService();
+ });
+
+ describe("generateFunName", () => {
+ test("should generate a name in format adjective-noun", () => {
+ const name = service.generateFunName("postgresql", 5432);
+ expect(name).toMatch(/^[a-z]+-[a-z]+$/);
+ });
+
+ test("should add port suffix for non-default PostgreSQL port", () => {
+ const name = service.generateFunName("postgresql", 5433);
+ expect(name).toMatch(/^[a-z]+-[a-z]+-5433$/);
+ });
+
+ test("should not add port suffix for default PostgreSQL port", () => {
+ const name = service.generateFunName("postgresql", 5432);
+ expect(name).not.toContain("-5432");
+ });
+
+ test("should not add port suffix for default MySQL port", () => {
+ const name = service.generateFunName("mysql", 3306);
+ expect(name).not.toContain("-3306");
+ });
+
+ test("should not add port suffix for default MariaDB port", () => {
+ const name = service.generateFunName("mariadb", 3306);
+ expect(name).not.toContain("-3306");
+ });
+
+ test("should add port suffix for non-default MySQL port", () => {
+ const name = service.generateFunName("mysql", 3307);
+ expect(name).toMatch(/^[a-z]+-[a-z]+-3307$/);
+ });
+
+ test("should generate different names on multiple calls", () => {
+ // Given the randomness, most calls should produce different names
+ const names = new Set();
+ for (let i = 0; i < 20; i++) {
+ names.add(service.generateFunName("postgresql", 5432));
+ }
+ // At least some variety expected (not all same)
+ expect(names.size).toBeGreaterThan(1);
+ });
+ });
+
+ describe("extractCredentialsFromEnv (via reflection)", () => {
+ // Access private method via prototype for testing
+ const extractCredentials = (
+ svc: DiscoveryService,
+ dbType: "postgresql" | "mysql" | "mariadb",
+ envVars: Map
+ ) => {
+ return (svc as any).extractCredentialsFromEnv(dbType, envVars);
+ };
+
+ describe("PostgreSQL", () => {
+ test("should extract PostgreSQL credentials from env vars", () => {
+ const envVars = new Map([
+ ["POSTGRES_USER", "testuser"],
+ ["POSTGRES_PASSWORD", "testpass"],
+ ["POSTGRES_DB", "testdb"],
+ ]);
+
+ const result = extractCredentials(service, "postgresql", envVars);
+
+ expect(result.user).toBe("testuser");
+ expect(result.password).toBe("testpass");
+ expect(result.database).toBe("testdb");
+ });
+
+ test("should use defaults when PostgreSQL env vars are missing", () => {
+ const envVars = new Map();
+
+ const result = extractCredentials(service, "postgresql", envVars);
+
+ expect(result.user).toBe("postgres");
+ expect(result.password).toBe("");
+ expect(result.database).toBe("postgres");
+ });
+
+ test("should use POSTGRES_USER as database when POSTGRES_DB is missing", () => {
+ const envVars = new Map([
+ ["POSTGRES_USER", "customuser"],
+ ]);
+
+ const result = extractCredentials(service, "postgresql", envVars);
+
+ expect(result.user).toBe("customuser");
+ expect(result.database).toBe("customuser");
+ });
+ });
+
+ describe("MySQL", () => {
+ test("should extract MySQL credentials with custom user", () => {
+ const envVars = new Map([
+ ["MYSQL_USER", "myuser"],
+ ["MYSQL_PASSWORD", "mypass"],
+ ["MYSQL_DATABASE", "mydb"],
+ ]);
+
+ const result = extractCredentials(service, "mysql", envVars);
+
+ expect(result.user).toBe("myuser");
+ expect(result.password).toBe("mypass");
+ expect(result.database).toBe("mydb");
+ });
+
+ test("should use root password when user is root", () => {
+ const envVars = new Map([
+ ["MYSQL_ROOT_PASSWORD", "rootpass"],
+ ["MYSQL_DATABASE", "mydb"],
+ ]);
+
+ const result = extractCredentials(service, "mysql", envVars);
+
+ expect(result.user).toBe("root");
+ expect(result.password).toBe("rootpass");
+ expect(result.database).toBe("mydb");
+ });
+
+ test("should use defaults when MySQL env vars are missing", () => {
+ const envVars = new Map();
+
+ const result = extractCredentials(service, "mysql", envVars);
+
+ expect(result.user).toBe("root");
+ expect(result.password).toBe("");
+ expect(result.database).toBe("mysql");
+ });
+ });
+
+ describe("MariaDB", () => {
+ test("should extract MariaDB credentials with MARIADB_ prefix", () => {
+ const envVars = new Map([
+ ["MARIADB_USER", "mariauser"],
+ ["MARIADB_PASSWORD", "mariapass"],
+ ["MARIADB_DATABASE", "mariadb"],
+ ]);
+
+ const result = extractCredentials(service, "mariadb", envVars);
+
+ expect(result.user).toBe("mariauser");
+ expect(result.password).toBe("mariapass");
+ expect(result.database).toBe("mariadb");
+ });
+
+ test("should fall back to MYSQL_ prefix for MariaDB", () => {
+ const envVars = new Map([
+ ["MYSQL_USER", "fallbackuser"],
+ ["MYSQL_PASSWORD", "fallbackpass"],
+ ["MYSQL_DATABASE", "fallbackdb"],
+ ]);
+
+ const result = extractCredentials(service, "mariadb", envVars);
+
+ expect(result.user).toBe("fallbackuser");
+ expect(result.password).toBe("fallbackpass");
+ expect(result.database).toBe("fallbackdb");
+ });
+
+ test("should use MARIADB_ROOT_PASSWORD for root user", () => {
+ const envVars = new Map([
+ ["MARIADB_ROOT_PASSWORD", "mariarootpass"],
+ ]);
+
+ const result = extractCredentials(service, "mariadb", envVars);
+
+ expect(result.user).toBe("root");
+ expect(result.password).toBe("mariarootpass");
+ });
+
+ test("should fall back to MYSQL_ROOT_PASSWORD for root user", () => {
+ const envVars = new Map([
+ ["MYSQL_ROOT_PASSWORD", "mysqlrootpass"],
+ ]);
+
+ const result = extractCredentials(service, "mariadb", envVars);
+
+ expect(result.user).toBe("root");
+ expect(result.password).toBe("mysqlrootpass");
+ });
+ });
+ });
+
+ describe("getDefaultPort (private)", () => {
+ const getDefaultPort = (svc: DiscoveryService, type: string) => {
+ return (svc as any).getDefaultPort(type);
+ };
+
+ test("should return 5432 for postgresql", () => {
+ expect(getDefaultPort(service, "postgresql")).toBe(5432);
+ });
+
+ test("should return 3306 for mysql", () => {
+ expect(getDefaultPort(service, "mysql")).toBe(3306);
+ });
+
+ test("should return 3306 for mariadb", () => {
+ expect(getDefaultPort(service, "mariadb")).toBe(3306);
+ });
+
+ test("should return 0 for unknown type", () => {
+ expect(getDefaultPort(service, "unknown")).toBe(0);
+ });
+ });
+
+ describe("getDefaultUser (private)", () => {
+ const getDefaultUser = (svc: DiscoveryService, type: string) => {
+ return (svc as any).getDefaultUser(type);
+ };
+
+ test("should return postgres for postgresql", () => {
+ expect(getDefaultUser(service, "postgresql")).toBe("postgres");
+ });
+
+ test("should return root for mysql", () => {
+ expect(getDefaultUser(service, "mysql")).toBe("root");
+ });
+
+ test("should return root for mariadb", () => {
+ expect(getDefaultUser(service, "mariadb")).toBe("root");
+ });
+
+ test("should return empty string for unknown type", () => {
+ expect(getDefaultUser(service, "unknown")).toBe("");
+ });
+ });
+
+ describe("getDefaultDatabase (private)", () => {
+ const getDefaultDatabase = (svc: DiscoveryService, type: string) => {
+ return (svc as any).getDefaultDatabase(type);
+ };
+
+ test("should return postgres for postgresql", () => {
+ expect(getDefaultDatabase(service, "postgresql")).toBe("postgres");
+ });
+
+ test("should return mysql for mysql", () => {
+ expect(getDefaultDatabase(service, "mysql")).toBe("mysql");
+ });
+
+ test("should return mysql for mariadb", () => {
+ expect(getDefaultDatabase(service, "mariadb")).toBe("mysql");
+ });
+
+ test("should return empty string for unknown type", () => {
+ expect(getDefaultDatabase(service, "unknown")).toBe("");
+ });
+ });
+
+ describe("DiscoveredDatabase interface", () => {
+ test("should have all required properties for local source", () => {
+ const db: DiscoveredDatabase = {
+ type: "postgresql",
+ host: "localhost",
+ port: 5432,
+ source: "local",
+ suggestedName: "test-db",
+ defaultUser: "postgres",
+ defaultDatabase: "postgres",
+ };
+
+ expect(db.type).toBe("postgresql");
+ expect(db.host).toBe("localhost");
+ expect(db.port).toBe(5432);
+ expect(db.source).toBe("local");
+ expect(db.suggestedName).toBe("test-db");
+ expect(db.defaultUser).toBe("postgres");
+ expect(db.defaultDatabase).toBe("postgres");
+ expect(db.containerName).toBeUndefined();
+ expect(db.defaultPassword).toBeUndefined();
+ });
+
+ test("should accept optional properties for Docker containers", () => {
+ const db: DiscoveredDatabase = {
+ type: "mysql",
+ host: "localhost",
+ port: 3306,
+ source: "docker",
+ containerName: "mysql-container",
+ suggestedName: "test-mysql",
+ defaultUser: "root",
+ defaultDatabase: "mydb",
+ defaultPassword: "secret123",
+ };
+
+ expect(db.source).toBe("docker");
+ expect(db.containerName).toBe("mysql-container");
+ expect(db.defaultPassword).toBe("secret123");
+ });
+
+ test("should accept all database types", () => {
+ const pgDb: DiscoveredDatabase = {
+ type: "postgresql",
+ host: "localhost",
+ port: 5432,
+ source: "local",
+ suggestedName: "pg-db",
+ defaultUser: "postgres",
+ defaultDatabase: "postgres",
+ };
+
+ const mysqlDb: DiscoveredDatabase = {
+ type: "mysql",
+ host: "localhost",
+ port: 3306,
+ source: "local",
+ suggestedName: "mysql-db",
+ defaultUser: "root",
+ defaultDatabase: "mysql",
+ };
+
+ const mariaDb: DiscoveredDatabase = {
+ type: "mariadb",
+ host: "localhost",
+ port: 3306,
+ source: "docker",
+ suggestedName: "maria-db",
+ defaultUser: "root",
+ defaultDatabase: "mysql",
+ };
+
+ expect(pgDb.type).toBe("postgresql");
+ expect(mysqlDb.type).toBe("mysql");
+ expect(mariaDb.type).toBe("mariadb");
+ });
+ });
+
+ describe("discoverLocalDatabases", () => {
+ test("should return an array", async () => {
+ // This test just verifies the method exists and returns an array
+ // Actual discovery depends on system state
+ const result = await service.discoverLocalDatabases();
+ expect(Array.isArray(result)).toBe(true);
+ });
+
+ test("each discovered database should have required fields", async () => {
+ const result = await service.discoverLocalDatabases();
+
+ for (const db of result) {
+ expect(db.type).toBeDefined();
+ expect(["postgresql", "mysql", "mariadb"]).toContain(db.type);
+ expect(db.host).toBeDefined();
+ expect(typeof db.port).toBe("number");
+ expect(db.source).toBeDefined();
+ expect(["local", "docker"]).toContain(db.source);
+ expect(db.suggestedName).toBeDefined();
+ expect(db.defaultUser).toBeDefined();
+ expect(db.defaultDatabase).toBeDefined();
+ }
+ });
+ });
+});
diff --git a/bridge/package.json b/bridge/package.json
index 047480c..61fe8fd 100644
--- a/bridge/package.json
+++ b/bridge/package.json
@@ -1,6 +1,6 @@
{
"name": "db-visualizer-bridge",
- "version": "0.1.0",
+ "version": "0.1.0-beta.5",
"type": "commonjs",
"main": "dist/index.cjs",
"scripts": {
diff --git a/bridge/src/jsonRpcHandler.ts b/bridge/src/jsonRpcHandler.ts
index 607879b..ba92907 100644
--- a/bridge/src/jsonRpcHandler.ts
+++ b/bridge/src/jsonRpcHandler.ts
@@ -7,6 +7,7 @@ import { DatabaseHandlers } from "./handlers/databaseHandlers";
import { SessionHandlers } from "./handlers/sessionHandlers";
import { StatsHandlers } from "./handlers/statsHandlers";
import { MigrationHandlers } from "./handlers/migrationHandlers";
+import { discoveryService } from "./services/discoveryService";
import { Logger } from "pino";
/**
@@ -164,6 +165,18 @@ export function registerDbHandlers(
statsHandlers.handleGetTotalStats(p, id)
);
+ // ==========================================
+ // DATABASE DISCOVERY HANDLERS
+ // ==========================================
+ rpcRegister("db.discover", async (_p, id) => {
+ try {
+ const discovered = await discoveryService.discoverLocalDatabases();
+ rpc.sendResponse(id, { ok: true, data: discovered });
+ } catch (error: any) {
+ rpc.sendError(id, { code: "DISCOVERY_ERROR", message: error.message });
+ }
+ });
+
logger?.info("All RPC handlers registered successfully");
}
diff --git a/bridge/src/services/discoveryService.ts b/bridge/src/services/discoveryService.ts
new file mode 100644
index 0000000..7045c97
--- /dev/null
+++ b/bridge/src/services/discoveryService.ts
@@ -0,0 +1,337 @@
+/**
+ * Database Discovery Service
+ *
+ * Automatically discovers locally running databases by scanning common ports
+ * and detecting Docker containers with database images.
+ */
+
+import { exec } from "child_process";
+import { promisify } from "util";
+import * as net from "net";
+
+const execAsync = promisify(exec);
+
+// Port configurations for each database type
+const DATABASE_PORTS: Record = {
+ postgresql: [5432, 5433, 5434],
+ mysql: [3306, 3307, 3308],
+ mariadb: [3306, 3307, 3308],
+};
+
+// Fun adjectives and nouns for generating database names
+const ADJECTIVES = [
+ "swift", "cosmic", "stellar", "nimble", "turbo", "quantum", "neon", "cyber",
+ "atomic", "hyper", "mega", "ultra", "blazing", "electric", "dynamic", "rapid",
+ "mighty", "brave", "clever", "noble", "fierce", "silent", "golden", "crystal",
+ "shadow", "frost", "thunder", "ember", "azure", "crimson", "violet", "lunar",
+];
+
+const NOUNS = [
+ "phoenix", "dragon", "falcon", "panther", "tiger", "wolf", "hawk", "eagle",
+ "lion", "bear", "shark", "cobra", "viper", "raven", "storm", "blaze",
+ "nova", "comet", "nebula", "galaxy", "cosmos", "orbit", "pulse", "flux",
+ "spark", "bolt", "wave", "surge", "core", "nexus", "vertex", "matrix",
+];
+
+export interface DiscoveredDatabase {
+ type: "postgresql" | "mysql" | "mariadb";
+ host: string;
+ port: number;
+ source: "local" | "docker";
+ containerName?: string;
+ suggestedName: string;
+ defaultUser: string;
+ defaultDatabase: string;
+ defaultPassword?: string; // Only available for Docker containers
+}
+
+export class DiscoveryService {
+ private connectionTimeout = 500; // ms to wait for port response
+
+ /**
+ * Generate a fun name for a database connection
+ */
+ generateFunName(type: string, port: number): string {
+ const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
+ const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
+ const suffix = port !== this.getDefaultPort(type) ? `-${port}` : "";
+ return `${adjective}-${noun}${suffix}`;
+ }
+
+ /**
+ * Get the default port for a database type
+ */
+ private getDefaultPort(type: string): number {
+ switch (type) {
+ case "postgresql":
+ return 5432;
+ case "mysql":
+ case "mariadb":
+ return 3306;
+ default:
+ return 0;
+ }
+ }
+
+ /**
+ * Get default username for a database type
+ */
+ private getDefaultUser(type: string): string {
+ switch (type) {
+ case "postgresql":
+ return "postgres";
+ case "mysql":
+ case "mariadb":
+ return "root";
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Get default database name for a database type
+ */
+ private getDefaultDatabase(type: string): string {
+ switch (type) {
+ case "postgresql":
+ return "postgres";
+ case "mysql":
+ case "mariadb":
+ return "mysql";
+ default:
+ return "";
+ }
+ }
+
+ /**
+ * Check if a specific port is open on a host
+ */
+ private async isPortOpen(host: string, port: number): Promise {
+ return new Promise((resolve) => {
+ const socket = new net.Socket();
+
+ socket.setTimeout(this.connectionTimeout);
+
+ socket.on("connect", () => {
+ socket.destroy();
+ resolve(true);
+ });
+
+ socket.on("timeout", () => {
+ socket.destroy();
+ resolve(false);
+ });
+
+ socket.on("error", () => {
+ socket.destroy();
+ resolve(false);
+ });
+
+ socket.connect(port, host);
+ });
+ }
+
+ /**
+ * Scan localhost for open database ports
+ */
+ private async scanLocalPorts(): Promise {
+ const discovered: DiscoveredDatabase[] = [];
+ const hosts = ["127.0.0.1", "localhost"];
+ const scannedPorts = new Set();
+
+ for (const host of hosts) {
+ for (const [dbType, ports] of Object.entries(DATABASE_PORTS)) {
+ for (const port of ports) {
+ const key = `${port}`;
+ if (scannedPorts.has(key)) continue;
+
+ const isOpen = await this.isPortOpen(host, port);
+ if (isOpen) {
+ scannedPorts.add(key);
+
+ // Determine the actual type based on the port
+ let actualType: "postgresql" | "mysql" | "mariadb" = dbType as any;
+ if (port >= 5432 && port <= 5434) {
+ actualType = "postgresql";
+ } else if (port >= 3306 && port <= 3308) {
+ // Can't distinguish MySQL from MariaDB by port alone
+ actualType = "mysql";
+ }
+
+ discovered.push({
+ type: actualType,
+ host: "localhost",
+ port,
+ source: "local",
+ suggestedName: this.generateFunName(actualType, port),
+ defaultUser: this.getDefaultUser(actualType),
+ defaultDatabase: this.getDefaultDatabase(actualType),
+ });
+ }
+ }
+ }
+ }
+
+ return discovered;
+ }
+
+ /**
+ * Get environment variables from a Docker container
+ */
+ private async getContainerEnvVars(containerName: string): Promise