institutional-trader/backend/src/db.js

352 lines
13 KiB
JavaScript

import { createClient } from '@supabase/supabase-js';
import dotenv from 'dotenv';
import { Pool } from 'pg';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
// Get the directory of the current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load .env from backend root (parent directory of src)
dotenv.config({ path: join(__dirname, '..', '.env') });
// Check if using local database
const useLocalDb = process.env.USE_LOCAL_DB === 'true';
// Direct Postgres pool for heavy queries (local or remote)
let pgPool = null;
if (useLocalDb) {
// Use local PostgreSQL database
const localDbConfig = {
host: process.env.LOCAL_DB_HOST || '192.168.8.151',
port: parseInt(process.env.LOCAL_DB_PORT || '5432'),
user: process.env.LOCAL_DB_USER || 'postgres',
password: process.env.LOCAL_DB_PASSWORD || 'postgres',
database: process.env.LOCAL_DB_NAME || 'institutional_trader'
};
if (!localDbConfig.password) {
throw new Error(
'Missing LOCAL_DB_PASSWORD for local database.\n' +
'Please add to your .env file:\n' +
'USE_LOCAL_DB=true\n' +
'LOCAL_DB_PASSWORD=your_postgres_password\n' +
'See LOCAL_DB_SETUP.md for setup instructions'
);
}
pgPool = new Pool({
host: localDbConfig.host,
port: localDbConfig.port,
user: localDbConfig.user,
password: localDbConfig.password,
database: localDbConfig.database,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000,
});
pgPool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
});
console.log('📦 Using local PostgreSQL database');
} else if (process.env.DATABASE_URL) {
// Use remote PostgreSQL via connection string
try {
pgPool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000, // Increased timeout
});
pgPool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
});
} catch (err) {
console.warn('Failed to create PostgreSQL pool:', err.message);
console.warn('Will fall back to Supabase client');
}
}
// Supabase client (only initialized if not using local DB)
let supabase = null;
let supabaseAnon = null;
if (!useLocalDb) {
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_KEY;
const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_KEY;
if (supabaseUrl && supabaseKey) {
// Create Supabase client with service key for admin operations
supabase = createClient(supabaseUrl, supabaseKey, {
auth: {
persistSession: false,
},
db: {
schema: 'public',
},
global: {
headers: { 'x-application-name': 'flow-platform' },
},
});
// Create Supabase client with anon key for regular operations
supabaseAnon = createClient(supabaseUrl, supabaseAnonKey);
}
}
export { pgPool, supabase, supabaseAnon };
/**
* Convert any date/timestamp to PostgreSQL-safe ISO 8601 format
* Best practice: Normalize dates at the Node.js boundary before hitting PostgreSQL
*
* @param {Date|string|number} date - Date object, ISO string, timestamp, or date string
* @param {boolean} includeTime - If true, includes time component (for timestamps), default: false (date only)
* @returns {string} PostgreSQL-safe format: YYYY-MM-DD or YYYY-MM-DD HH:MM:SS.000
*
* @example
* toPgTimestamp(new Date('2025-12-17 3:59:59 PM')) // "2025-12-17 15:59:59.000"
* toPgTimestamp('2025-12-17 3:59:59 PM') // "2025-12-17 15:59:59.000"
* toPgTimestamp('2025-12-17') // "2025-12-17"
* toPgTimestamp(new Date('2025-12-17'), false) // "2025-12-17" (date only)
*/
export function toPgTimestamp(date, includeTime = false) {
if (!date) return null;
// Handle Date objects
if (date instanceof Date) {
if (includeTime) {
// Return ISO format with space instead of T, remove Z, add milliseconds
return date.toISOString().replace('T', ' ').replace('Z', '');
} else {
// Return date only (YYYY-MM-DD)
return date.toISOString().split('T')[0];
}
}
// Handle timestamps (numbers)
if (typeof date === 'number') {
const d = new Date(date);
if (includeTime) {
return d.toISOString().replace('T', ' ').replace('Z', '');
} else {
return d.toISOString().split('T')[0];
}
}
// Handle strings
if (typeof date === 'string') {
// If it's already in YYYY-MM-DD format and no time needed, return as-is
if (!includeTime && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
return date;
}
// Try to parse the string as a date
// Handle common formats: "2025-12-17 3:59:59 PM", "2025-12-17T15:59:59Z", etc.
const parsed = new Date(date);
// Check if parsing was successful
if (isNaN(parsed.getTime())) {
// If parsing failed, try to extract just the date part if it looks like a date
const dateMatch = date.match(/^\d{4}-\d{2}-\d{2}/);
if (dateMatch) {
return dateMatch[0];
}
// If we can't parse it, return the original (might cause an error, but that's better than corrupting data)
console.warn(`Warning: Could not parse date string "${date}", returning as-is`);
return date;
}
if (includeTime) {
return parsed.toISOString().replace('T', ' ').replace('Z', '');
} else {
return parsed.toISOString().split('T')[0];
}
}
// Unknown type
console.warn(`Warning: Unknown date type: ${typeof date}, returning as-is`);
return String(date);
}
/**
* Convert date to PostgreSQL date format (YYYY-MM-DD)
* Alias for toPgTimestamp(date, false)
*
* @param {Date|string|number} date - Date to convert
* @returns {string} YYYY-MM-DD format
*/
export function toPgDate(date) {
return toPgTimestamp(date, false);
}
/**
* Raw SQL query helper
* Uses direct PostgreSQL connection if available, otherwise falls back to Supabase RPC
*
* @param {string} sql - SQL query with parameterized placeholders ($1, $2, etc.)
* @param {Array} params - Array of parameter values
* @returns {Promise<any>} Query results
*/
export async function rawQuery(sql, params = []) {
// Use direct PostgreSQL connection if available (faster for heavy queries)
if (pgPool) {
const client = await pgPool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
}
// Fallback to Supabase RPC method (only if Supabase client is available)
if (supabase) {
try {
const paramsJsonb = JSON.stringify(params);
const { data, error } = await supabase.rpc('exec_sql', {
query: sql,
params: paramsJsonb
});
if (error) {
console.error('Raw SQL query error:', error);
throw new Error(`SQL query failed: ${error.message}`);
}
return data;
} catch (err) {
// Fallback: if RPC doesn't exist, throw helpful error
if (err.message?.includes('function exec_sql') || err.code === '42883') {
throw new Error(
'exec_sql RPC function not found. Please create it in Supabase SQL Editor or set DATABASE_URL for direct connection. ' +
'See db.js comments for the SQL function definition.'
);
}
throw err;
}
}
// No database connection available
throw new Error(
'No database connection available. Please set:\n' +
' - USE_LOCAL_DB=true with LOCAL_DB_* variables (for local PostgreSQL)\n' +
' - OR DATABASE_URL (for remote PostgreSQL)\n' +
' - OR SUPABASE_URL + SUPABASE_SERVICE_KEY (for Supabase)'
);
}
// Test connection
export async function testConnection() {
try {
// Prefer direct PostgreSQL connection if available
if (pgPool) {
try {
await Promise.race([
pgPool.query('SELECT 1'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Connection timeout')), 10000)
)
]);
console.log('✅ Database connection successful (PostgreSQL direct)');
return true;
} catch (pgErr) {
if (pgErr.code === 'ENOTFOUND' || pgErr.message?.includes('ENOTFOUND')) {
console.error('❌ Database connection error: Hostname not found');
console.error('\nTroubleshooting:');
console.error('1. Verify your DATABASE_URL hostname is correct');
console.error('2. Check if your Supabase project is active (not paused)');
console.error('3. Check if project has exceeded quota (see error message)');
console.error('4. Try using the Pooler connection string instead:');
console.error(' - Go to Supabase Dashboard > Settings > Database');
console.error(' - Use "Connection Pooling" tab instead of "URI" tab');
console.error(' - Format: postgresql://postgres:[password]@[project-ref].pooler.supabase.com:6543/postgres');
console.error('5. Or use Supabase client with API keys (set SUPABASE_URL and SUPABASE_SERVICE_KEY)');
console.error('\nError details:', pgErr.message);
// Fall through to try Supabase client
} else if (pgErr.message?.includes('exceed_db_size_quota') || pgErr.message?.includes('restricted')) {
console.error('❌ Database connection error: Project quota exceeded');
console.error('\nYour Supabase project has exceeded the database size quota.');
console.error('\nSolutions:');
console.error('1. Contact Supabase support: https://supabase.help');
console.error('2. Upgrade your plan in Supabase Dashboard > Settings > Billing');
console.error('3. Clean up old data to reduce database size');
console.error('4. See DATABASE_QUOTA_FIX.md for detailed steps');
console.error('\nError details:', pgErr.message);
return false;
} else {
throw pgErr;
}
}
}
// Fallback to Supabase client (only if available)
if (!supabase) {
console.error('❌ No database connection available');
console.error('Please set either:');
console.error(' - USE_LOCAL_DB=true with LOCAL_DB_* variables (for local PostgreSQL)');
console.error(' - OR DATABASE_URL (for remote PostgreSQL)');
console.error(' - OR SUPABASE_URL + SUPABASE_SERVICE_KEY (for Supabase client)');
return false;
}
const { data, error } = await supabase.from('options_flow').select('count').limit(1);
if (error) {
// PGRST116 = table doesn't exist (ok for setup)
// PGRST301 = invalid API key
if (error.code === 'PGRST116') {
console.log('✅ Database connection successful (table may not exist yet)');
return true;
}
if (error.code === 'PGRST301' || error.message?.includes('Invalid API key')) {
console.error('❌ Database connection error: Invalid API key');
console.error('\nTroubleshooting:');
console.error('1. Check your .env file has the correct SUPABASE_SERVICE_KEY');
console.error('2. Get the service_role key from: Supabase Dashboard > Settings > API');
console.error('3. Make sure you\'re using the service_role key (not anon key) for backend');
console.error('4. Alternatively, use DATABASE_URL for direct PostgreSQL connection');
console.error('\nError details:', error);
return false;
}
if (error.message?.includes('exceed_db_size_quota') || error.message?.includes('restricted')) {
console.error('❌ Database connection error: Project quota exceeded');
console.error('\nYour Supabase project has exceeded the database size quota.');
console.error('\nSolutions:');
console.error('1. Contact Supabase support: https://supabase.help');
console.error('2. Upgrade your plan in Supabase Dashboard > Settings > Billing');
console.error('3. Clean up old data to reduce database size');
console.error('4. See DATABASE_QUOTA_FIX.md for detailed steps');
console.error('\nError details:', error);
return false;
}
console.error('Database connection error:', error);
return false;
}
console.log('✅ Database connection successful (Supabase)');
return true;
} catch (err) {
console.error('Database connection failed:', err);
if (err.message?.includes('Invalid API key')) {
console.error('\nTroubleshooting:');
console.error('1. Verify SUPABASE_URL and SUPABASE_SERVICE_KEY in your .env file');
console.error('2. Get keys from: Supabase Dashboard > Settings > API');
console.error('3. Consider using local database: set USE_LOCAL_DB=true');
console.error('4. Or use DATABASE_URL for direct PostgreSQL connection');
} else if (useLocalDb) {
console.error('\nTroubleshooting for local database:');
console.error('1. Verify PostgreSQL is running');
console.error('2. Check LOCAL_DB_* variables in .env file');
console.error('3. See LOCAL_DB_SETUP.md for setup instructions');
}
return false;
}
}