Skip to content

Commit 3acc903

Browse files
committed
Added injectAlias and implements decorators
1 parent 6aa3298 commit 3acc903

File tree

4 files changed

+161
-5
lines changed

4 files changed

+161
-5
lines changed

packages/common/src/config/ModuleContainer.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from "./ConfigurableModule";
2929
import { ChildContainerProvider } from "./ChildContainerProvider";
3030
import { ChildContainerCreatable } from "./ChildContainerCreatable";
31+
import { getInjectAliases } from "./injectAlias";
3132

3233
const errors = {
3334
configNotSetInContainer: (moduleName: string) =>
@@ -228,6 +229,16 @@ export class ModuleContainer<
228229
}
229230
}
230231

232+
protected registerAliases(originalToken: string, clas: TypedClass<any>) {
233+
const aliases = getInjectAliases(clas);
234+
235+
aliases.forEach((alias) =>
236+
this.container.register(alias, {
237+
useToken: originalToken,
238+
})
239+
);
240+
}
241+
231242
/**
232243
* Register modules into the current container, and registers
233244
* a respective resolution hook in order to decorate the module
@@ -250,6 +261,8 @@ export class ModuleContainer<
250261
{ lifecycle: Lifecycle.ContainerScoped }
251262
);
252263
this.onAfterModuleResolution(moduleName);
264+
265+
this.registerAliases(moduleName, useClass);
253266
}
254267
});
255268
}
@@ -412,7 +425,11 @@ export class ModuleContainer<
412425
this.container.register(key, declaration, {
413426
lifecycle: Lifecycle.Singleton,
414427
});
415-
// eslint-disable-next-line sonarjs/no-duplicated-branches
428+
this.registerAliases(
429+
key,
430+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
431+
declaration.useClass as TypedClass<unknown>
432+
);
416433
} else if (isTokenProvider(declaration)) {
417434
this.container.register(key, declaration, {
418435
lifecycle: Lifecycle.Singleton,
@@ -454,8 +471,6 @@ export class ModuleContainer<
454471
);
455472
}
456473

457-
protected createSuperClassAliases(module: any) {}
458-
459474
/**
460475
* This is a placeholder for individual modules to override.
461476
* This method will be called whenever the underlying container fully
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { TypedClass } from "../types";
2+
3+
export const injectAliasMetadataKey = "protokit-inject-alias";
4+
5+
/**
6+
* Attaches metadata to the class that the ModuleContainer can pick up
7+
* and inject this class in the DI container under the specified aliases.
8+
* This method supports inheritance, therefore also gets aliases defined
9+
* on superclasses
10+
*/
11+
export function injectAlias(aliases: string[]) {
12+
return (target: TypedClass<unknown>) => {
13+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
14+
const superAliases = Reflect.getMetadata(
15+
injectAliasMetadataKey,
16+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
17+
Object.getPrototypeOf(target)
18+
) as string[] | undefined;
19+
20+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
21+
const existingAliases = Reflect.getMetadata(
22+
injectAliasMetadataKey,
23+
target
24+
) as string[] | undefined;
25+
26+
let allAliases = aliases;
27+
28+
if (superAliases !== undefined) {
29+
allAliases = allAliases.concat(superAliases);
30+
}
31+
if (existingAliases !== undefined) {
32+
allAliases = allAliases.concat(existingAliases);
33+
}
34+
35+
Reflect.defineMetadata(
36+
injectAliasMetadataKey,
37+
allAliases.filter(
38+
(value, index, array) => array.indexOf(value) === index
39+
),
40+
target
41+
);
42+
};
43+
}
44+
45+
/**
46+
* Marks the class to implement a certain interface T, while also attaching
47+
* a DI-injection alias as metadata, that will be picked up by the ModuleContainer
48+
* to allow resolving by that interface name
49+
* @param name The name of the injection alias, convention is to use the same as the name of T
50+
*/
51+
export function implement<T>(name: string) {
52+
return (
53+
/**
54+
* Check if the target class extends RuntimeModule, while
55+
* also providing static config presets
56+
*/
57+
target: TypedClass<T>
58+
) => {
59+
injectAlias([name])(target);
60+
};
61+
}
62+
63+
export function getInjectAliases(target: TypedClass<unknown>): string[] {
64+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
65+
const aliases = Reflect.getMetadata(
66+
injectAliasMetadataKey,
67+
target
68+
) as string[];
69+
return aliases ?? [];
70+
}

