Skip to content

Commit b17e806

Browse files
committed
inject transform & type
0 parents  commit b17e806

28 files changed

+7391
-0
lines changed

.github/workflows/main.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Test library
2+
on: "push"
3+
jobs:
4+
ci:
5+
runs-on: ubuntu-latest
6+
strategy:
7+
matrix:
8+
node-version: [ 16.x ]
9+
steps:
10+
- name: Checkout
11+
uses: actions/checkout@v2
12+
- name: Use Node.js ${{ matrix.node-version }}
13+
uses: actions/setup-node@v2
14+
with:
15+
node-version: ${{ matrix.node-version }}
16+
cache: 'npm'
17+
- name: Install dependencies
18+
run: |
19+
npm install
20+
- name: Build lib
21+
run: npm run build
22+
- name: Test
23+
run: |
24+
npm run test

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# dependencies
2+
/node_modules
3+
4+
# IDE
5+
/.vscode
6+
/.idea
7+
8+
# misc
9+
npm-debug.log
10+
.DS_Store
11+
12+
# dist
13+
/dist
14+
15+
# test
16+
.testdbs/

.npmignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
lib
2+
sample
3+
index.ts
4+
package-lock.json
5+
.eslintrc.js
6+
tsconfig.json
7+
.prettierrc
8+
.commitlintrc.json

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Edward Anthony
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<p align="center">
2+
Amp up your NestJS and `class-transformer` stack with dependency injection!
3+
</p>
4+
5+
## Installation and setup
6+
7+
### Install the dependency
8+
9+
```
10+
npm install nestjs-inject-transformer --save
11+
```
12+
13+
### Import the `InjectTransformModule`
14+
15+
```ts
16+
import { InjectTransformModule } from 'nestjs-inject-transformer';
17+
18+
@Module({
19+
imports: [InjectTransformModule]
20+
})
21+
export class ApplicationModule {}
22+
```
23+
24+
## Usage
25+
26+
This package provides the `InjectTransform` and `InjectType` decorators
27+
which support all options of their `class-transformer` respective counterparts `Transform` and `Type`.
28+
29+
### `InjectTransform`
30+
31+
Replace your `Transform` decorator with the dependency injection enabled `InjectTransform` decorator.
32+
33+
To inject a dependencies pass an array of injection tokens to the `inject` option. They will be passed
34+
as additional arguments to your transform function, in the order they were given:
35+
36+
```ts
37+
import { InjectTransform } from 'nestjs-inject-transformer';
38+
39+
export class MyDTO {
40+
@InjectTransform(
41+
(params, myService: MyService) => myService.parse(params.value),
42+
{inject: [MyService]}
43+
)
44+
myAttr: number;
45+
}
46+
```
47+
48+
> [!WARN]
49+
>
50+
> `class-transformer` operates strictly synchronously. Promises can not be awaited!
51+
52+
Alternatively, you can pass an `InjectTransformer` to tidy up more extensive use cases:
53+
54+
```ts
55+
import {InjectTransform, InjectTransformer} from 'nestjs-inject-transformer';
56+
import {TransformFnParams} from "class-transformer";
57+
58+
export class MyTransformer implements InjectTransformer {
59+
constructor(
60+
private readonly dep1: Dependency1,
61+
private readonly dep2: Dependency2,
62+
private readonly dep3: Dependency3
63+
) {}
64+
65+
transform(params: TransformFnParams): any {
66+
return this.dep1.parse(this.dep2.format(this.dep3.trim(params.value)));
67+
}
68+
}
69+
70+
export class MyDTO {
71+
@InjectTransform(MyTransformer)
72+
myAttr: number;
73+
}
74+
```
75+
76+
### `InjectType`
77+
78+
This decorator allows you to provide a dependency injection enabled type injector. Like the
79+
type transformer you can use the type injector's class body to scaffold your dependencies.
80+
81+
Its `inject` function is called with the same arguments as the `Type` function would have been.
82+
83+
The following example illustrates how you could return different DTO types (and thereby different
84+
validation schemes when used in combination with `class-validator`) based on a supposed client's
85+
configuration:
86+
87+
```ts
88+
@Injectable()
89+
class ClientDtoInjector implements TypeInjector {
90+
constructor(
91+
private readonly service: ClientConfigurationService
92+
) {}
93+
94+
inject(type?: TypeHelpOptions) {
95+
const client = type.object['client'] ?? 'default';
96+
const clientConfig = this.service.getClientConfiguration(client);
97+
const dto = clientConfig.getNestedDTO(type.newObject, type.property);
98+
return dto
99+
}
100+
}
101+
102+
class OpenAccountDTO {
103+
@IsString()
104+
client: string;
105+
106+
@ValidateNested()
107+
@InjectType(ClientDtoInjector)
108+
accountInfo: AccountInfoDTO
109+
}
110+
```
111+
112+
## 📜 License
113+
114+
`nestjs-inject-transformer` is [MIT licensed](LICENSE).

lib/app-state.service.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { OnApplicationBootstrap, OnApplicationShutdown } from "@nestjs/common";
2+
3+
export class AppStateService
4+
implements OnApplicationBootstrap, OnApplicationShutdown
5+
{
6+
private appRunning = false;
7+
8+
onApplicationBootstrap() {
9+
this.appRunning = true;
10+
}
11+
12+
onApplicationShutdown(signal?: string) {
13+
this.appRunning = false;
14+
}
15+
16+
get isRunning() {
17+
return this.appRunning;
18+
}
19+
}

