di-refactor coming along

This commit is contained in:
Boki 2025-06-22 18:14:34 -04:00
parent 7d9044ab29
commit 60ada5f6a3
20 changed files with 582 additions and 335 deletions

View file

@ -1,27 +1,8 @@
import { MongoDBClient } from '@stock-bot/mongodb';
import { PostgreSQLClient } from '@stock-bot/postgres';
/**
* Client exports for backward compatibility
*
* @deprecated Use ServiceContainer parameter instead
* This file will be removed once all routes and services are migrated
*/
let postgresClient: PostgreSQLClient | null = null;
let mongodbClient: MongoDBClient | null = null;
export function setPostgreSQLClient(client: PostgreSQLClient): void {
postgresClient = client;
}
export function getPostgreSQLClient(): PostgreSQLClient {
if (!postgresClient) {
throw new Error('PostgreSQL client not initialized. Call setPostgreSQLClient first.');
}
return postgresClient;
}
export function setMongoDBClient(client: MongoDBClient): void {
mongodbClient = client;
}
export function getMongoDBClient(): MongoDBClient {
if (!mongodbClient) {
throw new Error('MongoDB client not initialized. Call setMongoDBClient first.');
}
return mongodbClient;
}
export { getMongoDBClient, getPostgreSQLClient } from './migration-helper';

View file

@ -0,0 +1,34 @@
/**
* Service Container Setup for Web API
* Configures dependency injection for the web API service
*/
import type { ServiceContainer } from '@stock-bot/di';
import { getLogger } from '@stock-bot/logger';
import type { AppConfig } from '@stock-bot/config';
const logger = getLogger('web-api-container');
/**
* Configure the service container for web API workloads
*/
export function setupServiceContainer(
config: AppConfig,
container: ServiceContainer
): ServiceContainer {
logger.info('Configuring web API service container...');
// Web API specific configuration
// This service mainly reads data, so smaller pool sizes are fine
const poolSizes = {
mongodb: config.environment === 'production' ? 20 : 10,
postgres: config.environment === 'production' ? 30 : 15,
cache: config.environment === 'production' ? 20 : 10,
};
logger.info('Web API pool sizes configured', poolSizes);
// The container is already configured with connections
// Just return it with our logging
return container;
}

View file

