@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
package/src/http/decorators.ts
CHANGED
|
@@ -26,12 +26,13 @@ export const All = createRouteDecorator('ALL');
|
|
|
26
26
|
|
|
27
27
|
export interface ParameterConfig {
|
|
28
28
|
index: number;
|
|
29
|
-
type: 'req' | 'res' | 'param' | 'query' | 'body' | 'headers';
|
|
29
|
+
type: 'req' | 'res' | 'param' | 'query' | 'body' | 'headers' | 'custom';
|
|
30
30
|
name?: string;
|
|
31
31
|
pipes?: any[];
|
|
32
|
+
factory?: (data: any, ctx: any) => any;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function
|
|
35
|
+
function createHttpParamDecorator(type: ParameterConfig['type'], name?: string, pipes: any[] = []): ParameterDecorator {
|
|
35
36
|
return (target, propertyKey, parameterIndex) => {
|
|
36
37
|
if (!propertyKey) return;
|
|
37
38
|
const existingParams: ParameterConfig[] =
|
|
@@ -55,30 +56,30 @@ function parseParamArgs(first?: any, ...rest: any[]) {
|
|
|
55
56
|
return { name, pipes };
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
export const Req = () =>
|
|
59
|
+
export const Req = () => createHttpParamDecorator('req');
|
|
59
60
|
export const Request = Req;
|
|
60
61
|
|
|
61
|
-
export const Res = () =>
|
|
62
|
+
export const Res = (options?: { passthrough: boolean }) => createHttpParamDecorator('res', options?.passthrough ? 'passthrough' : undefined);
|
|
62
63
|
export const Response = Res;
|
|
63
64
|
|
|
64
65
|
export const Param = (first?: any, ...pipes: any[]) => {
|
|
65
66
|
const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
|
|
66
|
-
return
|
|
67
|
+
return createHttpParamDecorator('param', name, parsedPipes);
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
export const Query = (first?: any, ...pipes: any[]) => {
|
|
70
71
|
const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
|
|
71
|
-
return
|
|
72
|
+
return createHttpParamDecorator('query', name, parsedPipes);
|
|
72
73
|
};
|
|
73
74
|
|
|
74
75
|
export const Body = (first?: any, ...pipes: any[]) => {
|
|
75
76
|
const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
|
|
76
|
-
return
|
|
77
|
+
return createHttpParamDecorator('body', name, parsedPipes);
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
export const Headers = (first?: any, ...pipes: any[]) => {
|
|
80
81
|
const { name, pipes: parsedPipes } = parseParamArgs(first, ...pipes);
|
|
81
|
-
return
|
|
82
|
+
return createHttpParamDecorator('headers', name, parsedPipes);
|
|
82
83
|
};
|
|
83
84
|
|
|
84
85
|
export function HttpCode(code: number): MethodDecorator {
|
|
@@ -101,3 +102,23 @@ export function Redirect(url: string, statusCode = 302): MethodDecorator {
|
|
|
101
102
|
Reflect.defineMetadata(METADATA_KEYS.REDIRECT, { url, statusCode }, target, propertyKey);
|
|
102
103
|
};
|
|
103
104
|
}
|
|
105
|
+
|
|
106
|
+
export function createParamDecorator<Data = any, Return = any>(
|
|
107
|
+
factory: (data: Data, ctx: any) => Return
|
|
108
|
+
): (data?: Data, ...pipes: any[]) => ParameterDecorator {
|
|
109
|
+
return (data?: Data, ...pipes: any[]): ParameterDecorator => {
|
|
110
|
+
return (target, propertyKey, parameterIndex) => {
|
|
111
|
+
if (!propertyKey) return;
|
|
112
|
+
const existingParams: ParameterConfig[] =
|
|
113
|
+
Reflect.getOwnMetadata(METADATA_KEYS.HTTP_PARAMS, target, propertyKey) || [];
|
|
114
|
+
existingParams.push({
|
|
115
|
+
index: parameterIndex,
|
|
116
|
+
type: 'custom',
|
|
117
|
+
name: data as any,
|
|
118
|
+
pipes,
|
|
119
|
+
factory,
|
|
120
|
+
});
|
|
121
|
+
Reflect.defineMetadata(METADATA_KEYS.HTTP_PARAMS, existingParams, target, propertyKey);
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
}
|
package/src/http/factory.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CalyxApplication } from './application.ts';
|
|
2
2
|
|
|
3
|
-
export class
|
|
4
|
-
static async create(rootModule: any): Promise<
|
|
5
|
-
const app = new
|
|
3
|
+
export class CalyxFactory {
|
|
4
|
+
static async create(rootModule: any): Promise<CalyxApplication> {
|
|
5
|
+
const app = new CalyxApplication(rootModule);
|
|
6
6
|
return app;
|
|
7
7
|
}
|
|
8
8
|
}
|
package/src/http/router.ts
CHANGED
|
@@ -13,8 +13,14 @@ class RouterNode<T> {
|
|
|
13
13
|
|
|
14
14
|
export class RadixRouter<T> {
|
|
15
15
|
private root = new RouterNode<T>();
|
|
16
|
+
private staticRoutes = new Map<string, T>();
|
|
16
17
|
|
|
17
18
|
insert(method: string, path: string, handler: T) {
|
|
19
|
+
const hasParams = path.includes(':') || path.includes('*');
|
|
20
|
+
if (!hasParams) {
|
|
21
|
+
this.staticRoutes.set(method.toUpperCase() + ' ' + path, handler);
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const segments = path.split('/').filter(Boolean);
|
|
19
25
|
let node = this.root;
|
|
20
26
|
|
|
@@ -46,6 +52,12 @@ export class RadixRouter<T> {
|
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
match(method: string, path: string): RouteMatch<T> | null {
|
|
55
|
+
const key = method.toUpperCase() + ' ' + path;
|
|
56
|
+
const staticHandler = this.staticRoutes.get(key);
|
|
57
|
+
if (staticHandler) {
|
|
58
|
+
return { handler: staticHandler, params: {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
const segments = path.split('/').filter(Boolean);
|
|
50
62
|
const params: Record<string, string> = {};
|
|
51
63
|
const matchNode = this.matchSegment(this.root, segments, 0, params);
|
package/src/lifecycle/context.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ArgumentsHost, HttpArgumentsHost, ExecutionContext } from './interfaces.ts';
|
|
2
2
|
import { Type } from '../core/metadata.ts';
|
|
3
3
|
|
|
4
|
-
export class
|
|
4
|
+
export class CalyxArgumentsHost implements ArgumentsHost {
|
|
5
5
|
constructor(private readonly req: Request, private readonly res: any) {}
|
|
6
6
|
|
|
7
7
|
getArgs<T extends any[] = any[]>(): T {
|
|
@@ -21,7 +21,7 @@ export class calyxArgumentsHost implements ArgumentsHost {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export class
|
|
24
|
+
export class CalyxExecutionContext extends CalyxArgumentsHost implements ExecutionContext {
|
|
25
25
|
constructor(
|
|
26
26
|
req: Request,
|
|
27
27
|
res: any,
|
|
@@ -47,3 +47,23 @@ export interface PipeTransform<T = any, R = any> {
|
|
|
47
47
|
export interface ExceptionFilter<T = any> {
|
|
48
48
|
catch(exception: T, host: ArgumentsHost): any;
|
|
49
49
|
}
|
|
50
|
+
|
|
51
|
+
export interface OnModuleInit {
|
|
52
|
+
onModuleInit(): any | Promise<any>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface OnApplicationBootstrap {
|
|
56
|
+
onApplicationBootstrap(): any | Promise<any>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface OnModuleDestroy {
|
|
60
|
+
onModuleDestroy(): any | Promise<any>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface BeforeApplicationShutdown {
|
|
64
|
+
beforeApplicationShutdown(signal?: string): any | Promise<any>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface OnApplicationShutdown {
|
|
68
|
+
onApplicationShutdown(signal?: string): any | Promise<any>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
2
|
+
import { spawnSync } from 'child_process';
|
|
3
|
+
import { existsSync, rmSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
const testAppName = 'scratch/my-cli-test-app';
|
|
7
|
+
|
|
8
|
+
describe('Calyx CLI Integration Tests', () => {
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
if (existsSync(testAppName)) {
|
|
11
|
+
rmSync(testAppName, { recursive: true, force: true });
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
if (existsSync(testAppName)) {
|
|
17
|
+
rmSync(testAppName, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('should display version details on info command', () => {
|
|
22
|
+
const proc = spawnSync('bun', ['./src/cli/index.ts', 'info'], { encoding: 'utf-8' });
|
|
23
|
+
expect(proc.status).toBe(0);
|
|
24
|
+
expect(proc.stdout).toContain('Calyx CLI Information');
|
|
25
|
+
expect(proc.stdout).toContain('Calyx Version: 0.1.0');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should scaffold a new application on new command', () => {
|
|
29
|
+
console.log('Scaffolding app, this may take a few seconds to run bun install...');
|
|
30
|
+
const proc = spawnSync('bun', ['./src/cli/index.ts', 'new', testAppName], { stdio: 'inherit' });
|
|
31
|
+
expect(proc.status).toBe(0);
|
|
32
|
+
|
|
33
|
+
expect(existsSync(join(testAppName, 'package.json'))).toBe(true);
|
|
34
|
+
expect(existsSync(join(testAppName, 'tsconfig.json'))).toBe(true);
|
|
35
|
+
expect(existsSync(join(testAppName, 'src/main.ts'))).toBe(true);
|
|
36
|
+
expect(existsSync(join(testAppName, 'src/app.module.ts'))).toBe(true);
|
|
37
|
+
expect(existsSync(join(testAppName, 'src/app.controller.ts'))).toBe(true);
|
|
38
|
+
expect(existsSync(join(testAppName, 'src/app.service.ts'))).toBe(true);
|
|
39
|
+
expect(existsSync(join(testAppName, 'node_modules'))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should generate controllers, services, and modules with auto-registration', () => {
|
|
43
|
+
// Generate a controller
|
|
44
|
+
const genCtrl = spawnSync('bun', ['../../src/cli/index.ts', 'g', 'controller', 'users'], {
|
|
45
|
+
cwd: testAppName,
|
|
46
|
+
encoding: 'utf-8',
|
|
47
|
+
});
|
|
48
|
+
expect(genCtrl.status).toBe(0);
|
|
49
|
+
expect(existsSync(join(testAppName, 'src/users/users.controller.ts'))).toBe(true);
|
|
50
|
+
|
|
51
|
+
// Verify registration inside app.module.ts
|
|
52
|
+
const moduleContent = readFileSync(join(testAppName, 'src/app.module.ts'), 'utf-8');
|
|
53
|
+
expect(moduleContent).toContain("import { UsersController } from './users/users.controller';");
|
|
54
|
+
expect(moduleContent).toContain('controllers: [AppController, UsersController]');
|
|
55
|
+
|
|
56
|
+
// Generate a service
|
|
57
|
+
const genSvc = spawnSync('bun', ['../../src/cli/index.ts', 'g', 'service', 'users'], {
|
|
58
|
+
cwd: testAppName,
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
});
|
|
61
|
+
expect(genSvc.status).toBe(0);
|
|
62
|
+
expect(existsSync(join(testAppName, 'src/users/users.service.ts'))).toBe(true);
|
|
63
|
+
|
|
64
|
+
const moduleContent2 = readFileSync(join(testAppName, 'src/app.module.ts'), 'utf-8');
|
|
65
|
+
expect(moduleContent2).toContain("import { UsersService } from './users/users.service';");
|
|
66
|
+
expect(moduleContent2).toContain('providers: [AppService, UsersService]');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should build the application using Bun compiler', () => {
|
|
70
|
+
// Manually modify package.json to point to local CLI script for Windows/Link test compatibility
|
|
71
|
+
const pkgPath = join(testAppName, 'package.json');
|
|
72
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
73
|
+
pkg.scripts.build = "bun ../../src/cli/index.ts build";
|
|
74
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
75
|
+
|
|
76
|
+
// Manually add path mapping to tsconfig.json for TypeScript source resolution during tests
|
|
77
|
+
const tsconfigPath = join(testAppName, 'tsconfig.json');
|
|
78
|
+
const tsconfig = JSON.parse(readFileSync(tsconfigPath, 'utf-8'));
|
|
79
|
+
tsconfig.compilerOptions.paths = {
|
|
80
|
+
"@martel/calyx": ["../../src/index.ts"]
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
83
|
+
|
|
84
|
+
const buildProc = spawnSync('bun', ['run', 'build'], {
|
|
85
|
+
cwd: testAppName,
|
|
86
|
+
encoding: 'utf-8',
|
|
87
|
+
});
|
|
88
|
+
console.log('Build STDOUT:', buildProc.stdout);
|
|
89
|
+
console.log('Build STDERR:', buildProc.stderr);
|
|
90
|
+
expect(buildProc.status).toBe(0);
|
|
91
|
+
expect(existsSync(join(testAppName, 'dist/main.js'))).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/tests/di.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
Optional,
|
|
7
7
|
Global,
|
|
8
8
|
forwardRef,
|
|
9
|
-
|
|
9
|
+
CalyxContainer,
|
|
10
10
|
ModuleRef,
|
|
11
11
|
} from '../src/index.ts';
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ describe('Dependency Injection System', () => {
|
|
|
29
29
|
})
|
|
30
30
|
class RootModule {}
|
|
31
31
|
|
|
32
|
-
const container = new
|
|
32
|
+
const container = new CalyxContainer();
|
|
33
33
|
container.bootstrap(RootModule);
|
|
34
34
|
|
|
35
35
|
const target = container.getGlobalOrAnyInstance(TargetService);
|
|
@@ -57,7 +57,7 @@ describe('Dependency Injection System', () => {
|
|
|
57
57
|
})
|
|
58
58
|
class RootModule {}
|
|
59
59
|
|
|
60
|
-
const container = new
|
|
60
|
+
const container = new CalyxContainer();
|
|
61
61
|
container.bootstrap(RootModule);
|
|
62
62
|
|
|
63
63
|
const consumer = container.getGlobalOrAnyInstance(ConfigConsumer);
|
|
@@ -94,7 +94,7 @@ describe('Dependency Injection System', () => {
|
|
|
94
94
|
})
|
|
95
95
|
class RootModule {}
|
|
96
96
|
|
|
97
|
-
const container = new
|
|
97
|
+
const container = new CalyxContainer();
|
|
98
98
|
container.bootstrap(RootModule);
|
|
99
99
|
|
|
100
100
|
const consumer = container.getGlobalOrAnyInstance(Consumer);
|
|
@@ -127,7 +127,7 @@ describe('Dependency Injection System', () => {
|
|
|
127
127
|
})
|
|
128
128
|
class RootModule {}
|
|
129
129
|
|
|
130
|
-
const container = new
|
|
130
|
+
const container = new CalyxContainer();
|
|
131
131
|
container.bootstrap(RootModule);
|
|
132
132
|
|
|
133
133
|
const consumer = container.getGlobalOrAnyInstance(Consumer);
|
|
@@ -145,7 +145,7 @@ describe('Dependency Injection System', () => {
|
|
|
145
145
|
})
|
|
146
146
|
class RootModule {}
|
|
147
147
|
|
|
148
|
-
const container = new
|
|
148
|
+
const container = new CalyxContainer();
|
|
149
149
|
container.bootstrap(RootModule);
|
|
150
150
|
|
|
151
151
|
const consumer = container.getGlobalOrAnyInstance(Consumer);
|
|
@@ -163,7 +163,7 @@ describe('Dependency Injection System', () => {
|
|
|
163
163
|
})
|
|
164
164
|
class RootModule {}
|
|
165
165
|
|
|
166
|
-
const container = new
|
|
166
|
+
const container = new CalyxContainer();
|
|
167
167
|
expect(() => container.bootstrap(RootModule)).toThrow(/Cannot resolve dependency/);
|
|
168
168
|
});
|
|
169
169
|
|
|
@@ -183,7 +183,7 @@ describe('Dependency Injection System', () => {
|
|
|
183
183
|
})
|
|
184
184
|
class RootModule {}
|
|
185
185
|
|
|
186
|
-
const container = new
|
|
186
|
+
const container = new CalyxContainer();
|
|
187
187
|
expect(() => container.bootstrap(RootModule)).toThrow(/Circular dependency detected/);
|
|
188
188
|
});
|
|
189
189
|
|
|
@@ -210,7 +210,7 @@ describe('Dependency Injection System', () => {
|
|
|
210
210
|
})
|
|
211
211
|
class AppModule {}
|
|
212
212
|
|
|
213
|
-
const container = new
|
|
213
|
+
const container = new CalyxContainer();
|
|
214
214
|
container.bootstrap(AppModule);
|
|
215
215
|
|
|
216
216
|
const appService = container.getGlobalOrAnyInstance(AppService);
|
|
@@ -243,7 +243,7 @@ describe('Dependency Injection System', () => {
|
|
|
243
243
|
})
|
|
244
244
|
class AppModule {}
|
|
245
245
|
|
|
246
|
-
const container = new
|
|
246
|
+
const container = new CalyxContainer();
|
|
247
247
|
container.bootstrap(AppModule);
|
|
248
248
|
|
|
249
249
|
const appService = container.getGlobalOrAnyInstance(AppService);
|
|
@@ -272,7 +272,7 @@ describe('Dependency Injection System', () => {
|
|
|
272
272
|
})
|
|
273
273
|
class RootModule {}
|
|
274
274
|
|
|
275
|
-
const container = new
|
|
275
|
+
const container = new CalyxContainer();
|
|
276
276
|
container.bootstrap(RootModule);
|
|
277
277
|
|
|
278
278
|
const consumer = container.getGlobalOrAnyInstance(ConsumerService);
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
Module,
|
|
4
4
|
Injectable,
|
|
5
5
|
Inject,
|
|
6
|
-
|
|
6
|
+
CalyxContainer,
|
|
7
7
|
DynamicModule,
|
|
8
8
|
} from '../src/index.ts';
|
|
9
9
|
|
|
@@ -42,7 +42,7 @@ class AppModule {}
|
|
|
42
42
|
|
|
43
43
|
describe('Dynamic Modules', () => {
|
|
44
44
|
test('should resolve providers defined dynamically in a DynamicModule', () => {
|
|
45
|
-
const container = new
|
|
45
|
+
const container = new CalyxContainer();
|
|
46
46
|
container.bootstrap(AppModule);
|
|
47
47
|
|
|
48
48
|
const appService = container.getGlobalOrAnyInstance(AppService);
|
package/tests/lifecycle.test.ts
CHANGED
|
@@ -18,11 +18,11 @@ import {
|
|
|
18
18
|
ExceptionFilter,
|
|
19
19
|
ArgumentsHost,
|
|
20
20
|
ExecutionContext,
|
|
21
|
-
|
|
21
|
+
CalyxFactory,
|
|
22
22
|
HttpException,
|
|
23
23
|
ForbiddenException,
|
|
24
24
|
BadRequestException,
|
|
25
|
-
|
|
25
|
+
CalyxResponse,
|
|
26
26
|
} from '../src/index.ts';
|
|
27
27
|
|
|
28
28
|
// 1. Guard
|
|
@@ -67,7 +67,7 @@ class CustomError extends Error {
|
|
|
67
67
|
@Catch(CustomError)
|
|
68
68
|
class CustomExceptionFilter implements ExceptionFilter {
|
|
69
69
|
catch(exception: CustomError, host: ArgumentsHost) {
|
|
70
|
-
const res = host.switchToHttp().getResponse<
|
|
70
|
+
const res = host.switchToHttp().getResponse<CalyxResponse>();
|
|
71
71
|
res.status(418).json({
|
|
72
72
|
customFilter: true,
|
|
73
73
|
errorMessage: exception.message,
|
|
@@ -112,7 +112,7 @@ describe('Request Lifecycle Pipeline (Guards, Interceptors, Pipes, Filters)', ()
|
|
|
112
112
|
const PORT = 3848;
|
|
113
113
|
|
|
114
114
|
beforeAll(async () => {
|
|
115
|
-
app = await
|
|
115
|
+
app = await CalyxFactory.create(AppModule);
|
|
116
116
|
await app.listen(PORT);
|
|
117
117
|
baseUrl = `http://localhost:${PORT}`;
|
|
118
118
|
});
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
createParamDecorator,
|
|
11
|
+
ExecutionContext,
|
|
12
|
+
OnModuleInit,
|
|
13
|
+
OnApplicationBootstrap,
|
|
14
|
+
OnModuleDestroy,
|
|
15
|
+
BeforeApplicationShutdown,
|
|
16
|
+
OnApplicationShutdown,
|
|
17
|
+
} from '../src/index.ts';
|
|
18
|
+
|
|
19
|
+
describe('Phase 1: Extended DI & Lifecycle Features', () => {
|
|
20
|
+
|
|
21
|
+
// 1. Property Injection Test
|
|
22
|
+
test('should resolve property-level injections', () => {
|
|
23
|
+
const VALUE_TOKEN = 'VALUE_TOKEN';
|
|
24
|
+
|
|
25
|
+
@Injectable()
|
|
26
|
+
class ValueService {
|
|
27
|
+
constructor() {}
|
|
28
|
+
getData() { return 'property_value'; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Injectable()
|
|
32
|
+
class ConsumerService {
|
|
33
|
+
@Inject(VALUE_TOKEN)
|
|
34
|
+
public customVal!: string;
|
|
35
|
+
|
|
36
|
+
@Inject()
|
|
37
|
+
public service!: ValueService;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Module({
|
|
41
|
+
providers: [
|
|
42
|
+
ConsumerService,
|
|
43
|
+
ValueService,
|
|
44
|
+
{
|
|
45
|
+
provide: VALUE_TOKEN,
|
|
46
|
+
useValue: 'injected_string',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
})
|
|
50
|
+
class RootModule {}
|
|
51
|
+
|
|
52
|
+
const container = new CalyxContainer();
|
|
53
|
+
container.bootstrap(RootModule);
|
|
54
|
+
|
|
55
|
+
const consumer = container.getGlobalOrAnyInstance(ConsumerService);
|
|
56
|
+
expect(consumer.customVal).toBe('injected_string');
|
|
57
|
+
expect(consumer.service).toBeInstanceOf(ValueService);
|
|
58
|
+
expect(consumer.service.getData()).toBe('property_value');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 2. Custom Param Decorators Test
|
|
62
|
+
test('should support custom route parameter decorators', async () => {
|
|
63
|
+
const CustomUser = createParamDecorator((data: string, ctx: ExecutionContext) => {
|
|
64
|
+
const req = ctx.switchToHttp().getRequest<Request>();
|
|
65
|
+
const customHeader = req.headers.get('x-user-id');
|
|
66
|
+
return {
|
|
67
|
+
id: customHeader,
|
|
68
|
+
role: data,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
@Controller('test-custom-param')
|
|
73
|
+
class TestController {
|
|
74
|
+
@Get()
|
|
75
|
+
getUser(@CustomUser('admin') user: any) {
|
|
76
|
+
return user;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Module({
|
|
81
|
+
controllers: [TestController],
|
|
82
|
+
})
|
|
83
|
+
class RootModule {}
|
|
84
|
+
|
|
85
|
+
const app = await CalyxFactory.create(RootModule);
|
|
86
|
+
await app.listen(3990);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch('http://localhost:3990/test-custom-param', {
|
|
90
|
+
headers: { 'x-user-id': 'user_123' },
|
|
91
|
+
});
|
|
92
|
+
expect(res.status).toBe(200);
|
|
93
|
+
const body = await res.json();
|
|
94
|
+
expect(body).toEqual({ id: 'user_123', role: 'admin' });
|
|
95
|
+
} finally {
|
|
96
|
+
await app.close();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 3. Lifecycle Hooks Test
|
|
101
|
+
test('should run all lifecycle hooks in order', async () => {
|
|
102
|
+
const hookSequence: string[] = [];
|
|
103
|
+
|
|
104
|
+
@Injectable()
|
|
105
|
+
class LifecycleService implements OnModuleInit, OnApplicationBootstrap, OnModuleDestroy, BeforeApplicationShutdown, OnApplicationShutdown {
|
|
106
|
+
onModuleInit() {
|
|
107
|
+
hookSequence.push('init');
|
|
108
|
+
}
|
|
109
|
+
onApplicationBootstrap() {
|
|
110
|
+
hookSequence.push('bootstrap');
|
|
111
|
+
}
|
|
112
|
+
onModuleDestroy() {
|
|
113
|
+
hookSequence.push('destroy');
|
|
114
|
+
}
|
|
115
|
+
beforeApplicationShutdown(signal?: string) {
|
|
116
|
+
hookSequence.push(`beforeShutdown_${signal}`);
|
|
117
|
+
}
|
|
118
|
+
onApplicationShutdown(signal?: string) {
|
|
119
|
+
hookSequence.push(`shutdown_${signal}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Module({
|
|
124
|
+
providers: [LifecycleService],
|
|
125
|
+
})
|
|
126
|
+
class RootModule {}
|
|
127
|
+
|
|
128
|
+
const app = await CalyxFactory.create(RootModule);
|
|
129
|
+
await app.listen(3991);
|
|
130
|
+
|
|
131
|
+
expect(hookSequence).toEqual(['init', 'bootstrap']);
|
|
132
|
+
|
|
133
|
+
await app.close('SIGTERM');
|
|
134
|
+
|
|
135
|
+
expect(hookSequence).toEqual([
|
|
136
|
+
'init',
|
|
137
|
+
'bootstrap',
|
|
138
|
+
'destroy',
|
|
139
|
+
'beforeShutdown_SIGTERM',
|
|
140
|
+
'shutdown_SIGTERM'
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { of, delay } from 'rxjs';
|
|
3
|
+
import {
|
|
4
|
+
Module,
|
|
5
|
+
Controller,
|
|
6
|
+
Get,
|
|
7
|
+
Res,
|
|
8
|
+
CalyxFactory,
|
|
9
|
+
CalyxResponse,
|
|
10
|
+
} from '../src/index.ts';
|
|
11
|
+
|
|
12
|
+
describe('Phase 2: Execution Context, RxJS, and Passthrough Response', () => {
|
|
13
|
+
|
|
14
|
+
// 1. RxJS Observable Test
|
|
15
|
+
test('should automatically subscribe and resolve RxJS Observables', async () => {
|
|
16
|
+
@Controller('rxjs')
|
|
17
|
+
class RxjsController {
|
|
18
|
+
@Get('sync')
|
|
19
|
+
getSync() {
|
|
20
|
+
return of({ value: 'hello_rxjs' });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Get('async')
|
|
24
|
+
getAsync() {
|
|
25
|
+
// Return an observable that resolves after 10ms
|
|
26
|
+
return of({ value: 'hello_async_rxjs' }).pipe(delay(10));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Module({
|
|
31
|
+
controllers: [RxjsController],
|
|
32
|
+
})
|
|
33
|
+
class RootModule {}
|
|
34
|
+
|
|
35
|
+
const app = await CalyxFactory.create(RootModule);
|
|
36
|
+
await app.listen(3992);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Test sync observable
|
|
40
|
+
const resSync = await fetch('http://localhost:3992/rxjs/sync');
|
|
41
|
+
expect(resSync.status).toBe(200);
|
|
42
|
+
expect(await resSync.json()).toEqual({ value: 'hello_rxjs' });
|
|
43
|
+
|
|
44
|
+
// Test async observable
|
|
45
|
+
const resAsync = await fetch('http://localhost:3992/rxjs/async');
|
|
46
|
+
expect(resAsync.status).toBe(200);
|
|
47
|
+
expect(await resAsync.json()).toEqual({ value: 'hello_async_rxjs' });
|
|
48
|
+
} finally {
|
|
49
|
+
await app.close();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// 2. Passthrough Response vs Standard Response
|
|
54
|
+
test('should differentiate between passthrough and standard @Res injection', async () => {
|
|
55
|
+
@Controller('res-test')
|
|
56
|
+
class ResController {
|
|
57
|
+
@Get('standard')
|
|
58
|
+
getStandard(@Res() res: CalyxResponse) {
|
|
59
|
+
res.status(202).json({ manual: true });
|
|
60
|
+
// The return value should be ignored
|
|
61
|
+
return { ignored: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Get('standard-norender')
|
|
65
|
+
getStandardNoRender(@Res() res: CalyxResponse) {
|
|
66
|
+
// User does not call res.send or res.json, return is ignored, returns empty
|
|
67
|
+
return { ignored: true };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Get('passthrough')
|
|
71
|
+
getPassthrough(@Res({ passthrough: true }) res: CalyxResponse) {
|
|
72
|
+
res.status(206);
|
|
73
|
+
res.set('x-custom-pass', 'yes');
|
|
74
|
+
// Return value should be automatically serialized and sent
|
|
75
|
+
return { automatic: true };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Module({
|
|
80
|
+
controllers: [ResController],
|
|
81
|
+
})
|
|
82
|
+
class RootModule {}
|
|
83
|
+
|
|
84
|
+
const app = await CalyxFactory.create(RootModule);
|
|
85
|
+
await app.listen(3993);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// Standard @Res - manual response
|
|
89
|
+
const resStd = await fetch('http://localhost:3993/res-test/standard');
|
|
90
|
+
expect(resStd.status).toBe(202);
|
|
91
|
+
expect(await resStd.json()).toEqual({ manual: true });
|
|
92
|
+
|
|
93
|
+
// Standard @Res without render - ignored return value
|
|
94
|
+
const resNoRender = await fetch('http://localhost:3993/res-test/standard-norender');
|
|
95
|
+
expect(resNoRender.status).toBe(200);
|
|
96
|
+
expect(await resNoRender.text()).toBe('');
|
|
97
|
+
|
|
98
|
+
// Passthrough @Res - sets code/headers, serializes return value
|
|
99
|
+
const resPass = await fetch('http://localhost:3993/res-test/passthrough');
|
|
100
|
+
expect(resPass.status).toBe(206);
|
|
101
|
+
expect(resPass.headers.get('x-custom-pass')).toBe('yes');
|
|
102
|
+
expect(await resPass.json()).toEqual({ automatic: true });
|
|
103
|
+
} finally {
|
|
104
|
+
await app.close();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|