fixed format issues
This commit is contained in:
parent
a700818a06
commit
08f713d98b
55 changed files with 5680 additions and 5533 deletions
|
|
@ -2,8 +2,8 @@
|
|||
* Handler registration for data pipeline service
|
||||
*/
|
||||
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import { getLogger } from '@stock-bot/logger';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
// Import handlers directly
|
||||
import { ExchangesHandler } from './exchanges/exchanges.handler';
|
||||
import { SymbolsHandler } from './symbols/symbols.handler';
|
||||
|
|
@ -18,10 +18,7 @@ export async function initializeAllHandlers(container: IServiceContainer): Promi
|
|||
|
||||
try {
|
||||
// Register handlers manually
|
||||
const handlers = [
|
||||
ExchangesHandler,
|
||||
SymbolsHandler,
|
||||
];
|
||||
const handlers = [ExchangesHandler, SymbolsHandler];
|
||||
|
||||
for (const Handler of handlers) {
|
||||
try {
|
||||
|
|
|
|||
12
libs/core/cache/src/cache-factory.ts
vendored
12
libs/core/cache/src/cache-factory.ts
vendored
|
|
@ -44,11 +44,13 @@ export function createNamespacedCache(
|
|||
* Type guard to check if cache is available
|
||||
*/
|
||||
export function isCacheAvailable(cache: unknown): cache is CacheProvider {
|
||||
return cache !== null &&
|
||||
cache !== undefined &&
|
||||
typeof cache === 'object' &&
|
||||
'get' in cache &&
|
||||
typeof (cache as CacheProvider).get === 'function';
|
||||
return (
|
||||
cache !== null &&
|
||||
cache !== undefined &&
|
||||
typeof cache === 'object' &&
|
||||
'get' in cache &&
|
||||
typeof (cache as CacheProvider).get === 'function'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
6
libs/core/cache/src/connection-manager.ts
vendored
6
libs/core/cache/src/connection-manager.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import Redis from 'ioredis';
|
||||
import type { RedisConfig } from './types';
|
||||
import { REDIS_DEFAULTS } from './constants';
|
||||
import type { RedisConfig } from './types';
|
||||
|
||||
interface ConnectionConfig {
|
||||
name: string;
|
||||
|
|
@ -68,9 +68,7 @@ export class RedisConnectionManager {
|
|||
* Close all connections
|
||||
*/
|
||||
static async closeAll(): Promise<void> {
|
||||
const promises = Array.from(this.connections.values()).map(conn =>
|
||||
conn.quit().catch(() => {})
|
||||
);
|
||||
const promises = Array.from(this.connections.values()).map(conn => conn.quit().catch(() => {}));
|
||||
await Promise.all(promises);
|
||||
this.connections.clear();
|
||||
}
|
||||
|
|
|
|||
27
libs/core/cache/src/redis-cache.ts
vendored
27
libs/core/cache/src/redis-cache.ts
vendored
|
|
@ -8,7 +8,8 @@ import type { CacheOptions, CacheProvider, CacheStats } from './types';
|
|||
*/
|
||||
export class RedisCache implements CacheProvider {
|
||||
private redis: Redis;
|
||||
private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } = console;
|
||||
private logger: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void } =
|
||||
console;
|
||||
private defaultTTL: number;
|
||||
private keyPrefix: string;
|
||||
private stats: CacheStats = {
|
||||
|
|
@ -72,13 +73,15 @@ export class RedisCache implements CacheProvider {
|
|||
async set<T>(
|
||||
key: string,
|
||||
value: T,
|
||||
options?: number | {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}
|
||||
options?:
|
||||
| number
|
||||
| {
|
||||
ttl?: number;
|
||||
preserveTTL?: boolean;
|
||||
onlyIfExists?: boolean;
|
||||
onlyIfNotExists?: boolean;
|
||||
getOldValue?: boolean;
|
||||
}
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
const fullKey = this.getKey(key);
|
||||
|
|
@ -145,7 +148,7 @@ export class RedisCache implements CacheProvider {
|
|||
try {
|
||||
const stream = this.redis.scanStream({
|
||||
match: `${this.keyPrefix}*`,
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT,
|
||||
});
|
||||
|
||||
const pipeline = this.redis.pipeline();
|
||||
|
|
@ -172,7 +175,7 @@ export class RedisCache implements CacheProvider {
|
|||
const keys: string[] = [];
|
||||
const stream = this.redis.scanStream({
|
||||
match: `${this.keyPrefix}${pattern}`,
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT
|
||||
count: CACHE_DEFAULTS.SCAN_COUNT,
|
||||
});
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
|
|
@ -206,7 +209,9 @@ export class RedisCache implements CacheProvider {
|
|||
}
|
||||
|
||||
async waitForReady(timeout = 5000): Promise<void> {
|
||||
if (this.redis.status === 'ready') {return;}
|
||||
if (this.redis.status === 'ready') {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
|
|
|
|||
7
libs/core/cache/src/types.ts
vendored
7
libs/core/cache/src/types.ts
vendored
|
|
@ -113,7 +113,12 @@ export interface CacheOptions {
|
|||
name?: string; // Name for connection identification
|
||||
shared?: boolean; // Whether to use shared connection
|
||||
redisConfig: RedisConfig;
|
||||
logger?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void; debug?: (...args: unknown[]) => void };
|
||||
logger?: {
|
||||
info?: (...args: unknown[]) => void;
|
||||
error?: (...args: unknown[]) => void;
|
||||
warn?: (...args: unknown[]) => void;
|
||||
debug?: (...args: unknown[]) => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CacheStats {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { RedisConnectionManager } from '../src/connection-manager';
|
||||
|
||||
describe('RedisConnectionManager', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { NamespacedCache, CacheAdapter } from '../src/namespaced-cache';
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { CacheAdapter, NamespacedCache } from '../src/namespaced-cache';
|
||||
import type { CacheProvider, ICache } from '../src/types';
|
||||
|
||||
describe('NamespacedCache', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { RedisCache } from '../src/redis-cache';
|
||||
import type { CacheOptions } from '../src/types';
|
||||
|
||||
|
|
|
|||
2
libs/core/cache/test/redis-cache.test.ts
vendored
2
libs/core/cache/test/redis-cache.test.ts
vendored
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, beforeEach } from 'bun:test';
|
||||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { RedisCache } from '../src/redis-cache';
|
||||
import type { CacheOptions } from '../src/types';
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
this.loaders = options.loaders;
|
||||
} else {
|
||||
const configPath = options.configPath || join(process.cwd(), 'config');
|
||||
this.loaders = [
|
||||
new FileLoader(configPath, this.environment),
|
||||
new EnvLoader(''),
|
||||
];
|
||||
this.loaders = [new FileLoader(configPath, this.environment), new EnvLoader('')];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +58,11 @@ export class ConfigManager<T = Record<string, unknown>> {
|
|||
const mergedConfig = this.merge(...configs) as T;
|
||||
|
||||
// Add environment if not present
|
||||
if (typeof mergedConfig === 'object' && mergedConfig !== null && !('environment' in mergedConfig)) {
|
||||
if (
|
||||
typeof mergedConfig === 'object' &&
|
||||
mergedConfig !== null &&
|
||||
!('environment' in mergedConfig)
|
||||
) {
|
||||
(mergedConfig as Record<string, unknown>)['environment'] = this.environment;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ export {
|
|||
} from './schemas';
|
||||
|
||||
// createAppConfig function for apps/stock
|
||||
export function createAppConfig<T>(schema: unknown, options?: ConfigManagerOptions): ConfigManager<T> {
|
||||
export function createAppConfig<T>(
|
||||
schema: unknown,
|
||||
options?: ConfigManagerOptions
|
||||
): ConfigManager<T> {
|
||||
return new ConfigManager<T>(options);
|
||||
}
|
||||
|
|
@ -133,10 +133,7 @@ export class EnvLoader implements ConfigLoader {
|
|||
|
||||
private shouldPreserveStringForKey(key: string): boolean {
|
||||
// Keys that should preserve string values even if they look like numbers
|
||||
const preserveStringKeys = [
|
||||
'QM_WEBMASTER_ID',
|
||||
'IB_MARKET_DATA_TYPE'
|
||||
];
|
||||
const preserveStringKeys = ['QM_WEBMASTER_ID', 'IB_MARKET_DATA_TYPE'];
|
||||
return preserveStringKeys.includes(key);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,23 +24,27 @@ export const eodProviderConfigSchema = baseProviderConfigSchema.extend({
|
|||
|
||||
// Interactive Brokers provider
|
||||
export const ibProviderConfigSchema = baseProviderConfigSchema.extend({
|
||||
gateway: z.object({
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(5000),
|
||||
clientId: z.number().default(1),
|
||||
}).default({
|
||||
host: 'localhost',
|
||||
port: 5000,
|
||||
clientId: 1,
|
||||
}),
|
||||
account: z.string().optional(),
|
||||
marketDataType: z.union([
|
||||
z.enum(['live', 'delayed', 'frozen']),
|
||||
z.enum(['1', '2', '3']).transform((val) => {
|
||||
const mapping = { '1': 'live', '2': 'frozen', '3': 'delayed' } as const;
|
||||
return mapping[val];
|
||||
gateway: z
|
||||
.object({
|
||||
host: z.string().default('localhost'),
|
||||
port: z.number().default(5000),
|
||||
clientId: z.number().default(1),
|
||||
})
|
||||
.default({
|
||||
host: 'localhost',
|
||||
port: 5000,
|
||||
clientId: 1,
|
||||
}),
|
||||
]).default('delayed'),
|
||||
account: z.string().optional(),
|
||||
marketDataType: z
|
||||
.union([
|
||||
z.enum(['live', 'delayed', 'frozen']),
|
||||
z.enum(['1', '2', '3']).transform(val => {
|
||||
const mapping = { '1': 'live', '2': 'frozen', '3': 'delayed' } as const;
|
||||
return mapping[val];
|
||||
}),
|
||||
])
|
||||
.default('delayed'),
|
||||
});
|
||||
|
||||
// QuoteMedia provider
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect, beforeEach, mock, spyOn } from 'bun:test';
|
||||
import { beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import { ConfigManager } from '../src/config-manager';
|
||||
import { ConfigError, ConfigValidationError } from '../src/errors';
|
||||
|
|
@ -11,7 +11,7 @@ mock.module('@stock-bot/logger', () => ({
|
|||
error: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
debug: mock(() => {}),
|
||||
})
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock loader class
|
||||
|
|
@ -49,12 +49,12 @@ describe('ConfigManager', () => {
|
|||
|
||||
it('should handle various environment values', () => {
|
||||
const envMap: Record<string, Environment> = {
|
||||
'production': 'production',
|
||||
'prod': 'production',
|
||||
'test': 'test',
|
||||
'development': 'development',
|
||||
'dev': 'development',
|
||||
'unknown': 'development',
|
||||
production: 'production',
|
||||
prod: 'production',
|
||||
test: 'test',
|
||||
development: 'development',
|
||||
dev: 'development',
|
||||
unknown: 'development',
|
||||
};
|
||||
|
||||
for (const [input, expected] of Object.entries(envMap)) {
|
||||
|
|
@ -348,7 +348,9 @@ describe('ConfigManager', () => {
|
|||
environment: z.string(),
|
||||
});
|
||||
|
||||
manager = new ConfigManager({ loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })] });
|
||||
manager = new ConfigManager({
|
||||
loaders: [new MockLoader({ app: { name: 'test', version: '1.0.0' }, port: 3000 })],
|
||||
});
|
||||
manager.initialize(schema);
|
||||
|
||||
// Valid update
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
baseAppSchema,
|
||||
ConfigManager,
|
||||
createAppConfig,
|
||||
} from '../src';
|
||||
import { baseAppSchema, ConfigManager, createAppConfig } from '../src';
|
||||
import { ConfigError, ConfigValidationError } from '../src/errors';
|
||||
|
||||
// Mock loader for testing
|
||||
|
|
@ -160,7 +156,6 @@ describe('ConfigManager', () => {
|
|||
expect(validated).toEqual({ name: 'test', port: 3000 });
|
||||
});
|
||||
|
||||
|
||||
it('should add environment if not present', () => {
|
||||
const mockManager = new ConfigManager({
|
||||
environment: 'test',
|
||||
|
|
@ -172,7 +167,6 @@ describe('ConfigManager', () => {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
describe('Config Builders', () => {
|
||||
it('should create app config with schema', () => {
|
||||
const schema = z.object({
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
|
||||
import { readFileSync } from 'fs';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||
import { ConfigLoaderError } from '../src/errors';
|
||||
import { EnvLoader } from '../src/loaders/env.loader';
|
||||
|
||||
// Mock fs module
|
||||
mock.module('fs', () => ({
|
||||
readFileSync: mock(() => '')
|
||||
readFileSync: mock(() => ''),
|
||||
}));
|
||||
|
||||
describe('EnvLoader', () => {
|
||||
|
|
@ -133,9 +133,9 @@ describe('EnvLoader', () => {
|
|||
APP: {
|
||||
NAME: 'myapp',
|
||||
CONFIG: {
|
||||
PORT: 3000
|
||||
}
|
||||
}
|
||||
PORT: 3000,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -152,9 +152,9 @@ describe('EnvLoader', () => {
|
|||
host: 'localhost',
|
||||
port: 5432,
|
||||
credentials: {
|
||||
user: 'admin'
|
||||
}
|
||||
}
|
||||
user: 'admin',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -422,7 +422,7 @@ KEY_WITHOUT_VALUE=
|
|||
const config = { readonly: 'original' };
|
||||
Object.defineProperty(config, 'readonly', {
|
||||
writable: false,
|
||||
configurable: false
|
||||
configurable: false,
|
||||
});
|
||||
|
||||
process.env.READONLY = 'new_value';
|
||||
|
|
@ -463,11 +463,11 @@ KEY_WITHOUT_VALUE=
|
|||
c: {
|
||||
d: {
|
||||
e: {
|
||||
f: 'deep'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
f: 'deep',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -513,16 +513,18 @@ KEY_WITHOUT_VALUE=
|
|||
loader = new EnvLoader();
|
||||
const config = loader.load();
|
||||
|
||||
expect(config.providers).toEqual(expect.objectContaining({
|
||||
qm: {
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
baseUrl: 'https://api.quotemedia.com',
|
||||
webmasterId: '12345',
|
||||
enabled: true,
|
||||
priority: 5,
|
||||
},
|
||||
}));
|
||||
expect(config.providers).toEqual(
|
||||
expect.objectContaining({
|
||||
qm: {
|
||||
username: 'testuser',
|
||||
password: 'testpass',
|
||||
baseUrl: 'https://api.quotemedia.com',
|
||||
webmasterId: '12345',
|
||||
enabled: true,
|
||||
priority: 5,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Yahoo Finance provider mappings', () => {
|
||||
|
|
@ -535,15 +537,17 @@ KEY_WITHOUT_VALUE=
|
|||
loader = new EnvLoader();
|
||||
const config = loader.load();
|
||||
|
||||
expect(config.providers).toEqual(expect.objectContaining({
|
||||
yahoo: {
|
||||
baseUrl: 'https://finance.yahoo.com',
|
||||
cookieJar: '/path/to/cookies',
|
||||
crumb: 'abc123',
|
||||
enabled: false,
|
||||
priority: 10,
|
||||
},
|
||||
}));
|
||||
expect(config.providers).toEqual(
|
||||
expect.objectContaining({
|
||||
yahoo: {
|
||||
baseUrl: 'https://finance.yahoo.com',
|
||||
cookieJar: '/path/to/cookies',
|
||||
crumb: 'abc123',
|
||||
enabled: false,
|
||||
priority: 10,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle additional provider mappings', () => {
|
||||
|
|
@ -557,14 +561,18 @@ KEY_WITHOUT_VALUE=
|
|||
loader = new EnvLoader();
|
||||
const config = loader.load();
|
||||
|
||||
expect(config.webshare).toEqual(expect.objectContaining({
|
||||
apiUrl: 'https://api.webshare.io',
|
||||
}));
|
||||
expect(config.providers?.ib).toEqual(expect.objectContaining({
|
||||
account: 'DU123456',
|
||||
marketDataType: '1',
|
||||
priority: 3,
|
||||
}));
|
||||
expect(config.webshare).toEqual(
|
||||
expect.objectContaining({
|
||||
apiUrl: 'https://api.webshare.io',
|
||||
})
|
||||
);
|
||||
expect(config.providers?.ib).toEqual(
|
||||
expect.objectContaining({
|
||||
account: 'DU123456',
|
||||
marketDataType: '1',
|
||||
priority: 3,
|
||||
})
|
||||
);
|
||||
expect(config.version).toBe('1.2.3');
|
||||
expect(config.debug).toBe(true);
|
||||
});
|
||||
|
|
@ -610,7 +618,7 @@ KEY_WITHOUT_VALUE=
|
|||
|
||||
// CONFIG should be an object with nested value
|
||||
expect((config as any).config).toEqual({
|
||||
nested: 'nested_value'
|
||||
nested: 'nested_value',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -620,7 +628,7 @@ KEY_WITHOUT_VALUE=
|
|||
Object.defineProperty(testConfig, 'protected', {
|
||||
value: 'immutable',
|
||||
writable: false,
|
||||
configurable: false
|
||||
configurable: false,
|
||||
});
|
||||
|
||||
process.env.PROTECTED_NESTED_VALUE = 'test';
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { ConfigLoaderError } from '../src/errors';
|
||||
import { FileLoader } from '../src/loaders/file.loader';
|
||||
|
||||
// Mock fs module
|
||||
mock.module('fs', () => ({
|
||||
existsSync: mock(() => false),
|
||||
readFileSync: mock(() => '')
|
||||
readFileSync: mock(() => ''),
|
||||
}));
|
||||
|
||||
describe('FileLoader', () => {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
baseConfigSchema,
|
||||
environmentSchema,
|
||||
serviceConfigSchema,
|
||||
loggingConfigSchema,
|
||||
queueConfigSchema,
|
||||
httpConfigSchema,
|
||||
webshareConfigSchema,
|
||||
browserConfigSchema,
|
||||
proxyConfigSchema,
|
||||
postgresConfigSchema,
|
||||
questdbConfigSchema,
|
||||
mongodbConfigSchema,
|
||||
dragonflyConfigSchema,
|
||||
databaseConfigSchema,
|
||||
baseProviderConfigSchema,
|
||||
browserConfigSchema,
|
||||
databaseConfigSchema,
|
||||
dragonflyConfigSchema,
|
||||
environmentSchema,
|
||||
eodProviderConfigSchema,
|
||||
httpConfigSchema,
|
||||
ibProviderConfigSchema,
|
||||
qmProviderConfigSchema,
|
||||
yahooProviderConfigSchema,
|
||||
webshareProviderConfigSchema,
|
||||
loggingConfigSchema,
|
||||
mongodbConfigSchema,
|
||||
postgresConfigSchema,
|
||||
providerConfigSchema,
|
||||
proxyConfigSchema,
|
||||
qmProviderConfigSchema,
|
||||
questdbConfigSchema,
|
||||
queueConfigSchema,
|
||||
serviceConfigSchema,
|
||||
webshareConfigSchema,
|
||||
webshareProviderConfigSchema,
|
||||
yahooProviderConfigSchema,
|
||||
} from '../src/schemas';
|
||||
|
||||
describe('Config Schemas', () => {
|
||||
|
|
@ -202,7 +202,7 @@ describe('Config Schemas', () => {
|
|||
describe('queueConfigSchema', () => {
|
||||
it('should accept minimal config with defaults', () => {
|
||||
const config = queueConfigSchema.parse({
|
||||
redis: {}, // redis is required, but its properties have defaults
|
||||
redis: {}, // redis is required, but its properties have defaults
|
||||
});
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
|
|
@ -493,19 +493,23 @@ describe('Config Schemas', () => {
|
|||
});
|
||||
|
||||
it('should validate poolSize range', () => {
|
||||
expect(() => postgresConfigSchema.parse({
|
||||
database: 'testdb',
|
||||
user: 'testuser',
|
||||
password: 'testpass',
|
||||
poolSize: 0,
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
postgresConfigSchema.parse({
|
||||
database: 'testdb',
|
||||
user: 'testuser',
|
||||
password: 'testpass',
|
||||
poolSize: 0,
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
expect(() => postgresConfigSchema.parse({
|
||||
database: 'testdb',
|
||||
user: 'testuser',
|
||||
password: 'testpass',
|
||||
poolSize: 101,
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
postgresConfigSchema.parse({
|
||||
database: 'testdb',
|
||||
user: 'testuser',
|
||||
password: 'testpass',
|
||||
poolSize: 101,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -574,24 +578,30 @@ describe('Config Schemas', () => {
|
|||
});
|
||||
|
||||
it('should validate URI format', () => {
|
||||
expect(() => mongodbConfigSchema.parse({
|
||||
uri: 'invalid-uri',
|
||||
database: 'testdb',
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
mongodbConfigSchema.parse({
|
||||
uri: 'invalid-uri',
|
||||
database: 'testdb',
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it('should validate poolSize range', () => {
|
||||
expect(() => mongodbConfigSchema.parse({
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'testdb',
|
||||
poolSize: 0,
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
mongodbConfigSchema.parse({
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'testdb',
|
||||
poolSize: 0,
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
expect(() => mongodbConfigSchema.parse({
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'testdb',
|
||||
poolSize: 101,
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
mongodbConfigSchema.parse({
|
||||
uri: 'mongodb://localhost',
|
||||
database: 'testdb',
|
||||
poolSize: 101,
|
||||
})
|
||||
).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -703,11 +713,13 @@ describe('Config Schemas', () => {
|
|||
});
|
||||
|
||||
it('should validate tier values', () => {
|
||||
expect(() => eodProviderConfigSchema.parse({
|
||||
name: 'eod',
|
||||
apiKey: 'test-key',
|
||||
tier: 'premium',
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
eodProviderConfigSchema.parse({
|
||||
name: 'eod',
|
||||
apiKey: 'test-key',
|
||||
tier: 'premium',
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
const validTiers = ['free', 'fundamentals', 'all-in-one'];
|
||||
for (const tier of validTiers) {
|
||||
|
|
@ -759,10 +771,12 @@ describe('Config Schemas', () => {
|
|||
});
|
||||
|
||||
it('should validate marketDataType', () => {
|
||||
expect(() => ibProviderConfigSchema.parse({
|
||||
name: 'ib',
|
||||
marketDataType: 'realtime',
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
ibProviderConfigSchema.parse({
|
||||
name: 'ib',
|
||||
marketDataType: 'realtime',
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
const validTypes = ['live', 'delayed', 'frozen'];
|
||||
for (const type of validTypes) {
|
||||
|
|
@ -777,9 +791,11 @@ describe('Config Schemas', () => {
|
|||
|
||||
describe('qmProviderConfigSchema', () => {
|
||||
it('should require all credentials', () => {
|
||||
expect(() => qmProviderConfigSchema.parse({
|
||||
name: 'qm',
|
||||
})).toThrow();
|
||||
expect(() =>
|
||||
qmProviderConfigSchema.parse({
|
||||
name: 'qm',
|
||||
})
|
||||
).toThrow();
|
||||
|
||||
const config = qmProviderConfigSchema.parse({
|
||||
name: 'qm',
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
SecretValue,
|
||||
secret,
|
||||
checkRequiredEnvVars,
|
||||
COMMON_SECRET_PATTERNS,
|
||||
createStrictSchema,
|
||||
formatValidationResult,
|
||||
isSecret,
|
||||
redactSecrets,
|
||||
isSecretEnvVar,
|
||||
wrapSecretEnvVars,
|
||||
mergeSchemas,
|
||||
redactSecrets,
|
||||
secret,
|
||||
secretSchema,
|
||||
secretStringSchema,
|
||||
COMMON_SECRET_PATTERNS,
|
||||
validateConfig,
|
||||
checkRequiredEnvVars,
|
||||
SecretValue,
|
||||
validateCompleteness,
|
||||
formatValidationResult,
|
||||
createStrictSchema,
|
||||
mergeSchemas,
|
||||
validateConfig,
|
||||
wrapSecretEnvVars,
|
||||
type ValidationResult,
|
||||
} from '../src';
|
||||
|
||||
|
|
@ -443,9 +443,7 @@ describe('Config Utils', () => {
|
|||
it('should format warnings', () => {
|
||||
const result: ValidationResult = {
|
||||
valid: true,
|
||||
warnings: [
|
||||
{ path: 'deprecated.feature', message: 'This feature is deprecated' },
|
||||
],
|
||||
warnings: [{ path: 'deprecated.feature', message: 'This feature is deprecated' }],
|
||||
};
|
||||
|
||||
const formatted = formatValidationResult(result);
|
||||
|
|
|
|||
|
|
@ -166,82 +166,102 @@ export class ServiceApplication {
|
|||
private registerShutdownHandlers(): void {
|
||||
// Priority 1: Queue system (highest priority)
|
||||
if (this.serviceConfig.enableScheduledJobs) {
|
||||
this.shutdown.onShutdown(async () => {
|
||||
this.logger.info('Shutting down queue system...');
|
||||
try {
|
||||
const queueManager = this.container?.resolve('queueManager');
|
||||
if (queueManager) {
|
||||
await queueManager.shutdown();
|
||||
this.shutdown.onShutdown(
|
||||
async () => {
|
||||
this.logger.info('Shutting down queue system...');
|
||||
try {
|
||||
const queueManager = this.container?.resolve('queueManager');
|
||||
if (queueManager) {
|
||||
await queueManager.shutdown();
|
||||
}
|
||||
this.logger.info('Queue system shut down');
|
||||
} catch (error) {
|
||||
this.logger.error('Error shutting down queue system', { error });
|
||||
}
|
||||
this.logger.info('Queue system shut down');
|
||||
} catch (error) {
|
||||
this.logger.error('Error shutting down queue system', { error });
|
||||
}
|
||||
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'Queue System');
|
||||
},
|
||||
SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
|
||||
'Queue System'
|
||||
);
|
||||
}
|
||||
|
||||
// Priority 1: HTTP Server (high priority)
|
||||
this.shutdown.onShutdown(async () => {
|
||||
if (this.server) {
|
||||
this.logger.info('Stopping HTTP server...');
|
||||
try {
|
||||
this.server.stop();
|
||||
this.logger.info('HTTP server stopped');
|
||||
} catch (error) {
|
||||
this.logger.error('Error stopping HTTP server', { error });
|
||||
this.shutdown.onShutdown(
|
||||
async () => {
|
||||
if (this.server) {
|
||||
this.logger.info('Stopping HTTP server...');
|
||||
try {
|
||||
this.server.stop();
|
||||
this.logger.info('HTTP server stopped');
|
||||
} catch (error) {
|
||||
this.logger.error('Error stopping HTTP server', { error });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'HTTP Server');
|
||||
},
|
||||
SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
|
||||
'HTTP Server'
|
||||
);
|
||||
|
||||
// Custom shutdown hook
|
||||
if (this.hooks.onBeforeShutdown) {
|
||||
this.shutdown.onShutdown(async () => {
|
||||
try {
|
||||
await this.hooks.onBeforeShutdown!();
|
||||
} catch (error) {
|
||||
this.logger.error('Error in custom shutdown hook', { error });
|
||||
}
|
||||
}, SHUTDOWN_DEFAULTS.HIGH_PRIORITY, 'Custom Shutdown');
|
||||
this.shutdown.onShutdown(
|
||||
async () => {
|
||||
try {
|
||||
await this.hooks.onBeforeShutdown!();
|
||||
} catch (error) {
|
||||
this.logger.error('Error in custom shutdown hook', { error });
|
||||
}
|
||||
},
|
||||
SHUTDOWN_DEFAULTS.HIGH_PRIORITY,
|
||||
'Custom Shutdown'
|
||||
);
|
||||
}
|
||||
|
||||
// Priority 2: Services and connections (medium priority)
|
||||
this.shutdown.onShutdown(async () => {
|
||||
this.logger.info('Disposing services and connections...');
|
||||
try {
|
||||
if (this.container) {
|
||||
// Disconnect database clients
|
||||
const mongoClient = this.container.resolve('mongoClient');
|
||||
if (mongoClient?.disconnect) {
|
||||
await mongoClient.disconnect();
|
||||
}
|
||||
this.shutdown.onShutdown(
|
||||
async () => {
|
||||
this.logger.info('Disposing services and connections...');
|
||||
try {
|
||||
if (this.container) {
|
||||
// Disconnect database clients
|
||||
const mongoClient = this.container.resolve('mongoClient');
|
||||
if (mongoClient?.disconnect) {
|
||||
await mongoClient.disconnect();
|
||||
}
|
||||
|
||||
const postgresClient = this.container.resolve('postgresClient');
|
||||
if (postgresClient?.disconnect) {
|
||||
await postgresClient.disconnect();
|
||||
}
|
||||
const postgresClient = this.container.resolve('postgresClient');
|
||||
if (postgresClient?.disconnect) {
|
||||
await postgresClient.disconnect();
|
||||
}
|
||||
|
||||
const questdbClient = this.container.resolve('questdbClient');
|
||||
if (questdbClient?.disconnect) {
|
||||
await questdbClient.disconnect();
|
||||
}
|
||||
const questdbClient = this.container.resolve('questdbClient');
|
||||
if (questdbClient?.disconnect) {
|
||||
await questdbClient.disconnect();
|
||||
}
|
||||
|
||||
this.logger.info('All services disposed successfully');
|
||||
this.logger.info('All services disposed successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error disposing services', { error });
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error disposing services', { error });
|
||||
}
|
||||
}, SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, 'Services');
|
||||
},
|
||||
SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY,
|
||||
'Services'
|
||||
);
|
||||
|
||||
// Priority 3: Logger shutdown (lowest priority - runs last)
|
||||
this.shutdown.onShutdown(async () => {
|
||||
try {
|
||||
this.logger.info('Shutting down loggers...');
|
||||
await shutdownLoggers();
|
||||
// Don't log after shutdown
|
||||
} catch {
|
||||
// Silently ignore logger shutdown errors
|
||||
}
|
||||
}, SHUTDOWN_DEFAULTS.LOW_PRIORITY, 'Loggers');
|
||||
this.shutdown.onShutdown(
|
||||
async () => {
|
||||
try {
|
||||
this.logger.info('Shutting down loggers...');
|
||||
await shutdownLoggers();
|
||||
// Don't log after shutdown
|
||||
} catch {
|
||||
// Silently ignore logger shutdown errors
|
||||
}
|
||||
},
|
||||
SHUTDOWN_DEFAULTS.LOW_PRIORITY,
|
||||
'Loggers'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import type { ServiceDefinitions, ServiceContainer, ServiceCradle, ServiceContainerOptions } from '../src/awilix-container';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type {
|
||||
ServiceContainer,
|
||||
ServiceContainerOptions,
|
||||
ServiceCradle,
|
||||
ServiceDefinitions,
|
||||
} from '../src/awilix-container';
|
||||
|
||||
describe('Awilix Container Types', () => {
|
||||
it('should export ServiceDefinitions interface', () => {
|
||||
|
|
|
|||
|
|
@ -31,10 +31,18 @@ mock.module('@stock-bot/config', () => ({
|
|||
}
|
||||
|
||||
// Copy flat configs to nested if they exist
|
||||
if (result.redis) {result.database.dragonfly = result.redis;}
|
||||
if (result.mongodb) {result.database.mongodb = result.mongodb;}
|
||||
if (result.postgres) {result.database.postgres = result.postgres;}
|
||||
if (result.questdb) {result.database.questdb = result.questdb;}
|
||||
if (result.redis) {
|
||||
result.database.dragonfly = result.redis;
|
||||
}
|
||||
if (result.mongodb) {
|
||||
result.database.mongodb = result.mongodb;
|
||||
}
|
||||
if (result.postgres) {
|
||||
result.database.postgres = result.postgres;
|
||||
}
|
||||
if (result.questdb) {
|
||||
result.database.questdb = result.questdb;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import * as diExports from '../src/index';
|
||||
|
||||
describe('DI Package Exports', () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { BaseAppConfig } from '@stock-bot/config';
|
||||
import { ServiceApplication } from '../src/service-application';
|
||||
import type { ServiceApplicationConfig, ServiceLifecycleHooks } from '../src/service-application';
|
||||
import type { BaseAppConfig } from '@stock-bot/config';
|
||||
|
||||
// Mock logger module
|
||||
const mockLogger = {
|
||||
|
|
@ -193,7 +193,6 @@ describe.skip('ServiceApplication', () => {
|
|||
app = new ServiceApplication(configWithoutServiceName as any, serviceConfig);
|
||||
expect(app).toBeDefined();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('start method', () => {
|
||||
|
|
@ -228,7 +227,7 @@ describe.skip('ServiceApplication', () => {
|
|||
const { Hono } = require('hono');
|
||||
const routes = new Hono();
|
||||
// Add a simple test route
|
||||
routes.get('/test', (c) => c.json({ test: true }));
|
||||
routes.get('/test', c => c.json({ test: true }));
|
||||
return routes;
|
||||
});
|
||||
const mockHandlerInitializer = mock(() => Promise.resolve());
|
||||
|
|
@ -243,9 +242,11 @@ describe.skip('ServiceApplication', () => {
|
|||
|
||||
await app.start(mockContainerFactory, mockRouteFactory);
|
||||
|
||||
expect(mockContainerFactory).toHaveBeenCalledWith(expect.objectContaining({
|
||||
service: expect.objectContaining({ serviceName: 'test-service' }),
|
||||
}));
|
||||
expect(mockContainerFactory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
service: expect.objectContaining({ serviceName: 'test-service' }),
|
||||
})
|
||||
);
|
||||
expect(mockRouteFactory).toHaveBeenCalledWith({ test: 'container' });
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('test-service service started on port 3000');
|
||||
});
|
||||
|
|
@ -260,10 +261,12 @@ describe.skip('ServiceApplication', () => {
|
|||
|
||||
await app.start(mockContainerFactory, mockRouteFactory, mockHandlerInitializer);
|
||||
|
||||
expect(mockHandlerInitializer).toHaveBeenCalledWith(expect.objectContaining({
|
||||
test: 'container',
|
||||
_diContainer: mockContainer,
|
||||
}));
|
||||
expect(mockHandlerInitializer).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
test: 'container',
|
||||
_diContainer: mockContainer,
|
||||
})
|
||||
);
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('Handlers initialized');
|
||||
});
|
||||
|
||||
|
|
@ -300,7 +303,9 @@ describe.skip('ServiceApplication', () => {
|
|||
|
||||
app = new ServiceApplication(mockConfig, serviceConfig);
|
||||
|
||||
await expect(app.start(errorFactory, mockRouteFactory)).rejects.toThrow('Container creation failed');
|
||||
await expect(app.start(errorFactory, mockRouteFactory)).rejects.toThrow(
|
||||
'Container creation failed'
|
||||
);
|
||||
expect(mockLogger.error).toHaveBeenCalledWith('DETAILED ERROR:', expect.any(Error));
|
||||
});
|
||||
|
||||
|
|
@ -311,17 +316,23 @@ describe.skip('ServiceApplication', () => {
|
|||
};
|
||||
|
||||
const mockHandlerRegistry = {
|
||||
getAllHandlersWithSchedule: () => new Map([
|
||||
['testHandler', {
|
||||
scheduledJobs: [{
|
||||
operation: 'processData',
|
||||
cronPattern: '0 * * * *',
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
payload: { test: true },
|
||||
}],
|
||||
}],
|
||||
]),
|
||||
getAllHandlersWithSchedule: () =>
|
||||
new Map([
|
||||
[
|
||||
'testHandler',
|
||||
{
|
||||
scheduledJobs: [
|
||||
{
|
||||
operation: 'processData',
|
||||
cronPattern: '0 * * * *',
|
||||
priority: 5,
|
||||
immediately: false,
|
||||
payload: { test: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]),
|
||||
getHandlerService: () => 'test-service',
|
||||
getHandlerNames: () => ['testHandler'],
|
||||
getOperation: () => ({ name: 'processData' }),
|
||||
|
|
@ -339,9 +350,15 @@ describe.skip('ServiceApplication', () => {
|
|||
|
||||
const containerWithJobs = {
|
||||
resolve: mock((name: string) => {
|
||||
if (name === 'serviceContainer') {return { test: 'container' };}
|
||||
if (name === 'handlerRegistry') {return mockHandlerRegistry;}
|
||||
if (name === 'queueManager') {return mockQueueManager;}
|
||||
if (name === 'serviceContainer') {
|
||||
return { test: 'container' };
|
||||
}
|
||||
if (name === 'handlerRegistry') {
|
||||
return mockHandlerRegistry;
|
||||
}
|
||||
if (name === 'queueManager') {
|
||||
return mockQueueManager;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
|
|
@ -359,7 +376,7 @@ describe.skip('ServiceApplication', () => {
|
|||
'processData',
|
||||
{ handler: 'testHandler', operation: 'processData', payload: { test: true } },
|
||||
'0 * * * *',
|
||||
expect.objectContaining({ priority: 5, repeat: { immediately: false } }),
|
||||
expect.objectContaining({ priority: 5, repeat: { immediately: false } })
|
||||
);
|
||||
expect(mockQueueManager.startAllWorkers).toHaveBeenCalled();
|
||||
expect(mockLogger.info).toHaveBeenCalledWith('Scheduled jobs created', { totalJobs: 1 });
|
||||
|
|
@ -451,18 +468,30 @@ describe.skip('ServiceApplication', () => {
|
|||
|
||||
const mockContainer = {
|
||||
resolve: mock((name: string) => {
|
||||
if (name === 'serviceContainer') {return { test: 'container' };}
|
||||
if (name === 'handlerRegistry') {return {
|
||||
getAllHandlersWithSchedule: () => new Map(),
|
||||
getHandlerNames: () => [],
|
||||
};}
|
||||
if (name === 'queueManager') {return {
|
||||
shutdown: mock(() => Promise.resolve()),
|
||||
startAllWorkers: mock(() => {}),
|
||||
};}
|
||||
if (name === 'mongoClient') {return { disconnect: mock(() => Promise.resolve()) };}
|
||||
if (name === 'postgresClient') {return { disconnect: mock(() => Promise.resolve()) };}
|
||||
if (name === 'questdbClient') {return { disconnect: mock(() => Promise.resolve()) };}
|
||||
if (name === 'serviceContainer') {
|
||||
return { test: 'container' };
|
||||
}
|
||||
if (name === 'handlerRegistry') {
|
||||
return {
|
||||
getAllHandlersWithSchedule: () => new Map(),
|
||||
getHandlerNames: () => [],
|
||||
};
|
||||
}
|
||||
if (name === 'queueManager') {
|
||||
return {
|
||||
shutdown: mock(() => Promise.resolve()),
|
||||
startAllWorkers: mock(() => {}),
|
||||
};
|
||||
}
|
||||
if (name === 'mongoClient') {
|
||||
return { disconnect: mock(() => Promise.resolve()) };
|
||||
}
|
||||
if (name === 'postgresClient') {
|
||||
return { disconnect: mock(() => Promise.resolve()) };
|
||||
}
|
||||
if (name === 'questdbClient') {
|
||||
return { disconnect: mock(() => Promise.resolve()) };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type {
|
||||
GenericClientConfig,
|
||||
ConnectionPoolConfig,
|
||||
MongoDBPoolConfig,
|
||||
PostgreSQLPoolConfig,
|
||||
CachePoolConfig,
|
||||
QueuePoolConfig,
|
||||
ConnectionFactory,
|
||||
ConnectionFactoryConfig,
|
||||
ConnectionPool,
|
||||
ConnectionPoolConfig,
|
||||
GenericClientConfig,
|
||||
MongoDBPoolConfig,
|
||||
PoolMetrics,
|
||||
ConnectionFactory,
|
||||
PostgreSQLPoolConfig,
|
||||
QueuePoolConfig,
|
||||
} from '../src/types';
|
||||
|
||||
describe('DI Types', () => {
|
||||
|
|
@ -197,7 +197,7 @@ describe('DI Types', () => {
|
|||
describe('ConnectionFactory', () => {
|
||||
it('should define connection factory interface', () => {
|
||||
const mockFactory: ConnectionFactory = {
|
||||
createMongoDB: async (config) => ({
|
||||
createMongoDB: async config => ({
|
||||
name: config.name,
|
||||
client: {},
|
||||
metrics: {
|
||||
|
|
@ -211,7 +211,7 @@ describe('DI Types', () => {
|
|||
health: async () => true,
|
||||
dispose: async () => {},
|
||||
}),
|
||||
createPostgreSQL: async (config) => ({
|
||||
createPostgreSQL: async config => ({
|
||||
name: config.name,
|
||||
client: {},
|
||||
metrics: {
|
||||
|
|
@ -225,7 +225,7 @@ describe('DI Types', () => {
|
|||
health: async () => true,
|
||||
dispose: async () => {},
|
||||
}),
|
||||
createCache: async (config) => ({
|
||||
createCache: async config => ({
|
||||
name: config.name,
|
||||
client: {},
|
||||
metrics: {
|
||||
|
|
@ -239,7 +239,7 @@ describe('DI Types', () => {
|
|||
health: async () => true,
|
||||
dispose: async () => {},
|
||||
}),
|
||||
createQueue: async (config) => ({
|
||||
createQueue: async config => ({
|
||||
name: config.name,
|
||||
client: {},
|
||||
metrics: {
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ export class HandlerRegistry {
|
|||
return this.handlers.has(handlerName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set service ownership for a handler
|
||||
*/
|
||||
|
|
@ -107,7 +106,10 @@ export class HandlerRegistry {
|
|||
getServiceHandlers(serviceName: string): HandlerMetadata[] {
|
||||
const handlers: HandlerMetadata[] = [];
|
||||
for (const [handlerName, metadata] of this.handlers) {
|
||||
if (this.handlerServices.get(handlerName) === serviceName || metadata.service === serviceName) {
|
||||
if (
|
||||
this.handlerServices.get(handlerName) === serviceName ||
|
||||
metadata.service === serviceName
|
||||
) {
|
||||
handlers.push(metadata);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import * as handlerRegistryExports from '../src';
|
||||
import { HandlerRegistry } from '../src';
|
||||
|
||||
|
|
@ -50,8 +50,8 @@ describe('Handler Registry Package Exports', () => {
|
|||
totalOperations: 10,
|
||||
totalSchedules: 3,
|
||||
handlersByService: {
|
||||
'service1': 2,
|
||||
'service2': 3,
|
||||
service1: 2,
|
||||
service2: 3,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { JobHandler, ScheduledJob } from '@stock-bot/types';
|
||||
import { HandlerRegistry } from '../src/registry';
|
||||
import type {
|
||||
HandlerConfiguration,
|
||||
|
|
@ -6,7 +7,6 @@ import type {
|
|||
OperationMetadata,
|
||||
ScheduleMetadata,
|
||||
} from '../src/types';
|
||||
import type { JobHandler, ScheduledJob } from '@stock-bot/types';
|
||||
|
||||
describe('HandlerRegistry Edge Cases', () => {
|
||||
let registry: HandlerRegistry;
|
||||
|
|
@ -324,9 +324,7 @@ describe('HandlerRegistry Edge Cases', () => {
|
|||
it('should count schedules from metadata', () => {
|
||||
const metadata: HandlerMetadata = {
|
||||
name: 'ScheduledHandler',
|
||||
operations: [
|
||||
{ name: 'op1', method: 'method1' },
|
||||
],
|
||||
operations: [{ name: 'op1', method: 'method1' }],
|
||||
schedules: [
|
||||
{ operation: 'op1', cronPattern: '* * * * *' },
|
||||
{ operation: 'op1', cronPattern: '0 * * * *' },
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
|
||||
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
|
||||
|
||||
describe('Auto Registration - Simple Tests', () => {
|
||||
describe('autoRegisterHandlers', () => {
|
||||
|
|
@ -33,7 +33,7 @@ describe('Auto Registration - Simple Tests', () => {
|
|||
it('should handle excluded patterns', async () => {
|
||||
const mockServices = {} as IServiceContainer;
|
||||
const result = await autoRegisterHandlers('./test', mockServices, {
|
||||
exclude: ['test']
|
||||
exclude: ['test'],
|
||||
});
|
||||
|
||||
expect(result.registered).toEqual([]);
|
||||
|
|
@ -43,7 +43,7 @@ describe('Auto Registration - Simple Tests', () => {
|
|||
it('should accept custom pattern', async () => {
|
||||
const mockServices = {} as IServiceContainer;
|
||||
const result = await autoRegisterHandlers('./test', mockServices, {
|
||||
pattern: '.custom.'
|
||||
pattern: '.custom.',
|
||||
});
|
||||
|
||||
expect(result.registered).toEqual([]);
|
||||
|
|
@ -66,10 +66,7 @@ describe('Auto Registration - Simple Tests', () => {
|
|||
const mockServices = {} as IServiceContainer;
|
||||
const registry = createAutoHandlerRegistry(mockServices);
|
||||
|
||||
const result = await registry.registerDirectories([
|
||||
'./non-existent-1',
|
||||
'./non-existent-2'
|
||||
]);
|
||||
const result = await registry.registerDirectories(['./non-existent-1', './non-existent-2']);
|
||||
|
||||
expect(result.registered).toEqual([]);
|
||||
expect(result.failed).toEqual([]);
|
||||
|
|
|
|||
|
|
@ -52,27 +52,18 @@ describe('Auto Registration Unit Tests', () => {
|
|||
];
|
||||
|
||||
const pattern = '.handler.';
|
||||
const filtered = files.filter(file =>
|
||||
file.includes(pattern) &&
|
||||
file.endsWith('.ts') &&
|
||||
!file.startsWith('.')
|
||||
const filtered = files.filter(
|
||||
file => file.includes(pattern) && file.endsWith('.ts') && !file.startsWith('.')
|
||||
);
|
||||
|
||||
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
|
||||
});
|
||||
|
||||
it('should handle different patterns', () => {
|
||||
const files = [
|
||||
'test.handler.ts',
|
||||
'test.custom.ts',
|
||||
'another.custom.ts',
|
||||
];
|
||||
const files = ['test.handler.ts', 'test.custom.ts', 'another.custom.ts'];
|
||||
|
||||
const customPattern = '.custom.';
|
||||
const filtered = files.filter(file =>
|
||||
file.includes(customPattern) &&
|
||||
file.endsWith('.ts')
|
||||
);
|
||||
const filtered = files.filter(file => file.includes(customPattern) && file.endsWith('.ts'));
|
||||
|
||||
expect(filtered).toEqual(['test.custom.ts', 'another.custom.ts']);
|
||||
});
|
||||
|
|
@ -158,16 +149,10 @@ describe('Auto Registration Unit Tests', () => {
|
|||
|
||||
describe('Options Handling', () => {
|
||||
it('should apply exclude patterns', () => {
|
||||
const files = [
|
||||
'test.handler.ts',
|
||||
'excluded.handler.ts',
|
||||
'another.handler.ts',
|
||||
];
|
||||
const files = ['test.handler.ts', 'excluded.handler.ts', 'another.handler.ts'];
|
||||
const exclude = ['excluded'];
|
||||
|
||||
const filtered = files.filter(file =>
|
||||
!exclude.some(ex => file.includes(ex))
|
||||
);
|
||||
const filtered = files.filter(file => !exclude.some(ex => file.includes(ex)));
|
||||
|
||||
expect(filtered).toEqual(['test.handler.ts', 'another.handler.ts']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { IServiceContainer } from '@stock-bot/types';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import { autoRegisterHandlers, createAutoHandlerRegistry } from '../src/registry/auto-register';
|
||||
|
||||
describe('Auto Registration', () => {
|
||||
describe('autoRegisterHandlers', () => {
|
||||
|
|
@ -89,7 +89,7 @@ describe('Auto Registration', () => {
|
|||
const mockServices = {} as any;
|
||||
|
||||
const result = await autoRegisterHandlers('./test', mockServices, {
|
||||
serviceName: 'test-service'
|
||||
serviceName: 'test-service',
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
|
@ -107,7 +107,7 @@ describe('Auto Registration', () => {
|
|||
it('should handle excluded files', async () => {
|
||||
const mockServices = {} as any;
|
||||
const result = await autoRegisterHandlers('./test', mockServices, {
|
||||
exclude: ['test']
|
||||
exclude: ['test'],
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
|
||||
|
||||
// Test handler with metadata
|
||||
class ConfigTestHandler extends BaseHandler {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
|
||||
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
|
||||
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
|
||||
|
||||
// Test handler implementation
|
||||
class TestHandler extends BaseHandler {
|
||||
|
|
@ -96,14 +96,18 @@ describe('BaseHandler Edge Cases', () => {
|
|||
const handler = new TestHandler(mockServices);
|
||||
const context: ExecutionContext = { type: 'queue', metadata: {} };
|
||||
|
||||
await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow('Unknown operation: unknownOp');
|
||||
await expect(handler.execute('unknownOp', {}, context)).rejects.toThrow(
|
||||
'Unknown operation: unknownOp'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle operation with no operations metadata', async () => {
|
||||
const handler = new EmptyHandler(mockServices);
|
||||
const context: ExecutionContext = { type: 'queue', metadata: {} };
|
||||
|
||||
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow('Unknown operation: anyOp');
|
||||
await expect(handler.execute('anyOp', {}, context)).rejects.toThrow(
|
||||
'Unknown operation: anyOp'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when method is not a function', async () => {
|
||||
|
|
@ -122,7 +126,7 @@ describe('BaseHandler Edge Cases', () => {
|
|||
|
||||
const context: ExecutionContext = {
|
||||
type: 'queue',
|
||||
metadata: { source: 'test' }
|
||||
metadata: { source: 'test' },
|
||||
};
|
||||
|
||||
const result = await handler.execute('test', { data: 'test' }, context);
|
||||
|
|
@ -271,9 +275,7 @@ describe('BaseHandler Edge Cases', () => {
|
|||
it('should create handler config with operations', () => {
|
||||
const HandlerWithMeta = class extends BaseHandler {
|
||||
static __handlerName = 'config-handler';
|
||||
static __operations = [
|
||||
{ name: 'process', method: 'processData' },
|
||||
];
|
||||
static __operations = [{ name: 'process', method: 'processData' }];
|
||||
static __schedules = [];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, it, expect, mock, beforeEach, afterEach, spyOn } from 'bun:test';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import type { IServiceContainer, ExecutionContext } from '@stock-bot/types';
|
||||
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
|
||||
import type { ExecutionContext, IServiceContainer } from '@stock-bot/types';
|
||||
import * as utils from '@stock-bot/utils';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
|
||||
// Mock fetch
|
||||
const mockFetch = mock();
|
||||
|
|
@ -70,7 +70,8 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
|
||||
await handler.testGet('https://api.example.com/data');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
logger: expect.any(Object),
|
||||
|
|
@ -88,12 +89,13 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
await handler.testGet('https://api.example.com/data', {
|
||||
headers: { 'Authorization': 'Bearer token' },
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/data',
|
||||
expect.objectContaining({
|
||||
headers: { 'Authorization': 'Bearer token' },
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
method: 'GET',
|
||||
logger: expect.any(Object),
|
||||
})
|
||||
|
|
@ -115,7 +117,8 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
const data = { name: 'test', value: 123 };
|
||||
await handler.testPost('https://api.example.com/create', data);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/create',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
|
|
@ -134,11 +137,16 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
};
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
await handler.testPost('https://api.example.com/create', { test: 'data' }, {
|
||||
headers: { 'X-Custom': 'value' },
|
||||
});
|
||||
await handler.testPost(
|
||||
'https://api.example.com/create',
|
||||
{ test: 'data' },
|
||||
{
|
||||
headers: { 'X-Custom': 'value' },
|
||||
}
|
||||
);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/create',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/create',
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ test: 'data' }),
|
||||
|
|
@ -165,7 +173,8 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
const data = { id: 1, name: 'updated' };
|
||||
await handler.testPut('https://api.example.com/update/1', data);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update/1',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/update/1',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
|
|
@ -184,12 +193,17 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
};
|
||||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
await handler.testPut('https://api.example.com/update', { data: 'test' }, {
|
||||
headers: { 'If-Match': 'etag' },
|
||||
timeout: 5000,
|
||||
});
|
||||
await handler.testPut(
|
||||
'https://api.example.com/update',
|
||||
{ data: 'test' },
|
||||
{
|
||||
headers: { 'If-Match': 'etag' },
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/update',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/update',
|
||||
expect.objectContaining({
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ data: 'test' }),
|
||||
|
|
@ -216,7 +230,8 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
|
||||
await handler.testDelete('https://api.example.com/delete/1');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/delete/1',
|
||||
expect.objectContaining({
|
||||
method: 'DELETE',
|
||||
logger: expect.any(Object),
|
||||
|
|
@ -234,12 +249,13 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
mockFetch.mockResolvedValue(mockResponse);
|
||||
|
||||
await handler.testDelete('https://api.example.com/delete/1', {
|
||||
headers: { 'Authorization': 'Bearer token' },
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/delete/1',
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/delete/1',
|
||||
expect.objectContaining({
|
||||
headers: { 'Authorization': 'Bearer token' },
|
||||
headers: { Authorization: 'Bearer token' },
|
||||
method: 'DELETE',
|
||||
logger: expect.any(Object),
|
||||
})
|
||||
|
|
@ -251,7 +267,9 @@ describe('BaseHandler HTTP Methods', () => {
|
|||
it('should propagate fetch errors', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow('Network error');
|
||||
await expect(handler.testGet('https://api.example.com/data')).rejects.toThrow(
|
||||
'Network error'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-ok responses', async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { Handler, Operation, QueueSchedule, ScheduledOperation, Disabled } from '../src/decorators/decorators';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { BaseHandler } from '../src/base/BaseHandler';
|
||||
import {
|
||||
Disabled,
|
||||
Handler,
|
||||
Operation,
|
||||
QueueSchedule,
|
||||
ScheduledOperation,
|
||||
} from '../src/decorators/decorators';
|
||||
|
||||
describe('Decorators Edge Cases', () => {
|
||||
describe('Handler Decorator', () => {
|
||||
|
|
@ -22,10 +28,9 @@ describe('Decorators Edge Cases', () => {
|
|||
});
|
||||
|
||||
it('should work with context parameter', () => {
|
||||
const HandlerClass = Handler('with-context')(
|
||||
class TestClass extends BaseHandler {},
|
||||
{ kind: 'class' }
|
||||
);
|
||||
const HandlerClass = Handler('with-context')(class TestClass extends BaseHandler {}, {
|
||||
kind: 'class',
|
||||
});
|
||||
|
||||
const ctor = HandlerClass as any;
|
||||
expect(ctor.__handlerName).toBe('with-context');
|
||||
|
|
@ -72,7 +77,7 @@ describe('Decorators Edge Cases', () => {
|
|||
delayInHours: 24,
|
||||
priority: 5,
|
||||
direct: false,
|
||||
}
|
||||
},
|
||||
})
|
||||
batchMethod() {}
|
||||
}
|
||||
|
|
@ -93,7 +98,7 @@ describe('Decorators Edge Cases', () => {
|
|||
batch: {
|
||||
enabled: true,
|
||||
size: 50,
|
||||
}
|
||||
},
|
||||
})
|
||||
partialBatchMethod() {}
|
||||
}
|
||||
|
|
@ -288,10 +293,7 @@ describe('Decorators Edge Cases', () => {
|
|||
});
|
||||
|
||||
it('should work with context parameter', () => {
|
||||
const DisabledClass = Disabled()(
|
||||
class TestClass extends BaseHandler {},
|
||||
{ kind: 'class' }
|
||||
);
|
||||
const DisabledClass = Disabled()(class TestClass extends BaseHandler {}, { kind: 'class' });
|
||||
|
||||
const ctor = DisabledClass as any;
|
||||
expect(ctor.__disabled).toBe(true);
|
||||
|
|
@ -326,11 +328,18 @@ describe('Decorators Edge Cases', () => {
|
|||
|
||||
// Operations (3 total - simple, batch, and combined)
|
||||
expect(ctor.__operations).toHaveLength(3);
|
||||
expect(ctor.__operations.map((op: any) => op.name)).toEqual(['simple-op', 'batch-op', 'combined']);
|
||||
expect(ctor.__operations.map((op: any) => op.name)).toEqual([
|
||||
'simple-op',
|
||||
'batch-op',
|
||||
'combined',
|
||||
]);
|
||||
|
||||
// Schedules (2 total - scheduledOnly and combined)
|
||||
expect(ctor.__schedules).toHaveLength(2);
|
||||
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual(['scheduledOnly', 'combinedMethod']);
|
||||
expect(ctor.__schedules.map((s: any) => s.operation)).toEqual([
|
||||
'scheduledOnly',
|
||||
'combinedMethod',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle disabled handler with operations', () => {
|
||||
|
|
@ -372,7 +381,11 @@ describe('Decorators Edge Cases', () => {
|
|||
}
|
||||
|
||||
const ctor = TestHandler as any;
|
||||
expect(ctor.__operations.map((op: any) => op.method)).toEqual(['toString', 'valueOf', 'hasOwnProperty']);
|
||||
expect(ctor.__operations.map((op: any) => op.method)).toEqual([
|
||||
'toString',
|
||||
'valueOf',
|
||||
'hasOwnProperty',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import * as handlersExports from '../src';
|
||||
import { BaseHandler, ScheduledHandler } from '../src';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ export {
|
|||
parseQueueName,
|
||||
} from './service-utils';
|
||||
|
||||
|
||||
// Batch processing
|
||||
export { processBatchJob, processItems } from './batch-processor';
|
||||
|
||||
|
|
|
|||
|
|
@ -219,10 +219,14 @@ export class QueueManager {
|
|||
ttl: 86400, // 24 hours default
|
||||
enableMetrics: true,
|
||||
logger: {
|
||||
info: (...args: unknown[]) => this.logger.info(String(args[0]), args[1] as Record<string, unknown>),
|
||||
error: (...args: unknown[]) => this.logger.error(String(args[0]), args[1] as Record<string, unknown>),
|
||||
warn: (...args: unknown[]) => this.logger.warn(String(args[0]), args[1] as Record<string, unknown>),
|
||||
debug: (...args: unknown[]) => this.logger.debug(String(args[0]), args[1] as Record<string, unknown>),
|
||||
info: (...args: unknown[]) =>
|
||||
this.logger.info(String(args[0]), args[1] as Record<string, unknown>),
|
||||
error: (...args: unknown[]) =>
|
||||
this.logger.error(String(args[0]), args[1] as Record<string, unknown>),
|
||||
warn: (...args: unknown[]) =>
|
||||
this.logger.warn(String(args[0]), args[1] as Record<string, unknown>),
|
||||
debug: (...args: unknown[]) =>
|
||||
this.logger.debug(String(args[0]), args[1] as Record<string, unknown>),
|
||||
},
|
||||
});
|
||||
this.caches.set(queueName, cacheProvider);
|
||||
|
|
|
|||
|
|
@ -33,8 +33,14 @@ export class Shutdown {
|
|||
/**
|
||||
* Register a cleanup callback
|
||||
*/
|
||||
onShutdown(callback: ShutdownCallback, priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY, name?: string): void {
|
||||
if (this.isShuttingDown) { return };
|
||||
onShutdown(
|
||||
callback: ShutdownCallback,
|
||||
priority: number = SHUTDOWN_DEFAULTS.MEDIUM_PRIORITY,
|
||||
name?: string
|
||||
): void {
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
this.callbacks.push({ callback, priority, name });
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +48,9 @@ export class Shutdown {
|
|||
* Initiate graceful shutdown
|
||||
*/
|
||||
async shutdown(): Promise<void> {
|
||||
if (this.isShuttingDown) { return };
|
||||
if (this.isShuttingDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isShuttingDown = true;
|
||||
|
||||
|
|
@ -71,7 +79,9 @@ export class Shutdown {
|
|||
}
|
||||
|
||||
private setupSignalHandlers(): void {
|
||||
if (this.signalHandlersRegistered) { return };
|
||||
if (this.signalHandlersRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT'];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { describe, it, expect } from 'bun:test';
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import * as shutdownExports from '../src';
|
||||
import { Shutdown } from '../src';
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import {
|
|||
onShutdownMedium,
|
||||
resetShutdown,
|
||||
setShutdownTimeout,
|
||||
shutdownAndExit,
|
||||
Shutdown,
|
||||
shutdownAndExit,
|
||||
} from '../src';
|
||||
import type { ShutdownOptions, ShutdownResult } from '../src/types';
|
||||
|
||||
|
|
@ -104,7 +104,9 @@ describe('Shutdown Comprehensive Tests', () => {
|
|||
|
||||
it('should handle negative timeout values', () => {
|
||||
// Should throw for negative values
|
||||
expect(() => setShutdownTimeout(-1000)).toThrow('Shutdown timeout must be a positive number');
|
||||
expect(() => setShutdownTimeout(-1000)).toThrow(
|
||||
'Shutdown timeout must be a positive number'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle zero timeout', () => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,9 @@ export class SimpleMongoDBClient {
|
|||
}
|
||||
|
||||
async find(collection: string, filter: any = {}): Promise<any[]> {
|
||||
if (!this.connected) {await this.connect();}
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
const docs = this.collections.get(collection) || [];
|
||||
|
||||
// Simple filter matching
|
||||
|
|
@ -26,7 +28,9 @@ export class SimpleMongoDBClient {
|
|||
|
||||
return docs.filter(doc => {
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (doc[key] !== value) {return false;}
|
||||
if (doc[key] !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
|
@ -38,7 +42,9 @@ export class SimpleMongoDBClient {
|
|||
}
|
||||
|
||||
async insert(collection: string, doc: any): Promise<void> {
|
||||
if (!this.connected) {await this.connect();}
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
const docs = this.collections.get(collection) || [];
|
||||
docs.push({ ...doc, _id: Math.random().toString(36) });
|
||||
this.collections.set(collection, docs);
|
||||
|
|
@ -51,10 +57,14 @@ export class SimpleMongoDBClient {
|
|||
}
|
||||
|
||||
async update(collection: string, filter: any, update: any): Promise<number> {
|
||||
if (!this.connected) {await this.connect();}
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
const docs = await this.find(collection, filter);
|
||||
|
||||
if (docs.length === 0) {return 0;}
|
||||
if (docs.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const doc = docs[0];
|
||||
if (update.$set) {
|
||||
|
|
@ -65,7 +75,9 @@ export class SimpleMongoDBClient {
|
|||
}
|
||||
|
||||
async updateMany(collection: string, filter: any, update: any): Promise<number> {
|
||||
if (!this.connected) {await this.connect();}
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
const docs = await this.find(collection, filter);
|
||||
|
||||
for (const doc of docs) {
|
||||
|
|
@ -78,11 +90,15 @@ export class SimpleMongoDBClient {
|
|||
}
|
||||
|
||||
async delete(collection: string, filter: any): Promise<number> {
|
||||
if (!this.connected) {await this.connect();}
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
const allDocs = this.collections.get(collection) || [];
|
||||
const toDelete = await this.find(collection, filter);
|
||||
|
||||
if (toDelete.length === 0) {return 0;}
|
||||
if (toDelete.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const remaining = allDocs.filter(doc => !toDelete.includes(doc));
|
||||
this.collections.set(collection, remaining);
|
||||
|
|
@ -91,7 +107,9 @@ export class SimpleMongoDBClient {
|
|||
}
|
||||
|
||||
async deleteMany(collection: string, filter: any): Promise<number> {
|
||||
if (!this.connected) {await this.connect();}
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
const allDocs = this.collections.get(collection) || [];
|
||||
const toDelete = await this.find(collection, filter);
|
||||
|
||||
|
|
@ -102,7 +120,9 @@ export class SimpleMongoDBClient {
|
|||
}
|
||||
|
||||
async batchUpsert(collection: string, documents: any[], uniqueKeys: string[]): Promise<void> {
|
||||
if (!this.connected) {await this.connect();}
|
||||
if (!this.connected) {
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
for (const doc of documents) {
|
||||
const filter: any = {};
|
||||
|
|
|
|||
|
|
@ -22,18 +22,24 @@ export class SimplePostgresClient {
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (match) {return row;}
|
||||
if (match) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async find(table: string, where: any): Promise<any[]> {
|
||||
const rows = this.tables.get(table) || [];
|
||||
if (Object.keys(where).length === 0) {return rows;}
|
||||
if (Object.keys(where).length === 0) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
return rows.filter(row => {
|
||||
for (const [key, value] of Object.entries(where)) {
|
||||
if (row[key] !== value) {return false;}
|
||||
if (row[key] !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
|
@ -72,7 +78,9 @@ export class SimplePostgresClient {
|
|||
const rows = this.tables.get(table) || [];
|
||||
const remaining = rows.filter(row => {
|
||||
for (const [key, value] of Object.entries(where)) {
|
||||
if (row[key] !== value) {return true;}
|
||||
if (row[key] !== value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import { SimpleBrowser } from '../src/simple-browser';
|
||||
|
||||
|
||||
describe('Browser', () => {
|
||||
let browser: SimpleBrowser;
|
||||
const logger = {
|
||||
|
|
|
|||
|
|
@ -128,9 +128,9 @@ describe('Enhanced Fetch', () => {
|
|||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetch('https://api.example.com/data', { timeout: 50 })
|
||||
).rejects.toThrow('The operation was aborted');
|
||||
await expect(fetch('https://api.example.com/data', { timeout: 50 })).rejects.toThrow(
|
||||
'The operation was aborted'
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear timeout on success', async () => {
|
||||
|
|
@ -147,9 +147,9 @@ describe('Enhanced Fetch', () => {
|
|||
it('should clear timeout on error', async () => {
|
||||
mockFetch.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await expect(
|
||||
fetch('https://api.example.com/data', { timeout: 1000 })
|
||||
).rejects.toThrow('Network error');
|
||||
await expect(fetch('https://api.example.com/data', { timeout: 1000 })).rejects.toThrow(
|
||||
'Network error'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -188,9 +188,9 @@ describe('Enhanced Fetch', () => {
|
|||
const error = new Error('Connection failed');
|
||||
mockFetch.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
fetch('https://api.example.com/data', { logger: mockLogger })
|
||||
).rejects.toThrow('Connection failed');
|
||||
await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toThrow(
|
||||
'Connection failed'
|
||||
);
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
|
||||
url: 'https://api.example.com/data',
|
||||
|
|
@ -264,17 +264,15 @@ describe('Enhanced Fetch', () => {
|
|||
const error = new TypeError('Failed to fetch');
|
||||
mockFetch.mockRejectedValue(error);
|
||||
|
||||
await expect(fetch('https://api.example.com/data')).rejects.toThrow(
|
||||
'Failed to fetch'
|
||||
);
|
||||
await expect(fetch('https://api.example.com/data')).rejects.toThrow('Failed to fetch');
|
||||
});
|
||||
|
||||
it('should handle non-Error objects', async () => {
|
||||
mockFetch.mockRejectedValue('string error');
|
||||
|
||||
await expect(
|
||||
fetch('https://api.example.com/data', { logger: mockLogger })
|
||||
).rejects.toBe('string error');
|
||||
await expect(fetch('https://api.example.com/data', { logger: mockLogger })).rejects.toBe(
|
||||
'string error'
|
||||
);
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith('HTTP error', {
|
||||
url: 'https://api.example.com/data',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue