@onebun/core 0.1.19 → 0.1.20
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 +2 -2
- package/src/docs-examples.test.ts +43 -0
- package/src/module/module.test.ts +146 -2
- package/src/module/module.ts +41 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"effect": "^3.13.10",
|
|
43
43
|
"arktype": "^2.0.0",
|
|
44
|
-
"@onebun/logger": "^0.1.
|
|
44
|
+
"@onebun/logger": "^0.1.7",
|
|
45
45
|
"@onebun/envs": "^0.1.4",
|
|
46
46
|
"@onebun/metrics": "^0.1.6",
|
|
47
47
|
"@onebun/requests": "^0.1.3",
|
|
@@ -2327,6 +2327,49 @@ describe('Architecture Documentation (docs/architecture.md)', () => {
|
|
|
2327
2327
|
expect(SharedModule).toBeDefined();
|
|
2328
2328
|
expect(ApiModule).toBeDefined();
|
|
2329
2329
|
});
|
|
2330
|
+
|
|
2331
|
+
/**
|
|
2332
|
+
* Exports are only required for cross-module injection.
|
|
2333
|
+
* Within a module, any provider can be injected into controllers without being in exports.
|
|
2334
|
+
*/
|
|
2335
|
+
it('should allow controller to inject same-module provider without exports', async () => {
|
|
2336
|
+
const effectLib = await import('effect');
|
|
2337
|
+
const moduleMod = await import('./module/module');
|
|
2338
|
+
const testUtils = await import('./testing/test-utils');
|
|
2339
|
+
const decorators = await import('./decorators/decorators');
|
|
2340
|
+
|
|
2341
|
+
@Service()
|
|
2342
|
+
class InternalService extends BaseService {
|
|
2343
|
+
getData(): string {
|
|
2344
|
+
return 'internal';
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
@Controller('/local')
|
|
2349
|
+
class LocalController extends BaseController {
|
|
2350
|
+
constructor(@decorators.Inject(InternalService) private readonly internal: InternalService) {
|
|
2351
|
+
super();
|
|
2352
|
+
}
|
|
2353
|
+
getData(): string {
|
|
2354
|
+
return this.internal.getData();
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
@Module({
|
|
2359
|
+
providers: [InternalService],
|
|
2360
|
+
controllers: [LocalController],
|
|
2361
|
+
// No exports - InternalService is only used inside this module
|
|
2362
|
+
})
|
|
2363
|
+
class LocalModule {}
|
|
2364
|
+
|
|
2365
|
+
const mod = new moduleMod.OneBunModule(LocalModule, testUtils.makeMockLoggerLayer());
|
|
2366
|
+
mod.getLayer();
|
|
2367
|
+
await effectLib.Effect.runPromise(mod.setup() as import('effect').Effect.Effect<unknown, never, never>);
|
|
2368
|
+
|
|
2369
|
+
const controller = mod.getControllerInstance(LocalController) as LocalController;
|
|
2370
|
+
expect(controller).toBeDefined();
|
|
2371
|
+
expect(controller.getData()).toBe('internal');
|
|
2372
|
+
});
|
|
2330
2373
|
});
|
|
2331
2374
|
});
|
|
2332
2375
|
|
|
@@ -10,7 +10,11 @@ import {
|
|
|
10
10
|
afterEach,
|
|
11
11
|
mock,
|
|
12
12
|
} from 'bun:test';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Context,
|
|
15
|
+
Effect,
|
|
16
|
+
Layer,
|
|
17
|
+
} from 'effect';
|
|
14
18
|
|
|
15
19
|
import { Module } from '../decorators/decorators';
|
|
16
20
|
import { makeMockLoggerLayer } from '../testing/test-utils';
|
|
@@ -157,7 +161,6 @@ describe('OneBunModule', () => {
|
|
|
157
161
|
|
|
158
162
|
test('should detect circular dependencies and provide detailed error message', () => {
|
|
159
163
|
const { registerDependencies } = require('../decorators/decorators');
|
|
160
|
-
const { Effect, Layer } = require('effect');
|
|
161
164
|
const { LoggerService } = require('@onebun/logger');
|
|
162
165
|
|
|
163
166
|
// Collect error messages
|
|
@@ -849,4 +852,145 @@ describe('OneBunModule', () => {
|
|
|
849
852
|
expect((apiService as ApiService).getConnectionTimeout()).toBe(5000);
|
|
850
853
|
});
|
|
851
854
|
});
|
|
855
|
+
|
|
856
|
+
describe('Module DI scoping (exports only for cross-module)', () => {
|
|
857
|
+
const {
|
|
858
|
+
Controller: ControllerDecorator,
|
|
859
|
+
Get,
|
|
860
|
+
Inject,
|
|
861
|
+
clearGlobalModules,
|
|
862
|
+
} = require('../decorators/decorators');
|
|
863
|
+
const { Controller: BaseController } = require('./controller');
|
|
864
|
+
const { clearGlobalServicesRegistry: clearRegistry, OneBunModule: ModuleClass } = require('./module');
|
|
865
|
+
|
|
866
|
+
beforeEach(() => {
|
|
867
|
+
clearGlobalModules();
|
|
868
|
+
clearRegistry();
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
afterEach(() => {
|
|
872
|
+
clearGlobalModules();
|
|
873
|
+
clearRegistry();
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test('controller can inject provider from same module without exports', async () => {
|
|
877
|
+
@Service()
|
|
878
|
+
class CounterService {
|
|
879
|
+
private count = 0;
|
|
880
|
+
getCount() {
|
|
881
|
+
return this.count;
|
|
882
|
+
}
|
|
883
|
+
increment() {
|
|
884
|
+
this.count += 1;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
class CounterController extends BaseController {
|
|
889
|
+
constructor(@Inject(CounterService) private readonly counterService: CounterService) {
|
|
890
|
+
super();
|
|
891
|
+
}
|
|
892
|
+
getCount() {
|
|
893
|
+
return this.counterService.getCount();
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
const CounterControllerDecorated = ControllerDecorator('/counter')(CounterController);
|
|
897
|
+
Get('/')(CounterControllerDecorated.prototype, 'getCount', Object.getOwnPropertyDescriptor(CounterControllerDecorated.prototype, 'getCount')!);
|
|
898
|
+
|
|
899
|
+
@Module({
|
|
900
|
+
providers: [CounterService],
|
|
901
|
+
controllers: [CounterControllerDecorated],
|
|
902
|
+
// No exports - CounterService is only used inside this module
|
|
903
|
+
})
|
|
904
|
+
class FeatureModule {}
|
|
905
|
+
|
|
906
|
+
const module = new ModuleClass(FeatureModule, mockLoggerLayer);
|
|
907
|
+
module.getLayer();
|
|
908
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
909
|
+
|
|
910
|
+
const controller = module.getControllerInstance(CounterControllerDecorated) as CounterController;
|
|
911
|
+
expect(controller).toBeDefined();
|
|
912
|
+
expect(controller.getCount()).toBe(0);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
test('child module controller injects own provider; root can resolve controller', async () => {
|
|
916
|
+
@Service()
|
|
917
|
+
class ChildService {
|
|
918
|
+
getValue() {
|
|
919
|
+
return 'child';
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
class ChildController extends BaseController {
|
|
924
|
+
constructor(@Inject(ChildService) private readonly childService: ChildService) {
|
|
925
|
+
super();
|
|
926
|
+
}
|
|
927
|
+
getValue() {
|
|
928
|
+
return this.childService.getValue();
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const ChildControllerDecorated = ControllerDecorator('/child')(ChildController);
|
|
932
|
+
Get('/')(ChildControllerDecorated.prototype, 'getValue', Object.getOwnPropertyDescriptor(ChildControllerDecorated.prototype, 'getValue')!);
|
|
933
|
+
|
|
934
|
+
@Module({
|
|
935
|
+
providers: [ChildService],
|
|
936
|
+
controllers: [ChildControllerDecorated],
|
|
937
|
+
})
|
|
938
|
+
class ChildModule {}
|
|
939
|
+
|
|
940
|
+
@Module({
|
|
941
|
+
imports: [ChildModule],
|
|
942
|
+
})
|
|
943
|
+
class RootModule {}
|
|
944
|
+
|
|
945
|
+
const rootModule = new ModuleClass(RootModule, mockLoggerLayer);
|
|
946
|
+
rootModule.getLayer();
|
|
947
|
+
await Effect.runPromise(rootModule.setup() as Effect.Effect<unknown, never, never>);
|
|
948
|
+
|
|
949
|
+
const allControllers = rootModule.getControllers();
|
|
950
|
+
expect(allControllers).toContain(ChildControllerDecorated);
|
|
951
|
+
const controller = rootModule.getControllerInstance(ChildControllerDecorated) as ChildController;
|
|
952
|
+
expect(controller).toBeDefined();
|
|
953
|
+
expect(controller.getValue()).toBe('child');
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
test('exported service from imported module is injectable in importing module', async () => {
|
|
957
|
+
@Service()
|
|
958
|
+
class SharedService {
|
|
959
|
+
getLabel() {
|
|
960
|
+
return 'shared';
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
@Module({
|
|
965
|
+
providers: [SharedService],
|
|
966
|
+
exports: [SharedService],
|
|
967
|
+
})
|
|
968
|
+
class SharedModule {}
|
|
969
|
+
|
|
970
|
+
class AppController extends BaseController {
|
|
971
|
+
constructor(@Inject(SharedService) private readonly sharedService: SharedService) {
|
|
972
|
+
super();
|
|
973
|
+
}
|
|
974
|
+
getLabel() {
|
|
975
|
+
return this.sharedService.getLabel();
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
const AppControllerDecorated = ControllerDecorator('/app')(AppController);
|
|
979
|
+
Get('/')(AppControllerDecorated.prototype, 'getLabel', Object.getOwnPropertyDescriptor(AppControllerDecorated.prototype, 'getLabel')!);
|
|
980
|
+
|
|
981
|
+
@Module({
|
|
982
|
+
imports: [SharedModule],
|
|
983
|
+
controllers: [AppControllerDecorated],
|
|
984
|
+
})
|
|
985
|
+
class AppModule {}
|
|
986
|
+
|
|
987
|
+
const module = new ModuleClass(AppModule, mockLoggerLayer);
|
|
988
|
+
module.getLayer();
|
|
989
|
+
await Effect.runPromise(module.setup() as Effect.Effect<unknown, never, never>);
|
|
990
|
+
|
|
991
|
+
const controller = module.getControllerInstance(AppControllerDecorated) as AppController;
|
|
992
|
+
expect(controller).toBeDefined();
|
|
993
|
+
expect(controller.getLabel()).toBe('shared');
|
|
994
|
+
});
|
|
995
|
+
});
|
|
852
996
|
});
|
package/src/module/module.ts
CHANGED
|
@@ -169,9 +169,6 @@ export class OneBunModule implements ModuleInstance {
|
|
|
169
169
|
// Merge layers
|
|
170
170
|
layer = Layer.merge(layer, childModule.getLayer());
|
|
171
171
|
|
|
172
|
-
// Add controllers from child module
|
|
173
|
-
controllers.push(...childModule.getControllers());
|
|
174
|
-
|
|
175
172
|
// Get exported services from child module and register them for DI
|
|
176
173
|
const exportedServices = childModule.getExportedServices();
|
|
177
174
|
for (const [tag, instance] of exportedServices) {
|
|
@@ -386,48 +383,20 @@ export class OneBunModule implements ModuleInstance {
|
|
|
386
383
|
*/
|
|
387
384
|
createControllerInstances(): Effect.Effect<unknown, never, void> {
|
|
388
385
|
return Effect.sync(() => {
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (moduleMetadata && moduleMetadata.providers) {
|
|
395
|
-
// Create map of available services for dependency resolution
|
|
396
|
-
const availableServices = new Map<string, Function>();
|
|
397
|
-
|
|
398
|
-
// For each provider that is a class constructor, add to available services map
|
|
399
|
-
for (const provider of moduleMetadata.providers) {
|
|
400
|
-
if (typeof provider === 'function') {
|
|
401
|
-
availableServices.set(provider.name, provider);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Also add services from imported modules
|
|
406
|
-
for (const childModule of this.childModules) {
|
|
407
|
-
const childMetadata = getModuleMetadata(childModule.moduleClass);
|
|
408
|
-
if (childMetadata?.exports) {
|
|
409
|
-
for (const exported of childMetadata.exports) {
|
|
410
|
-
if (typeof exported === 'function') {
|
|
411
|
-
availableServices.set(exported.name, exported);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Add global services to available services for dependency resolution
|
|
418
|
-
for (const [, instance] of globalServicesRegistry) {
|
|
419
|
-
if (instance && typeof instance === 'object') {
|
|
420
|
-
availableServices.set(instance.constructor.name, instance.constructor);
|
|
421
|
-
}
|
|
386
|
+
// Build map of available services from this module's DI scope (own providers + imported exports + global)
|
|
387
|
+
const availableServices = new Map<string, Function>();
|
|
388
|
+
for (const [, instance] of this.serviceInstances) {
|
|
389
|
+
if (instance && typeof instance === 'object') {
|
|
390
|
+
availableServices.set(instance.constructor.name, instance.constructor);
|
|
422
391
|
}
|
|
392
|
+
}
|
|
423
393
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
394
|
+
// Automatically analyze and register dependencies for all controllers of this module
|
|
395
|
+
for (const controllerClass of this.controllers) {
|
|
396
|
+
registerControllerDependencies(controllerClass, availableServices);
|
|
428
397
|
}
|
|
429
398
|
|
|
430
|
-
//
|
|
399
|
+
// Create controller instances with automatic dependency injection
|
|
431
400
|
this.createControllersWithDI();
|
|
432
401
|
}).pipe(Effect.provide(this.rootLayer));
|
|
433
402
|
}
|
|
@@ -567,7 +536,12 @@ export class OneBunModule implements ModuleInstance {
|
|
|
567
536
|
discard: true,
|
|
568
537
|
}),
|
|
569
538
|
),
|
|
570
|
-
//
|
|
539
|
+
// Create controller instances in child modules first, then this module (each uses its own DI scope)
|
|
540
|
+
Effect.flatMap(() =>
|
|
541
|
+
Effect.forEach(this.childModules, (childModule) => childModule.createControllerInstances(), {
|
|
542
|
+
discard: true,
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
571
545
|
Effect.flatMap(() => this.createControllerInstances()),
|
|
572
546
|
// Then call onModuleInit for controllers
|
|
573
547
|
Effect.flatMap(() => this.callControllersOnModuleInit()),
|
|
@@ -780,30 +754,46 @@ export class OneBunModule implements ModuleInstance {
|
|
|
780
754
|
}
|
|
781
755
|
|
|
782
756
|
/**
|
|
783
|
-
* Get all controllers from this module
|
|
757
|
+
* Get all controllers from this module and child modules (recursive).
|
|
758
|
+
* Used by the application layer for routing and lifecycle.
|
|
784
759
|
*/
|
|
785
760
|
getControllers(): Function[] {
|
|
786
|
-
|
|
761
|
+
const fromChildren = this.childModules.flatMap((child) => child.getControllers());
|
|
762
|
+
|
|
763
|
+
return [...this.controllers, ...fromChildren];
|
|
787
764
|
}
|
|
788
765
|
|
|
789
766
|
/**
|
|
790
|
-
* Get controller instance
|
|
767
|
+
* Get controller instance (searches this module then child modules recursively).
|
|
791
768
|
*/
|
|
792
769
|
getControllerInstance(controllerClass: Function): Controller | undefined {
|
|
793
770
|
const instance = this.controllerInstances.get(controllerClass);
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
771
|
+
if (instance) {
|
|
772
|
+
return instance;
|
|
773
|
+
}
|
|
774
|
+
for (const childModule of this.childModules) {
|
|
775
|
+
const childInstance = childModule.getControllerInstance(controllerClass);
|
|
776
|
+
if (childInstance) {
|
|
777
|
+
return childInstance;
|
|
778
|
+
}
|
|
797
779
|
}
|
|
780
|
+
this.logger.warn(`No instance found for controller ${controllerClass.name}`);
|
|
798
781
|
|
|
799
|
-
return
|
|
782
|
+
return undefined;
|
|
800
783
|
}
|
|
801
784
|
|
|
802
785
|
/**
|
|
803
|
-
* Get all controller instances
|
|
786
|
+
* Get all controller instances from this module and child modules (recursive).
|
|
804
787
|
*/
|
|
805
788
|
getControllerInstances(): Map<Function, Controller> {
|
|
806
|
-
|
|
789
|
+
const merged = new Map<Function, Controller>(this.controllerInstances);
|
|
790
|
+
for (const childModule of this.childModules) {
|
|
791
|
+
for (const [cls, instance] of childModule.getControllerInstances()) {
|
|
792
|
+
merged.set(cls, instance);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return merged;
|
|
807
797
|
}
|
|
808
798
|
|
|
809
799
|
/**
|