@martel/calyx 1.7.0 → 1.9.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +71 -27
  3. package/benchmarks/graphql-benchmark.ts +81 -0
  4. package/benchmarks/index.ts +32 -0
  5. package/benchmarks/openapi-benchmark.ts +168 -0
  6. package/benchmarks/serialization-benchmark.ts +52 -0
  7. package/benchmarks/techniques-benchmark.ts +84 -0
  8. package/benchmarks/validation-benchmark.ts +74 -0
  9. package/bun.lock +14 -0
  10. package/package.json +8 -6
  11. package/src/cli/index.ts +19 -3
  12. package/src/compression/compression.middleware.ts +7 -0
  13. package/src/cookies/cookies.ts +69 -0
  14. package/src/database/mongoose.module.ts +250 -0
  15. package/src/database/typeorm.module.ts +276 -0
  16. package/src/file-upload/file-upload.interceptor.ts +93 -0
  17. package/src/file-upload/index.ts +1 -0
  18. package/src/graphql/decorators.ts +132 -0
  19. package/src/graphql/graphql.module.ts +316 -0
  20. package/src/graphql/index.ts +2 -0
  21. package/src/http/application.ts +380 -70
  22. package/src/http/factory.ts +1 -0
  23. package/src/http/router.ts +13 -0
  24. package/src/http-client/http-client.module.ts +124 -0
  25. package/src/http-client/index.ts +1 -0
  26. package/src/index.ts +15 -0
  27. package/src/logger/index.ts +1 -0
  28. package/src/logger/logger.service.ts +118 -0
  29. package/src/mvc/index.ts +1 -0
  30. package/src/mvc/mvc.ts +22 -0
  31. package/src/openapi/decorators.ts +203 -0
  32. package/src/openapi/index.ts +2 -0
  33. package/src/openapi/swagger.module.ts +326 -0
  34. package/src/queue/queue.module.ts +174 -0
  35. package/src/session/index.ts +1 -0
  36. package/src/session/session.middleware.ts +82 -0
  37. package/src/sse/index.ts +1 -0
  38. package/src/sse/sse.ts +18 -0
  39. package/src/streaming/index.ts +1 -0
  40. package/src/streaming/streamable-file.ts +32 -0
  41. package/src/validation/pipe.ts +79 -10
  42. package/src/versioning/versioning.ts +46 -0
  43. package/tests/graphql.test.ts +176 -0
  44. package/tests/openapi.test.ts +162 -0
  45. package/tests/techniques.test.ts +471 -0
