import { createClient } from '@supabase/supabase-js';
import { Database } from './types/database.js';
import * as dotenv from 'dotenv';
import { logger } from '../utils/logger.js';
import { DatabaseError, ConfigurationError } from '../utils/errors.js';
dotenv.config();
// Validate environment variables
if (!process.env.SUPABASE_URL || !process.env.SUPABASE_ANON_KEY) {
const error = new ConfigurationError(
'Missing Supabase environment variables. Please set SUPABASE_URL and SUPABASE_ANON_KEY',
{
hasUrl: !!process.env.SUPABASE_URL,
hasAnonKey: !!process.env.SUPABASE_ANON_KEY
}
);
logger.error('Failed to initialize Supabase client', error);
throw error;
}
logger.info('Initializing Supabase clients', {
hasServiceKey: !!process.env.SUPABASE_SERVICE_KEY
});
// Public client for user operations (with RLS)
export const supabase = createClient<Database>(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{
auth: {
persistSession: false
}
}
);
// Admin client for server-side operations (internal use only)
const supabaseAdmin = process.env.SUPABASE_SERVICE_KEY
? createClient<Database>(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_KEY,
{
auth: {
persistSession: false
}
}
)
: null;
if (!supabaseAdmin) {
logger.warn('Admin client not available - some features may be limited');
}
// Data access layer - all admin operations should go through these functions
export const dataAccess = {
// System-level operations that bypass RLS (use with caution)
async systemQuery<T = any>(query: () => Promise<T>): Promise<T> {
if (!supabaseAdmin) {
throw new ConfigurationError(
'Admin client not available - service key required for system operations',
{ operation: 'systemQuery' }
);
}
try {
logger.debug('Executing system query');
const result = await query();
logger.debug('System query completed successfully');
return result;
} catch (error) {
logger.error('System query failed', error as Error);
throw new DatabaseError(
`System query failed: ${(error as Error).message}`,
{ originalError: (error as Error).message }
);
}
},
// Migrate existing data to include user_id
async migrateProjectOwnership(projectId: string, userId: string) {
if (!supabaseAdmin) {
throw new ConfigurationError(
'Admin client required for migration',
{ operation: 'migrateProjectOwnership' }
);
}
logger.info('Migrating project ownership', { projectId, userId });
try {
const { error } = await supabaseAdmin
.from('projects')
.update({ user_id: userId })
.eq('id', projectId);
if (error) {
throw new DatabaseError(
`Failed to migrate project ownership: ${error.message}`,
{ projectId, userId, code: error.code }
);
}
logger.info('Project ownership migrated successfully', { projectId, userId });
} catch (error) {
if (error instanceof DatabaseError) throw error;
logger.error('Unexpected error during migration', error as Error);
throw new DatabaseError(
`Migration failed: ${(error as Error).message}`,
{ projectId, userId }
);
}
},
// System health checks
async checkDatabaseHealth() {
const client = supabaseAdmin || supabase;
try {
logger.debug('Checking database health');
const { error } = await client
.from('projects')
.select('count')
.limit(0);
if (error) {
logger.error('Database health check failed', error);
return false;
}
logger.debug('Database health check passed');
return true;
} catch (error) {
logger.error('Unexpected error during health check', error as Error);
return false;
}
},
// Get anonymous statistics (no user data)
async getSystemStats() {
if (!supabaseAdmin) {
throw new ConfigurationError(
'Admin client required for system stats',
{ operation: 'getSystemStats' }
);
}
logger.debug('Fetching system statistics');
try {
const { data, error } = await supabaseAdmin
.from('projects')
.select('industry, status, created_at');
if (error) {
throw new DatabaseError(
`Failed to fetch system stats: ${error.message}`,
{ code: error.code }
);
}
// Return anonymized statistics only
const stats = {
totalProjects: data?.length || 0,
byIndustry: data?.reduce((acc, p) => {
acc[p.industry] = (acc[p.industry] || 0) + 1;
return acc;
}, {} as Record<string, number>) || {},
byStatus: data?.reduce((acc, p) => {
acc[p.status] = (acc[p.status] || 0) + 1;
return acc;
}, {} as Record<string, number>) || {}
};
logger.debug('System statistics fetched', { totalProjects: stats.totalProjects });
return stats;
} catch (error) {
if (error instanceof DatabaseError) throw error;
logger.error('Unexpected error fetching stats', error as Error);
throw new DatabaseError(
`Failed to get system stats: ${(error as Error).message}`,
{}
);
}
}
};
// Export types for data access
export type DataAccess = typeof dataAccess;
// Export the appropriate client for MCP server operations
// MCP servers run server-side and should use the admin client when available
export const mcpDb = supabaseAdmin || supabase;