@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,47 @@
1
+ export class HttpException extends Error {
2
+ constructor(
3
+ public readonly response: string | object,
4
+ public readonly status: number
5
+ ) {
6
+ super(typeof response === 'string' ? response : JSON.stringify(response));
7
+ this.name = 'HttpException';
8
+ }
9
+
10
+ getResponse() {
11
+ return this.response;
12
+ }
13
+
14
+ getStatus() {
15
+ return this.status;
16
+ }
17
+ }
18
+
19
+ export class BadRequestException extends HttpException {
20
+ constructor(message: string | object = 'Bad Request') {
21
+ super(message, 400);
22
+ }
23
+ }
24
+
25
+ export class UnauthorizedException extends HttpException {
26
+ constructor(message: string | object = 'Unauthorized') {
27
+ super(message, 401);
28
+ }
29
+ }
30
+
31
+ export class ForbiddenException extends HttpException {
32
+ constructor(message: string | object = 'Forbidden') {
33
+ super(message, 403);
34
+ }
35
+ }
36
+
37
+ export class NotFoundException extends HttpException {
38
+ constructor(message: string | object = 'Not Found') {
39
+ super(message, 404);
40
+ }
41
+ }
42
+
43
+ export class InternalServerErrorException extends HttpException {
44
+ constructor(message: string | object = 'Internal Server Error') {
45
+ super(message, 500);
46
+ }
47
+ }
@@ -0,0 +1,8 @@
1
+ import { calyxApplication } from './application.ts';
2
+
3
+ export class calyxFactory {
4
+ static async create(rootModule: any): Promise<calyxApplication> {
5
+ const app = new calyxApplication(rootModule);
6
+ return app;
7
+ }
8
+ }
@@ -0,0 +1,5 @@
1
+ export * from './decorators.ts';
2
+ export * from './router.ts';
3
+ export * from './exceptions.ts';
4
+ export * from './application.ts';
5
+ export * from './factory.ts';
@@ -0,0 +1,97 @@
1
+ export interface RouteMatch<T> {
2
+ handler: T;
3
+ params: Record<string, string>;
4
+ }
5
+
6
+ class RouterNode<T> {
7
+ children = new Map<string, RouterNode<T>>();
8
+ paramChild: RouterNode<T> | null = null;
9
+ paramName: string | null = null;
10
+ wildcardChild: RouterNode<T> | null = null;
11
+ handlers = new Map<string, T>();
12
+ }
13
+
14
+ export class RadixRouter<T> {
15
+ private root = new RouterNode<T>();
16
+
17
+ insert(method: string, path: string, handler: T) {
18
+ const segments = path.split('/').filter(Boolean);
19
+ let node = this.root;
20
+
21
+ for (const segment of segments) {
22
+ if (segment.startsWith(':')) {
23
+ const paramName = segment.slice(1);
24
+ if (!node.paramChild) {
25
+ node.paramChild = new RouterNode<T>();
26
+ node.paramName = paramName;
27
+ }
28
+ node = node.paramChild;
29
+ } else if (segment === '*') {
30
+ if (!node.wildcardChild) {
31
+ node.wildcardChild = new RouterNode<T>();
32
+ }
33
+ node = node.wildcardChild;
34
+ break; // Wildcard matches everything rest and is terminal
35
+ } else {
36
+ let child = node.children.get(segment);
37
+ if (!child) {
38
+ child = new RouterNode<T>();
39
+ node.children.set(segment, child);
40
+ }
41
+ node = child;
42
+ }
43
+ }
44
+
45
+ node.handlers.set(method.toUpperCase(), handler);
46
+ }
47
+
48
+ match(method: string, path: string): RouteMatch<T> | null {
49
+ const segments = path.split('/').filter(Boolean);
50
+ const params: Record<string, string> = {};
51
+ const matchNode = this.matchSegment(this.root, segments, 0, params);
52
+
53
+ if (!matchNode) return null;
54
+
55
+ const handler = matchNode.handlers.get(method.toUpperCase()) ?? matchNode.handlers.get('ALL');
56
+ if (!handler) return null;
57
+
58
+ return { handler, params };
59
+ }
60
+
61
+ private matchSegment(
62
+ node: RouterNode<T>,
63
+ segments: string[],
64
+ index: number,
65
+ params: Record<string, string>
66
+ ): RouterNode<T> | null {
67
+ if (index === segments.length) {
68
+ // If we've reached the end of segments, the current node must have handlers
69
+ return node.handlers.size > 0 ? node : null;
70
+ }
71
+
72
+ const segment = segments[index];
73
+
74
+ // 1. Try static match
75
+ const staticChild = node.children.get(segment);
76
+ if (staticChild) {
77
+ const match = this.matchSegment(staticChild, segments, index + 1, params);
78
+ if (match) return match;
79
+ }
80
+
81
+ // 2. Try parameter match
82
+ if (node.paramChild && node.paramName) {
83
+ params[node.paramName] = segment;
84
+ const match = this.matchSegment(node.paramChild, segments, index + 1, params);
85
+ if (match) return match;
86
+ delete params[node.paramName]; // backtrack
87
+ }
88
+
89
+ // 3. Try wildcard match
90
+ if (node.wildcardChild) {
91
+ params['*'] = segments.slice(index).join('/');
92
+ return node.wildcardChild;
93
+ }
94
+
95
+ return null;
96
+ }
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ import 'reflect-metadata';
2
+ export * from './core/index.ts';
3
+ export * from './http/index.ts';
4
+ export * from './lifecycle/index.ts';
@@ -0,0 +1,41 @@
1
+ import { ArgumentsHost, HttpArgumentsHost, ExecutionContext } from './interfaces.ts';
2
+ import { Type } from '../core/metadata.ts';
3
+
4
+ export class calyxArgumentsHost implements ArgumentsHost {
5
+ constructor(private readonly req: Request, private readonly res: any) {}
6
+
7
+ getArgs<T extends any[] = any[]>(): T {
8
+ return [this.req, this.res] as unknown as T;
9
+ }
10
+
11
+ getArgByIndex<T = any>(index: number): T {
12
+ return this.getArgs()[index];
13
+ }
14
+
15
+ switchToHttp(): HttpArgumentsHost {
16
+ return {
17
+ getRequest: <T = any>() => this.req as unknown as T,
18
+ getResponse: <T = any>() => this.res as unknown as T,
19
+ getNext: <T = any>() => (() => {}) as unknown as T,
20
+ };
21
+ }
22
+ }
23
+
24
+ export class calyxExecutionContext extends calyxArgumentsHost implements ExecutionContext {
25
+ constructor(
26
+ req: Request,
27
+ res: any,
28
+ private readonly targetClass: Type<any>,
29
+ private readonly handlerMethod: Function
30
+ ) {
31
+ super(req, res);
32
+ }
33
+
34
+ getClass<T = any>(): Type<T> {
35
+ return this.targetClass as unknown as Type<T>;
36
+ }
37
+
38
+ getHandler(): Function {
39
+ return this.handlerMethod;
40
+ }
41
+ }
@@ -0,0 +1,37 @@
1
+ import 'reflect-metadata';
2
+ import { METADATA_KEYS } from '../core/metadata.ts';
3
+ import { CanActivate, NestInterceptor, PipeTransform, ExceptionFilter } from './interfaces.ts';
4
+
5
+ function createClassOrMethodDecorator(key: string, values: any[]): any {
6
+ return (target: any, propertyKey?: string | symbol) => {
7
+ if (propertyKey) {
8
+ const existing = Reflect.getOwnMetadata(key, target, propertyKey) || [];
9
+ Reflect.defineMetadata(key, [...existing, ...values], target, propertyKey);
10
+ } else {
11
+ const existing = Reflect.getOwnMetadata(key, target) || [];
12
+ Reflect.defineMetadata(key, [...existing, ...values], target);
13
+ }
14
+ };
15
+ }
16
+
17
+ export function UseGuards(...guards: any[]) {
18
+ return createClassOrMethodDecorator(METADATA_KEYS.GUARDS, guards);
19
+ }
20
+
21
+ export function UseInterceptors(...interceptors: any[]) {
22
+ return createClassOrMethodDecorator(METADATA_KEYS.INTERCEPTORS, interceptors);
23
+ }
24
+
25
+ export function UsePipes(...pipes: any[]) {
26
+ return createClassOrMethodDecorator(METADATA_KEYS.PIPES, pipes);
27
+ }
28
+
29
+ export function UseFilters(...filters: any[]) {
30
+ return createClassOrMethodDecorator(METADATA_KEYS.FILTERS, filters);
31
+ }
32
+
33
+ export function Catch(...exceptions: any[]): ClassDecorator {
34
+ return (target) => {
35
+ Reflect.defineMetadata(METADATA_KEYS.CATCH, exceptions, target);
36
+ };
37
+ }
@@ -0,0 +1,3 @@
1
+ export * from './interfaces.ts';
2
+ export * from './context.ts';
3
+ export * from './decorators.ts';
@@ -0,0 +1,49 @@
1
+ import { Type } from '../core/metadata.ts';
2
+
3
+ export interface ArgumentsHost {
4
+ getArgs<T extends any[] = any[]>(): T;
5
+ getArgByIndex<T = any>(index: number): T;
6
+ switchToHttp(): HttpArgumentsHost;
7
+ }
8
+
9
+ export interface HttpArgumentsHost {
10
+ getRequest<T = any>(): T;
11
+ getResponse<T = any>(): T;
12
+ getNext<T = any>(): T;
13
+ }
14
+
15
+ export interface ExecutionContext extends ArgumentsHost {
16
+ getClass<T = any>(): Type<T>;
17
+ getHandler(): Function;
18
+ }
19
+
20
+ export interface CanActivate {
21
+ canActivate(
22
+ context: ExecutionContext
23
+ ): boolean | Promise<boolean>;
24
+ }
25
+
26
+ export interface CallHandler<T = any> {
27
+ handle(): Promise<T>;
28
+ }
29
+
30
+ export interface NestInterceptor<T = any, R = any> {
31
+ intercept(
32
+ context: ExecutionContext,
33
+ next: CallHandler<T>
34
+ ): R | Promise<R>;
35
+ }
36
+
37
+ export interface ArgumentMetadata {
38
+ readonly type: 'body' | 'query' | 'param' | 'custom';
39
+ readonly metatype?: Type<any> | undefined;
40
+ readonly data?: string | undefined;
41
+ }
42
+
43
+ export interface PipeTransform<T = any, R = any> {
44
+ transform(value: T, metadata: ArgumentMetadata): R | Promise<R>;
45
+ }
46
+
47
+ export interface ExceptionFilter<T = any> {
48
+ catch(exception: T, host: ArgumentsHost): any;
49
+ }
@@ -0,0 +1,283 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ Module,
4
+ Injectable,
5
+ Inject,
6
+ Optional,
7
+ Global,
8
+ forwardRef,
9
+ calyxContainer,
10
+ ModuleRef,
11
+ } from '../src/index.ts';
12
+
13
+ describe('Dependency Injection System', () => {
14
+ test('should resolve basic singleton provider', () => {
15
+ @Injectable()
16
+ class DependencyService {
17
+ getValue() {
18
+ return 'dependency';
19
+ }
20
+ }
21
+
22
+ @Injectable()
23
+ class TargetService {
24
+ constructor(public dep: DependencyService) {}
25
+ }
26
+
27
+ @Module({
28
+ providers: [DependencyService, TargetService],
29
+ })
30
+ class RootModule {}
31
+
32
+ const container = new calyxContainer();
33
+ container.bootstrap(RootModule);
34
+
35
+ const target = container.getGlobalOrAnyInstance(TargetService);
36
+ expect(target).toBeInstanceOf(TargetService);
37
+ expect(target.dep).toBeInstanceOf(DependencyService);
38
+ expect(target.dep.getValue()).toBe('dependency');
39
+ });
40
+
41
+ test('should resolve custom useValue provider', () => {
42
+ const CONFIG_TOKEN = 'CONFIG';
43
+
44
+ @Injectable()
45
+ class ConfigConsumer {
46
+ constructor(@Inject(CONFIG_TOKEN) public config: { port: number }) {}
47
+ }
48
+
49
+ @Module({
50
+ providers: [
51
+ ConfigConsumer,
52
+ {
53
+ provide: CONFIG_TOKEN,
54
+ useValue: { port: 3000 },
55
+ },
56
+ ],
57
+ })
58
+ class RootModule {}
59
+
60
+ const container = new calyxContainer();
61
+ container.bootstrap(RootModule);
62
+
63
+ const consumer = container.getGlobalOrAnyInstance(ConfigConsumer);
64
+ expect(consumer.config).toEqual({ port: 3000 });
65
+ });
66
+
67
+ test('should resolve custom useClass provider', () => {
68
+ const BASE_SERVICE_TOKEN = 'BASE_SERVICE';
69
+
70
+ interface BaseService {
71
+ hello(): string;
72
+ }
73
+
74
+ @Injectable()
75
+ class ImplementationService implements BaseService {
76
+ hello() {
77
+ return 'hello from implementation';
78
+ }
79
+ }
80
+
81
+ @Injectable()
82
+ class Consumer {
83
+ constructor(@Inject(BASE_SERVICE_TOKEN) public service: BaseService) {}
84
+ }
85
+
86
+ @Module({
87
+ providers: [
88
+ Consumer,
89
+ {
90
+ provide: BASE_SERVICE_TOKEN,
91
+ useClass: ImplementationService,
92
+ },
93
+ ],
94
+ })
95
+ class RootModule {}
96
+
97
+ const container = new calyxContainer();
98
+ container.bootstrap(RootModule);
99
+
100
+ const consumer = container.getGlobalOrAnyInstance(Consumer);
101
+ expect(consumer.service).toBeInstanceOf(ImplementationService);
102
+ expect(consumer.service.hello()).toBe('hello from implementation');
103
+ });
104
+
105
+ test('should resolve custom useFactory provider with injection', () => {
106
+ const FACTORY_TOKEN = 'FACTORY_RESULT';
107
+ const DEP_TOKEN = 'DEP_VALUE';
108
+
109
+ @Injectable()
110
+ class Consumer {
111
+ constructor(@Inject(FACTORY_TOKEN) public value: string) {}
112
+ }
113
+
114
+ @Module({
115
+ providers: [
116
+ Consumer,
117
+ {
118
+ provide: DEP_TOKEN,
119
+ useValue: 'Hello',
120
+ },
121
+ {
122
+ provide: FACTORY_TOKEN,
123
+ useFactory: (dep: string) => `${dep} Factory`,
124
+ inject: [DEP_TOKEN],
125
+ },
126
+ ],
127
+ })
128
+ class RootModule {}
129
+
130
+ const container = new calyxContainer();
131
+ container.bootstrap(RootModule);
132
+
133
+ const consumer = container.getGlobalOrAnyInstance(Consumer);
134
+ expect(consumer.value).toBe('Hello Factory');
135
+ });
136
+
137
+ test('should resolve optional parameter to undefined if missing', () => {
138
+ @Injectable()
139
+ class Consumer {
140
+ constructor(@Optional() @Inject('MISSING_TOKEN') public optionalVal: any) {}
141
+ }
142
+
143
+ @Module({
144
+ providers: [Consumer],
145
+ })
146
+ class RootModule {}
147
+
148
+ const container = new calyxContainer();
149
+ container.bootstrap(RootModule);
150
+
151
+ const consumer = container.getGlobalOrAnyInstance(Consumer);
152
+ expect(consumer.optionalVal).toBeUndefined();
153
+ });
154
+
155
+ test('should throw error when missing non-optional dependency', () => {
156
+ @Injectable()
157
+ class Consumer {
158
+ constructor(@Inject('MISSING_TOKEN') public val: any) {}
159
+ }
160
+
161
+ @Module({
162
+ providers: [Consumer],
163
+ })
164
+ class RootModule {}
165
+
166
+ const container = new calyxContainer();
167
+ expect(() => container.bootstrap(RootModule)).toThrow(/Cannot resolve dependency/);
168
+ });
169
+
170
+ test('should detect circular dependencies and throw error', () => {
171
+ @Injectable()
172
+ class ServiceA {
173
+ constructor(@Inject(forwardRef(() => ServiceB)) public b: any) {}
174
+ }
175
+
176
+ @Injectable()
177
+ class ServiceB {
178
+ constructor(@Inject(forwardRef(() => ServiceA)) public a: any) {}
179
+ }
180
+
181
+ @Module({
182
+ providers: [ServiceA, ServiceB],
183
+ })
184
+ class RootModule {}
185
+
186
+ const container = new calyxContainer();
187
+ expect(() => container.bootstrap(RootModule)).toThrow(/Circular dependency detected/);
188
+ });
189
+
190
+ test('should isolate module scopes and resolve exported providers', () => {
191
+ @Injectable()
192
+ class SharedService {
193
+ id = Math.random();
194
+ }
195
+
196
+ @Module({
197
+ providers: [SharedService],
198
+ exports: [SharedService],
199
+ })
200
+ class LibModule {}
201
+
202
+ @Injectable()
203
+ class AppService {
204
+ constructor(public shared: SharedService) {}
205
+ }
206
+
207
+ @Module({
208
+ imports: [LibModule],
209
+ providers: [AppService],
210
+ })
211
+ class AppModule {}
212
+
213
+ const container = new calyxContainer();
214
+ container.bootstrap(AppModule);
215
+
216
+ const appService = container.getGlobalOrAnyInstance(AppService);
217
+ expect(appService.shared).toBeInstanceOf(SharedService);
218
+ });
219
+
220
+ test('should make global module providers available everywhere', () => {
221
+ @Injectable()
222
+ class GlobalService {
223
+ getValue() {
224
+ return 'global';
225
+ }
226
+ }
227
+
228
+ @Global()
229
+ @Module({
230
+ providers: [GlobalService],
231
+ exports: [GlobalService],
232
+ })
233
+ class GlobalLibModule {}
234
+
235
+ @Injectable()
236
+ class AppService {
237
+ constructor(public glob: GlobalService) {}
238
+ }
239
+
240
+ @Module({
241
+ imports: [GlobalLibModule], // If it is global, importing it registers it globally
242
+ providers: [AppService],
243
+ })
244
+ class AppModule {}
245
+
246
+ const container = new calyxContainer();
247
+ container.bootstrap(AppModule);
248
+
249
+ const appService = container.getGlobalOrAnyInstance(AppService);
250
+ expect(appService.glob.getValue()).toBe('global');
251
+ });
252
+
253
+ test('should support ModuleRef dynamic resolution', () => {
254
+ @Injectable()
255
+ class DummyService {
256
+ sayHi() {
257
+ return 'hi';
258
+ }
259
+ }
260
+
261
+ @Injectable()
262
+ class ConsumerService {
263
+ constructor(public moduleRef: ModuleRef) {}
264
+
265
+ getDummy() {
266
+ return this.moduleRef.get(DummyService);
267
+ }
268
+ }
269
+
270
+ @Module({
271
+ providers: [DummyService, ConsumerService],
272
+ })
273
+ class RootModule {}
274
+
275
+ const container = new calyxContainer();
276
+ container.bootstrap(RootModule);
277
+
278
+ const consumer = container.getGlobalOrAnyInstance(ConsumerService);
279
+ expect(consumer.moduleRef).toBeInstanceOf(ModuleRef);
280
+ expect(consumer.getDummy()).toBeInstanceOf(DummyService);
281
+ expect(consumer.getDummy().sayHi()).toBe('hi');
282
+ });
283
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, test, expect } from 'bun:test';
2
+ import {
3
+ Module,
4
+ Injectable,
5
+ Inject,
6
+ calyxContainer,
7
+ DynamicModule,
8
+ } from '../src/index.ts';
9
+
10
+ @Injectable()
11
+ class ConfigService {
12
+ constructor(@Inject('CONFIG_OPTIONS') public options: { dbHost: string }) {}
13
+ }
14
+
15
+ @Module({})
16
+ class ConfigModule {
17
+ static register(options: { dbHost: string }): DynamicModule {
18
+ return {
19
+ module: ConfigModule,
20
+ providers: [
21
+ {
22
+ provide: 'CONFIG_OPTIONS',
23
+ useValue: options,
24
+ },
25
+ ConfigService,
26
+ ],
27
+ exports: [ConfigService],
28
+ };
29
+ }
30
+ }
31
+
32
+ @Injectable()
33
+ class AppService {
34
+ constructor(public config: ConfigService) {}
35
+ }
36
+
37
+ @Module({
38
+ imports: [ConfigModule.register({ dbHost: 'localhost:5432' })],
39
+ providers: [AppService],
40
+ })
41
+ class AppModule {}
42
+
43
+ describe('Dynamic Modules', () => {
44
+ test('should resolve providers defined dynamically in a DynamicModule', () => {
45
+ const container = new calyxContainer();
46
+ container.bootstrap(AppModule);
47
+
48
+ const appService = container.getGlobalOrAnyInstance(AppService);
49
+ expect(appService).toBeInstanceOf(AppService);
50
+ expect(appService.config).toBeInstanceOf(ConfigService);
51
+ expect(appService.config.options).toEqual({ dbHost: 'localhost:5432' });
52
+ });
53
+ });