@@ -0,0 +1,124 @@
1
+ import { Observable } from 'rxjs';
2
+ import { Module, DynamicModule, Injectable } from '../core/decorators.ts';
3
+
4
+ export interface AxiosResponse<T = any> {
5
+ data: T;
6
+ status: number;
7
+ statusText: string;
8
+ headers: any;
9
+ config: any;
10
+ request?: any;
11
+ }
12
+
13
+ @Injectable()
14
+ export class HttpService {
15
+ request<T = any>(config: any): Observable<AxiosResponse<T>> {
16
+ return new Observable((subscriber) => {
17
+ const controller = new AbortController();
18
+ const { url, method = 'GET', data, headers, ...rest } = config;
19
+
20
+ const options: RequestInit = {
21
+ method,
22
+ headers,
23
+ signal: controller.signal,
24
+ ...rest,
25
+ };
26
+
27
+ if (data !== undefined) {
28
+ if (typeof data === 'object') {
29
+ options.body = JSON.stringify(data);
30
+ options.headers = { 'content-type': 'application/json', ...options.headers };
31
+ } else {
32
+ options.body = data;
33
+ }
34
+ }
35
+
36
+ fetch(url, options)
37
+ .then(async (res) => {
38
+ let responseData: any;
39
+ const contentType = res.headers.get('content-type') || '';
40
+ if (contentType.includes('application/json')) {
41
+ try {
42
+ responseData = await res.json();
43
+ } catch {
44
+ responseData = {};
45
+ }
46
+ } else {
47
+ try {
48
+ responseData = await res.text();
49
+ } catch {
50
+ responseData = '';
51
+ }
52
+ }
53
+
54
+ const headersObj: Record<string, string> = {};
55
+ res.headers.forEach((val, key) => {
56
+ headersObj[key] = val;
57
+ });
58
+
59
+ subscriber.next({
60
+ data: responseData,
61
+ status: res.status,
62
+ statusText: res.statusText,
63
+ headers: headersObj,
64
+ config,
65
+ });
66
+ subscriber.complete();
67
+ })
68
+ .catch((err) => {
69
+ subscriber.error(err);
70
+ });
71
+
72
+ return () => {
73
+ controller.abort();
74
+ };
75
+ });
76
+ }
77
+
78
+ get<T = any>(url: string, config?: any): Observable<AxiosResponse<T>> {
79
+ return this.request({ ...config, url, method: 'GET' });
80
+ }
81
+
82
+ post<T = any>(url: string, data?: any, config?: any): Observable<AxiosResponse<T>> {
83
+ return this.request({ ...config, url, data, method: 'POST' });
84
+ }
85
+
86
+ put<T = any>(url: string, data?: any, config?: any): Observable<AxiosResponse<T>> {
87
+ return this.request({ ...config, url, data, method: 'PUT' });
88
+ }
89
+
90
+ delete<T = any>(url: string, config?: any): Observable<AxiosResponse<T>> {
91
+ return this.request({ ...config, url, method: 'DELETE' });
92
+ }
93
+
94
+ patch<T = any>(url: string, data?: any, config?: any): Observable<AxiosResponse<T>> {
95
+ return this.request({ ...config, url, data, method: 'PATCH' });
96
+ }
97
+
98
+ head<T = any>(url: string, config?: any): Observable<AxiosResponse<T>> {
99
+ return this.request({ ...config, url, method: 'HEAD' });
100
+ }
101
+
102
+ options<T = any>(url: string, config?: any): Observable<AxiosResponse<T>> {
103
+ return this.request({ ...config, url, method: 'OPTIONS' });
104
+ }
105
+ }
106
+
107
+ @Module({
108
+ providers: [HttpService],
109
+ exports: [HttpService],
110
+ })
111
+ export class HttpModule {
112
+ static register(options: any = {}): DynamicModule {
113
+ return {
114
+ module: HttpModule,
115
+ providers: [
116
+ {
117
+ provide: HttpService,
118
+ useValue: new HttpService(),
119
+ },
120
+ ],
121
+ exports: [HttpService],
122
+ };
123
+ }
124
+ }
@@ -0,0 +1 @@
1
+ export * from './http-client.module.ts';
package/src/index.ts CHANGED
@@ -10,3 +10,18 @@ export * from './websockets/index.ts';
10
10
  export * from './microservices/index.ts';
11
11
  export * from './cache/index.ts';
12
12
  export * from './validation/index.ts';
