UNPKG

create-sekiban-app

Version:

Create a new Sekiban application with Dapr

294 lines (248 loc) • 10.2 kB
import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import morgan from 'morgan'; import { Pool } from 'pg'; import { DaprServer, DaprClient, CommunicationProtocolEnum, HttpMethod, ActorProxyBuilder, ActorId } from '@dapr/dapr'; import { MultiProjectorActorFactory, getDaprCradle, MultiProjectorActor } from '@sekiban/dapr'; import { InMemoryEventStore, StorageProviderType, IEventStore } from '@sekiban/core'; import { PostgresEventStore } from '@sekiban/postgres'; // Import domain types at the top to ensure registration happens import '@dapr-sample/domain'; import { createTaskDomainTypes } from '@dapr-sample/domain'; import { config } from './config/index.js'; import { errorHandler } from './middleware/error-handler.js'; import { healthRoutes } from './routes/health-routes.js'; import { multiProjectorRoutes } from './routes/multi-projector-routes.js'; import logger from './utils/logger.js'; import type { MultiProjectorActorFactoryConfigureMethod, MultiProjectorActorFactoryCreateMethod } from './types/factory-types.js'; import type { PubSubEventData, SerializationService, ActorProxyFactory } from './types/domain-types.js'; async function startServer() { const app = express(); // Middleware app.use(helmet()); app.use(cors({ origin: config.CORS_ORIGIN })); app.use(compression()); app.use(morgan(config.NODE_ENV === 'production' ? 'combined' : 'dev')); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Debug middleware to log all requests app.use((req, res, next) => { logger.debug(`[${new Date().toISOString()}] ${req.method} ${req.path}`); if (req.path.startsWith('/actors/')) { logger.info(`Actor route called: ${req.method} ${req.path}`); } next(); }); // CRITICAL: Convert POST to PUT for actor method calls app.use((req, res, next) => { if (req.path.includes('/method/') && req.method === 'POST') { req.method = 'PUT'; logger.debug(`Converted POST to PUT for actor method: ${req.path}`); } next(); }); // Routes app.use('/', healthRoutes); app.use(config.API_PREFIX, multiProjectorRoutes); // Error handling (must be last) app.use(errorHandler); // Create DaprServer and pass the Express app const daprServer = await setupDaprActorsWithApp(app); // Start the DaprServer await daprServer.start(); console.log(` šŸš€ MultiProjector Service is running! šŸ“” Environment: ${config.NODE_ENV} šŸ”— URL: http://localhost:${config.PORT} šŸ”— API: http://localhost:${config.PORT}${config.API_PREFIX} šŸŽ­ Dapr App ID: ${config.DAPR_APP_ID} šŸŽ­ Actors: MultiProjectorActor `); // Graceful shutdown const gracefulShutdown = async (signal: string) => { console.log(`\n${signal} received, starting graceful shutdown...`); const forceExitTimeout = setTimeout(() => { console.error('Forced shutdown after timeout'); process.exit(1); }, 30000); try { await daprServer.stop(); console.log('DaprServer stopped'); clearTimeout(forceExitTimeout); } catch (error) { console.error('Error during cleanup:', error); } process.exit(0); }; process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); process.on('SIGINT', () => gracefulShutdown('SIGINT')); } async function setupDaprActorsWithApp(app: express.Express) { logger.info('Setting up Dapr actors with Express app...'); // Initialize domain types const domainTypes = createTaskDomainTypes(); // Choose storage type based on environment variable or config const storageType = config.STORAGE_TYPE; let eventStore: any; // Use any to avoid neverthrow version mismatch if (storageType === 'postgres') { // Initialize PostgreSQL event store logger.info('Using PostgreSQL event store'); const pool = new Pool({ connectionString: config.DATABASE_URL }); eventStore = new PostgresEventStore(pool); // Initialize the database schema logger.info('Initializing PostgreSQL schema...'); try { const result = await eventStore.initialize(); if (result.isOk()) { logger.info('PostgreSQL schema initialized successfully'); } else { logger.error('Failed to initialize PostgreSQL schema:', result.error); throw result.error; } } catch (error) { logger.error('Failed to initialize PostgreSQL:', error); throw error; } } else if (storageType === 'cosmos') { // Initialize Cosmos DB event store logger.info('Using Cosmos DB event store'); const { CosmosEventStore } = await import('@sekiban/cosmos'); const { CosmosClient } = await import('@azure/cosmos'); if (!config.COSMOS_CONNECTION_STRING) { throw new Error('COSMOS_CONNECTION_STRING is required when using Cosmos DB storage'); } // Create Cosmos client and database reference const cosmosClient = new CosmosClient(config.COSMOS_CONNECTION_STRING); const database = cosmosClient.database(config.COSMOS_DATABASE!); // Create event store with the database object eventStore = new CosmosEventStore(database as any); // Initialize the Cosmos DB container logger.info('Initializing Cosmos DB...'); try { const result = await eventStore.initialize(); if (result.isOk()) { logger.info('Cosmos DB initialized successfully'); } else { logger.error('Failed to initialize Cosmos DB:', result.error); throw result.error; } } catch (error) { logger.error('Failed to initialize Cosmos DB:', error); throw error; } } else { // Initialize in-memory event store logger.info('Using in-memory event store'); eventStore = new InMemoryEventStore({ type: StorageProviderType.InMemory, enableLogging: config.NODE_ENV === 'development' }); } // Create DaprClient for actor proxy factory const daprClient = new DaprClient({ daprHost: "127.0.0.1", daprPort: String(config.DAPR_HTTP_PORT) }); // Create actor proxy factory const actorProxyFactory: ActorProxyFactory = { createActorProxy: (actorId, actorType: string) => { const actorIdObj = typeof actorId === 'string' ? { id: actorId } : actorId; const actorIdStr = 'id' in actorIdObj ? (actorIdObj as { id: string }).id : (actorIdObj as any).getId ? (actorIdObj as any).getId() : String(actorId); logger.debug(`Creating actor proxy for ${actorType}/${actorIdStr}`); // For now, we only need MultiProjectorActor proxies in this service if (actorType === 'MultiProjectorActor') { // Type assertion needed due to factory pattern limitations const factory = MultiProjectorActorFactory as unknown as MultiProjectorActorFactoryCreateMethod; const MultiProjectorActorClass = factory.createActorClass(); const builder = new ActorProxyBuilder(MultiProjectorActorClass, daprClient); return builder.build(new ActorId(actorIdStr)); } // Fallback for other actor types console.warn(`[ActorProxyFactory] Unknown actor type: ${actorType}`); return null; } }; // Create a simple serialization service const serializationService: SerializationService = { async deserializeAggregateAsync(surrogate: unknown) { return surrogate; }, async serializeAggregateAsync(aggregate: unknown) { return aggregate; } }; // Configure MultiProjectorActorFactory const configureMethod = MultiProjectorActorFactory as unknown as MultiProjectorActorFactoryConfigureMethod; configureMethod.configure( domainTypes, {}, // service provider actorProxyFactory, serializationService, eventStore ); // Create actor class - use direct reference to avoid type issues const MultiProjectorActorClass = MultiProjectorActor as any; logger.info('[DEBUG] MultiProjectorActorClass name:', MultiProjectorActorClass.name); // Create DaprServer const daprServer = new DaprServer({ serverHost: "127.0.0.1", serverPort: String(config.PORT), serverHttp: app, // Pass the configured Express app communicationProtocol: CommunicationProtocolEnum.HTTP, clientOptions: { daprHost: "127.0.0.1", daprPort: String(config.DAPR_HTTP_PORT), communicationProtocol: CommunicationProtocolEnum.HTTP } }); // Register MultiProjectorActor await daprServer.actor.registerActor(MultiProjectorActorClass); logger.info('Registered MultiProjectorActor'); // Initialize actor runtime logger.info('Initializing actor runtime...'); await daprServer.actor.init(); logger.info('Actor runtime initialized'); // Add diagnostic logging console.log('[DEBUG] Adding diagnostic logging...'); // Type assertion for private properties // Type for Express Request and Response interface ExpressRequest { url: string; [key: string]: unknown; } interface ExpressResponse { [key: string]: unknown; } const serverWithActorHandler = daprServer as unknown as { actor?: { actorHandler?: (req: ExpressRequest, res: ExpressResponse) => Promise<void>; }; }; const originalActorHandler = serverWithActorHandler.actor?.actorHandler; if (originalActorHandler && serverWithActorHandler.actor) { serverWithActorHandler.actor.actorHandler = async (req: ExpressRequest, res: ExpressResponse) => { console.log('[DIAGNOSTIC] Actor handler called for:', req.url); try { await originalActorHandler.call(serverWithActorHandler.actor, req, res); } catch (e) { console.error('[DIAGNOSTIC] Actor handler error:', e); console.error('[DIAGNOSTIC] Stack trace:', (e as Error).stack); throw e; } }; console.log('[DEBUG] Diagnostic handler installed'); } else { console.log('[DEBUG] Could not install diagnostic handler'); } logger.info('Dapr actors integrated with Express app'); return daprServer; } // Start the server startServer().catch((error) => { console.error('Failed to start server:', error); process.exit(1); });