UNPKG

@0xobelisk/graphql-server

Version:

Tookit for interacting with dubhe graphql server

325 lines (275 loc) 11.2 kB
import { postgraphile } from 'postgraphile'; import { Pool } from 'pg'; import * as dotenv from 'dotenv'; import { dbLogger, serverLogger, systemLogger, subscriptionLogger, logPerformance } from './utils/logger'; import { DatabaseIntrospector, createPostGraphileConfig, PostGraphileConfigOptions, WelcomePageConfig } from './plugins'; import { EnhancedServerManager } from './plugins/enhanced-server-manager'; import { subscriptionConfig, SubscriptionConfigInput } from './config/subscription-config'; import { generateStoreTablesInfo, createUniversalSubscriptionsPlugin } from './universal-subscriptions'; // Load environment variables dotenv.config(); // Server configuration interface export interface ServerConfig { // Basic server configuration port: string; databaseUrl: string; schema: string; endpoint: string; cors: boolean; subscriptions: boolean; env: string; // Debug configuration debug: boolean; // Performance configuration queryTimeout: number; maxConnections: number; heartbeatInterval: number; enableMetrics: boolean; // Subscription capabilities enableLiveQueries: boolean; enablePgSubscriptions: boolean; enableNativeWebSocket: boolean; realtimePort?: number; // Internal debug flags debugNotifications: boolean; } // Start server export const startServer = async (config: ServerConfig): Promise<void> => { // Set log level from config to environment (for logger compatibility) process.env.LOG_LEVEL = config.debug ? 'debug' : 'info'; // Extract variables from config const { port: PORT, databaseUrl: DATABASE_URL, schema: PG_SCHEMA, endpoint: GRAPHQL_ENDPOINT, cors: ENABLE_CORS, subscriptions: ENABLE_SUBSCRIPTIONS_BOOL, env: NODE_ENV } = config; // Convert boolean values to string format const ENABLE_SUBSCRIPTIONS = ENABLE_SUBSCRIPTIONS_BOOL ? 'true' : 'false'; // Build subscription configuration and refresh systemLogger.info('Refreshing subscription configuration with new settings...'); const subscriptionConfigInput: SubscriptionConfigInput = { enableSubscriptions: ENABLE_SUBSCRIPTIONS_BOOL, databaseUrl: DATABASE_URL, port: PORT, // Use configuration from ServerConfig instead of defaults enableLiveQueries: config.enableLiveQueries, enablePgSubscriptions: config.enablePgSubscriptions, enableNativeWebSocket: config.enableNativeWebSocket, realtimePort: config.realtimePort?.toString(), maxConnections: config.maxConnections.toString(), heartbeatInterval: config.heartbeatInterval.toString(), debugNotifications: config.debugNotifications, enableMetrics: config.enableMetrics }; subscriptionConfig.refresh(subscriptionConfigInput); // === Unified Connection Pool Architecture: Single pool handles all operations === systemLogger.info('🔄 Connection pool configuration', { maxConnections: config.maxConnections, strategy: 'single-pool-unified', operations: ['query', 'mutation', 'subscription'] }); // Unified connection pool - handles all GraphQL operations const pgPool = new Pool({ connectionString: DATABASE_URL, // === Connection Pool Configuration === max: config.maxConnections, // Use configured maximum connections min: Math.min(5, Math.floor(config.maxConnections * 0.1)), // Keep minimum connections // === Balanced Configuration: Support both short-term queries and long-term subscriptions === connectionTimeoutMillis: 10000, // 10 second timeout (balanced value) idleTimeoutMillis: 600000, // 10 minute idle cleanup (support subscriptions but not too long) maxLifetimeSeconds: 3600, // 1 hour rotation (prevent connection leaks) allowExitOnIdle: config.env === 'development' }); // Add connection pool event listeners pgPool.on('connect', (_client) => { dbLogger.debug('📤 New connection established', { totalCount: pgPool.totalCount, idleCount: pgPool.idleCount, waitingCount: pgPool.waitingCount }); }); pgPool.on('error', (err, _client) => { dbLogger.error('❌ Connection pool error', err, { totalCount: pgPool.totalCount }); }); const startTime = Date.now(); try { // 1. Test database connection and scan table structure systemLogger.info('Initializing database connection and scanning table structure...', { schema: PG_SCHEMA, databaseUrl: DATABASE_URL.replace(/:[^:]*@/, ':****@') // Hide password }); const introspector = new DatabaseIntrospector(pgPool, PG_SCHEMA); const isConnected = await introspector.testConnection(); if (!isConnected) { throw new Error('Database connection failed'); } dbLogger.info('Database connection successful', { schema: PG_SCHEMA }); const allTables = await introspector.getAllTables(); const tableNames = allTables.map((t) => t.table_name); dbLogger.info('Table structure scan completed', { tableCount: allTables.length, storeTableCount: tableNames.filter((name) => name.startsWith('store_')).length, tableNames: tableNames.slice(0, 10) // Only show first 10 table names }); // 2. Display subscription configuration status const subscriptionConfigData = subscriptionConfig.getConfig(); subscriptionLogger.info('📡 Subscription system configuration status', { enableSubscriptions: subscriptionConfigData.enableSubscriptions, capabilities: { pgSubscriptions: subscriptionConfigData.capabilities.pgSubscriptions }, recommendedMethod: 'pg-subscriptions', walLevel: subscriptionConfigData.walLevel }); // 3. Pre-generate store table information for dynamic queries subscriptionLogger.info('Pre-generating store table information for tool queries...'); const storeTablesInfo = await generateStoreTablesInfo(pgPool); const storeTableNames = Object.keys(storeTablesInfo); subscriptionLogger.info(`Discovered store tables: ${storeTableNames.join(', ')}`); // 4. Create PostGraphile configuration const postgraphileConfigOptions: PostGraphileConfigOptions = { port: PORT, nodeEnv: NODE_ENV, graphqlEndpoint: GRAPHQL_ENDPOINT, enableSubscriptions: ENABLE_SUBSCRIPTIONS, enableCors: ENABLE_CORS ? 'true' : 'false', databaseUrl: DATABASE_URL, availableTables: tableNames, // Pass additional configuration from CLI disableQueryLog: !config.debug, // Disable query log unless debug mode enableQueryLog: config.debug, // Enable query log in debug mode queryTimeout: config.queryTimeout }; serverLogger.info('Creating PostGraphile configuration', { endpoint: GRAPHQL_ENDPOINT, enableCors: ENABLE_CORS, enableSubscriptions: ENABLE_SUBSCRIPTIONS, debug: config.debug, disableQueryLog: !config.debug, enableQueryLog: config.debug }); // Use simplified configuration const postgraphileConfig = { ...createPostGraphileConfig(postgraphileConfigOptions), ...subscriptionConfig.generatePostGraphileConfig() }; // Add tools query plugin const toolsPlugin = createUniversalSubscriptionsPlugin(storeTablesInfo); postgraphileConfig.appendPlugins = [...(postgraphileConfig.appendPlugins || []), toolsPlugin]; // 5. Create PostGraphile middleware console.log('🔧 Creating PostGraphile middleware...'); const postgraphileMiddleware = postgraphile(pgPool, PG_SCHEMA, { ...postgraphileConfig }); console.log('✅ PostGraphile middleware creation completed:', typeof postgraphileMiddleware); // 6. Configure welcome page const welcomeConfig: WelcomePageConfig = { port: PORT, graphqlEndpoint: GRAPHQL_ENDPOINT, nodeEnv: NODE_ENV, schema: PG_SCHEMA, enableCors: ENABLE_CORS ? 'true' : 'false', enableSubscriptions: ENABLE_SUBSCRIPTIONS }; // 7. Create Express server manager const serverManager = new EnhancedServerManager(); // 8. Create Express server await serverManager.createEnhancedServer({ postgraphileMiddleware, pgPool, tableNames, databaseUrl: DATABASE_URL, allTables, welcomeConfig, postgraphileConfigOptions }); // 9. Start Express server await serverManager.startServer(); logPerformance('Express server startup', startTime, { port: PORT, tableCount: allTables.length, storeTableCount: storeTableNames.length, nodeEnv: NODE_ENV, framework: 'Express', capabilities: { pgSubscriptions: subscriptionConfigData.capabilities.pgSubscriptions } }); // 10. Display usage instructions if (NODE_ENV === 'development') { console.log('\n' + '='.repeat(80)); console.log('📖 Quick Access (Express Architecture):'); console.log(`Visit http://localhost:${PORT}/ to view homepage`); console.log(`Visit http://localhost:${PORT}/playground to use GraphQL Playground`); console.log(`Visit http://localhost:${PORT}/health to check server status`); console.log(`Visit http://localhost:${PORT}/subscription-config to get client configuration`); console.log(`Visit http://localhost:${PORT}/subscription-docs to view configuration guide`); console.log('='.repeat(80) + '\n'); } // 11. Set up simple and direct shutdown handling let isShuttingDown = false; const quickShutdown = (signal: string) => { if (isShuttingDown) { systemLogger.info('⚡ Force exiting process...'); process.exit(0); } isShuttingDown = true; systemLogger.info(`🛑 Received ${signal} signal, shutting down Express server...`); // Set 1 second force exit timeout setTimeout(() => { systemLogger.info('⚡ Quick exit'); process.exit(0); }, 1000); // Try to shutdown Express server quickly serverManager.quickShutdown().finally(() => { process.exit(0); }); }; process.on('SIGINT', () => quickShutdown('SIGINT')); process.on('SIGTERM', () => quickShutdown('SIGTERM')); // Simplified exception handling process.on('unhandledRejection', (reason) => { console.error('❌ Unhandled Promise rejection:', reason); }); process.on('uncaughtException', (error) => { console.error('❌ Uncaught exception:', error.message); process.exit(1); }); } catch (error) { systemLogger.error('Failed to start Express server', error, { databaseUrl: DATABASE_URL.replace(/:[^:]*@/, ':****@'), schema: PG_SCHEMA, port: PORT }); systemLogger.info('💡 Possible causes:'); systemLogger.info('1. Database connection failed - check DATABASE_URL'); systemLogger.info( '2. Expected table structure not found in database - ensure dubhe-indexer is running' ); systemLogger.info('3. Permission issues - ensure database user has sufficient permissions'); systemLogger.info('4. Missing dependencies - run pnpm install'); // Display subscription configuration help console.log('\n' + subscriptionConfig.generateDocumentation()); process.exit(1); } };