Skip to content

Commit

Permalink
fix(di): fix DI.invoke when inject() is used on nested class
Browse files Browse the repository at this point in the history
  • Loading branch information
Romakita committed Oct 9, 2024
1 parent bea2874 commit 82019db
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 113 deletions.
26 changes: 26 additions & 0 deletions packages/di/src/common/decorators/inject.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import {catchAsyncError} from "@tsed/core";

import {DITest} from "../../node/index.js";
import {inject} from "../fn/inject.js";
import {injector} from "../fn/injector.js";
import {registerProvider} from "../registries/ProviderRegistry.js";
import {InjectorService} from "../services/InjectorService.js";
import {Inject} from "./inject.js";
import {Injectable} from "./injectable.js";

@Injectable()
class ProvidersList extends Map<string, string> {}

@Injectable()
class MyService {
@Inject(ProvidersList)
providersList: ProvidersList;

getValue() {
return this.providersList.get("key");
}
}

describe("@Inject()", () => {
beforeEach(() => DITest.create());
afterEach(() => DITest.reset());
Expand Down Expand Up @@ -279,4 +293,16 @@ describe("@Inject()", () => {
expect(error?.message).toContain("Object isn't a valid token. Please check the token set on Test.test");
});
});
it("should rebuild all dependencies using invoke", async () => {
const providersList = inject(ProvidersList);
const myService = inject(MyService);
providersList.set("key", "value");

expect(inject(ProvidersList).get("key")).toEqual("value");
expect(myService.getValue()).toEqual("value");

const newMyService = await DITest.invoke(MyService, []);
expect(newMyService.getValue()).toEqual(undefined);
expect(myService.getValue()).toEqual("value");
});
});
26 changes: 25 additions & 1 deletion packages/di/src/common/fn/inject.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import {DITest} from "../../node/index.js";
import {Injectable} from "../decorators/injectable.js";
import {InjectorService} from "../services/InjectorService.js";
import {inject} from "./inject.js";

@Injectable()
class ProvidersList extends Map<string, string> {}

@Injectable()
class MyService {
readonly providersList = inject(ProvidersList);

getValue() {
return this.providersList.get("key");
}
}

describe("inject()", () => {
beforeEach(() => DITest.create());
afterEach(() => DITest.reset());
Expand All @@ -26,5 +39,16 @@ describe("inject()", () => {
}
]);
});
it("should rebuild all dependencies using invoke", async () => {});
it("should rebuild all dependencies using invoke", async () => {
const providersList = inject(ProvidersList);
const myService = inject(MyService);
providersList.set("key", "value");

expect(inject(ProvidersList).get("key")).toEqual("value");
expect(myService.getValue()).toEqual("value");

const newMyService = await DITest.invoke(MyService, []);
expect(newMyService.getValue()).toEqual(undefined);
expect(myService.getValue()).toEqual("value");
});
});
7 changes: 5 additions & 2 deletions packages/di/src/common/fn/inject.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {InvokeOptions} from "../interfaces/InvokeOptions.js";
import {TokenProvider} from "../interfaces/TokenProvider.js";
import {injector} from "./injector.js";
import {localsContainer} from "./localsContainer.js";
import {invokeOptions, localsContainer} from "./localsContainer.js";

/**
* Inject a provider to another provider.
Expand All @@ -21,5 +21,8 @@ import {localsContainer} from "./localsContainer.js";
* @decorator
*/
export function inject<T>(token: TokenProvider<T>, opts?: Partial<Pick<InvokeOptions, "useOpts" | "rebuild" | "locals">>): T {
return injector().invoke(token, opts?.locals || localsContainer(), opts);
return injector().invoke(token, opts?.locals || localsContainer(), {
...opts,
...invokeOptions()
});
}
18 changes: 17 additions & 1 deletion packages/di/src/common/fn/localsContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import {InjectorService} from "../services/InjectorService.js";
import {injector} from "./injector.js";

let globalLocals: LocalsContainer | undefined;
let globalInvOpts: any = {};
const stagedLocals: LocalsContainer[] = [];

