@martel/calyx 0.1.0 → 1.1.0
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/.github/workflows/release.yml +6 -2
- package/CHANGELOG.md +13 -0
- package/README.md +130 -0
- package/benchmarks/di-benchmark.ts +15 -15
- package/benchmarks/run-calyx-lifecycle.ts +2 -2
- package/benchmarks/run-calyx.ts +2 -2
- package/bun.lock +1261 -0
- package/docs/controllers.md +2 -2
- package/docs/dependency-injection.md +1 -1
- package/docs/lifecycle.md +2 -2
- package/docs/migration.md +5 -5
- package/package.json +9 -5
- package/src/cli/index.ts +323 -0
- package/src/core/container.ts +252 -27
- package/src/core/decorators.ts +64 -10
- package/src/core/index.ts +1 -0
- package/src/core/metadata.ts +13 -0
- package/src/core/module-ref.ts +3 -1
- package/src/core/reflector.ts +32 -0
- package/src/http/application.ts +323 -154
- package/src/http/decorators.ts +29 -8
- package/src/http/factory.ts +4 -4
- package/src/http/router.ts +12 -0
- package/src/lifecycle/context.ts +2 -2
- package/src/lifecycle/interfaces.ts +20 -0
- package/tests/cli.test.ts +93 -0
- package/tests/di.test.ts +11 -11
- package/tests/dynamic-module.test.ts +2 -2
- package/tests/lifecycle.test.ts +4 -4
- package/tests/phase1.test.ts +143 -0
- package/tests/phase2.test.ts +107 -0
- package/tests/phase3.test.ts +203 -0
- package/tests/phase5.test.ts +73 -0
- package/tests/routing.test.ts +4 -4
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
Module,
|
|
4
|
+
Injectable,
|
|
5
|
+
Inject,
|
|
6
|
+
Controller,
|
|
7
|
+
Get,
|
|
8
|
+
CalyxContainer,
|
|
9
|
+
CalyxFactory,
|
|
10
|
+
Scope,
|
|
11
|
+
REQUEST,
|
|
12
|
+
ModuleRef,
|
|
13
|
+
} from '../src/index.ts';
|
|
14
|
+
|
|
15
|
+
describe('Phase 3: Dependency Injection Scopes (Request & Transient)', () => {
|
|
16
|
+
|
|
17
|
+
// 1. Transient Scope Test
|
|
18
|
+
test('should resolve transient provider as new instances every time', () => {
|
|
19
|
+
let transientInstantiations = 0;
|
|
20
|
+
|
|
21
|
+
@Injectable({ scope: Scope.TRANSIENT })
|
|
22
|
+
class TransientService {
|
|
23
|
+
id = ++transientInstantiations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@Injectable()
|
|
27
|
+
class ConsumerService {
|
|
28
|
+
constructor(public t1: TransientService, public t2: TransientService) {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Module({
|
|
32
|
+
providers: [TransientService, ConsumerService],
|
|
33
|
+
})
|
|
34
|
+
class RootModule {}
|
|
35
|
+
|
|
36
|
+
const container = new CalyxContainer();
|
|
37
|
+
container.bootstrap(RootModule);
|
|
38
|
+
|
|
39
|
+
const consumer = container.getGlobalOrAnyInstance(ConsumerService);
|
|
40
|
+
expect(consumer.t1.id).toBe(1);
|
|
41
|
+
expect(consumer.t2.id).toBe(2);
|
|
42
|
+
expect(consumer.t1).not.toBe(consumer.t2);
|
|
43
|
+
|
|
44
|
+
const t3 = container.resolveTokenGlobally(TransientService);
|
|
45
|
+
expect(t3.id).toBe(3);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 2. Request Scope and Bubble Up Test
|
|
49
|
+
test('should bubble up Request scope and instantiate once per request', async () => {
|
|
50
|
+
let requestScopedInstantiations = 0;
|
|
51
|
+
let controllerInstantiations = 0;
|
|
52
|
+
|
|
53
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
54
|
+
class ScopedService {
|
|
55
|
+
id = ++requestScopedInstantiations;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Injectable() // implicitly request-scoped because of dependency ScopedService
|
|
59
|
+
class DependentService {
|
|
60
|
+
constructor(public scoped: ScopedService) {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Controller('scoped')
|
|
64
|
+
class ScopedController {
|
|
65
|
+
id = ++controllerInstantiations;
|
|
66
|
+
constructor(public dep: DependentService, public scoped: ScopedService) {}
|
|
67
|
+
|
|
68
|
+
@Get()
|
|
69
|
+
getIds() {
|
|
70
|
+
return {
|
|
71
|
+
controllerId: this.id,
|
|
72
|
+
depScopedId: this.dep.scoped.id,
|
|
73
|
+
scopedId: this.scoped.id,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Module({
|
|
79
|
+
controllers: [ScopedController],
|
|
80
|
+
providers: [ScopedService, DependentService],
|
|
81
|
+
})
|
|
82
|
+
class RootModule {}
|
|
83
|
+
|
|
84
|
+
const app = await CalyxFactory.create(RootModule);
|
|
85
|
+
await app.listen(3994);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// First request
|
|
89
|
+
const res1 = await fetch('http://localhost:3994/scoped');
|
|
90
|
+
expect(res1.status).toBe(200);
|
|
91
|
+
const data1 = await res1.json();
|
|
92
|
+
expect(data1).toEqual({
|
|
93
|
+
controllerId: 1,
|
|
94
|
+
depScopedId: 1,
|
|
95
|
+
scopedId: 1,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Second request
|
|
99
|
+
const res2 = await fetch('http://localhost:3994/scoped');
|
|
100
|
+
expect(res2.status).toBe(200);
|
|
101
|
+
const data2 = await res2.json();
|
|
102
|
+
expect(data2).toEqual({
|
|
103
|
+
controllerId: 2,
|
|
104
|
+
depScopedId: 2,
|
|
105
|
+
scopedId: 2,
|
|
106
|
+
});
|
|
107
|
+
} finally {
|
|
108
|
+
await app.close();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 3. REQUEST Object Injection Test
|
|
113
|
+
test('should inject the current Request object into request-scoped providers', async () => {
|
|
114
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
115
|
+
class ReqService {
|
|
116
|
+
constructor(@Inject(REQUEST) public request: Request) {}
|
|
117
|
+
|
|
118
|
+
getUrl() {
|
|
119
|
+
return this.request.url;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Controller('request-inject')
|
|
124
|
+
class ReqController {
|
|
125
|
+
constructor(public reqService: ReqService) {}
|
|
126
|
+
|
|
127
|
+
@Get()
|
|
128
|
+
testInject() {
|
|
129
|
+
return { url: this.reqService.getUrl() };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Module({
|
|
134
|
+
controllers: [ReqController],
|
|
135
|
+
providers: [ReqService],
|
|
136
|
+
})
|
|
137
|
+
class RootModule {}
|
|
138
|
+
|
|
139
|
+
const app = await CalyxFactory.create(RootModule);
|
|
140
|
+
await app.listen(3995);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch('http://localhost:3995/request-inject?query=123');
|
|
144
|
+
expect(res.status).toBe(200);
|
|
145
|
+
const body = await res.json();
|
|
146
|
+
expect(body.url).toContain('/request-inject?query=123');
|
|
147
|
+
} finally {
|
|
148
|
+
await app.close();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 4. ModuleRef resolve() and create() Test
|
|
153
|
+
test('should support ModuleRef.resolve() and ModuleRef.create()', async () => {
|
|
154
|
+
let transientCount = 0;
|
|
155
|
+
|
|
156
|
+
@Injectable({ scope: Scope.TRANSIENT })
|
|
157
|
+
class TransService {
|
|
158
|
+
id = ++transientCount;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Injectable()
|
|
162
|
+
class ConsumerService {
|
|
163
|
+
constructor(public moduleRef: ModuleRef) {}
|
|
164
|
+
|
|
165
|
+
async resolveTransient() {
|
|
166
|
+
return this.moduleRef.resolve(TransService);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async createDynamicClass() {
|
|
170
|
+
class DynamicClass {
|
|
171
|
+
constructor(public t: TransService) {}
|
|
172
|
+
}
|
|
173
|
+
Reflect.defineMetadata('design:paramtypes', [TransService], DynamicClass);
|
|
174
|
+
Reflect.defineMetadata('calyx:injectable', true, DynamicClass);
|
|
175
|
+
return this.moduleRef.create(DynamicClass);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
@Module({
|
|
180
|
+
providers: [TransService, ConsumerService],
|
|
181
|
+
})
|
|
182
|
+
class RootModule {}
|
|
183
|
+
|
|
184
|
+
const container = new CalyxContainer();
|
|
185
|
+
container.bootstrap(RootModule);
|
|
186
|
+
|
|
187
|
+
const consumer = container.getGlobalOrAnyInstance(ConsumerService);
|
|
188
|
+
|
|
189
|
+
// Test resolve
|
|
190
|
+
const t1 = await consumer.resolveTransient();
|
|
191
|
+
const t2 = await consumer.resolveTransient();
|
|
192
|
+
expect(t1).toBeInstanceOf(TransService);
|
|
193
|
+
expect(t2).toBeInstanceOf(TransService);
|
|
194
|
+
expect(t1.id).toBe(1);
|
|
195
|
+
expect(t2.id).toBe(2);
|
|
196
|
+
expect(t1).not.toBe(t2);
|
|
197
|
+
|
|
198
|
+
// Test create
|
|
199
|
+
const dynamic = await consumer.createDynamicClass();
|
|
200
|
+
expect(dynamic.t).toBeInstanceOf(TransService);
|
|
201
|
+
expect(dynamic.t.id).toBe(3);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
SetMetadata,
|
|
4
|
+
Reflector,
|
|
5
|
+
applyDecorators,
|
|
6
|
+
Controller,
|
|
7
|
+
Get,
|
|
8
|
+
} from '../src/index.ts';
|
|
9
|
+
|
|
10
|
+
describe('Phase 5: NestJS Parity Extensions (Reflector & Metadata)', () => {
|
|
11
|
+
test('should set and get metadata using SetMetadata and Reflector', () => {
|
|
12
|
+
const reflector = new Reflector();
|
|
13
|
+
|
|
14
|
+
@SetMetadata('roles', ['admin'])
|
|
15
|
+
class SecuredController {
|
|
16
|
+
@SetMetadata('permission', 'write')
|
|
17
|
+
handler() {}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const roles = reflector.get<string[]>('roles', SecuredController);
|
|
21
|
+
expect(roles).toEqual(['admin']);
|
|
22
|
+
|
|
23
|
+
const controllerInstance = new SecuredController();
|
|
24
|
+
const permission = reflector.get<string>('permission', controllerInstance.handler);
|
|
25
|
+
expect(permission).toBe('write');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should support Reflector.getAllAndOverride and getAllAndMerge', () => {
|
|
29
|
+
const reflector = new Reflector();
|
|
30
|
+
|
|
31
|
+
@SetMetadata('roles', ['user'])
|
|
32
|
+
class TestController {
|
|
33
|
+
@SetMetadata('roles', ['admin'])
|
|
34
|
+
handler() {}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const controllerInstance = new TestController();
|
|
38
|
+
|
|
39
|
+
// getAllAndOverride: returns first non-undefined (reverses targets parameter order in Nest, e.g. [handler, class])
|
|
40
|
+
const overridenRoles = reflector.getAllAndOverride<string[]>('roles', [
|
|
41
|
+
controllerInstance.handler,
|
|
42
|
+
TestController,
|
|
43
|
+
]);
|
|
44
|
+
expect(overridenRoles).toEqual(['admin']);
|
|
45
|
+
|
|
46
|
+
// getAllAndMerge: merges values from targets
|
|
47
|
+
const mergedRoles = reflector.getAllAndMerge<string[]>('roles', [
|
|
48
|
+
controllerInstance.handler,
|
|
49
|
+
TestController,
|
|
50
|
+
]);
|
|
51
|
+
expect(mergedRoles).toEqual(['admin', 'user']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should support applyDecorators to group multiple decorators', () => {
|
|
55
|
+
const reflector = new Reflector();
|
|
56
|
+
|
|
57
|
+
function CustomSecured(permission: string, roles: string[]) {
|
|
58
|
+
return applyDecorators(
|
|
59
|
+
SetMetadata('permission', permission),
|
|
60
|
+
SetMetadata('roles', roles)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@CustomSecured('write', ['admin'])
|
|
65
|
+
class GroupSecuredController {}
|
|
66
|
+
|
|
67
|
+
const permission = reflector.get<string>('permission', GroupSecuredController);
|
|
68
|
+
const roles = reflector.get<string[]>('roles', GroupSecuredController);
|
|
69
|
+
|
|
70
|
+
expect(permission).toBe('write');
|
|
71
|
+
expect(roles).toEqual(['admin']);
|
|
72
|
+
});
|
|
73
|
+
});
|
package/tests/routing.test.ts
CHANGED
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
Header,
|
|
15
15
|
Res,
|
|
16
16
|
Redirect,
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
CalyxFactory,
|
|
18
|
+
CalyxResponse,
|
|
19
19
|
NotFoundException,
|
|
20
20
|
BadRequestException,
|
|
21
21
|
} from '../src/index.ts';
|
|
@@ -59,7 +59,7 @@ class UsersController {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
@Get('manual')
|
|
62
|
-
manual(@Res() res:
|
|
62
|
+
manual(@Res() res: CalyxResponse) {
|
|
63
63
|
res.status(202).json({ manual: true });
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -80,7 +80,7 @@ describe('HTTP Routing and Controller System', () => {
|
|
|
80
80
|
const PORT = 3847;
|
|
81
81
|
|
|
82
82
|
beforeAll(async () => {
|
|
83
|
-
app = await
|
|
83
|
+
app = await CalyxFactory.create(AppModule);
|
|
84
84
|
await app.listen(PORT);
|
|
85
85
|
baseUrl = `http://localhost:${PORT}`;
|
|
86
86
|
});
|