Files
cannaiq/backend/src/dutchie-az/db/dispensary-columns.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

138 lines
4.5 KiB
TypeScript

/**
* Dispensary Column Definitions
*
* Centralized column list for dispensaries table queries.
* Handles optional columns that may not exist in all environments.
*
* USAGE:
* import { DISPENSARY_COLUMNS, DISPENSARY_COLUMNS_WITH_FAILED } from '../db/dispensary-columns';
* const result = await query(`SELECT ${DISPENSARY_COLUMNS} FROM dispensaries WHERE ...`);
*/
/**
* Core dispensary columns that always exist.
* These are guaranteed to be present in all environments.
*/
const CORE_COLUMNS = `
id, name, slug, city, state, zip, address, latitude, longitude,
menu_type, menu_url, platform_dispensary_id, website,
created_at, updated_at
`;
/**
* Optional columns with NULL fallback.
*
* provider_detection_data: Added in migration 044
* active_crawler_profile_id: Added in migration 041
*
* Using COALESCE ensures the query works whether or not the column exists:
* - If column exists: returns the actual value
* - If column doesn't exist: query fails (but migration should be run)
*
* For pre-migration compatibility, we select NULL::jsonb which always works.
* After migration 044 is applied, this can be changed to the real column.
*/
// TEMPORARY: Use NULL fallback until migration 044 is applied
// After running 044, change this to: provider_detection_data
const PROVIDER_DETECTION_COLUMN = `NULL::jsonb AS provider_detection_data`;
// After migration 044 is applied, uncomment this line and remove the above:
// const PROVIDER_DETECTION_COLUMN = `provider_detection_data`;
/**
* Standard dispensary columns for most queries.
* Includes provider_detection_data with NULL fallback for pre-migration compatibility.
*/
export const DISPENSARY_COLUMNS = `${CORE_COLUMNS.trim()},
${PROVIDER_DETECTION_COLUMN}`;
/**
* Dispensary columns including active_crawler_profile_id.
* Used by routes that need profile information.
*/
export const DISPENSARY_COLUMNS_WITH_PROFILE = `${CORE_COLUMNS.trim()},
${PROVIDER_DETECTION_COLUMN},
active_crawler_profile_id`;
/**
* Dispensary columns including failed_at.
* Used by worker for compatibility checks.
*/
export const DISPENSARY_COLUMNS_WITH_FAILED = `${CORE_COLUMNS.trim()},
${PROVIDER_DETECTION_COLUMN},
failed_at`;
/**
* NOTE: After migration 044 is applied, update PROVIDER_DETECTION_COLUMN above
* to use the real column instead of NULL fallback.
*
* To verify migration status:
* SELECT column_name FROM information_schema.columns
* WHERE table_name = 'dispensaries' AND column_name = 'provider_detection_data';
*/
// Cache for column existence check
let _providerDetectionColumnExists: boolean | null = null;
/**
* Check if provider_detection_data column exists in dispensaries table.
* Result is cached after first check.
*/
export async function hasProviderDetectionColumn(pool: { query: (sql: string) => Promise<{ rows: any[] }> }): Promise<boolean> {
if (_providerDetectionColumnExists !== null) {
return _providerDetectionColumnExists;
}
try {
const result = await pool.query(`
SELECT 1 FROM information_schema.columns
WHERE table_name = 'dispensaries' AND column_name = 'provider_detection_data'
`);
_providerDetectionColumnExists = result.rows.length > 0;
} catch {
_providerDetectionColumnExists = false;
}
return _providerDetectionColumnExists;
}
/**
* Safely update provider_detection_data column.
* If column doesn't exist, logs a warning but doesn't crash.
*
* @param pool - Database pool with query method
* @param dispensaryId - ID of dispensary to update
* @param data - JSONB data to merge into provider_detection_data
* @returns true if update succeeded, false if column doesn't exist
*/
export async function safeUpdateProviderDetectionData(
pool: { query: (sql: string, params?: any[]) => Promise<any> },
dispensaryId: number,
data: Record<string, any>
): Promise<boolean> {
const hasColumn = await hasProviderDetectionColumn(pool);
if (!hasColumn) {
console.warn(`[DispensaryColumns] provider_detection_data column not found. Run migration 044 to add it.`);
return false;
}
try {
await pool.query(
`UPDATE dispensaries
SET provider_detection_data = COALESCE(provider_detection_data, '{}'::jsonb) || $1::jsonb,
updated_at = NOW()
WHERE id = $2`,
[JSON.stringify(data), dispensaryId]
);
return true;
} catch (error: any) {
if (error.message?.includes('provider_detection_data')) {
console.warn(`[DispensaryColumns] Failed to update provider_detection_data: ${error.message}`);
return false;
}
throw error;
}
}