/**
* Get the locals container initiated by DITest or .bootstrap() method.
*/
export function localsContainer({providers}: {providers?: UseImportTokenProviderOpts[]; rebuild?: boolean} = {}) {
export function localsContainer({
providers,
rebuild
}: {
providers?: UseImportTokenProviderOpts[];
rebuild?: boolean;
} = {}) {
if (!globalLocals || providers) {
globalLocals = new LocalsContainer();

Expand All @@ -20,17 +27,26 @@ export function localsContainer({providers}: {providers?: UseImportTokenProvider

globalLocals.set(InjectorService, injector());
}

if (rebuild) {
globalInvOpts.rebuild = rebuild;
}
}

return globalLocals;
}

export function invokeOptions() {
return {...globalInvOpts};
}

/**
* Reset the locals container.
*/
export function detachLocalsContainer() {
globalLocals && stagedLocals.push(globalLocals);
globalLocals = undefined;
globalInvOpts = {};
}

export function cleanAllLocalsContainer() {
Expand Down
1 change: 0 additions & 1 deletion packages/di/src/common/services/InjectorService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,6 @@ export class InjectorService extends Container {
Reflect.defineProperty(instance, DI_INVOKE_OPTIONS, {
get: () => ({rebuild: options.rebuild, locals})
});
// TODO add a way to notify DI consumer when a class instance is build
}

return instance;
Expand Down
164 changes: 67 additions & 97 deletions packages/platform/common/src/builder/PlatformBuilder.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {catchAsyncError, Type} from "@tsed/core";
import {Configuration, Controller, Injectable, injector, InjectorService, Module} from "@tsed/di";
import {Configuration, configuration, Controller, destroyInjector, Injectable, injector, Module} from "@tsed/di";

import {AfterInit} from "../interfaces/AfterInit.js";
import {AfterListen} from "../interfaces/AfterListen.js";
Expand Down Expand Up @@ -105,9 +105,6 @@ class ServerModule implements BeforeInit, AfterInit, BeforeRoutesInit, AfterRout
}

describe("PlatformBuilder", () => {
beforeEach(() => {
injector().destroy();
});
describe("loadStatics()", () => {
it("should loadStatics", async () => {
// WHEN
Expand Down Expand Up @@ -219,31 +216,10 @@ describe("PlatformBuilder", () => {
);
});
});
describe("static boostrap()", () => {
beforeAll(() => {
vi.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterRoutesInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterListen").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$beforeInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$beforeListen").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$onReady").mockReturnValue(undefined);
vi.spyOn(PlatformBuilder.prototype, "loadStatics");
// @ts-ignore
vi.spyOn(PlatformBuilder.prototype, "listenServers");
vi.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined);
vi.spyOn(Platform.prototype, "addRoutes").mockReturnValue(undefined);
});
it("should boostrap a custom platform", async () => {
const result = await PlatformBuilder.bootstrap(ServerModule, {
adapter: FakeAdapter
});

expect(result).toBeInstanceOf(PlatformBuilder);
});
});
describe("static create()", () => {
describe("boostrap", () => {
beforeEach(() => {
destroyInjector();
const inj = injector();
vi.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterRoutesInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterInit").mockReturnValue(undefined);
Expand All @@ -254,89 +230,82 @@ describe("PlatformBuilder", () => {
vi.spyOn(PlatformBuilder.prototype, "loadStatics");
// @ts-ignore
vi.spyOn(PlatformBuilder.prototype, "listenServers");
vi.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined);
vi.spyOn(Platform.prototype, "addRoutes").mockReturnValue(undefined);
});
afterAll(() => {
vi.resetAllMocks();

describe("static boostrap()", () => {
it("should boostrap a custom platform", async () => {
const result = await PlatformBuilder.bootstrap(ServerModule, {
adapter: FakeAdapter
});

expect(result).toBeInstanceOf(PlatformBuilder);
});
});
it("should boostrap a custom platform", () => {
const platform = PlatformBuilder.create(ServerModule, {
adapter: FakeAdapter
describe("static create()", () => {
it("should boostrap a custom platform", () => {
PlatformBuilder.create(ServerModule, {
adapter: FakeAdapter
});

expect(configuration().get("httpPort")).toEqual(false);
expect(configuration().get("httpsPort")).toEqual(false);
});

expect(platform.settings.get("httpPort")).toEqual(false);
expect(platform.settings.get("httpsPort")).toEqual(false);
});
});
describe("bootstrap()", () => {
it("should bootstrap platform", async () => {
// WHEN
const stub = ServerModule.prototype.$beforeRoutesInit;
const server = await PlatformCustom.bootstrap(ServerModule, {
httpPort: false,
httpsPort: false
describe("bootstrap()", () => {
it("should bootstrap platform", async () => {
// WHEN
const spyOn = vi.spyOn(injector().hooks, "asyncEmit").mockResolvedValue(undefined);
const stub = ServerModule.prototype.$beforeRoutesInit;
const server = await PlatformCustom.bootstrap(ServerModule, {
httpPort: false,
httpsPort: false
});
// THEN
await server.listen();

// THEN
// @ts-ignore
expect(server.listenServers).toHaveBeenCalledWith();
expect(server.loadStatics).toHaveBeenCalledWith("$beforeRoutesInit");
expect(server.loadStatics).toHaveBeenCalledWith("$afterRoutesInit");
expect(spyOn).toHaveBeenCalledWith("$afterInit", []);
expect(spyOn).toHaveBeenCalledWith("$beforeRoutesInit", []);
expect(spyOn).toHaveBeenCalledWith("$afterRoutesInit", []);
expect(spyOn).toHaveBeenCalledWith("$afterListen", []);
expect(spyOn).toHaveBeenCalledWith("$beforeListen", []);
expect(spyOn).toHaveBeenCalledWith("$onServerReady", []);
expect(spyOn).toHaveBeenCalledWith("$onReady", []);

// THEN
expect(server.rootModule).toBeInstanceOf(ServerModule);
expect(stub).toHaveBeenCalled();
expect(server.name).toEqual("custom");

await server.stop();
expect(spyOn).toHaveBeenCalledWith("$onDestroy", []);
});
});
describe("adapter()", () => {
it("should boostrap a custom platform", async () => {
const platformBuilder = await PlatformBuilder.bootstrap(ServerModule, {
adapter: FakeAdapter
});

// THEN
await server.listen();
expect(platformBuilder.callback()).toBeInstanceOf(Function);

// THEN
// @ts-ignore
expect(server.listenServers).toHaveBeenCalledWith();
expect(server.loadStatics).toHaveBeenCalledWith("$beforeRoutesInit");
expect(server.loadStatics).toHaveBeenCalledWith("$afterRoutesInit");
expect(server.injector.emit).toHaveBeenCalledWith("$afterInit");
expect(server.injector.emit).toHaveBeenCalledWith("$beforeRoutesInit");
expect(server.injector.emit).toHaveBeenCalledWith("$afterRoutesInit");
expect(server.injector.emit).toHaveBeenCalledWith("$afterListen");
expect(server.injector.emit).toHaveBeenCalledWith("$beforeListen");
expect(server.injector.emit).toHaveBeenCalledWith("$onServerReady");
expect(server.injector.emit).toHaveBeenCalledWith("$onReady");

// THEN
expect(server.rootModule).toBeInstanceOf(ServerModule);
expect(stub).toHaveBeenCalled();
expect(server.name).toEqual("custom");

await server.stop();
expect(server.injector.emit).toHaveBeenCalledWith("$onDestroy");
});
});
describe("adapter()", () => {
beforeAll(() => {
vi.spyOn(ServerModule.prototype, "$beforeRoutesInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterRoutesInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$afterListen").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$beforeInit").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$beforeListen").mockReturnValue(undefined);
vi.spyOn(ServerModule.prototype, "$onReady").mockReturnValue(undefined);
vi.spyOn(PlatformBuilder.prototype, "loadStatics").mockResolvedValue(undefined as never);
// @ts-ignore
vi.spyOn(PlatformBuilder.prototype, "listenServers");
vi.spyOn(InjectorService.prototype, "emit").mockResolvedValue(undefined);
vi.spyOn(Platform.prototype, "addRoutes").mockReturnValue(undefined);
});
it("should boostrap a custom platform", async () => {
const platformBuilder = await PlatformBuilder.bootstrap(ServerModule, {
adapter: FakeAdapter
expect(platformBuilder.adapter).toBeInstanceOf(FakeAdapter);
});

expect(platformBuilder.callback()).toBeInstanceOf(Function);

expect(platformBuilder.adapter).toBeInstanceOf(FakeAdapter);
});
it("should listen a custom platform", async () => {
const platform = await PlatformBuilder.create(ServerModule, {
adapter: FakeAdapter
});

it("should listen a custom platform", async () => {
const platform = await PlatformBuilder.create(ServerModule, {
adapter: FakeAdapter
await platform.listen();
});

await platform.listen();
});
});

describe("useProvider()", () => {
it("should add provider", async () => {
// WHEN
Expand All @@ -356,6 +325,7 @@ describe("PlatformBuilder", () => {
});
});
describe("addControllers", () => {
beforeEach(() => destroyInjector());
it("should add controllers", async () => {
// GIVEN
const server = await PlatformCustom.bootstrap(ServerModule, {});
Expand Down
4 changes: 2 additions & 2 deletions packages/platform/platform-router/vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ export default defineConfig(
...presets.test.coverage,
thresholds: {
statements: 100,
branches: 94.92,
branches: 94.89,
functions: 100,
lines: 100
}
}
}
}
);
);
Loading

0 comments on commit 82019db

Please sign in to comment.