@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
@@ -11,6 +11,12 @@ import { cors, CorsOptions } from '../security/cors.middleware.ts';
11
11
  import { helmet, HelmetOptions } from '../security/helmet.middleware.ts';
12
12
  import { CronMatcher } from '../schedule/cron.matcher.ts';
13
13
  import { SerializationCompiler } from '../validation/compiler.ts';
14
+ import { VersioningOptions, VersioningType, VersionExtractor, VERSION_METADATA_KEY } from '../versioning/versioning.ts';
15
+ import { QueueManager, PROCESSOR_METADATA_KEY } from '../queue/queue.module.ts';
16
+ import { parseCookies, formatCookie } from '../cookies/cookies.ts';
17
+ import { StreamableFile } from '../streaming/streamable-file.ts';
18
+ import { defaultRenderEngine, ViewEngine } from '../mvc/mvc.ts';
19
+ import { join } from 'path';
14
20
 
15
21
  class ObjectPool<T> {
16
22
  private pool: T[] = [];
@@ -53,6 +59,13 @@ export class CalyxResponse {
53
59
  this.headers[name.toLowerCase()] = value;
54
60
  return this;
55
61
  }
62
+
63
+ cookiesList: string[] = [];
64
+
65
+ cookie(name: string, value: string, options?: any) {
66
+ this.cookiesList.push(formatCookie(name, value, options));
67
+ return this;
68
+ }
56
69
  }
57
70
 
