/ opencodehub / src / db / index.ts
swadhinbiswas fix(security): remove global TLS bypass for DB connections · 2/15/2026
6345c71
159 lines · 4.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import Database from "better-sqlite3";
import { BetterSQLite3Database, drizzle } from "drizzle-orm/better-sqlite3";
import { drizzle as drizzleLibSQL, LibSQLDatabase } from "drizzle-orm/libsql";
import { drizzle as drizzlePg, NodePgDatabase } from "drizzle-orm/node-postgres";
import { createClient } from "@libsql/client";
import pg from "pg";
import { existsSync, mkdirSync } from "fs";
import { dirname } from "path";
import { logger } from "@/lib/logger";
import * as schema from "./schema";

// Database driver types
export type DatabaseDriver = "sqlite" | "postgres" | "mysql" | "libsql" | "turso";

let db: BetterSQLite3Database<typeof schema> | LibSQLDatabase<typeof schema> | NodePgDatabase<typeof schema> | null =
  null;

/**
 * Get database configuration from environment
 */
function getDbConfig(): { driver: DatabaseDriver; url: string } {
  // Use import.meta.env for Astro/Vite, fallback to process.env for scripts
  const envDriver = import.meta.env?.DATABASE_DRIVER || process.env.DATABASE_DRIVER;
  const envUrl = import.meta.env?.DATABASE_URL || process.env.DATABASE_URL;

  const driver = (envDriver || "sqlite") as DatabaseDriver;
  const url = envUrl || "./data/opencodehub.db";

  return { driver, url };
}

function isTruthy(value: string | undefined): boolean {
  return value === "1" || value?.toLowerCase() === "true";
}

function shouldRejectUnauthorized(value: string | undefined): boolean {
  if (value === undefined) return true;
  return !["0", "false"].includes(value.toLowerCase());
}

/**
 * Get or create database connection
 */
export function getDatabase():
  | BetterSQLite3Database<typeof schema>
  | LibSQLDatabase<typeof schema>
  | NodePgDatabase<typeof schema> {
  if (db) return db;

  const { driver, url } = getDbConfig();

  // Support for Turso/LibSQL
  if (driver === "libsql" || driver === "turso") {
    const authToken = import.meta.env?.DATABASE_AUTH_TOKEN || process.env.DATABASE_AUTH_TOKEN;
    const client = createClient({
      url,
      authToken,
    });
    db = drizzleLibSQL(client, { schema });
    logger.info({ driver: "libsql", url }, "Database connected (LibSQL)");
    return db;
  }

  // Support for PostgreSQL
  if (driver === "postgres") {
    const sslEnabled =
      isTruthy(process.env.DATABASE_SSL) ||
      /[?&]sslmode=require/i.test(url);
    const rejectUnauthorized = shouldRejectUnauthorized(
      process.env.DATABASE_SSL_REJECT_UNAUTHORIZED
    );

    const cleanUrl = url.replace(/([?&])sslmode=require(&|$)/i, (_, prefix, suffix) =>
      suffix === "&" ? prefix : ""
    );
    const pool = new pg.Pool({
      connectionString: cleanUrl,
      ssl: sslEnabled
        ? { rejectUnauthorized }
        : undefined,
      max: parseInt(process.env.DATABASE_POOL_SIZE || "10", 10),
    });
    db = drizzlePg(pool, { schema });
    logger.info(
      { driver: "postgres", sslEnabled, rejectUnauthorized: sslEnabled ? rejectUnauthorized : undefined },
      "Database connected (PostgreSQL)"
    );
    return db;
  }

  // Currently only SQLite, LibSQL and Postgres are fully supported
  if (driver === "mysql") {
    logger.warn(
      { driver, url },
      "MySQL driver validation pending. Falling back to SQLite."
    );
  }

  const dbPath = (driver === "sqlite" || driver === "mysql") ? url : "./data/opencodehub.db";

  // Ensure directory exists for local SQLite
  if (!url.includes("://")) {
    const dir = dirname(dbPath);
    if (!existsSync(dir)) {
      mkdirSync(dir, { recursive: true });
    }
  }

  const sqlite = new Database(dbPath);
  sqlite.pragma("journal_mode = WAL");
  sqlite.pragma("foreign_keys = ON");

  db = drizzle(sqlite, { schema });
  logger.info({ driver: "sqlite", path: dbPath }, "Database connected");

  return db;
}

/**
 * Get current database driver type
 */
export function getDriverType(): DatabaseDriver {
  const { driver } = getDbConfig();
  return driver;
}

/**
 * Check if database is connected
 */
export function isDatabaseConnected(): boolean {
  return db !== null;
}

/**
 * Close database connection
 */
export async function closeDatabase(): Promise<void> {
  if (db) {
    if ('close' in db) {
      // Handle SQLite close
      // @ts-ignore
      db.close();
    }
    // For PG pool, we might need to close the pool if we had access to it, 
    // but Drizzle doesn't expose it directly on the db instance easily without type casting.
    // In serverless/long-running app, closing might not be strictly necessary unless ensuring graceful shutdown.

    logger.info("Database connection closed");
    db = null;
  }
}

// Export schema and types
export { schema };
export type Database =
  | BetterSQLite3Database<typeof schema>
  | LibSQLDatabase<typeof schema>
  | NodePgDatabase<typeof schema>;