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>
138 lines
4.5 KiB
TypeScript
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;
|
|
}
|
|
}
|