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>
This commit is contained in:
@@ -1,50 +1,99 @@
|
||||
/**
|
||||
* Dutchie AZ Database Connection
|
||||
* CannaiQ Database Connection
|
||||
*
|
||||
* Isolated database connection for Dutchie Arizona data.
|
||||
* Uses a separate database/schema to prevent cross-contamination with main app data.
|
||||
* 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';
|
||||
|
||||
// Consolidated DB naming:
|
||||
// - Prefer CRAWLSY_DATABASE_URL (e.g., crawlsy_local, crawlsy_prod)
|
||||
// - Then DUTCHIE_AZ_DATABASE_URL (legacy)
|
||||
// - Finally DATABASE_URL (legacy main DB)
|
||||
const DUTCHIE_AZ_DATABASE_URL =
|
||||
process.env.CRAWLSY_DATABASE_URL ||
|
||||
process.env.DUTCHIE_AZ_DATABASE_URL ||
|
||||
process.env.DATABASE_URL ||
|
||||
'postgresql://dutchie:dutchie_local_pass@localhost:54320/crawlsy_local';
|
||||
/**
|
||||
* 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 Dutchie AZ database pool (singleton)
|
||||
* Get the CannaiQ database pool (singleton)
|
||||
*
|
||||
* This is the canonical pool for all CannaiQ services.
|
||||
* Do NOT create separate pools elsewhere.
|
||||
*/
|
||||
export function getDutchieAZPool(): Pool {
|
||||
export function getPool(): Pool {
|
||||
if (!pool) {
|
||||
pool = new Pool({
|
||||
connectionString: DUTCHIE_AZ_DATABASE_URL,
|
||||
connectionString: getConnectionString(),
|
||||
max: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('[DutchieAZ DB] Unexpected error on idle client:', err);
|
||||
console.error('[CannaiQ DB] Unexpected error on idle client:', err);
|
||||
});
|
||||
|
||||
console.log('[DutchieAZ DB] Pool initialized');
|
||||
console.log('[CannaiQ DB] Pool initialized');
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query on the Dutchie AZ database
|
||||
* @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 = getDutchieAZPool();
|
||||
const p = getPool();
|
||||
const result = await p.query(text, params);
|
||||
return { rows: result.rows as T[], rowCount: result.rowCount || 0 };
|
||||
}
|
||||
@@ -53,7 +102,7 @@ export async function query<T = any>(text: string, params?: any[]): Promise<{ ro
|
||||
* Get a client from the pool for transaction use
|
||||
*/
|
||||
export async function getClient(): Promise<PoolClient> {
|
||||
const p = getDutchieAZPool();
|
||||
const p = getPool();
|
||||
return p.connect();
|
||||
}
|
||||
|
||||
@@ -64,7 +113,7 @@ export async function closePool(): Promise<void> {
|
||||
if (pool) {
|
||||
await pool.end();
|
||||
pool = null;
|
||||
console.log('[DutchieAZ DB] Pool closed');
|
||||
console.log('[CannaiQ DB] Pool closed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +125,7 @@ export async function healthCheck(): Promise<boolean> {
|
||||
const result = await query('SELECT 1 as ok');
|
||||
return result.rows.length > 0 && result.rows[0].ok === 1;
|
||||
} catch (error) {
|
||||
console.error('[DutchieAZ DB] Health check failed:', error);
|
||||
console.error('[CannaiQ DB] Health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user