352 lines
13 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
|