packages/common/test/config/ModuleContainer.test.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
ModulesRecord,
1111
} from "../../src/config/ModuleContainer";
1212
import { TypedClass } from "../../src/types";
13-
import { DependencyFactory } from "../../src";
13+
import { DependencyFactory, expectDefined } from "../../src";
14+
import { injectAlias } from "../../src/config/injectAlias";
1415

1516
// module container will accept modules that extend this type
1617
class BaseTestModule<Config> extends ConfigurableModule<Config> {}
@@ -24,6 +25,7 @@ interface TestModuleConfig {
2425
}
2526

2627
@injectable()
28+
@injectAlias(["child-alias", "multi-alias"])
2729
class ChildModule extends BaseTestModule<NoConfig> {
2830
public constructor(@inject("TestModule") public readonly testModule: any) {
2931
super();
@@ -34,6 +36,7 @@ class ChildModule extends BaseTestModule<NoConfig> {
3436
}
3537
}
3638

39+
@injectAlias(["base-alias", "multi-alias"])
3740
class TestModule
3841
extends BaseTestModule<TestModuleConfig>
3942
implements DependencyFactory
@@ -66,7 +69,11 @@ class WrongTestModule {}
6669

6770
class TestModuleContainer<
6871
Modules extends TestModulesRecord,
69-
> extends ModuleContainer<Modules> {}
72+
> extends ModuleContainer<Modules> {
73+
public get dependencyContainer() {
74+
return this.container;
75+
}
76+
}
7077

7178
describe("moduleContainer", () => {
7279
let container: TestModuleContainer<{
@@ -169,4 +176,40 @@ describe("moduleContainer", () => {
169176
expect(config.testConfigProperty2).toBe(2);
170177
expect(config.testConfigProperty3).toBe(undefined);
171178
});
179+
180+
it("should resolve dependencies correctly via alias", () => {
181+
container.configure({
182+
TestModule: {
183+
testConfigProperty,
184+
},
185+
186+
OtherTestModule: {
187+
otherTestConfigProperty: testConfigProperty,
188+
},
189+
});
190+
191+
container.create(() => tsyringeContainer.createChildContainer());
192+
193+
// Unfortunately we still need this so that the dependencies are registered
194+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
195+
const m1 = container.resolve("base-alias" as any);
196+
const m2 = container.resolve("TestModule");
197+
198+
expectDefined(m1);
199+
// Check if its the same reference
200+
expect(m1).toBe(m2);
201+
202+
const dm1 = container.resolve("child-alias" as any) as ChildModule;
203+
const dm2 = container.resolve("DependencyModule1");
204+
205+
expect(dm1.x()).toBe("dependency factory works");
206+
expect(dm1.testModule).toBeDefined();
207+
expect(dm1).toBe(dm2);
208+
209+
const multi =
210+
container.dependencyContainer.resolveAll<BaseTestModule<unknown>>(
211+
"multi-alias"
212+
);
213+
expect(multi).toHaveLength(2);
214+
});
172215
});
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import "reflect-metadata";
2+
import { getInjectAliases, injectAlias } from "../../src/config/injectAlias";
3+
4+
@injectAlias(["foo", "bar"])
5+
class TestClass {}
6+
7+
@injectAlias(["ayy"])
8+
class TestClass2 extends TestClass {}
9+
10+
describe("injectAlias metadata", () => {
11+
it("set and retrieve", () => {
12+
expect.assertions(2);
13+
14+
const aliases = getInjectAliases(TestClass);
15+
16+
expect(aliases).toHaveLength(2);
17+
expect(aliases).toStrictEqual(["foo", "bar"]);
18+
});
19+
20+
it("recursive", () => {
21+
expect.assertions(2);
22+
23+
const aliases = getInjectAliases(TestClass2);
24+
25+
expect(aliases).toHaveLength(3);
26+
expect(aliases).toStrictEqual(["ayy", "foo", "bar"]);
27+
});
28+
});

0 commit comments

Comments
 (0)