58
71
  interface CompiledLifecycleItem {
@@ -85,6 +98,8 @@ interface HandlerConfig {
85
98
  interceptorsList: CompiledLifecycleItem[];
86
99
  filtersList: CompiledLifecycleItem[];
87
100
  middlewaresList: CompiledLifecycleItem[];
101
+ renderTemplate?: string;
102
+ isSse?: boolean;
88
103
  }
89
104
 
90
105
  export class CalyxApplication {
@@ -103,6 +118,34 @@ export class CalyxApplication {
103
118
  private sharedWebSockets: any[] = [];
104
119
  private hasWebSockets = false;
105
120
  private serverPort = 3000;
121
+ private graphqlSchema: any = null;
122
+ private isInitialized = false;
123
+ private versioningOptions?: VersioningOptions;
124
+
125
+ enableVersioning(options: VersioningOptions) {
126
+ this.versioningOptions = options;
127
+ return this;
128
+ }
129
+
130
+ private compressionEnabled = false;
131
+
132
+ enableCompression() {
133
+ this.compressionEnabled = true;
134
+ return this;
135
+ }
136
+
137
+ private viewsDir = 'views';
138
+ private viewEngine: ViewEngine = defaultRenderEngine;
139
+
140
+ setViewsDir(dir: string) {
141
+ this.viewsDir = dir;
142
+ return this;
143
+ }
144
+
145
+ setViewEngine(engine: ViewEngine) {
146
+ this.viewEngine = engine;
147
+ return this;
148
+ }
106
149
 
107
150
  use(...middlewares: any[]) {
108
151
  this.globalMiddlewares.push(...middlewares);
@@ -138,6 +181,9 @@ export class CalyxApplication {
138
181
  constructor(private rootModule: any) {}
139
182
 
140
183
  async init() {
184
+ if (this.isInitialized) return;
185
+ this.isInitialized = true;
186
+
141
187
  // Bootstrap the dependency injection container
142
188
  this.container.bootstrap(this.rootModule);
143
189
 
@@ -150,6 +196,9 @@ export class CalyxApplication {
150
196
  // Register Event listeners
151
197
  this.registerEventListeners();
152
198
 
199
+ // Register Queue Processors
200
+ this.registerQueueProcessors();
201
+
153
202
  // Register Scheduled tasks
154
203
  this.registerScheduledTasks();
155
204
 
@@ -161,9 +210,18 @@ export class CalyxApplication {
161
210
 
162
211
  // Call OnApplicationBootstrap hooks
163
212
  await this.runOnApplicationBootstrap();
213
+
214
+ // Build GraphQL Schema if GraphQLModule is loaded
215
+ try {
216
+ const { GraphQLModule } = await import('../graphql/graphql.module.ts');
217
+ this.graphqlSchema = GraphQLModule.buildSchema(this.container);
218
+ } catch {
219
+ // ignore
220
+ }
164
221
  }
165
222
 
166
223
  private buildRoutes() {
224
+ this.router.clear();
167
225
  const modules = this.container.getModules();
168
226
  for (const [moduleClass, record] of modules.entries()) {
169
227
  for (const controllerClass of record.controllers) {
@@ -245,26 +303,64 @@ export class CalyxApplication {
245
303
  hasReqScopedPipe ||
246
304
  hasReqScopedMiddleware;
247
305
 
248
- this.router.insert(routeMeta.method, fullPath, {
249
- controllerClass,
250
- moduleClass,
251
- instance: singletonInstance,
252
- methodName: method,
253
- paramsConfig: compiledParamsConfig,
254
- httpCode,
255
- headers,
256
- redirect,
257
- hasBodyParam,
258
- hasQueryParam,
259
- hasResParam,
260
- isPassthrough,
261
- isRequestScoped: isRouteRequestScoped,
262
- hasLifecycleMetadata,
263
- guardsList,
264
- interceptorsList,
265
- filtersList,
266
- middlewaresList,
267
- });
306
+ const methodFn = controllerClass.prototype[method];
307
+ // Compile versions if versioning is enabled
308
+ let versions: string[] = [];
309
+ if (this.versioningOptions) {
310
+ const rawVersion = Reflect.getMetadata(VERSION_METADATA_KEY, controllerClass.prototype, method) ??
311
+ Reflect.getMetadata(VERSION_METADATA_KEY, methodFn) ??
312
+ Reflect.getMetadata(VERSION_METADATA_KEY, controllerClass);
313
+ if (rawVersion !== undefined && rawVersion !== null) {
314
+ versions = Array.isArray(rawVersion) ? rawVersion : [rawVersion];
315
+ } else if (this.versioningOptions.defaultVersion !== undefined && this.versioningOptions.defaultVersion !== null) {
316
+ const defaultV = this.versioningOptions.defaultVersion;
317
+ versions = Array.isArray(defaultV) ? defaultV : [defaultV];
318
+ }
319
+ }
320
+
321
+ const renderTemplate = Reflect.getMetadata('calyx:render_template', controllerClass.prototype, method) ??
322
+ Reflect.getMetadata('calyx:render_template', methodFn);
323
+ const isSse = Reflect.hasMetadata('calyx:sse', controllerClass.prototype, method) ||
324
+ Reflect.hasMetadata('calyx:sse', methodFn);
325
+
326
+ const insertRoute = (methodStr: string, pathStr: string) => {
327
+ this.router.insert(methodStr, pathStr, {
328
+ controllerClass,
329
+ moduleClass,
330
+ instance: singletonInstance,
331
+ methodName: method,
332
+ paramsConfig: compiledParamsConfig,
333
+ httpCode,
334
+ headers,
335
+ redirect,
336
+ hasBodyParam,
337
+ hasQueryParam,
338
+ hasResParam,
339
+ isPassthrough,
340
+ isRequestScoped: isRouteRequestScoped,
341
+ hasLifecycleMetadata,
342
+ guardsList,
343
+ interceptorsList,
344
+ filtersList,
345
+ middlewaresList,
346
+ renderTemplate,
347
+ isSse,
348
+ });
349
+ };
350
+
351
+ if (this.versioningOptions && this.versioningOptions.type === VersioningType.URI && versions.length > 0) {
352
+ for (const version of versions) {
353
+ const versionPrefix = `/v${version}`;
354
+ const versionedPath = '/' + [versionPrefix.replace(/^\/|\/$/g, ''), normalizedPrefix, normalizedPath].filter(Boolean).join('/');
355
+ insertRoute(routeMeta.method, versionedPath);
356
+ }
357
+ } else if (this.versioningOptions && (this.versioningOptions.type === VersioningType.HEADER || this.versioningOptions.type === VersioningType.MEDIA_TYPE) && versions.length > 0) {
358
+ for (const version of versions) {
359
+ insertRoute(`${routeMeta.method}:v${version}`, fullPath);
360
+ }
361
+ } else {
362
+ insertRoute(routeMeta.method, fullPath);
363
+ }
268
364
  }
269
365
  }
270
366
  }
@@ -382,6 +478,10 @@ export class CalyxApplication {
382
478
  private handleRequest(req: Request): Response | Promise<Response> {
383
479
  try {
384
480
  const urlStr = req.url;
481
+ // Parse cookies
482
+ const cookieHeader = req.headers.get('cookie') || '';
483
+ (req as any).cookies = parseCookies(cookieHeader);
484
+
385
485
  // Fast path parsing
386
486
  let pathname = '/';
387
487
  let search = '';
@@ -398,7 +498,20 @@ export class CalyxApplication {
398
498
  pathname = pathname.substring(0, queryIdx);
399
499
  }
400
500
 
401
- const matched = this.router.match(req.method, pathname);
501
+ if (this.graphqlSchema && pathname === '/graphql' && req.method === 'POST') {
502
+ return this.handleGraphQLRequest(req);
503
+ }
504
+
505
+ let matched = null;
506
+ if (this.versioningOptions && this.versioningOptions.type !== VersioningType.URI) {
507
+ const version = VersionExtractor.extract(req, this.versioningOptions.type, this.versioningOptions);
508
+ if (version) {
509
+ matched = this.router.match(`${req.method}:v${version}`.toUpperCase(), pathname);
510
+ }
511
+ }
512
+ if (!matched) {
513
+ matched = this.router.match(req.method, pathname);
514
+ }
402
515
  if (!matched) {
403
516
  if (this.globalMiddlewares.length > 0) {
404
517
  return this.handleGlobalMiddlewaresAnd404(req, pathname);
@@ -466,6 +579,8 @@ export class CalyxApplication {
466
579
  } catch {
467
580
  body = {};
468
581
  }
582
+ } else if (contentType.includes('multipart/form-data')) {
583
+ body = null;
469
584
  } else {
470
585
  try {
471
586
  body = await req.text();
@@ -558,39 +673,41 @@ export class CalyxApplication {
558
673
  query = {};
559
674
  }
560
675
 
561
- // Execute pipes for each argument
562
- const args: any[] = [];
563
- for (const config of paramsConfig) {
564
- let val: any;
565
- switch (config.type) {
566
- case 'req': val = req; break;
567
- case 'res': val = resWrapper; break;
568
- case 'param': val = config.name ? params[config.name] : params; break;
569
- case 'query': val = config.name ? query?.[config.name] : query; break;
570
- case 'body': val = config.name ? body?.[config.name] : body; break;
571
- case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
572
- case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
573
- }
676
+ const resolveArgs = async (): Promise<any[]> => {
677
+ const args: any[] = [];
678
+ for (const config of paramsConfig) {
679
+ let val: any;
680
+ switch (config.type) {
681
+ case 'req': val = req; break;
682
+ case 'res': val = resWrapper; break;
683
+ case 'param': val = config.name ? params[config.name] : params; break;
684
+ case 'query': val = config.name ? query?.[config.name] : query; break;
685
+ case 'body': val = config.name ? body?.[config.name] : body; break;
686
+ case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
687
+ case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
688
+ }
574
689
 
575
- // Apply pipes
576
- for (const pipe of config.pipesList) {
577
- const pipeInstance = pipe.isReqScoped
578
- ? this.container.resolveTokenInModuleContext(moduleClass, pipe.token, requestContext)
579
- : pipe.instance;
580
- const transformed = pipeInstance.transform(val, {
581
- type: config.type,
582
- metatype: config.paramType,
583
- data: config.name,
584
- });
585
- if (transformed instanceof Promise) {
586
- val = await transformed;
587
- } else {
588
- val = transformed;
690
+ // Apply pipes
691
+ for (const pipe of config.pipesList) {
692
+ const pipeInstance = pipe.isReqScoped
693
+ ? this.container.resolveTokenInModuleContext(moduleClass, pipe.token, requestContext)
694
+ : pipe.instance;
695
+ const transformed = pipeInstance.transform(val, {
696
+ type: config.type,
697
+ metatype: config.paramType,
698
+ data: config.name,
699
+ });
700
+ if (transformed instanceof Promise) {
701
+ val = await transformed;
702
+ } else {
703
+ val = transformed;
704
+ }
589
705
  }
590
- }
591
706
 
592
- args[config.index] = val;
593
- }
707
+ args[config.index] = val;
708
+ }
709
+ return args;
710
+ };
594
711
 
595
712
  let result: any;
596
713
  if (handler.interceptorsList.length > 0) {
@@ -603,21 +720,23 @@ export class CalyxApplication {
603
720
  : interceptor.instance;
604
721
  return interceptorInstance.intercept(context, { handle: () => executeHandler() });
605
722
  }
723
+ const args = await resolveArgs();
606
724
  return instance[methodName](...args);
607
725
  };
608
726
  result = await executeHandler();
609
727
  } else {
728
+ const args = await resolveArgs();
610
729
  result = instance[methodName](...args);
611
730
  if (result instanceof Promise) {
612
731
  result = await result;
613
732
  }
614
733
  }
615
734
 
616
- if (this.isObservable(result)) {
735
+ if (this.isObservable(result) && !handler.isSse) {
617
736
  result = await this.resolveObservable(result);
618
737
  }
619
738
 
620
- return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam);
739
+ return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam, handler.renderTemplate, handler.isSse);
621
740
  } catch (err: any) {
622
741
  return await this.handleLifecycleError(err, host, handler, requestContext);
623
742
  } finally {
@@ -757,6 +876,8 @@ export class CalyxApplication {
757
876
  } catch {
758
877
  body = {};
759
878
  }
879
+ } else if (contentType.includes('multipart/form-data')) {
880
+ body = null;
760
881
  } else {
761
882
  try {
762
883
  body = await req.text();
@@ -826,15 +947,54 @@ export class CalyxApplication {
826
947
 
827
948
  let result = instance[methodName](...args);
828
949
 
829
- if (this.isObservable(result)) {
950
+ if (this.isObservable(result) && !handler.isSse) {
830
951
  result = this.resolveObservable(result);
831
952
  }
832
953
 
833
954
  if (result instanceof Promise) {
834
- return result.then((resolvedResult) => this.processResult(req, resolvedResult, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam));
955
+ return result.then((resolvedResult) => this.processResult(req, resolvedResult, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam, handler.renderTemplate, handler.isSse));
835
956
  }
836
957
 
837
- return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam);
958
+ return this.processResult(req, result, resWrapper, httpCode, headers, redirect, handler.isPassthrough, handler.hasResParam, handler.renderTemplate, handler.isSse);
959
+ }
960
+
961
+ private compressResponseIfNeeded(req: Request, body: any, headers: Headers, resWrapper: CalyxResponse | null): any {
962
+ const isEnabled = this.compressionEnabled || (resWrapper && (resWrapper as any).compressionEnabled);
963
+ if (!isEnabled) return body;
964
+
965
+ const acceptEncoding = req.headers.get('accept-encoding') || '';
966
+ if (!acceptEncoding.includes('gzip')) return body;
967
+
968
+ if (!body) return body;
969
+
970
+ const contentType = headers.get('content-type') || '';
971
+ const isCompressible = contentType.includes('text/') ||
972
+ contentType.includes('json') ||
973
+ contentType.includes('javascript') ||
974
+ contentType.includes('xml') ||
975
+ contentType === '';
976
+
977
+ if (!isCompressible) return body;
978
+
979
+ try {
980
+ let dataToCompress: Uint8Array;
981
+ if (typeof body === 'string') {
982
+ dataToCompress = new TextEncoder().encode(body);
983
+ } else if (body instanceof Uint8Array) {
984
+ dataToCompress = body;
985
+ } else if (body instanceof ArrayBuffer) {
986
+ dataToCompress = new Uint8Array(body);
987
+ } else {
988
+ dataToCompress = new TextEncoder().encode(JSON.stringify(body));
989
+ }
990
+
991
+ const compressed = Bun.gzipSync(dataToCompress);
992
+ headers.set('content-encoding', 'gzip');
993
+ headers.set('content-length', String(compressed.length));
994
+ return compressed;
995
+ } catch {
996
+ return body;
997
+ }
838
998
  }
839
999
 
840
1000
  private processResult(
@@ -845,23 +1005,47 @@ export class CalyxApplication {
845
1005
  headers: { name: string; value: string }[],
846
1006
  redirect: { url: string; statusCode?: number } | undefined,
847
1007
  isPassthrough = false,
848
- hasResParam = false
849
- ): Response {
1008
+ hasResParam = false,
1009
+ renderTemplate?: string,
1010
+ isSse = false
1011
+ ): Response | Promise<Response> {
850
1012
  if (redirect) {
851
1013
  return Response.redirect(redirect.url, redirect.statusCode || 302);
852
1014
  }
853
1015
 
854
1016
  if (resWrapper) {
855
1017
  if (resWrapper.sent && !isPassthrough) {
856
- return new Response(resWrapper.body, {
1018
+ const responseHeaders = new Headers();
1019
+ for (const [k, v] of Object.entries(resWrapper.headers)) {
1020
+ responseHeaders.set(k, v);
1021
+ }
1022
+ for (const c of resWrapper.cookiesList) {
1023
+ responseHeaders.append('Set-Cookie', c);
1024
+ }
1025
+ if ((req as any).finalizeSession) {
1026
+ (req as any).finalizeSession(responseHeaders);
1027
+ }
1028
+ const finalBody = this.compressResponseIfNeeded(req, resWrapper.body, responseHeaders, resWrapper);
1029
+ return new Response(finalBody, {
857
1030
  status: resWrapper.statusCode,
858
- headers: resWrapper.headers,
1031
+ headers: responseHeaders,
859
1032
  });
860
1033
  }
861
1034
  if (hasResParam && !isPassthrough) {
862
- return new Response(resWrapper.body, {
1035
+ const responseHeaders = new Headers();
1036
+ for (const [k, v] of Object.entries(resWrapper.headers)) {
1037
+ responseHeaders.set(k, v);
1038
+ }
1039
+ for (const c of resWrapper.cookiesList) {
1040
+ responseHeaders.append('Set-Cookie', c);
1041
+ }
1042
+ if ((req as any).finalizeSession) {
1043
+ (req as any).finalizeSession(responseHeaders);
1044
+ }
1045
+ const finalBody = this.compressResponseIfNeeded(req, resWrapper.body, responseHeaders, resWrapper);
1046
+ return new Response(finalBody, {
863
1047
  status: resWrapper.statusCode,
864
- headers: resWrapper.headers,
1048
+ headers: responseHeaders,
865
1049
  });
866
1050
  }
867
1051
  }
@@ -871,12 +1055,73 @@ export class CalyxApplication {
871
1055
  : (httpCode ?? (req.method === 'POST' ? 201 : 200));
872
1056
 
873
1057
  // Build headers as a plain object Record<string, string>
874
- const responseHeaders: Record<string, string> = resWrapper
875
- ? { ...resWrapper.headers }
876
- : {};
1058
+ const responseHeaders = new Headers();
1059
+ if (resWrapper) {
1060
+ for (const [k, v] of Object.entries(resWrapper.headers)) {
1061
+ responseHeaders.set(k, v);
1062
+ }
1063
+ for (const c of resWrapper.cookiesList) {
1064
+ responseHeaders.append('Set-Cookie', c);
1065
+ }
1066
+ }
1067
+
1068
+ if ((req as any).finalizeSession) {
1069
+ (req as any).finalizeSession(responseHeaders);
1070
+ }
877
1071
 
878
1072
  for (const header of headers) {
879
- responseHeaders[header.name.toLowerCase()] = header.value;
1073
+ responseHeaders.set(header.name.toLowerCase(), header.value);
1074
+ }
1075
+
1076
+ if (isSse) {
1077
+ responseHeaders.set('content-type', 'text/event-stream');
1078
+ responseHeaders.set('cache-control', 'no-cache');
1079
+ responseHeaders.set('connection', 'keep-alive');
1080
+
1081
+ let subscription: any;
1082
+ const stream = new ReadableStream({
1083
+ start(controller) {
1084
+ subscription = result.subscribe({
1085
+ next(event: any) {
1086
+ let msg = '';
1087
+ if (event.id) msg += `id: ${event.id}\n`;
1088
+ if (event.type) msg += `event: ${event.type}\n`;
1089
+ if (event.retry) msg += `retry: ${event.retry}\n`;
1090
+ const dataStr = typeof event.data === 'object' ? JSON.stringify(event.data) : String(event.data);
1091
+ msg += `data: ${dataStr}\n\n`;
1092
+ controller.enqueue(new TextEncoder().encode(msg));
1093
+ },
1094
+ error(err: any) {
1095
+ controller.error(err);
1096
+ },
1097
+ complete() {
1098
+ controller.close();
1099
+ }
1100
+ });
1101
+ },
1102
+ cancel() {
1103
+ if (subscription) {
1104
+ subscription.unsubscribe();
1105
+ }
1106
+ }
1107
+ });
1108
+
1109
+ return new Response(stream, { status, headers: responseHeaders });
1110
+ }
1111
+
1112
+ if (renderTemplate) {
1113
+ const templatePath = join(this.viewsDir, renderTemplate + (renderTemplate.includes('.') ? '' : '.html'));
1114
+ const renderResult = this.viewEngine(templatePath, result);
1115
+ if (renderResult instanceof Promise) {
1116
+ return renderResult.then((html) => {
1117
+ responseHeaders.set('content-type', 'text/html');
1118
+ const finalBody = this.compressResponseIfNeeded(req, html, responseHeaders, resWrapper);
1119
+ return new Response(finalBody, { status, headers: responseHeaders });
1120
+ });
1121
+ }
1122
+ responseHeaders.set('content-type', 'text/html');
1123
+ const finalBody = this.compressResponseIfNeeded(req, renderResult, responseHeaders, resWrapper);
1124
+ return new Response(finalBody, { status, headers: responseHeaders });
880
1125
  }
881
1126
 
882
1127
  if (result === undefined || result === null) {
@@ -884,24 +1129,46 @@ export class CalyxApplication {
884
1129
  }
885
1130
 
886
1131
  if (result instanceof Response) {
1132
+ if (resWrapper && resWrapper.cookiesList.length > 0) {
1133
+ for (const c of resWrapper.cookiesList) {
1134
+ result.headers.append('Set-Cookie', c);
1135
+ }
1136
+ }
1137
+ if ((req as any).finalizeSession) {
1138
+ (req as any).finalizeSession(result.headers);
1139
+ }
887
1140
  return result;
888
1141
  }
889
1142
 
1143
+ if (result instanceof StreamableFile) {
1144
+ const fileHeaders = result.getHeaders();
1145
+ for (const [k, v] of Object.entries(fileHeaders)) {
1146
+ responseHeaders.set(k, v);
1147
+ }
1148
+ const finalBody = this.compressResponseIfNeeded(req, result.getStream(), responseHeaders, resWrapper);
1149
+ return new Response(finalBody, { status, headers: responseHeaders });
1150
+ }
1151
+
890
1152
  if (typeof result === 'object') {
891
- responseHeaders['content-type'] = 'application/json';
1153
+ responseHeaders.set('content-type', 'application/json');
892
1154
  const constructor = result.constructor;
893
1155
  if (constructor) {
894
1156
  const hasRules = Reflect.hasMetadata('calyx:validation_rules', constructor);
895
1157
  const hasExpose = Reflect.hasMetadata('calyx:expose_properties', constructor);
896
1158
  if (hasRules || hasExpose) {
897
1159
  const serialize = SerializationCompiler.compile(constructor);
898
- return new Response(serialize(result), { status, headers: responseHeaders });
1160
+ const serialized = serialize(result);
1161
+ const finalBody = this.compressResponseIfNeeded(req, serialized, responseHeaders, resWrapper);
1162
+ return new Response(finalBody, { status, headers: responseHeaders });
899
1163
  }
900
1164
  }
901
- return new Response(JSON.stringify(result), { status, headers: responseHeaders });
1165
+ const serialized = JSON.stringify(result);
1166
+ const finalBody = this.compressResponseIfNeeded(req, serialized, responseHeaders, resWrapper);
1167
+ return new Response(finalBody, { status, headers: responseHeaders });
902
1168
  }
903
1169
 
904
- return new Response(String(result), { status, headers: responseHeaders });
1170
+ const finalBody = this.compressResponseIfNeeded(req, String(result), responseHeaders, resWrapper);
1171
+ return new Response(finalBody, { status, headers: responseHeaders });
905
1172
  }
906
1173
 
907
1174
  private isObservable(obj: any): boolean {
@@ -943,8 +1210,36 @@ export class CalyxApplication {
943
1210
  );
944
1211
  }
945
1212
 
1213
+ private async handleGraphQLRequest(req: Request): Promise<Response> {
1214
+ try {
1215
+ const body = await req.json() as any;
1216
+ const { query, variables } = body;
1217
+
1218
+ const { graphql } = await import('graphql');
1219
+ const result = await graphql({
1220
+ schema: this.graphqlSchema,
1221
+ source: query,
1222
+ variableValues: variables,
1223
+ });
1224
+
1225
+ return new Response(JSON.stringify(result), {
1226
+ status: 200,
1227
+ headers: { 'content-type': 'application/json' },
1228
+ });
1229
+ } catch (err: any) {
1230
+ return new Response(
1231
+ JSON.stringify({ errors: [{ message: err.message }] }),
1232
+ {
1233
+ status: 200,
1234
+ headers: { 'content-type': 'application/json' },
1235
+ }
1236
+ );
1237
+ }
1238
+ }
1239
+
946
1240
  async listen(port: number): Promise<any> {
947
1241
  this.serverPort = port;
1242
+ this.buildRoutes();
948
1243
  await this.init();
949
1244
 
950
1245
  const fetchHandler = (req: Request, server: any) => {
@@ -1058,6 +1353,17 @@ export class CalyxApplication {
1058
1353
  }
1059
1354
  }
1060
1355
 
1356
+ private registerQueueProcessors() {
1357
+ const instances = this.container.getProviderAndControllerInstances();
1358
+ for (const instance of instances) {
1359
+ if (!instance || !instance.constructor) continue;
1360
+ const queueName = Reflect.getMetadata(PROCESSOR_METADATA_KEY, instance.constructor);
1361
+ if (queueName) {
1362
+ QueueManager.registerProcessor(queueName, instance);
1363
+ }
1364
+ }
1365
+ }
1366
+
1061
1367
  private registerEventListeners() {
1062
1368
  let eventEmitter: EventEmitter;
1063
1369
  try {
@@ -1330,6 +1636,10 @@ export class CalyxApplication {
1330
1636
  // ignore non-json
1331
1637
  }
1332
1638
  }
1639
+
1640
+ getRoutes() {
1641
+ return this.router.getRoutes();
1642
+ }
1333
1643
  }
1334
1644
 
1335
1645
 
@@ -5,6 +5,7 @@ import { MicroserviceOptions } from '../microservices/interfaces.ts';
5
5
  export class CalyxFactory {
6
6
  static async create(rootModule: any): Promise<CalyxApplication> {
7
7
  const app = new CalyxApplication(rootModule);
8
+ await app.init();
8
9
  return app;
9
10
  }
10
11
 
@@ -16,8 +16,21 @@ export class RadixRouter<T> {
16
16
  private staticRoutes = new Map<string, T>();
17
17
  private handlersArray: T[] = [];
18
18
  private compiledMatch: ((method: string, path: string) => RouteMatch<T> | null) | null = null;
19
+ private routesList: { method: string; path: string; handler: T }[] = [];
20
+
21
+ getRoutes() {
22
+ return this.routesList;
23
+ }
24
+
25
+ clear() {
26
+ this.root = new RouterNode<T>();
27
+ this.staticRoutes.clear();
28
+ this.routesList = [];
29
+ this.compiledMatch = null;
30
+ }
19
31
 
20
32
  insert(method: string, path: string, handler: T) {
33
+ this.routesList.push({ method, path, handler });
21
34
  const hasParams = path.includes(':') || path.includes('*');
22
35
  if (!hasParams) {
23
36
  this.staticRoutes.set(method.toUpperCase() + ' ' + path, handler);