@onebun/core 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/src/application/application.ts +13 -7
- package/src/decorators/decorators.test.ts +54 -1
- package/src/decorators/decorators.ts +18 -1
- package/src/docs-examples.test.ts +15 -6
- package/src/index.ts +11 -1
- package/src/module/config.interface.ts +60 -0
- package/src/module/config.service.test.ts +48 -28
- package/src/module/config.service.ts +7 -6
- package/src/module/controller.test.ts +27 -29
- package/src/module/controller.ts +3 -2
- package/src/module/index.ts +1 -0
- package/src/module/module.test.ts +31 -32
- package/src/module/module.ts +9 -5
- package/src/module/service.test.ts +22 -20
- package/src/module/service.ts +5 -3
- package/src/queue/adapters/memory.adapter.test.ts +19 -3
- package/src/queue/adapters/redis.adapter.test.ts +289 -0
- package/src/queue/queue.service.test.ts +240 -0
- package/src/queue/scheduler.test.ts +22 -9
- package/src/redis/shared-redis.test.ts +255 -0
- package/src/testing/test-utils.ts +56 -0
- package/src/websocket/ws-client.test.ts +96 -48
- package/src/websocket/ws-integration.test.ts +21 -19
- package/src/websocket/ws-storage-redis.test.ts +517 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -41,14 +41,15 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"effect": "^3.13.10",
|
|
43
43
|
"arktype": "^2.0.0",
|
|
44
|
-
"@onebun/logger": "^0.1.
|
|
45
|
-
"@onebun/envs": "^0.1.
|
|
46
|
-
"@onebun/metrics": "^0.1.
|
|
44
|
+
"@onebun/logger": "^0.1.5",
|
|
45
|
+
"@onebun/envs": "^0.1.4",
|
|
46
|
+
"@onebun/metrics": "^0.1.6",
|
|
47
47
|
"@onebun/requests": "^0.1.3",
|
|
48
|
-
"@onebun/trace": "^0.1.
|
|
48
|
+
"@onebun/trace": "^0.1.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"bun-types": "1.2.2"
|
|
51
|
+
"bun-types": "1.2.2",
|
|
52
|
+
"testcontainers": "^11.7.1"
|
|
52
53
|
},
|
|
53
54
|
"engines": {
|
|
54
55
|
"bun": "1.2.2"
|
|
@@ -21,6 +21,11 @@ import {
|
|
|
21
21
|
import { makeTraceService, TraceService } from '@onebun/trace';
|
|
22
22
|
|
|
23
23
|
import { getControllerMetadata } from '../decorators/decorators';
|
|
24
|
+
import {
|
|
25
|
+
NotInitializedConfig,
|
|
26
|
+
type IConfig,
|
|
27
|
+
type OneBunAppConfig,
|
|
28
|
+
} from '../module/config.interface';
|
|
24
29
|
import { ConfigServiceImpl } from '../module/config.service';
|
|
25
30
|
import { OneBunModule } from '../module/module';
|
|
26
31
|
import { QueueService, type QueueAdapter } from '../queue';
|
|
@@ -110,8 +115,7 @@ export class OneBunApplication {
|
|
|
110
115
|
development: process.env.NODE_ENV !== 'production',
|
|
111
116
|
};
|
|
112
117
|
private logger: SyncLogger;
|
|
113
|
-
|
|
114
|
-
private config: any = null;
|
|
118
|
+
private config: IConfig<OneBunAppConfig>;
|
|
115
119
|
private configService: ConfigServiceImpl | null = null;
|
|
116
120
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
121
|
private metricsService: any = null;
|
|
@@ -135,10 +139,12 @@ export class OneBunApplication {
|
|
|
135
139
|
this.options = { ...this.options, ...options };
|
|
136
140
|
}
|
|
137
141
|
|
|
138
|
-
// Initialize configuration if schema
|
|
142
|
+
// Initialize configuration - TypedEnv if schema provided, otherwise NotInitializedConfig
|
|
139
143
|
if (this.options.envSchema) {
|
|
140
144
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
141
145
|
this.config = TypedEnv.create(this.options.envSchema as any, this.options.envOptions);
|
|
146
|
+
} else {
|
|
147
|
+
this.config = new NotInitializedConfig();
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
// Use provided logger layer or create a default one
|
|
@@ -155,8 +161,8 @@ export class OneBunApplication {
|
|
|
155
161
|
) as Logger;
|
|
156
162
|
this.logger = createSyncLogger(effectLogger);
|
|
157
163
|
|
|
158
|
-
// Create configuration service if config is
|
|
159
|
-
if (this.config) {
|
|
164
|
+
// Create configuration service if config is initialized
|
|
165
|
+
if (this.config.isInitialized || !(this.config instanceof NotInitializedConfig)) {
|
|
160
166
|
this.configService = new ConfigServiceImpl(this.logger, this.config);
|
|
161
167
|
}
|
|
162
168
|
|
|
@@ -289,8 +295,8 @@ export class OneBunApplication {
|
|
|
289
295
|
*/
|
|
290
296
|
async start(): Promise<void> {
|
|
291
297
|
try {
|
|
292
|
-
// Initialize configuration if provided
|
|
293
|
-
if (this.config) {
|
|
298
|
+
// Initialize configuration if schema was provided
|
|
299
|
+
if (!(this.config instanceof NotInitializedConfig)) {
|
|
294
300
|
await this.config.initialize();
|
|
295
301
|
this.logger.info('Application configuration initialized');
|
|
296
302
|
}
|
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
afterEach,
|
|
14
14
|
} from 'bun:test';
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { OneBunApplication } from '../application';
|
|
17
|
+
import {
|
|
18
|
+
BaseController,
|
|
19
|
+
BaseService,
|
|
20
|
+
Service,
|
|
21
|
+
} from '../module';
|
|
22
|
+
import { makeMockLoggerLayer } from '../testing';
|
|
17
23
|
import { HttpMethod, ParamType } from '../types';
|
|
18
24
|
|
|
19
25
|
import {
|
|
@@ -809,6 +815,53 @@ describe('decorators', () => {
|
|
|
809
815
|
const deps = getConstructorParamTypes(malformedClass);
|
|
810
816
|
expect(deps).toBeUndefined();
|
|
811
817
|
});
|
|
818
|
+
|
|
819
|
+
test('should properly inject module service into controller without @Inject', async () => {
|
|
820
|
+
@Service()
|
|
821
|
+
class TestService extends BaseService {
|
|
822
|
+
getValue() {
|
|
823
|
+
return 'injected-value';
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// No @Inject needed - automatic DI via emitDecoratorMetadata
|
|
828
|
+
@Controller('')
|
|
829
|
+
class TestController extends BaseController {
|
|
830
|
+
constructor(private service: TestService) {
|
|
831
|
+
super();
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
getServiceValue() {
|
|
835
|
+
return this.service.getValue();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
@Module({
|
|
840
|
+
controllers: [TestController],
|
|
841
|
+
providers: [TestService],
|
|
842
|
+
})
|
|
843
|
+
class TestModule {}
|
|
844
|
+
|
|
845
|
+
const app = new OneBunApplication(TestModule, {
|
|
846
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// Access rootModule to setup and verify DI
|
|
850
|
+
const rootModule = (app as any).rootModule;
|
|
851
|
+
|
|
852
|
+
// Setup module to trigger dependency injection
|
|
853
|
+
const { Effect } = await import('effect');
|
|
854
|
+
await Effect.runPromise(rootModule.setup());
|
|
855
|
+
|
|
856
|
+
// Get controller instance and verify service was injected
|
|
857
|
+
const controllerInstance = rootModule.getControllerInstance(TestController) as TestController;
|
|
858
|
+
|
|
859
|
+
expect(controllerInstance).toBeDefined();
|
|
860
|
+
expect(controllerInstance).toBeInstanceOf(TestController);
|
|
861
|
+
|
|
862
|
+
// Verify the injected service works correctly
|
|
863
|
+
expect(controllerInstance.getServiceValue()).toBe('injected-value');
|
|
864
|
+
});
|
|
812
865
|
});
|
|
813
866
|
|
|
814
867
|
describe('ApiResponse decorator', () => {
|
|
@@ -111,9 +111,17 @@ export function registerControllerDependencies(
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Get constructor parameter types (automatically detected or explicitly set)
|
|
114
|
+
* Priority: 1) Explicit @Inject registrations, 2) TypeScript's design:paramtypes
|
|
114
115
|
*/
|
|
115
116
|
export function getConstructorParamTypes(target: Function): Function[] | undefined {
|
|
116
|
-
|
|
117
|
+
// First check explicit @Inject registrations
|
|
118
|
+
const explicitDeps = META_CONSTRUCTOR_PARAMS.get(target);
|
|
119
|
+
if (explicitDeps && explicitDeps.length > 0) {
|
|
120
|
+
return explicitDeps;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Fallback to TypeScript's design:paramtypes (automatic DI)
|
|
124
|
+
return getDesignParamTypes(target);
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
/**
|
|
@@ -174,6 +182,15 @@ export function controllerDecorator(basePath: string = '') {
|
|
|
174
182
|
// Mark controller as injectable automatically
|
|
175
183
|
injectable()(WrappedController);
|
|
176
184
|
|
|
185
|
+
// Copy design:paramtypes from original class to wrapped class
|
|
186
|
+
// This enables automatic DI without @Inject when emitDecoratorMetadata is enabled
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
+
const designParamTypes = (globalThis as any).Reflect?.getMetadata?.('design:paramtypes', target);
|
|
189
|
+
if (designParamTypes) {
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
191
|
+
(globalThis as any).Reflect?.defineMetadata?.('design:paramtypes', designParamTypes, WrappedController);
|
|
192
|
+
}
|
|
193
|
+
|
|
177
194
|
// Copy constructor params metadata from original class to wrapped class
|
|
178
195
|
// This is needed for @Inject decorator to work correctly with @Controller wrapping
|
|
179
196
|
const existingDeps = META_CONSTRUCTOR_PARAMS.get(target);
|
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
createWsServiceDefinition,
|
|
81
81
|
createWsClient,
|
|
82
82
|
matchPattern,
|
|
83
|
+
makeMockLoggerLayer,
|
|
83
84
|
} from './';
|
|
84
85
|
|
|
85
86
|
/**
|
|
@@ -157,6 +158,7 @@ describe('Minimal Working Example (docs/index.md)', () => {
|
|
|
157
158
|
const app = new OneBunApplication(AppModule, {
|
|
158
159
|
port: 3000,
|
|
159
160
|
envSchema,
|
|
161
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
160
162
|
metrics: { enabled: true },
|
|
161
163
|
tracing: { enabled: true },
|
|
162
164
|
});
|
|
@@ -1302,6 +1304,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1302
1304
|
|
|
1303
1305
|
// From docs: OneBunApplication constructor
|
|
1304
1306
|
const app = new OneBunApplication(AppModule, {
|
|
1307
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
1305
1308
|
port: 3000,
|
|
1306
1309
|
basePath: '/api/v1',
|
|
1307
1310
|
});
|
|
@@ -1330,6 +1333,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1330
1333
|
basePath: '/api/v1',
|
|
1331
1334
|
routePrefix: 'myservice',
|
|
1332
1335
|
development: true,
|
|
1336
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
1333
1337
|
metrics: {
|
|
1334
1338
|
enabled: true,
|
|
1335
1339
|
path: '/metrics',
|
|
@@ -1369,7 +1373,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1369
1373
|
httpDurationBuckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
|
|
1370
1374
|
};
|
|
1371
1375
|
|
|
1372
|
-
const app = new OneBunApplication(AppModule, { metrics: metricsOptions });
|
|
1376
|
+
const app = new OneBunApplication(AppModule, { metrics: metricsOptions, loggerLayer: makeMockLoggerLayer() });
|
|
1373
1377
|
expect(app).toBeDefined();
|
|
1374
1378
|
});
|
|
1375
1379
|
|
|
@@ -1400,7 +1404,7 @@ describe('OneBunApplication (docs/api/core.md)', () => {
|
|
|
1400
1404
|
};
|
|
1401
1405
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
1402
1406
|
|
|
1403
|
-
const app = new OneBunApplication(AppModule, { tracing: tracingOptions });
|
|
1407
|
+
const app = new OneBunApplication(AppModule, { tracing: tracingOptions, loggerLayer: makeMockLoggerLayer() });
|
|
1404
1408
|
expect(app).toBeDefined();
|
|
1405
1409
|
});
|
|
1406
1410
|
});
|
|
@@ -1974,17 +1978,19 @@ describe('Architecture Documentation (docs/architecture.md)', () => {
|
|
|
1974
1978
|
});
|
|
1975
1979
|
|
|
1976
1980
|
/**
|
|
1977
|
-
* @source docs/architecture.md#
|
|
1981
|
+
* @source docs/architecture.md#automatic-injection
|
|
1978
1982
|
*/
|
|
1979
|
-
it('should demonstrate
|
|
1980
|
-
// From docs:
|
|
1983
|
+
it('should demonstrate automatic DI without @Inject', () => {
|
|
1984
|
+
// From docs: Automatic DI example
|
|
1985
|
+
// TypeScript's emitDecoratorMetadata provides type info automatically
|
|
1981
1986
|
@Service()
|
|
1982
1987
|
class UserService extends BaseService {}
|
|
1983
1988
|
|
|
1984
1989
|
@Service()
|
|
1985
1990
|
class CacheService extends BaseService {}
|
|
1986
1991
|
|
|
1987
|
-
//
|
|
1992
|
+
// No @Inject needed - automatic DI works via emitDecoratorMetadata
|
|
1993
|
+
// @Inject is only needed for: interfaces, abstract classes, token-based injection
|
|
1988
1994
|
@Controller('/users')
|
|
1989
1995
|
class UserController extends BaseController {
|
|
1990
1996
|
constructor(
|
|
@@ -2180,6 +2186,7 @@ describe('Getting Started Documentation (docs/getting-started.md)', () => {
|
|
|
2180
2186
|
loadDotEnv: true,
|
|
2181
2187
|
envFilePath: '.env',
|
|
2182
2188
|
},
|
|
2189
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
2183
2190
|
metrics: {
|
|
2184
2191
|
enabled: true,
|
|
2185
2192
|
path: '/metrics',
|
|
@@ -2587,6 +2594,7 @@ describe('WebSocket Gateway API Documentation (docs/api/websocket.md)', () => {
|
|
|
2587
2594
|
pingTimeout: 20000,
|
|
2588
2595
|
maxPayload: 1048576,
|
|
2589
2596
|
},
|
|
2597
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
2590
2598
|
});
|
|
2591
2599
|
|
|
2592
2600
|
expect(app).toBeDefined();
|
|
@@ -2863,6 +2871,7 @@ describe('WebSocket Chat Example (docs/examples/websocket-chat.md)', () => {
|
|
|
2863
2871
|
pingInterval: 25000,
|
|
2864
2872
|
pingTimeout: 20000,
|
|
2865
2873
|
},
|
|
2874
|
+
loggerLayer: makeMockLoggerLayer(),
|
|
2866
2875
|
});
|
|
2867
2876
|
|
|
2868
2877
|
expect(app).toBeDefined();
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
// Re-export from external packages
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
Env,
|
|
4
|
+
type EnvSchema,
|
|
5
|
+
EnvValidationError,
|
|
6
|
+
type InferConfigType,
|
|
7
|
+
type EnvVariableConfig,
|
|
8
|
+
} from '@onebun/envs';
|
|
3
9
|
export type { SyncLogger } from '@onebun/logger';
|
|
4
10
|
export {
|
|
5
11
|
createHttpClient,
|
|
@@ -56,6 +62,10 @@ export {
|
|
|
56
62
|
// Global modules support
|
|
57
63
|
clearGlobalServicesRegistry,
|
|
58
64
|
getGlobalServicesRegistry,
|
|
65
|
+
// Config interface types
|
|
66
|
+
type IConfig,
|
|
67
|
+
type OneBunAppConfig,
|
|
68
|
+
NotInitializedConfig,
|
|
59
69
|
} from './module';
|
|
60
70
|
|
|
61
71
|
// Application
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DeepPaths, DeepValue } from '@onebun/envs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Empty interface for module augmentation.
|
|
5
|
+
* Users extend this to provide typed config access.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* declare module '@onebun/core' {
|
|
9
|
+
* interface OneBunAppConfig {
|
|
10
|
+
* server: { port: number; host: string };
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface OneBunAppConfig {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generic typed configuration interface.
|
|
19
|
+
* Provides type-safe access to configuration values.
|
|
20
|
+
*/
|
|
21
|
+
export interface IConfig<T = OneBunAppConfig> {
|
|
22
|
+
/** Get configuration value by dot-notation path with full type inference */
|
|
23
|
+
get<P extends DeepPaths<T>>(path: P): DeepValue<T, P>;
|
|
24
|
+
/** Fallback for dynamic paths */
|
|
25
|
+
get(path: string): unknown;
|
|
26
|
+
/** Get all configuration values */
|
|
27
|
+
readonly values: T;
|
|
28
|
+
/** Get safe configuration for logging (sensitive data masked) */
|
|
29
|
+
getSafeConfig(): T;
|
|
30
|
+
/** Check if configuration is initialized */
|
|
31
|
+
readonly isInitialized: boolean;
|
|
32
|
+
/** Initialize configuration (async) */
|
|
33
|
+
initialize(): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Config stub that throws on any access.
|
|
38
|
+
* Used when envSchema is not provided.
|
|
39
|
+
*/
|
|
40
|
+
export class NotInitializedConfig implements IConfig<never> {
|
|
41
|
+
get(_path: string): never {
|
|
42
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get values(): never {
|
|
46
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getSafeConfig(): never {
|
|
50
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get isInitialized(): boolean {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async initialize(): Promise<void> {
|
|
58
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
mock,
|
|
12
12
|
} from 'bun:test';
|
|
13
13
|
|
|
14
|
+
import type { IConfig, OneBunAppConfig } from './config.interface';
|
|
15
|
+
|
|
14
16
|
import type { SyncLogger } from '@onebun/logger';
|
|
15
17
|
|
|
16
18
|
import {
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
describe('ConfigService', () => {
|
|
23
25
|
let mockLogger: SyncLogger;
|
|
24
26
|
|
|
25
|
-
let mockConfig:
|
|
27
|
+
let mockConfig: IConfig<OneBunAppConfig>;
|
|
26
28
|
|
|
27
29
|
beforeEach(() => {
|
|
28
30
|
mockLogger = {
|
|
@@ -38,8 +40,8 @@ describe('ConfigService', () => {
|
|
|
38
40
|
mockConfig = {
|
|
39
41
|
initialize: mock(async () => {}),
|
|
40
42
|
get: mock((path: string): any => `value-for-${path}`),
|
|
41
|
-
values: { test: 'value' },
|
|
42
|
-
getSafeConfig: mock(() => ({ test: '***' })),
|
|
43
|
+
values: { test: 'value' } as unknown as OneBunAppConfig,
|
|
44
|
+
getSafeConfig: mock(() => ({ test: '***' })) as unknown as () => OneBunAppConfig,
|
|
43
45
|
isInitialized: true,
|
|
44
46
|
};
|
|
45
47
|
});
|
|
@@ -63,7 +65,7 @@ describe('ConfigService', () => {
|
|
|
63
65
|
const service = new ConfigServiceImpl(mockLogger);
|
|
64
66
|
|
|
65
67
|
expect(service).toBeInstanceOf(ConfigServiceImpl);
|
|
66
|
-
expect(service.instance).
|
|
68
|
+
expect(service.instance).toBeNull();
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
test('should create instance with config but without logger (uninitialized)', () => {
|
|
@@ -83,8 +85,8 @@ describe('ConfigService', () => {
|
|
|
83
85
|
expect(mockLogger.info).toHaveBeenCalledWith('Configuration initialized successfully');
|
|
84
86
|
});
|
|
85
87
|
|
|
86
|
-
test('should not throw when config instance is
|
|
87
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
88
|
+
test('should not throw when config instance is undefined', async () => {
|
|
89
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
88
90
|
|
|
89
91
|
await expect(service.initialize()).resolves.toBeUndefined();
|
|
90
92
|
expect(mockLogger.info).not.toHaveBeenCalled();
|
|
@@ -109,7 +111,7 @@ describe('ConfigService', () => {
|
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
test('should throw error when config not initialized', () => {
|
|
112
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
114
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
113
115
|
|
|
114
116
|
expect(() => service.get('test.path')).toThrow(
|
|
115
117
|
'Configuration not initialized. Provide envSchema in ApplicationOptions.',
|
|
@@ -117,8 +119,14 @@ describe('ConfigService', () => {
|
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
test('should return typed value', () => {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
const typedConfig = {
|
|
123
|
+
initialize: mock(async () => {}),
|
|
124
|
+
get: mock((path: string) => path === 'test.number' ? 42 : undefined),
|
|
125
|
+
values: { test: 'value' } as unknown as OneBunAppConfig,
|
|
126
|
+
getSafeConfig: mock(() => ({ test: '***' })) as unknown as () => OneBunAppConfig,
|
|
127
|
+
isInitialized: true,
|
|
128
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
129
|
+
const service = new ConfigServiceImpl(mockLogger, typedConfig);
|
|
122
130
|
|
|
123
131
|
const result = service.get<number>('test.number');
|
|
124
132
|
|
|
@@ -137,7 +145,7 @@ describe('ConfigService', () => {
|
|
|
137
145
|
});
|
|
138
146
|
|
|
139
147
|
test('should throw error when config not initialized', () => {
|
|
140
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
148
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
141
149
|
|
|
142
150
|
expect(() => service.values).toThrow(
|
|
143
151
|
'Configuration not initialized. Provide envSchema in ApplicationOptions.',
|
|
@@ -156,7 +164,7 @@ describe('ConfigService', () => {
|
|
|
156
164
|
});
|
|
157
165
|
|
|
158
166
|
test('should throw error when config not initialized', () => {
|
|
159
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
167
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
160
168
|
|
|
161
169
|
expect(() => service.getSafeConfig()).toThrow(
|
|
162
170
|
'Configuration not initialized. Provide envSchema in ApplicationOptions.',
|
|
@@ -172,14 +180,20 @@ describe('ConfigService', () => {
|
|
|
172
180
|
});
|
|
173
181
|
|
|
174
182
|
test('should return false when config not initialized', () => {
|
|
175
|
-
|
|
176
|
-
|
|
183
|
+
const uninitConfig = {
|
|
184
|
+
initialize: mock(async () => {}),
|
|
185
|
+
get: mock((path: string): any => `value-for-${path}`),
|
|
186
|
+
values: { test: 'value' } as unknown as OneBunAppConfig,
|
|
187
|
+
getSafeConfig: mock(() => ({ test: '***' })) as unknown as () => OneBunAppConfig,
|
|
188
|
+
isInitialized: false,
|
|
189
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
190
|
+
const service = new ConfigServiceImpl(mockLogger, uninitConfig);
|
|
177
191
|
|
|
178
192
|
expect(service.isInitialized).toBe(false);
|
|
179
193
|
});
|
|
180
194
|
|
|
181
|
-
test('should return false when config instance is
|
|
182
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
195
|
+
test('should return false when config instance is undefined', () => {
|
|
196
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
183
197
|
|
|
184
198
|
expect(service.isInitialized).toBe(false);
|
|
185
199
|
});
|
|
@@ -193,7 +207,7 @@ describe('ConfigService', () => {
|
|
|
193
207
|
});
|
|
194
208
|
|
|
195
209
|
test('should return null when no config provided', () => {
|
|
196
|
-
const service = new ConfigServiceImpl(mockLogger,
|
|
210
|
+
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
197
211
|
|
|
198
212
|
expect(service.instance).toBeNull();
|
|
199
213
|
});
|
|
@@ -210,7 +224,7 @@ describe('ConfigService', () => {
|
|
|
210
224
|
test('should handle undefined config in constructor', () => {
|
|
211
225
|
const service = new ConfigServiceImpl(mockLogger, undefined);
|
|
212
226
|
|
|
213
|
-
expect(service.instance).
|
|
227
|
+
expect(service.instance).toBeNull();
|
|
214
228
|
expect(service.isInitialized).toBe(false);
|
|
215
229
|
});
|
|
216
230
|
});
|
|
@@ -219,8 +233,11 @@ describe('ConfigService', () => {
|
|
|
219
233
|
test('should handle initialization error gracefully', async () => {
|
|
220
234
|
const errorConfig = {
|
|
221
235
|
initialize: mock(() => Promise.reject(new Error('Init failed'))),
|
|
236
|
+
get: mock(() => undefined),
|
|
237
|
+
values: {} as unknown as OneBunAppConfig,
|
|
238
|
+
getSafeConfig: mock(() => ({})) as unknown as () => OneBunAppConfig,
|
|
222
239
|
isInitialized: false,
|
|
223
|
-
}
|
|
240
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
224
241
|
|
|
225
242
|
const service = new ConfigServiceImpl(mockLogger, errorConfig);
|
|
226
243
|
|
|
@@ -229,15 +246,16 @@ describe('ConfigService', () => {
|
|
|
229
246
|
|
|
230
247
|
test('should handle config method errors', () => {
|
|
231
248
|
const errorConfig = {
|
|
249
|
+
initialize: mock(async () => {}),
|
|
232
250
|
get: mock(() => {
|
|
233
251
|
throw new Error('Get failed');
|
|
234
252
|
}),
|
|
235
|
-
values: null,
|
|
253
|
+
values: null as unknown as unknown as OneBunAppConfig,
|
|
236
254
|
getSafeConfig: mock(() => {
|
|
237
255
|
throw new Error('Safe config failed');
|
|
238
|
-
}),
|
|
256
|
+
}) as unknown as () => OneBunAppConfig,
|
|
239
257
|
isInitialized: true,
|
|
240
|
-
}
|
|
258
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
241
259
|
|
|
242
260
|
const service = new ConfigServiceImpl(mockLogger, errorConfig);
|
|
243
261
|
|
|
@@ -249,11 +267,12 @@ describe('ConfigService', () => {
|
|
|
249
267
|
describe('edge cases', () => {
|
|
250
268
|
test('should handle config with null values', () => {
|
|
251
269
|
const nullConfig = {
|
|
270
|
+
initialize: mock(async () => {}),
|
|
252
271
|
get: mock(() => null),
|
|
253
|
-
values: null,
|
|
254
|
-
getSafeConfig: mock(() => null),
|
|
272
|
+
values: null as unknown as unknown as OneBunAppConfig,
|
|
273
|
+
getSafeConfig: mock(() => null as unknown as unknown as OneBunAppConfig),
|
|
255
274
|
isInitialized: true,
|
|
256
|
-
}
|
|
275
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
257
276
|
|
|
258
277
|
const service = new ConfigServiceImpl(mockLogger, nullConfig);
|
|
259
278
|
|
|
@@ -264,11 +283,12 @@ describe('ConfigService', () => {
|
|
|
264
283
|
|
|
265
284
|
test('should handle config with undefined values', () => {
|
|
266
285
|
const undefinedConfig = {
|
|
286
|
+
initialize: mock(async () => {}),
|
|
267
287
|
get: mock(() => undefined),
|
|
268
|
-
values: undefined,
|
|
269
|
-
getSafeConfig: mock(() => undefined),
|
|
288
|
+
values: undefined as unknown as unknown as OneBunAppConfig,
|
|
289
|
+
getSafeConfig: mock(() => undefined as unknown as unknown as OneBunAppConfig),
|
|
270
290
|
isInitialized: false,
|
|
271
|
-
}
|
|
291
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
272
292
|
|
|
273
293
|
const service = new ConfigServiceImpl(mockLogger, undefinedConfig);
|
|
274
294
|
|
|
@@ -318,7 +338,7 @@ describe('ConfigService', () => {
|
|
|
318
338
|
api: { key: '***', timeout: 30000 },
|
|
319
339
|
})),
|
|
320
340
|
isInitialized: true,
|
|
321
|
-
}
|
|
341
|
+
} as unknown as IConfig<OneBunAppConfig>;
|
|
322
342
|
|
|
323
343
|
const service = new ConfigServiceImpl(mockLogger, complexConfig);
|
|
324
344
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Context } from 'effect';
|
|
2
2
|
|
|
3
|
+
import type { IConfig, OneBunAppConfig } from './config.interface';
|
|
4
|
+
|
|
3
5
|
import type { SyncLogger } from '@onebun/logger';
|
|
4
6
|
|
|
5
7
|
import { BaseService, Service } from './service';
|
|
@@ -15,17 +17,16 @@ export const ConfigServiceTag = Context.GenericTag<ConfigServiceImpl>('ConfigSer
|
|
|
15
17
|
*/
|
|
16
18
|
@Service(ConfigServiceTag)
|
|
17
19
|
export class ConfigServiceImpl extends BaseService {
|
|
18
|
-
|
|
19
|
-
private configInstance: any = null;
|
|
20
|
+
private configInstance: IConfig<OneBunAppConfig> | null = null;
|
|
20
21
|
|
|
21
|
-
constructor(logger?: SyncLogger, config?:
|
|
22
|
+
constructor(logger?: SyncLogger, config?: IConfig<OneBunAppConfig>) {
|
|
22
23
|
super();
|
|
23
24
|
// If logger is provided directly (for backwards compatibility in tests),
|
|
24
25
|
// initialize the service immediately
|
|
25
|
-
if (logger) {
|
|
26
|
+
if (logger && config) {
|
|
26
27
|
this.initializeService(logger, config);
|
|
27
28
|
}
|
|
28
|
-
this.configInstance = config;
|
|
29
|
+
this.configInstance = config ?? null;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -46,7 +47,7 @@ export class ConfigServiceImpl extends BaseService {
|
|
|
46
47
|
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
return this.configInstance.get(path);
|
|
50
|
+
return this.configInstance.get(path) as T;
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
/**
|