13
+ export * from './openapi/index.ts';
14
+ export * from './database/typeorm.module.ts';
15
+ export * from './database/mongoose.module.ts';
16
+ export * from './versioning/versioning.ts';
17
+ export * from './queue/queue.module.ts';
18
+ export * from './logger/index.ts';
19
+ export * from './cookies/cookies.ts';
20
+ export * from './compression/compression.middleware.ts';
21
+ export * from './file-upload/index.ts';
22
+ export * from './streaming/index.ts';
23
+ export * from './http-client/index.ts';
24
+ export * from './session/index.ts';
25
+ export * from './mvc/index.ts';
26
+ export * from './sse/index.ts';
27
+
@@ -0,0 +1 @@
1
+ export * from './logger.service.ts';
@@ -0,0 +1,118 @@
1
+ import { Injectable } from '../core/decorators.ts';
2
+
3
+ export interface LoggerService {
4
+ log(message: any, context?: string): any;
5
+ error(message: any, trace?: string, context?: string): any;
6
+ warn(message: any, context?: string): any;
7
+ debug?(message: any, context?: string): any;
8
+ verbose?(message: any, context?: string): any;
9
+ }
10
+
11
+ export type LogLevel = 'log' | 'error' | 'warn' | 'debug' | 'verbose';
12
+
13
+ @Injectable()
14
+ export class Logger implements LoggerService {
15
+ private static instance: LoggerService | null = null;
16
+ private static levels: Set<LogLevel> = new Set(['log', 'error', 'warn']);
17
+
18
+ constructor(private context?: string) {}
19
+
20
+ static overrideLogger(logger: LoggerService | null) {
21
+ this.instance = logger;
22
+ }
23
+
24
+ static setLogLevels(levels: LogLevel[]) {
25
+ this.levels = new Set(levels);
26
+ }
27
+
28
+ log(message: any, context?: string) {
29
+ const activeContext = context ?? this.context;
30
+ if (Logger.instance) {
31
+ Logger.instance.log(message, activeContext);
32
+ return;
33
+ }
34
+ if (!Logger.levels.has('log')) return;
35
+ this.print('LOG', message, activeContext);
36
+ }
37
+
38
+ error(message: any, trace?: string, context?: string) {
39
+ const activeContext = context ?? this.context;
40
+ if (Logger.instance) {
41
+ Logger.instance.error(message, trace, activeContext);
42
+ return;
43
+ }
44
+ if (!Logger.levels.has('error')) return;
45
+ this.print('ERROR', message, activeContext, trace);
46
+ }
47
+
48
+ warn(message: any, context?: string) {
49
+ const activeContext = context ?? this.context;
50
+ if (Logger.instance) {
51
+ Logger.instance.warn(message, activeContext);
52
+ return;
53
+ }
54
+ if (!Logger.levels.has('warn')) return;
55
+ this.print('WARN', message, activeContext);
56
+ }
57
+
58
+ debug(message: any, context?: string) {
59
+ const activeContext = context ?? this.context;
60
+ if (Logger.instance) {
61
+ Logger.instance.debug?.(message, activeContext);
62
+ return;
63
+ }
64
+ if (!Logger.levels.has('debug')) return;
65
+ this.print('DEBUG', message, activeContext);
66
+ }
67
+
68
+ verbose(message: any, context?: string) {
69
+ const activeContext = context ?? this.context;
70
+ if (Logger.instance) {
71
+ Logger.instance.verbose?.(message, activeContext);
72
+ return;
73
+ }
74
+ if (!Logger.levels.has('verbose')) return;
75
+ this.print('VERBOSE', message, activeContext);
76
+ }
77
+
78
+ // Static methods
79
+ static log(message: any, context?: string) {
80
+ new Logger(context).log(message);
81
+ }
82
+
83
+ static error(message: any, trace?: string, context?: string) {
84
+ new Logger(context).error(message, trace);
85
+ }
86
+
87
+ static warn(message: any, context?: string) {
88
+ new Logger(context).warn(message);
89
+ }
90
+
91
+ static debug(message: any, context?: string) {
92
+ new Logger(context).debug(message);
93
+ }
94
+
95
+ static verbose(message: any, context?: string) {
96
+ new Logger(context).verbose(message);
97
+ }
98
+
99
+ private print(level: string, message: any, context?: string, trace?: string) {
100
+ const timestamp = new Date().toISOString();
101
+ const formattedContext = context ? ` [${context}]` : '';
102
+ const formattedTrace = trace ? `\n${trace}` : '';
103
+ const msgStr = typeof message === 'object' ? JSON.stringify(message) : String(message);
104
+
105
+ // Using process.stdout.write or console.log/error
106
+ if (level === 'ERROR') {
107
+ console.error(`${timestamp} - \x1b[31m${level}\x1b[0m:${formattedContext} ${msgStr}${formattedTrace}`);
108
+ } else if (level === 'WARN') {
109
+ console.warn(`${timestamp} - \x1b[33m${level}\x1b[0m:${formattedContext} ${msgStr}`);
110
+ } else if (level === 'DEBUG') {
111
+ console.log(`${timestamp} - \x1b[35m${level}\x1b[0m:${formattedContext} ${msgStr}`);
112
+ } else if (level === 'VERBOSE') {
113
+ console.log(`${timestamp} - \x1b[36m${level}\x1b[0m:${formattedContext} ${msgStr}`);
114
+ } else {
115
+ console.log(`${timestamp} - \x1b[32m${level}\x1b[0m:${formattedContext} ${msgStr}`);
116
+ }
117
+ }
118
+ }
@@ -0,0 +1 @@
1
+ export * from './mvc.ts';
package/src/mvc/mvc.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { SetMetadata } from '../core/decorators.ts';
2
+
3
+ export function Render(template: string): MethodDecorator {
4
+ return SetMetadata('calyx:render_template', template);
5
+ }
6
+
7
+ export type ViewEngine = (templatePath: string, data: any) => string | Promise<string>;
8
+
9
+ export async function defaultRenderEngine(templatePath: string, data: any): Promise<string> {
10
+ const file = Bun.file(templatePath);
11
+ if (!(await file.exists())) {
12
+ throw new Error(`Template not found at path: ${templatePath}`);
13
+ }
14
+ let content = await file.text();
15
+ if (data && typeof data === 'object') {
16
+ for (const [key, val] of Object.entries(data)) {
17
+ const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
18
+ content = content.replace(regex, String(val));
19
+ }
20
+ }
21
+ return content;
22
+ }
@@ -0,0 +1,203 @@
1
+ import 'reflect-metadata';
2
+
3
+ export function ApiTags(...tags: string[]): ClassDecorator & MethodDecorator {
4
+ return (target: any, propertyKey?: string | symbol) => {
5
+ const key = 'calyx:api_tags';
6
+ if (propertyKey) {
7
+ Reflect.defineMetadata(key, tags, target, propertyKey);
8
+ } else {
9
+ Reflect.defineMetadata(key, tags, target);
10
+ }
11
+ };
12
+ }
13
+
14
+ export function ApiOperation(options: { summary?: string; description?: string }): MethodDecorator {
15
+ return (target, propertyKey) => {
16
+ Reflect.defineMetadata('calyx:api_operation', options, target, propertyKey);
17
+ };
18
+ }
19
+
20
+ export function ApiResponse(options: {
21
+ status: number;
22
+ description: string;
23
+ type?: any;
24
+ }): MethodDecorator & ClassDecorator {
25
+ return (target: any, propertyKey?: string | symbol) => {
26
+ const key = 'calyx:api_responses';
27
+ if (propertyKey) {
28
+ const existing = Reflect.getOwnMetadata(key, target, propertyKey) || [];
29
+ existing.push(options);
30
+ Reflect.defineMetadata(key, existing, target, propertyKey);
31
+ } else {
32
+ const existing = Reflect.getOwnMetadata(key, target) || [];
33
+ existing.push(options);
34
+ Reflect.defineMetadata(key, existing, target);
35
+ }
36
+ };
37
+ }
38
+
39
+ export function ApiProperty(options: {
40
+ description?: string;
41
+ type?: any;
42
+ required?: boolean;
43
+ } = {}): PropertyDecorator {
44
+ return (target, propertyKey) => {
45
+ const properties = Reflect.getOwnMetadata('calyx:api_properties', target.constructor) || [];
46
+ properties.push({ propertyKey: String(propertyKey), ...options });
47
+ Reflect.defineMetadata('calyx:api_properties', properties, target.constructor);
48
+ };
49
+ }
50
+
51
+ export function ApiBody(options: { type?: any; required?: boolean; description?: string; schema?: any }): MethodDecorator {
52
+ return (target, propertyKey) => {
53
+ Reflect.defineMetadata('calyx:api_body', options, target, propertyKey);
54
+ };
55
+ }
56
+
57
+ export function ApiQuery(options: { name: string; type?: any; required?: boolean; description?: string }): MethodDecorator {
58
+ return (target, propertyKey) => {
59
+ const queries = Reflect.getOwnMetadata('calyx:api_queries', target, propertyKey) || [];
60
+ queries.push(options);
61
+ Reflect.defineMetadata('calyx:api_queries', queries, target, propertyKey);
62
+ };
63
+ }
64
+
65
+ export function ApiParam(options: { name: string; type?: any; required?: boolean; description?: string }): MethodDecorator {
66
+ return (target, propertyKey) => {
67
+ const params = Reflect.getOwnMetadata('calyx:api_params', target, propertyKey) || [];
68
+ params.push(options);
69
+ Reflect.defineMetadata('calyx:api_params', params, target, propertyKey);
70
+ };
71
+ }
72
+
73
+ export function ApiHeader(options: { name: string; description?: string; required?: boolean }): MethodDecorator {
74
+ return (target, propertyKey) => {
75
+ const headers = Reflect.getOwnMetadata('calyx:api_headers', target, propertyKey) || [];
76
+ headers.push(options);
77
+ Reflect.defineMetadata('calyx:api_headers', headers, target, propertyKey);
78
+ };
79
+ }
80
+
81
+ export function ApiExtraModels(...models: any[]): ClassDecorator {
82
+ return (target) => {
83
+ Reflect.defineMetadata('calyx:api_extra_models', models, target);
84
+ };
85
+ }
86
+
87
+ export function ApiBearerAuth(name = 'bearer'): ClassDecorator & MethodDecorator {
88
+ return (target: any, propertyKey?: string | symbol) => {
89
+ const key = 'calyx:api_security';
90
+ const securityReq = { [name]: [] };
91
+ if (propertyKey) {
92
+ const existing = Reflect.getOwnMetadata(key, target, propertyKey) || [];
93
+ existing.push(securityReq);
94
+ Reflect.defineMetadata(key, existing, target, propertyKey);
95
+ } else {
96
+ const existing = Reflect.getOwnMetadata(key, target) || [];
97
+ existing.push(securityReq);
98
+ Reflect.defineMetadata(key, existing, target);
99
+ }
100
+ };
101
+ }
102
+
103
+ export function ApiBasicAuth(name = 'basic'): ClassDecorator & MethodDecorator {
104
+ return (target: any, propertyKey?: string | symbol) => {
105
+ const key = 'calyx:api_security';
106
+ const securityReq = { [name]: [] };
107
+ if (propertyKey) {
108
+ const existing = Reflect.getOwnMetadata(key, target, propertyKey) || [];
109
+ existing.push(securityReq);
110
+ Reflect.defineMetadata(key, existing, target, propertyKey);
111
+ } else {
112
+ const existing = Reflect.getOwnMetadata(key, target) || [];
113
+ existing.push(securityReq);
114
+ Reflect.defineMetadata(key, existing, target);
115
+ }
116
+ };
117
+ }
118
+
119
+ export function ApiOAuth2(options: any, name = 'oauth2'): ClassDecorator & MethodDecorator {
120
+ return (target: any, propertyKey?: string | symbol) => {
121
+ const key = 'calyx:api_security';
122
+ const securityReq = { [name]: options.scopes || [] };
123
+ if (propertyKey) {
124
+ const existing = Reflect.getOwnMetadata(key, target, propertyKey) || [];
125
+ existing.push(securityReq);
126
+ Reflect.defineMetadata(key, existing, target, propertyKey);
127
+ } else {
128
+ const existing = Reflect.getOwnMetadata(key, target) || [];
129
+ existing.push(securityReq);
130
+ Reflect.defineMetadata(key, existing, target);
131
+ }
132
+ };
133
+ }
134
+
135
+ export function ApiPropertyOptional(options: any = {}): PropertyDecorator {
136
+ return ApiProperty({ ...options, required: false });
137
+ }
138
+
139
+ export function PartialType<T>(classRef: new (...args: any[]) => T): new (...args: any[]) => Partial<T> {
140
+ const properties = Reflect.getMetadata('calyx:api_properties', classRef) || [];
141
+
142
+ class PartialClass {}
143
+ Object.defineProperty(PartialClass, 'name', { value: `Partial${classRef.name}` });
144
+
145
+ const partialProps = properties.map((p: any) => ({ ...p, required: false }));
146
+ Reflect.defineMetadata('calyx:api_properties', partialProps, PartialClass);
147
+
148
+ return PartialClass as any;
149
+ }
150
+
151
+ export function PickType<T, K extends keyof T>(
152
+ classRef: new (...args: any[]) => T,
153
+ keys: readonly K[]
154
+ ): new (...args: any[]) => Pick<T, K> {
155
+ const properties = Reflect.getMetadata('calyx:api_properties', classRef) || [];
156
+ const stringKeys = keys.map(String);
157
+
158
+ class PickClass {}
159
+ Object.defineProperty(PickClass, 'name', { value: `Pick${classRef.name}` });
160
+
161
+ const pickProps = properties.filter((p: any) => stringKeys.includes(p.propertyKey));
162
+ Reflect.defineMetadata('calyx:api_properties', pickProps, PickClass);
163
+
164
+ return PickClass as any;
165
+ }
166
+
167
+ export function OmitType<T, K extends keyof T>(
168
+ classRef: new (...args: any[]) => T,
169
+ keys: readonly K[]
170
+ ): new (...args: any[]) => Omit<T, K> {
171
+ const properties = Reflect.getMetadata('calyx:api_properties', classRef) || [];
172
+ const stringKeys = keys.map(String);
173
+
174
+ class OmitClass {}
175
+ Object.defineProperty(OmitClass, 'name', { value: `Omit${classRef.name}` });
176
+
177
+ const omitProps = properties.filter((p: any) => !stringKeys.includes(p.propertyKey));
178
+ Reflect.defineMetadata('calyx:api_properties', omitProps, OmitClass);
179
+
180
+ return OmitClass as any;
181
+ }
182
+
183
+ export function IntersectionType<A, B>(
184
+ classA: new (...args: any[]) => A,
185
+ classB: new (...args: any[]) => B
186
+ ): new (...args: any[]) => A & B {
187
+ const propsA = Reflect.getMetadata('calyx:api_properties', classA) || [];
188
+ const propsB = Reflect.getMetadata('calyx:api_properties', classB) || [];
189
+
190
+ class IntersectionClass {}
191
+ Object.defineProperty(IntersectionClass, 'name', { value: `${classA.name}And${classB.name}` });
192
+
193
+ const combinedProps = [...propsA];
194
+ for (const pB of propsB) {
195
+ if (!combinedProps.some((pA) => pA.propertyKey === pB.propertyKey)) {
196
+ combinedProps.push(pB);
197
+ }
198
+ }
199
+ Reflect.defineMetadata('calyx:api_properties', combinedProps, IntersectionClass);
200
+
201
+ return IntersectionClass as any;
202
+ }
203
+
@@ -0,0 +1,2 @@
1
+ export * from './decorators.ts';
2
+ export * from './swagger.module.ts';