@ -1,125 +1,137 @@
/**
* Stock Bot Web API - REST API service for web application
* Stock Bot Web API with Dependency Injection
* REST API service using Awilix container for managing connections
*/
// Framework imports
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { initializeServiceConfig } from '@stock-bot/config';
// Library imports
import {
createServiceContainer,
initializeServices as initializeAwilixServices,
type ServiceContainer
} from '@stock-bot/di';
import { getLogger, setLoggerConfig, shutdownLoggers } from '@stock-bot/logger';
import { MongoDBClient } from '@stock-bot/mongodb';
import { PostgreSQLClient } from '@stock-bot/postgres';
import { Shutdown } from '@stock-bot/shutdown';
import { exchangeRoutes } from './routes/exchange.routes';
import { healthRoutes } from './routes/health.routes';
// Import routes
import { setMongoDBClient, setPostgreSQLClient } from './clients';
// Initialize configuration with automatic monorepo config inheritance
const config = await initializeServiceConfig();
// Local imports
import { createRoutes } from './routes/create-routes';
import { setupServiceContainer } from './container-setup';
const config = initializeServiceConfig();
console.log('Web API Service Configuration:', JSON.stringify(config, null, 2));
const serviceConfig = config.service;
const databaseConfig = config.database;
// Initialize logger with config
const loggingConfig = config.logging;
if (loggingConfig) {
if (config.log) {
setLoggerConfig({
logLevel: loggingConfig.level,
logLevel: config.log.level,
logConsole: true,
logFile: false,
environment: config.environment,
hideObject: config.log.hideObject,
});
}
const app = new Hono();
// Add CORS middleware
app.use(
'*',
cors({
origin: ['http://localhost:4200', 'http://localhost:3000', 'http://localhost:3002'], // React dev server ports
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
})
);
// Create logger AFTER config is set
const logger = getLogger('web-api');
const PORT = serviceConfig.port;
let server: ReturnType<typeof Bun.serve> | null = null;
let postgresClient: PostgreSQLClient | null = null;
let mongoClient: MongoDBClient | null = null;
let container: ServiceContainer | null = null;
let app: Hono | null = null;
// Initialize shutdown manager
const shutdown = Shutdown.getInstance({ timeout: 15000 });
// Add routes
app.route('/health', healthRoutes);
app.route('/api/exchanges', exchangeRoutes);
// Basic API info endpoint
app.get('/', c => {
return c.json({
name: 'Stock Bot Web API',
version: '1.0.0',
status: 'running',
timestamp: new Date().toISOString(),
endpoints: {
health: '/health',
exchanges: '/api/exchanges',
},
});
});
// Initialize services
// Initialize services with DI pattern
async function initializeServices() {
logger.info('Initializing web API service...');
logger.info('Initializing web API service with DI...');
try {
// Initialize MongoDB client
logger.debug('Connecting to MongoDB...');
const mongoConfig = databaseConfig.mongodb;
mongoClient = new MongoDBClient(
{
uri: mongoConfig.uri,
database: mongoConfig.database,
host: mongoConfig.host,
port: mongoConfig.port,
timeouts: {
connectTimeout: 30000,
socketTimeout: 30000,
serverSelectionTimeout: 5000,
},
// Create Awilix container with proper config structure
logger.debug('Creating Awilix DI container...');
const awilixConfig = {
redis: {
host: config.database.dragonfly.host,
port: config.database.dragonfly.port,
db: config.database.dragonfly.db,
},
logger
);
await mongoClient.connect();
setMongoDBClient(mongoClient);
logger.info('MongoDB connected');
// Initialize PostgreSQL client
logger.debug('Connecting to PostgreSQL...');
const pgConfig = databaseConfig.postgres;
postgresClient = new PostgreSQLClient(
{
host: pgConfig.host,
port: pgConfig.port,
database: pgConfig.database,
username: pgConfig.user,
password: pgConfig.password,
poolSettings: {
min: 2,
max: pgConfig.poolSize || 10,
idleTimeoutMillis: pgConfig.idleTimeout || 30000,
},
mongodb: {
uri: config.database.mongodb.uri,
database: config.database.mongodb.database,
},
logger
postgres: {
host: config.database.postgres.host,
port: config.database.postgres.port,
database: config.database.postgres.database,
user: config.database.postgres.user,
password: config.database.postgres.password,
},
questdb: {
enabled: false, // Web API doesn't need QuestDB
host: config.database.questdb.host,
httpPort: config.database.questdb.httpPort,
pgPort: config.database.questdb.pgPort,
influxPort: config.database.questdb.ilpPort,
database: config.database.questdb.database,
},
};
container = createServiceContainer(awilixConfig);
await initializeAwilixServices(container);
logger.info('Awilix container created and initialized');
// Setup service-specific configuration
const serviceContainer = setupServiceContainer(config, container.resolve('serviceContainer'));
// Initialize migration helper for backward compatibility
const { setContainerForMigration } = await import('./migration-helper');
setContainerForMigration(serviceContainer);
logger.info('Migration helper initialized for backward compatibility');
// Create app with routes
app = new Hono();
// Add CORS middleware
app.use(
'*',
cors({
origin: ['http://localhost:4200', 'http://localhost:3000', 'http://localhost:3002'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
})
);
await postgresClient.connect();
setPostgreSQLClient(postgresClient);
logger.info('PostgreSQL connected');
// Basic API info endpoint
app.get('/', c => {
return c.json({
name: 'Stock Bot Web API',
version: '1.0.0',
status: 'running',
timestamp: new Date().toISOString(),
endpoints: {
health: '/health',
exchanges: '/api/exchanges',
},
});
});
// Create and mount routes using the service container
const routes = createRoutes(serviceContainer);
app.route('/', routes);
logger.info('All services initialized successfully');
} catch (error) {
logger.error('Failed to initialize services', { error });
console.error('DETAILED ERROR:', error);
logger.error('Failed to initialize services', {
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
details: JSON.stringify(error, null, 2)
});
throw error;
}
}
@ -128,17 +140,22 @@ async function initializeServices() {
async function startServer() {
await initializeServices();
if (!app) {
throw new Error('App not initialized');
}
server = Bun.serve({
port: PORT,
fetch: app.fetch,
development: config.environment === 'development',
});
logger.info(`Stock Bot Web API started on port ${PORT}`);
logger.info(`Web API service started on port ${PORT}`);
}
// Register shutdown handlers
shutdown.onShutdown(async () => {
// Register shutdown handlers with priorities
// Priority 1: HTTP Server (high priority)
shutdown.onShutdownHigh(async () => {
if (server) {
logger.info('Stopping HTTP server...');
try {
@ -148,36 +165,42 @@ shutdown.onShutdown(async () => {
logger.error('Error stopping HTTP server', { error });
}
}
});
}, 'HTTP Server');
shutdown.onShutdown(async () => {
logger.info('Disconnecting from databases...');
// Priority 2: Services and connections (medium priority)
shutdown.onShutdownMedium(async () => {
logger.info('Disposing services and connections...');
try {
if (mongoClient) {
await mongoClient.disconnect();
if (container) {
// Disconnect database clients
const mongoClient = container.resolve('mongoClient');
if (mongoClient?.disconnect) await mongoClient.disconnect();
const postgresClient = container.resolve('postgresClient');
if (postgresClient?.disconnect) await postgresClient.disconnect();
logger.info('All services disposed successfully');
}
if (postgresClient) {
await postgresClient.disconnect();
}
logger.info('Database connections closed');
} catch (error) {
logger.error('Error closing database connections', { error });
logger.error('Error disposing services', { error });
}
});
}, 'Services');
shutdown.onShutdown(async () => {
// Priority 3: Logger shutdown (lowest priority - runs last)
shutdown.onShutdownLow(async () => {
try {
logger.info('Shutting down loggers...');
await shutdownLoggers();
// process.stdout.write('Web API loggers shut down\n');
} catch (error) {
process.stderr.write(`Error shutting down loggers: ${error}\n`);
// Don't log after shutdown
} catch {
// Silently ignore logger shutdown errors
}
});
}, 'Loggers');
// Start the service
startServer().catch(error => {
logger.error('Failed to start web API service', { error });
logger.fatal('Failed to start web API service', { error });
process.exit(1);
});
logger.info('Web API service startup initiated');
logger.info('Web API service startup initiated with DI pattern');

View file

@ -0,0 +1,30 @@
/**
* Temporary migration helper for web-api service
* Provides backward compatibility while migrating to DI container
*
* TODO: Remove this file once all routes and services are migrated to use ServiceContainer
*/
import type { ServiceContainer } from '@stock-bot/di';
import type { MongoDBClient } from '@stock-bot/mongodb';
import type { PostgreSQLClient } from '@stock-bot/postgres';
let containerInstance: ServiceContainer | null = null;
export function setContainerForMigration(container: ServiceContainer): void {
containerInstance = container;
}
export function getMongoDBClient(): MongoDBClient {
if (!containerInstance) {
throw new Error('Container not initialized. This is a migration helper - please update the service to accept ServiceContainer parameter');
}
return containerInstance.mongodb;
}
export function getPostgreSQLClient(): PostgreSQLClient {
if (!containerInstance) {
throw new Error('Container not initialized. This is a migration helper - please update the service to accept ServiceContainer parameter');
}
return containerInstance.postgres;
}

View file

@ -0,0 +1,24 @@
/**
* Route factory for web API service
* Creates routes with access to the service container
*/
import { Hono } from 'hono';
import type { ServiceContainer } from '@stock-bot/di';
import { healthRoutes, exchangeRoutes } from './index';
export function createRoutes(container: ServiceContainer): Hono {
const app = new Hono();
// Add container to context for all routes
app.use('*', async (c, next) => {
c.set('container', container);
await next();
});
// Mount routes
app.route('/health', healthRoutes);
app.route('/api/exchanges', exchangeRoutes);
return app;
}