lib/decorators/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./inject-transform.decorator.js";
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { plainToInstance, TransformFnParams } from "class-transformer";
2+
import { Injectable, Module } from "@nestjs/common";
3+
import { NestFactory } from "@nestjs/core";
4+
import { InjectTransform } from "./index.js";
5+
import { InjectTransformModule } from "../inject-transform.module.js";
6+
import { InjectLifecycleError } from "../exceptions.js";
7+
import { InjectTransformer } from "../interfaces/inject-transformer.interface.js";
8+
9+
const TEST_TOKEN = Symbol("TestToken");
10+
const MISSING_TOKEN = Symbol("MissingToken");
11+
12+
@Injectable()
13+
class TestService {
14+
transform(a: number, b: number) {
15+
return a * b;
16+
}
17+
}
18+
19+
@Injectable()
20+
class TestTransformer implements InjectTransformer {
21+
transform(params: TransformFnParams) {
22+
return params.value * 4;
23+
}
24+
}
25+
26+
@Module({
27+
imports: [InjectTransformModule],
28+
providers: [
29+
{ provide: TEST_TOKEN, useValue: 5 },
30+
TestService,
31+
TestTransformer,
32+
],
33+
})
34+
class TestAppModule {}
35+
36+
class TestSubject {
37+
@InjectTransform(
38+
(params, tokenValue: number, service: TestService) =>
39+
service.transform(params.value, tokenValue),
40+
{
41+
inject: [TEST_TOKEN, TestService],
42+
}
43+
)
44+
x: number;
45+
46+
@InjectTransform(TestTransformer)
47+
xx: number;
48+
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+
59+
@InjectTransform((params) => 0, { inject: [MISSING_TOKEN] })
60+
z: number;
61+
}
62+
63+
describe("InjectTransform", () => {
64+
it("should inject transform functions", async () => {
65+
const app = await NestFactory.createApplicationContext(TestAppModule);
66+
await app.init();
67+
68+
// TestSubject.x injects the value 5 and a transform service that multiplies
69+
// by that value => 3 * 5 = 15.
70+
expect(plainToInstance(TestSubject, { x: 3 }).x).toEqual(15);
71+
72+
await app.close();
73+
});
74+
75+
it("should inject transformers", async () => {
76+
const app = await NestFactory.createApplicationContext(TestAppModule);
77+
await app.init();
78+
79+
// TestSubject.xx injects a transformer that multiplies by 4
80+
expect(plainToInstance(TestSubject, { xx: 3 }).xx).toEqual(12);
81+
82+
await app.close();
83+
});
84+
85+
it("should error outside of injection lifecycle", async () => {
86+
const app = await NestFactory.createApplicationContext(TestAppModule);
87+
await app.init();
88+
await app.close();
89+
90+
// Injecting while no app is open yet, or is closed already
91+
// is defended from by default as it may lead to unexpected behaviour.
92+
expect(() => plainToInstance(TestSubject, { x: 3 })).toThrow(
93+
InjectLifecycleError
94+
);
95+
});
96+
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+
106+
it("should error on missing providers", async () => {
107+
const app = await NestFactory.createApplicationContext(TestAppModule);
108+
await app.init();
109+
110+
// TestSubject.z injects the missing MISSING_TOKEN provider.
111+
expect(() => plainToInstance(TestSubject, { z: 3 }).z).toThrow(
112+
"Nest could not find Symbol(MissingToken) element"
113+
);
114+
await app.close();
115+
});
116+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Transform } from "class-transformer";
2+
import "reflect-metadata";
3+
import { InjectTransformModule } from "../inject-transform.module.js";
4+
import { InjectTransformOptions } from "../interfaces/index.js";
5+
import { InjectTransformFn } from "../interfaces/inject-transform-fn.interface.js";
6+
import { InjectTransformer } from "../interfaces/inject-transformer.interface.js";
7+
import { Type } from "@nestjs/common";
8+
import { isClass } from "../util/is-class.js";
9+
10+
export function InjectTransform(
11+
transformer: InjectTransformFn,
12+
options?: InjectTransformOptions
13+
): PropertyDecorator;
14+
export function InjectTransform(
15+
transformer: Type<InjectTransformer>,
16+
options?: Omit<InjectTransformOptions, "inject">
17+
): PropertyDecorator;
18+
export function InjectTransform(
19+
transformer: InjectTransformFn | Type<InjectTransformer>,
20+
options: InjectTransformOptions = {}
21+
) {
22+
return Transform((params) => {
23+
const injector = InjectTransformModule.getInjectTransformContainer(options);
24+
const providers = options.inject ?? [];
25+
26+
// Unify transformFn <-> transformer
27+
// * If it's a transformer, inject it and bind its transform function as transformFn.
28+
// * If it's a transformFn, use it directly.
29+
const transformerInstance = isClass(transformer)
30+
? injector.get(transformer)
31+
: undefined;
32+
const transformFn =
33+
transformerInstance?.transform.bind(transformerInstance) ?? transformer;
34+
35+
// Call the transform function, injecting all the dependencies in order.
36+
return transformFn(
37+
params,
38+
...providers.map((provider) => injector.get(provider))
39+
);
40+
}, options);
41+
}

0 commit comments

Comments
 (0)