Skip to content

Commit 095b82c

Browse files
committed
v1.1.0 - switched to asyncLocalStorage solution
1 parent 1a3da2a commit 095b82c

9 files changed

+2273
-165
lines changed

lib/decorators/inject-transform.decorator.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { InjectTransformOptions } from "../interfaces/index.mjs";
55
import { InjectTransformFn } from "../interfaces/inject-transform-fn.interface.mjs";
66
import { InjectTransformer } from "../interfaces/inject-transformer.interface.mjs";
77
import { Type } from "@nestjs/common";
8-
import { isClass } from "../util/is-class.js";
8+
import { isClass } from "../util/is-class.mjs";
99

1010
export function InjectTransform(
1111
transformer: InjectTransformFn,
@@ -20,7 +20,7 @@ export function InjectTransform(
2020
options: InjectTransformOptions = {}
2121
) {
2222
return Transform((params) => {
23-
const injector = InjectTransformModule.getInjectTransformContainer(options);
23+
const injector = InjectTransformModule.getInjectTransformContainer();
2424
const providers = options.inject ?? [];
2525

2626
// Unify transformFn <-> transformer

lib/decorators/inject-transform.decorator.spec.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,6 @@ class TestSubject {
4646
@InjectTransform(TestTransformer)
4747
xx: number;
4848

49-
@InjectTransform(
50-
(params, tokenValue: number, service: TestService) =>
51-
service.transform(params.value, tokenValue),
52-
{
53-
inject: [TEST_TOKEN, TestService],
54-
ignoreInjectLifecycle: true,
55-
}
56-
)
57-
y: number;
58-
5949
@InjectTransform((params) => 0, { inject: [MISSING_TOKEN] })
6050
z: number;
6151
}
@@ -94,15 +84,6 @@ describe("InjectTransform", () => {
9484
);
9585
});
9686

97-
it("should optionally ignore lifecycle errors", async () => {
98-
const app = await NestFactory.createApplicationContext(TestAppModule);
99-
await app.init();
100-
await app.close();
101-
102-
// TestSubject.y is marked with ignoreLifecycleErrors
103-
expect(plainToInstance(TestSubject, { y: 3 }).y).toEqual(15);
104-
});
105-
10687
it("should error on missing providers", async () => {
10788
const app = await NestFactory.createApplicationContext(TestAppModule);
10889
await app.init();

lib/decorators/inject-type.decorator.mts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import "reflect-metadata";
22
import { InjectTransformModule } from "../inject-transform.module.mjs";
33
import { Type } from "class-transformer";
4-
import { InjectTypeOptions } from "../interfaces/inject-type-options.interface.mjs";
5-
import { TypeInjector } from "../interfaces/type-injector.interface.mjs";
4+
import { InjectTypeOptions, TypeInjector } from "../interfaces/index.mjs";
65
import { Type as NestType } from "@nestjs/common";
76

87
export function InjectType(
@@ -11,8 +10,7 @@ export function InjectType(
1110
): PropertyDecorator {
1211
return (target, propertyKey) =>
1312
Type((type?) => {
14-
const injector =
15-
InjectTransformModule.getInjectTransformContainer(options);
13+
const injector = InjectTransformModule.getInjectTransformContainer();
1614

1715
return injector.get<TypeInjector>(typeInjector).inject(type);
1816
}, options)(target, propertyKey);

lib/inject-transform.module.mts

Lines changed: 20 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@ import {
33
Inject,
44
InjectionToken,
55
Module,
6+
OnModuleDestroy,
7+
OnModuleInit,
68
Optional,
79
} from "@nestjs/common";
810
import { ModuleRef } from "@nestjs/core";
911
import { AppStateService } from "./app-state.service.mjs";
10-
import { InjectTransformContainerOptions } from "./interfaces/index.mjs";
12+
import { InjectTransformModuleOptions } from "./interfaces/index.mjs";
1113
import { InjectLifecycleError } from "./exceptions.mjs";
12-
import { InjectTransformModuleOptions } from "./interfaces/inject-transform-module-options.interface.mjs";
1314
import { INJECT_TRANSFORM_MODULE_OPTIONS } from "./symbols.mjs";
15+
import { AsyncLocalStorage } from "node:async_hooks";
1416

1517
@Module({ providers: [AppStateService] })
16-
export class InjectTransformModule {
17-
private static injectTransformModule: InjectTransformModule;
18+
export class InjectTransformModule implements OnModuleInit, OnModuleDestroy {
19+
private static storage = new AsyncLocalStorage<ModuleRef>();
1820

1921
public static forRoot(
2022
options: InjectTransformModuleOptions = {}
@@ -27,46 +29,33 @@ export class InjectTransformModule {
2729
};
2830
}
2931

30-
public static getInjectTransformContainer(
31-
options?: InjectTransformContainerOptions
32-
): {
32+
public static getInjectTransformContainer(): {
3333
get: ModuleRef["get"];
3434
} {
35-
// Retrieve reference to currently running global module
36-
const { moduleRef, moduleOptions } =
37-
InjectTransformModule.injectTransformModule ??
38-
({} as InjectTransformModule);
39-
options = { ...moduleOptions, ...options };
40-
41-
const appRunning = moduleRef?.get(AppStateService).isRunning ?? false;
42-
if (!appRunning && !options.ignoreInjectLifecycle) {
35+
const moduleRef = InjectTransformModule.storage.getStore();
36+
if (!moduleRef)
4337
throw new InjectLifecycleError(
44-
"Dependency injection in class-transformer unavailable outside of module context." +
45-
(!InjectTransformModule.injectTransformModule
46-
? " Did you forget to import the InjectTransformModule?"
47-
: "")
38+
`Dependency injection in class-transformer only available during app lifecycle.` +
39+
` Did you forget to import the InjectTransformModule?`
4840
);
49-
}
5041

5142
return {
52-
get: (token: InjectionToken) => moduleRef?.get(token, { strict: false }),
43+
get: (token: InjectionToken) => moduleRef.get(token, { strict: false }),
5344
};
5445
}
5546

56-
public static getInjectTransformModule() {
57-
return this.injectTransformModule;
58-
}
59-
60-
public static clearInjectTransformModule() {
61-
this.injectTransformModule = undefined;
62-
}
63-
6447
constructor(
6548
private readonly moduleRef: ModuleRef,
6649
@Inject(INJECT_TRANSFORM_MODULE_OPTIONS)
6750
@Optional()
6851
private readonly moduleOptions: InjectTransformModuleOptions
69-
) {
70-
InjectTransformModule.injectTransformModule = this;
52+
) {}
53+
54+
onModuleInit() {
55+
InjectTransformModule.storage.enterWith(this.moduleRef);
56+
}
57+
58+
onModuleDestroy() {
59+
InjectTransformModule.storage.disable(); // Clean up the storage
7160
}
7261
}

lib/inject-transform.module.spec.ts

Lines changed: 34 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,55 @@
11
import { InjectTransformModule } from "./inject-transform.module.mjs";
2-
import { Module } from "@nestjs/common";
2+
import { Body, Controller, Module, Post } from "@nestjs/common";
33
import { NestFactory } from "@nestjs/core";
44
import { InjectTransform } from "./decorators/index.mjs";
55
import { plainToInstance } from "class-transformer";
6-
import { InjectLifecycleError } from "./exceptions.mjs";
6+
import { default as supertest } from "supertest";
77

8-
@Module({ imports: [InjectTransformModule] })
9-
class TestAppModuleWithImport {}
8+
const x = Symbol("eh");
9+
10+
class TestSubject {
11+
@InjectTransform((params, tokenValue: number) => params.value + tokenValue, {
12+
inject: [x],
13+
})
14+
x: number;
15+
}
1016

11-
@Module({ imports: [] })
12-
class TestAppModuleWithoutImport {}
17+
@Controller()
18+
class TestController {
19+
@Post("/")
20+
returnInject(@Body() subject: TestSubject) {
21+
return plainToInstance(TestSubject, subject);
22+
}
23+
}
1324

1425
@Module({
15-
imports: [InjectTransformModule.forRoot({ ignoreInjectLifecycle: true })],
26+
imports: [InjectTransformModule],
27+
controllers: [TestController],
28+
providers: [{ provide: x, useValue: 10 }],
1629
})
17-
class TestAppModuleWithOptions {}
30+
class TestAppModuleWithImport {}
1831

1932
describe("InjectTransformModule", () => {
20-
beforeEach(() => InjectTransformModule.clearInjectTransformModule());
21-
22-
it("should set the global module when injected", async () => {
23-
await NestFactory.createApplicationContext(TestAppModuleWithImport);
24-
expect(
25-
InjectTransformModule.getInjectTransformModule()
26-
).not.toBeUndefined();
27-
});
28-
29-
it("should not have a global module when not injected", async () => {
30-
await NestFactory.createApplicationContext(TestAppModuleWithoutImport);
31-
expect(InjectTransformModule.getInjectTransformModule()).toBeUndefined();
32-
});
33-
34-
it("should hint at import when using transform container without global module", () => {
33+
it("should hint at import when no context is found", () => {
3534
expect(() => InjectTransformModule.getInjectTransformContainer()).toThrow(
3635
"Did you forget to import the InjectTransformModule?"
3736
);
3837
});
3938

40-
describe("Module options", () => {
41-
it("should consider module options", async () => {
42-
const app = await NestFactory.createApplicationContext(
43-
TestAppModuleWithOptions
44-
);
45-
await app.close();
39+
it("should inject during controller requests", async () => {
40+
const app = await NestFactory.create(TestAppModuleWithImport);
41+
await app.init();
4642

47-
// Accessing the inject transform container when the app is closed should throw an error,
48-
// unless it has taken the `ignoreLifecycleErrors` module option into account, which this
49-
// test wants to assert.
50-
expect(
51-
InjectTransformModule.getInjectTransformContainer()
52-
).not.toBeUndefined();
53-
});
43+
await supertest(app.getHttpServer())
44+
.post("/")
45+
.send({ x: 5 })
46+
.expect(201)
47+
.expect({ x: 15 });
5448

55-
it("should override module options with decorator options", async () => {
56-
const app = await NestFactory.createApplicationContext(
57-
TestAppModuleWithOptions
58-
);
59-
await app.close();
60-
61-
class Test {
62-
@InjectTransform(() => 5, { ignoreInjectLifecycle: false })
63-
x: number;
64-
}
49+
await app.close();
50+
});
6551

66-
// Accessing the inject transform container when the app is closed should throw an error,
67-
// unless it has taken the `ignoreLifecycleErrors` module option into account, which this
68-
// test wants to assert to be overridden by the decorator options.
69-
expect(() => plainToInstance(Test, { x: 3 })).toThrow(
70-
InjectLifecycleError
71-
);
72-
});
52+
it("should error without an active application", async () => {
53+
expect(() => plainToInstance(TestSubject, { x: 5 })).toThrow();
7354
});
7455
});
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
export interface InjectTransformContainerOptions {
2-
ignoreInjectLifecycle?: boolean;
3-
}
1+
export interface InjectTransformContainerOptions {}
File renamed without changes.

0 commit comments

Comments
 (0)