cleaner dev experience refactor
This commit is contained in:
parent
8b17f98845
commit
742e590382
7 changed files with 407 additions and 17 deletions
94
apps/data-ingestion/src/handlers/example/example.handler.ts
Normal file
94
apps/data-ingestion/src/handlers/example/example.handler.ts
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* Example Handler - Demonstrates ergonomic handler patterns
|
||||
* Shows inline operations, service helpers, and scheduled operations
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseHandler,
|
||||
Handler,
|
||||
Operation,
|
||||
ScheduledOperation,
|
||||
type ExecutionContext,
|
||||
type IServiceContainer
|
||||
} from '@stock-bot/handlers';
|
||||
|
||||
@Handler('example')
|
||||
export class ExampleHandler extends BaseHandler {
|
||||
constructor(services: IServiceContainer) {
|
||||
super(services);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple inline operation - no separate action file needed
|
||||
*/
|
||||
@Operation('get-stats')
|
||||
async getStats(): Promise<{ total: number; active: number; cached: boolean }> {
|
||||
// Use collection helper for cleaner MongoDB access
|
||||
const total = await this.collection('items').countDocuments();
|
||||
const active = await this.collection('items').countDocuments({ status: 'active' });
|
||||
|
||||
// Use cache helpers with automatic prefixing
|
||||
const cached = await this.cacheGet<number>('last-total');
|
||||
await this.cacheSet('last-total', total, 300); // 5 minutes
|
||||
|
||||
// Use log helper with automatic handler context
|
||||
this.log('info', 'Stats retrieved', { total, active });
|
||||
|
||||
return { total, active, cached: cached !== null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Scheduled operation using combined decorator
|
||||
*/
|
||||
@ScheduledOperation('cleanup-old-items', '0 2 * * *', {
|
||||
priority: 5,
|
||||
description: 'Clean up items older than 30 days'
|
||||
})
|
||||
async cleanupOldItems(): Promise<{ deleted: number }> {
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const result = await this.collection('items').deleteMany({
|
||||
createdAt: { $lt: thirtyDaysAgo }
|
||||
});
|
||||
|
||||
this.log('info', 'Cleanup completed', { deleted: result.deletedCount });
|
||||
|
||||
// Schedule a follow-up task
|
||||
await this.scheduleIn('generate-report', { type: 'cleanup' }, 60); // 1 minute
|
||||
|
||||
return { deleted: result.deletedCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Operation that uses proxy service
|
||||
*/
|
||||
@Operation('fetch-external-data')
|
||||
async fetchExternalData(input: { url: string }): Promise<{ data: any }> {
|
||||
const proxyUrl = this.proxy.getProxy();
|
||||
|
||||
if (!proxyUrl) {
|
||||
throw new Error('No proxy available');
|
||||
}
|
||||
|
||||
// Use HTTP client with proxy
|
||||
const response = await this.http.get(input.url, {
|
||||
proxy: proxyUrl,
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
// Cache the result
|
||||
await this.cacheSet(`external:${input.url}`, response.data, 3600);
|
||||
|
||||
return { data: response.data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Complex operation that still uses action file
|
||||
*/
|
||||
@Operation('process-batch')
|
||||
async processBatch(input: any, context: ExecutionContext): Promise<unknown> {
|
||||
// For complex operations, still use action files
|
||||
const { processBatch } = await import('./actions/batch.action');
|
||||
return processBatch(this, input);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +1,74 @@
|
|||
/**
|
||||
* Handler auto-registration
|
||||
* Import all handlers here to trigger auto-registration
|
||||
* Automatically discovers and registers all handlers
|
||||
*/
|
||||
|
||||
import type { IDataIngestionServices } from '@stock-bot/di';
|
||||
import { createServiceAdapter } from '@stock-bot/di';
|
||||
import { QMHandler } from './qm/qm.handler';
|
||||
import { WebShareHandler } from './webshare/webshare.handler';
|
||||
import { autoRegisterHandlers } from '@stock-bot/handlers';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import { join } from 'path';
|
||||
|
||||
// Import handlers for bundling (ensures they're included in the build)
|
||||
import './qm/qm.handler';
|
||||
import './webshare/webshare.handler';
|
||||
// Add more handler imports as needed
|
||||
|
||||
const logger = getLogger('handler-init');
|
||||
|
||||
/**
|
||||
* Initialize and register all handlers
|
||||
* Initialize and register all handlers automatically
|
||||
*/
|
||||
export function initializeAllHandlers(services: IDataIngestionServices): void {
|
||||
export async function initializeAllHandlers(services: IDataIngestionServices): Promise<void> {
|
||||
// Create generic service container adapter
|
||||
const serviceContainer = createServiceAdapter(services);
|
||||
|
||||
// QM Handler
|
||||
const qmHandler = new QMHandler(serviceContainer);
|
||||
qmHandler.register();
|
||||
try {
|
||||
// Auto-register all handlers in this directory
|
||||
const result = await autoRegisterHandlers(
|
||||
__dirname,
|
||||
serviceContainer,
|
||||
{
|
||||
pattern: '.handler.',
|
||||
exclude: ['test', 'spec'],
|
||||
dryRun: false
|
||||
}
|
||||
);
|
||||
|
||||
logger.info('Handler auto-registration complete', {
|
||||
registered: result.registered,
|
||||
failed: result.failed
|
||||
});
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
logger.error('Some handlers failed to register', { failed: result.failed });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Handler auto-registration failed', { error });
|
||||
// Fall back to manual registration
|
||||
await manualHandlerRegistration(serviceContainer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual fallback registration
|
||||
*/
|
||||
async function manualHandlerRegistration(serviceContainer: any): Promise<void> {
|
||||
logger.warn('Falling back to manual handler registration');
|
||||
|
||||
// WebShare Handler
|
||||
const webShareHandler = new WebShareHandler(serviceContainer);
|
||||
webShareHandler.register();
|
||||
|
||||
// TODO: Add other handlers here as they're converted
|
||||
// const ibHandler = new IBHandler(serviceContainer);
|
||||
// ibHandler.register();
|
||||
try {
|
||||
// Import and register handlers manually
|
||||
const { QMHandler } = await import('./qm/qm.handler');
|
||||
const qmHandler = new QMHandler(serviceContainer);
|
||||
qmHandler.register();
|
||||
|
||||
const { WebShareHandler } = await import('./webshare/webshare.handler');
|
||||
const webShareHandler = new WebShareHandler(serviceContainer);
|
||||
webShareHandler.register();
|
||||
|
||||
logger.info('Manual handler registration complete');
|
||||
} catch (error) {
|
||||
logger.error('Manual handler registration failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -85,7 +85,7 @@ async function initializeServices() {
|
|||
logger.debug('Initializing data handlers with new DI pattern...');
|
||||
|
||||
// Auto-register all handlers
|
||||
initializeAllHandlers(services);
|
||||
await initializeAllHandlers(services);
|
||||
|
||||
logger.info('Data handlers initialized with new DI pattern');
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,52 @@ export abstract class BaseHandler implements IHandler {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods for common operations
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get a MongoDB collection with type safety
|
||||
*/
|
||||
protected collection(name: string) {
|
||||
return this.mongodb.collection(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheSet(key: string, value: any, ttl?: number): Promise<void> {
|
||||
return this.cache.set(`${this.handlerName}:${key}`, value, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheGet(key: string): Promise<any | null> {
|
||||
return this.cache.get(`${this.handlerName}:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cache with handler-prefixed key
|
||||
*/
|
||||
protected async cacheDel(key: string): Promise<void> {
|
||||
return this.cache.del(`${this.handlerName}:${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule operation with delay in seconds
|
||||
*/
|
||||
protected async scheduleIn(operation: string, payload: unknown, delaySeconds: number): Promise<void> {
|
||||
return this.scheduleOperation(operation, payload, delaySeconds * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log with handler context
|
||||
*/
|
||||
protected log(level: 'info' | 'warn' | 'error' | 'debug', message: string, meta?: any): void {
|
||||
this.logger[level](message, { handler: this.handlerName, ...meta });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event methods - commented for future
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -76,6 +76,34 @@ export function QueueSchedule(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined decorator for scheduled operations
|
||||
* Automatically creates both an operation and a schedule
|
||||
* @param name Operation name
|
||||
* @param cronPattern Cron pattern for scheduling
|
||||
* @param options Schedule options
|
||||
*/
|
||||
export function ScheduledOperation(
|
||||
name: string,
|
||||
cronPattern: string,
|
||||
options?: {
|
||||
priority?: number;
|
||||
immediately?: boolean;
|
||||
description?: string;
|
||||
}
|
||||
): any {
|
||||
return function (
|
||||
target: any,
|
||||
methodName: string,
|
||||
descriptor?: PropertyDescriptor
|
||||
): any {
|
||||
// Apply both decorators
|
||||
Operation(name)(target, methodName, descriptor);
|
||||
QueueSchedule(cronPattern, options)(target, methodName, descriptor);
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
|
||||
// Future event decorators - commented for now
|
||||
// export function EventListener(eventName: string) {
|
||||
// return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ export type { IServiceContainer } from './types/service-container';
|
|||
export { createJobHandler } from './types/types';
|
||||
|
||||
// Decorators
|
||||
export { Handler, Operation, QueueSchedule } from './decorators/decorators';
|
||||
export { Handler, Operation, QueueSchedule, ScheduledOperation } from './decorators/decorators';
|
||||
|
||||
// Auto-registration utilities
|
||||
export { autoRegisterHandlers, createAutoHandlerRegistry } from './registry/auto-register';
|
||||
|
||||
// Future exports - commented for now
|
||||
// export { EventListener, EventPublisher } from './decorators/decorators';
|
||||
174
libs/core/handlers/src/registry/auto-register.ts
Normal file
174
libs/core/handlers/src/registry/auto-register.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* Auto-registration utilities for handlers
|
||||
* Automatically discovers and registers handlers based on file patterns
|
||||
*/
|
||||
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { IServiceContainer } from '../types/service-container';
|
||||
import { BaseHandler } from '../base/BaseHandler';
|
||||
import { readdirSync, statSync } from 'fs';
|
||||
import { join, relative } from 'path';
|
||||
|
||||
const logger = getLogger('handler-auto-register');
|
||||
|
||||
/**
|
||||
* Recursively find all handler files in a directory
|
||||
*/
|
||||
function findHandlerFiles(dir: string, pattern = '.handler.'): string[] {
|
||||
const files: string[] = [];
|
||||
|
||||
function scan(currentDir: string) {
|
||||
const entries = readdirSync(currentDir);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(currentDir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
|
||||
scan(fullPath);
|
||||
} else if (stat.isFile() && entry.includes(pattern) && entry.endsWith('.ts')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scan(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract handler classes from a module
|
||||
*/
|
||||
function extractHandlerClasses(module: any): Array<new (services: IServiceContainer) => BaseHandler> {
|
||||
const handlers: Array<new (services: IServiceContainer) => BaseHandler> = [];
|
||||
|
||||
for (const key of Object.keys(module)) {
|
||||
const exported = module[key];
|
||||
|
||||
// Check if it's a class that extends BaseHandler
|
||||
if (
|
||||
typeof exported === 'function' &&
|
||||
exported.prototype &&
|
||||
exported.prototype instanceof BaseHandler
|
||||
) {
|
||||
handlers.push(exported);
|
||||
}
|
||||
}
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-register all handlers in a directory
|
||||
* @param directory The directory to scan for handlers
|
||||
* @param services The service container to inject into handlers
|
||||
* @param options Configuration options
|
||||
*/
|
||||
export async function autoRegisterHandlers(
|
||||
directory: string,
|
||||
services: IServiceContainer,
|
||||
options: {
|
||||
pattern?: string;
|
||||
exclude?: string[];
|
||||
dryRun?: boolean;
|
||||
} = {}
|
||||
): Promise<{ registered: string[]; failed: string[] }> {
|
||||
const { pattern = '.handler.', exclude = [], dryRun = false } = options;
|
||||
const registered: string[] = [];
|
||||
const failed: string[] = [];
|
||||
|
||||
try {
|
||||
logger.info('Starting auto-registration of handlers', { directory, pattern });
|
||||
|
||||
// Find all handler files
|
||||
const handlerFiles = findHandlerFiles(directory, pattern);
|
||||
logger.debug(`Found ${handlerFiles.length} handler files`, { files: handlerFiles });
|
||||
|
||||
// Process each handler file
|
||||
for (const file of handlerFiles) {
|
||||
const relativePath = relative(directory, file);
|
||||
|
||||
// Skip excluded files
|
||||
if (exclude.some(ex => relativePath.includes(ex))) {
|
||||
logger.debug(`Skipping excluded file: ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Import the module
|
||||
const module = await import(file);
|
||||
const handlerClasses = extractHandlerClasses(module);
|
||||
|
||||
if (handlerClasses.length === 0) {
|
||||
logger.warn(`No handler classes found in ${relativePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Register each handler class
|
||||
for (const HandlerClass of handlerClasses) {
|
||||
const handlerName = HandlerClass.name;
|
||||
|
||||
if (dryRun) {
|
||||
logger.info(`[DRY RUN] Would register handler: ${handlerName} from ${relativePath}`);
|
||||
registered.push(handlerName);
|
||||
} else {
|
||||
logger.info(`Registering handler: ${handlerName} from ${relativePath}`);
|
||||
|
||||
// Create instance and register
|
||||
const handler = new HandlerClass(services);
|
||||
handler.register();
|
||||
|
||||
registered.push(handlerName);
|
||||
logger.info(`Successfully registered handler: ${handlerName}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to process handler file: ${relativePath}`, { error });
|
||||
failed.push(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('Auto-registration complete', {
|
||||
totalFiles: handlerFiles.length,
|
||||
registered: registered.length,
|
||||
failed: failed.length
|
||||
});
|
||||
|
||||
return { registered, failed };
|
||||
} catch (error) {
|
||||
logger.error('Auto-registration failed', { error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a handler registry that auto-discovers handlers
|
||||
*/
|
||||
export function createAutoHandlerRegistry(services: IServiceContainer) {
|
||||
return {
|
||||
/**
|
||||
* Register all handlers from a directory
|
||||
*/
|
||||
async registerDirectory(directory: string, options?: Parameters<typeof autoRegisterHandlers>[2]) {
|
||||
return autoRegisterHandlers(directory, services, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Register handlers from multiple directories
|
||||
*/
|
||||
async registerDirectories(directories: string[], options?: Parameters<typeof autoRegisterHandlers>[2]) {
|
||||
const results = {
|
||||
registered: [] as string[],
|
||||
failed: [] as string[]
|
||||
};
|
||||
|
||||
for (const dir of directories) {
|
||||
const result = await autoRegisterHandlers(dir, services, options);
|
||||
results.registered.push(...result.registered);
|
||||
results.failed.push(...result.failed);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue