Files
cannaiq/backend/src/dutchie-az/db/connection.ts
Kelly b4a2fb7d03 feat: Add v2 architecture with multi-state support and orchestrator services
Major additions:
- Multi-state expansion: states table, StateSelector, NationalDashboard, StateHeatmap, CrossStateCompare
- Orchestrator services: trace service, error taxonomy, retry manager, proxy rotator
- Discovery system: dutchie discovery service, geo validation, city seeding scripts
- Analytics infrastructure: analytics v2 routes, brand/pricing/stores intelligence pages
- Local development: setup-local.sh starts all 5 services (postgres, backend, cannaiq, findadispo, findagram)
- Migrations 037-056: crawler profiles, states, analytics indexes, worker metadata

Frontend pages added:
- Discovery, ChainsDashboard, IntelligenceBrands, IntelligencePricing, IntelligenceStores
- StateHeatmap, CrossStateCompare, SyncInfoPanel

Components added:
- StateSelector, OrchestratorTraceModal, WorkflowStepper

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-07 11:30:57 -07:00

132 lines
3.7 KiB
TypeScript

/**
* CannaiQ Database Connection
*
* All database access for the CannaiQ platform goes through this module.
*
* SINGLE DATABASE ARCHITECTURE:
* - All services (auth, orchestrator, crawlers, admin) use this ONE database
* - States are modeled via states table + state_id on dispensaries (not separate DBs)
*
* CONFIGURATION (in priority order):
* 1. CANNAIQ_DB_URL - Full connection string (preferred)
* 2. Individual vars: CANNAIQ_DB_HOST, CANNAIQ_DB_PORT, CANNAIQ_DB_NAME, CANNAIQ_DB_USER, CANNAIQ_DB_PASS
* 3. DATABASE_URL - Legacy fallback for K8s compatibility
*
* IMPORTANT:
* - Do NOT create separate pools elsewhere
* - All services should import from this module
*/
import { Pool, PoolClient } from 'pg';
/**
* Get the database connection string from environment variables.
* Supports multiple configuration methods with fallback for legacy compatibility.
*/
function getConnectionString(): string {
// Priority 1: Full CANNAIQ connection URL
if (process.env.CANNAIQ_DB_URL) {
return process.env.CANNAIQ_DB_URL;
}
// Priority 2: Build from individual CANNAIQ env vars
const host = process.env.CANNAIQ_DB_HOST;
const port = process.env.CANNAIQ_DB_PORT;
const name = process.env.CANNAIQ_DB_NAME;
const user = process.env.CANNAIQ_DB_USER;
const pass = process.env.CANNAIQ_DB_PASS;
if (host && port && name && user && pass) {
return `postgresql://${user}:${pass}@${host}:${port}/${name}`;
}
// Priority 3: Fallback to DATABASE_URL for legacy/K8s compatibility
if (process.env.DATABASE_URL) {
return process.env.DATABASE_URL;
}
// Report what's missing
const required = ['CANNAIQ_DB_HOST', 'CANNAIQ_DB_PORT', 'CANNAIQ_DB_NAME', 'CANNAIQ_DB_USER', 'CANNAIQ_DB_PASS'];
const missing = required.filter((key) => !process.env[key]);
throw new Error(
`[CannaiQ DB] Missing database configuration.\n` +
`Set CANNAIQ_DB_URL, DATABASE_URL, or all of: ${missing.join(', ')}`
);
}
let pool: Pool | null = null;
/**
* Get the CannaiQ database pool (singleton)
*
* This is the canonical pool for all CannaiQ services.
* Do NOT create separate pools elsewhere.
*/
export function getPool(): Pool {
if (!pool) {
pool = new Pool({
connectionString: getConnectionString(),
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
pool.on('error', (err) => {
console.error('[CannaiQ DB] Unexpected error on idle client:', err);
});
console.log('[CannaiQ DB] Pool initialized');
}
return pool;
}
/**
* @deprecated Use getPool() instead
*/
export function getDutchieAZPool(): Pool {
console.warn('[CannaiQ DB] getDutchieAZPool() is deprecated. Use getPool() instead.');
return getPool();
}
/**
* Execute a query on the CannaiQ database
*/
export async function query<T = any>(text: string, params?: any[]): Promise<{ rows: T[]; rowCount: number }> {
const p = getPool();
const result = await p.query(text, params);
return { rows: result.rows as T[], rowCount: result.rowCount || 0 };
}
/**
* Get a client from the pool for transaction use
*/
export async function getClient(): Promise<PoolClient> {
const p = getPool();
return p.connect();
}
/**
* Close the pool connection
*/
export async function closePool(): Promise<void> {
if (pool) {
await pool.end();
pool = null;
console.log('[CannaiQ DB] Pool closed');
}
}
/**
* Check if the database is accessible
*/
export async function healthCheck(): Promise<boolean> {
try {
const result = await query('SELECT 1 as ok');
return result.rows.length > 0 && result.rows[0].ok === 1;
} catch (error) {
console.error('[CannaiQ DB] Health check failed:', error);
return false;
}
}