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>
132 lines
3.7 KiB
TypeScript
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;
|
|
}
|
|
}
|