@martel/calyx 0.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.
@@ -0,0 +1,169 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2
+ import {
3
+ Module,
4
+ Controller,
5
+ Get,
6
+ Post,
7
+ Body,
8
+ Query,
9
+ UseGuards,
10
+ UseInterceptors,
11
+ UsePipes,
12
+ UseFilters,
13
+ Catch,
14
+ CanActivate,
15
+ NestInterceptor,
16
+ CallHandler,
17
+ PipeTransform,
18
+ ExceptionFilter,
19
+ ArgumentsHost,
20
+ ExecutionContext,
21
+ calyxFactory,
22
+ HttpException,
23
+ ForbiddenException,
24
+ BadRequestException,
25
+ calyxResponse,
26
+ } from '../src/index.ts';
27
+
28
+ // 1. Guard
29
+ class TestGuard implements CanActivate {
30
+ canActivate(context: ExecutionContext): boolean {
31
+ const req = context.switchToHttp().getRequest<Request>();
32
+ const authHeader = req.headers.get('Authorization');
33
+ return authHeader === 'allow';
34
+ }
35
+ }
36
+
37
+ // 2. Interceptor
38
+ class TestInterceptor implements NestInterceptor {
39
+ async intercept(context: ExecutionContext, next: CallHandler): Promise<any> {
40
+ const res = await next.handle();
41
+ if (typeof res === 'object') {
42
+ return { ...res, intercepted: true };
43
+ }
44
+ return `${res} intercepted`;
45
+ }
46
+ }
47
+
48
+ // 3. Pipe
49
+ class ParseIntPipe implements PipeTransform {
50
+ transform(value: any) {
51
+ const val = Number(value);
52
+ if (isNaN(val)) {
53
+ throw new BadRequestException('Validation failed: value must be a numeric string');
54
+ }
55
+ return val;
56
+ }
57
+ }
58
+
59
+ // 4. Exception Filter
60
+ class CustomError extends Error {
61
+ constructor(message: string) {
62
+ super(message);
63
+ this.name = 'CustomError';
64
+ }
65
+ }
66
+
67
+ @Catch(CustomError)
68
+ class CustomExceptionFilter implements ExceptionFilter {
69
+ catch(exception: CustomError, host: ArgumentsHost) {
70
+ const res = host.switchToHttp().getResponse<calyxResponse>();
71
+ res.status(418).json({
72
+ customFilter: true,
73
+ errorMessage: exception.message,
74
+ });
75
+ }
76
+ }
77
+
78
+ @Controller('lifecycle')
79
+ class TestLifecycleController {
80
+ @Get('guard-test')
81
+ @UseGuards(TestGuard)
82
+ guardTest() {
83
+ return 'access granted';
84
+ }
85
+
86
+ @Get('interceptor-test')
87
+ @UseInterceptors(TestInterceptor)
88
+ interceptorTest() {
89
+ return { success: true };
90
+ }
91
+
92
+ @Get('pipe-test')
93
+ pipeTest(@Query('num', ParseIntPipe) num: number) {
94
+ return { value: num, type: typeof num };
95
+ }
96
+
97
+ @Get('filter-test')
98
+ @UseFilters(CustomExceptionFilter)
99
+ filterTest() {
100
+ throw new CustomError('Teapot trigger');
101
+ }
102
+ }
103
+
104
+ @Module({
105
+ controllers: [TestLifecycleController],
106
+ })
107
+ class AppModule {}
108
+
109
+ describe('Request Lifecycle Pipeline (Guards, Interceptors, Pipes, Filters)', () => {
110
+ let app: any;
111
+ let baseUrl: string;
112
+ const PORT = 3848;
113
+
114
+ beforeAll(async () => {
115
+ app = await calyxFactory.create(AppModule);
116
+ await app.listen(PORT);
117
+ baseUrl = `http://localhost:${PORT}`;
118
+ });
119
+
120
+ afterAll(async () => {
121
+ await app.close();
122
+ });
123
+
124
+ // Guard test
125
+ test('should return 403 Forbidden if guard returns false', async () => {
126
+ const res = await fetch(`${baseUrl}/lifecycle/guard-test`);
127
+ expect(res.status).toBe(403);
128
+ });
129
+
130
+ test('should return 200 if guard passes with header', async () => {
131
+ const res = await fetch(`${baseUrl}/lifecycle/guard-test`, {
132
+ headers: { Authorization: 'allow' },
133
+ });
134
+ expect(res.status).toBe(200);
135
+ const text = await res.text();
136
+ expect(text).toBe('access granted');
137
+ });
138
+
139
+ // Interceptor test
140
+ test('should transform output using NestInterceptor', async () => {
141
+ const res = await fetch(`${baseUrl}/lifecycle/interceptor-test`);
142
+ expect(res.status).toBe(200);
143
+ const body = await res.json();
144
+ expect(body).toEqual({ success: true, intercepted: true });
145
+ });
146
+
147
+ // Pipe test
148
+ test('should transform query parameter value using PipeTransform', async () => {
149
+ const res = await fetch(`${baseUrl}/lifecycle/pipe-test?num=100`);
150
+ expect(res.status).toBe(200);
151
+ const body = await res.json();
152
+ expect(body).toEqual({ value: 100, type: 'number' });
153
+ });
154
+
155
+ test('should return 400 Bad Request if PipeTransform fails validation', async () => {
156
+ const res = await fetch(`${baseUrl}/lifecycle/pipe-test?num=not-a-number`);
157
+ expect(res.status).toBe(400);
158
+ const body = await res.json();
159
+ expect(body.message).toContain('Validation failed');
160
+ });
161
+
162
+ // Filter test
163
+ test('should intercept custom error and format response using ExceptionFilter', async () => {
164
+ const res = await fetch(`${baseUrl}/lifecycle/filter-test`);
165
+ expect(res.status).toBe(418); // Teapot
166
+ const body = await res.json();
167
+ expect(body).toEqual({ customFilter: true, errorMessage: 'Teapot trigger' });
168
+ });
169
+ });
@@ -0,0 +1,155 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
2
+ import {
3
+ Module,
4
+ Controller,
5
+ Get,
6
+ Post,
7
+ Put,
8
+ Delete,
9
+ Param,
10
+ Query,
11
+ Body,
12
+ Headers,
13
+ HttpCode,
14
+ Header,
15
+ Res,
16
+ Redirect,
17
+ calyxFactory,
18
+ calyxResponse,
19
+ NotFoundException,
20
+ BadRequestException,
21
+ } from '../src/index.ts';
22
+
23
+ @Controller('users')
24
+ class UsersController {
25
+ @Get()
26
+ getAll() {
27
+ return [{ id: 1, name: 'John' }];
28
+ }
29
+
30
+ @Post()
31
+ @HttpCode(201)
32
+ create(@Body() body: any) {
33
+ return { created: true, data: body };
34
+ }
35
+
36
+ @Get(':id')
37
+ getOne(@Param('id') id: string) {
38
+ return { id: Number(id) };
39
+ }
40
+
41
+ @Put(':id')
42
+ @Header('X-Updated', 'true')
43
+ update(@Param('id') id: string, @Body('name') name: string) {
44
+ return { id: Number(id), name };
45
+ }
46
+
47
+ @Get(':id/posts/:postId')
48
+ getPost(@Param('id') id: string, @Param('postId') postId: string, @Query('limit') limit?: string) {
49
+ return { userId: Number(id), postId: Number(postId), limit: limit ? Number(limit) : undefined };
50
+ }
51
+
52
+ @Get('redirect-test')
53
+ @Redirect('/users/dest', 301)
54
+ redirect() {}
55
+
56
+ @Get('dest')
57
+ dest() {
58
+ return 'destination';
59
+ }
60
+
61
+ @Get('manual')
62
+ manual(@Res() res: calyxResponse) {
63
+ res.status(202).json({ manual: true });
64
+ }
65
+
66
+ @Get('error-test')
67
+ error() {
68
+ throw new BadRequestException('Invalid request parameter');
69
+ }
70
+ }
71
+
72
+ @Module({
73
+ controllers: [UsersController],
74
+ })
75
+ class AppModule {}
76
+
77
+ describe('HTTP Routing and Controller System', () => {
78
+ let app: any;
79
+ let baseUrl: string;
80
+ const PORT = 3847;
81
+
82
+ beforeAll(async () => {
83
+ app = await calyxFactory.create(AppModule);
84
+ await app.listen(PORT);
85
+ baseUrl = `http://localhost:${PORT}`;
86
+ });
87
+
88
+ afterAll(async () => {
89
+ await app.close();
90
+ });
91
+
92
+ test('should return 200 and JSON from GET /users', async () => {
93
+ const res = await fetch(`${baseUrl}/users`);
94
+ expect(res.status).toBe(200);
95
+ const body = await res.json();
96
+ expect(body).toEqual([{ id: 1, name: 'John' }]);
97
+ });
98
+
99
+ test('should return 201 and parsed body from POST /users', async () => {
100
+ const payload = { name: 'Alice' };
101
+ const res = await fetch(`${baseUrl}/users`, {
102
+ method: 'POST',
103
+ headers: { 'content-type': 'application/json' },
104
+ body: JSON.stringify(payload),
105
+ });
106
+ expect(res.status).toBe(201);
107
+ const body = await res.json();
108
+ expect(body).toEqual({ created: true, data: { name: 'Alice' } });
109
+ });
110
+
111
+ test('should extract path parameters from GET /users/:id', async () => {
112
+ const res = await fetch(`${baseUrl}/users/42`);
113
+ expect(res.status).toBe(200);
114
+ const body = await res.json();
115
+ expect(body).toEqual({ id: 42 });
116
+ });
117
+
118
+ test('should parse custom headers and body parameter from PUT /users/:id', async () => {
119
+ const res = await fetch(`${baseUrl}/users/42`, {
120
+ method: 'PUT',
121
+ headers: { 'content-type': 'application/json' },
122
+ body: JSON.stringify({ name: 'Bob' }),
123
+ });
124
+ expect(res.status).toBe(200);
125
+ expect(res.headers.get('x-updated')).toBe('true');
126
+ const body = await res.json();
127
+ expect(body).toEqual({ id: 42, name: 'Bob' });
128
+ });
129
+
130
+ test('should parse multiple path params and query params', async () => {
131
+ const res = await fetch(`${baseUrl}/users/42/posts/100?limit=5`);
132
+ expect(res.status).toBe(200);
133
+ const body = await res.json();
134
+ expect(body).toEqual({ userId: 42, postId: 100, limit: 5 });
135
+ });
136
+
137
+ test('should support manual response injection using @Res()', async () => {
138
+ const res = await fetch(`${baseUrl}/users/manual`);
139
+ expect(res.status).toBe(202);
140
+ const body = await res.json();
141
+ expect(body).toEqual({ manual: true });
142
+ });
143
+
144
+ test('should return correct HTTP error response when HttpException thrown', async () => {
145
+ const res = await fetch(`${baseUrl}/users/error-test`);
146
+ expect(res.status).toBe(400);
147
+ const body = await res.json();
148
+ expect(body).toEqual({ statusCode: 400, message: 'Invalid request parameter' });
149
+ });
150
+
151
+ test('should return 404 for non-existent route', async () => {
152
+ const res = await fetch(`${baseUrl}/users/nonexistent/route/path`);
153
+ expect(res.status).toBe(404);
154
+ });
155
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "experimentalDecorators": true,
11
+ "emitDecoratorMetadata": true,
12
+ "types": ["bun-types"]
13
+ },
14
+ "include": ["src/**/*", "tests/**/*", "benchmarks/**/*"]
15
+ }