@martel/calyx 1.8.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 (41) hide show
  1. package/CHANGELOG.md +8 -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 +11 -0
  10. package/package.json +7 -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 +70 -0
  19. package/src/graphql/graphql.module.ts +197 -47
  20. package/src/http/application.ts +330 -70
  21. package/src/http-client/http-client.module.ts +124 -0
  22. package/src/http-client/index.ts +1 -0
  23. package/src/index.ts +14 -0
  24. package/src/logger/index.ts +1 -0
  25. package/src/logger/logger.service.ts +118 -0
  26. package/src/mvc/index.ts +1 -0
  27. package/src/mvc/mvc.ts +22 -0
  28. package/src/openapi/decorators.ts +154 -0
  29. package/src/openapi/swagger.module.ts +172 -20
  30. package/src/queue/queue.module.ts +174 -0
  31. package/src/session/index.ts +1 -0
  32. package/src/session/session.middleware.ts +82 -0
  33. package/src/sse/index.ts +1 -0
  34. package/src/sse/sse.ts +18 -0
  35. package/src/streaming/index.ts +1 -0
  36. package/src/streaming/streamable-file.ts +32 -0
  37. package/src/validation/pipe.ts +79 -10
  38. package/src/versioning/versioning.ts +46 -0
  39. package/tests/graphql.test.ts +68 -4
  40. package/tests/openapi.test.ts +78 -11
  41. 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 {
@@ -105,6 +120,32 @@ export class CalyxApplication {
105
120
  private serverPort = 3000;
106
121
  private graphqlSchema: any = null;
107
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
+ }
108
149
 
109
150
  use(...middlewares: any[]) {
110
151
  this.globalMiddlewares.push(...middlewares);
@@ -155,6 +196,9 @@ export class CalyxApplication {
155
196
  // Register Event listeners
156
197
  this.registerEventListeners();
157
198
 
199
+ // Register Queue Processors
200
+ this.registerQueueProcessors();
201
+
158
202
  // Register Scheduled tasks
159
203
  this.registerScheduledTasks();
160
204
 
@@ -259,26 +303,64 @@ export class CalyxApplication {
259
303
  hasReqScopedPipe ||
260
304
  hasReqScopedMiddleware;
261
305
 
262
- this.router.insert(routeMeta.method, fullPath, {
263
- controllerClass,
264
- moduleClass,
265
- instance: singletonInstance,
266
- methodName: method,
267
- paramsConfig: compiledParamsConfig,
268
- httpCode,
269
- headers,
270
- redirect,
271
- hasBodyParam,
272
- hasQueryParam,
273
- hasResParam,
274
- isPassthrough,
275
- isRequestScoped: isRouteRequestScoped,
276
- hasLifecycleMetadata,
277
- guardsList,
278
- interceptorsList,
279
- filtersList,
280
- middlewaresList,
281
- });
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
+ }
282
364
  }
283
365
  }
284
366
  }
@@ -396,6 +478,10 @@ export class CalyxApplication {
396
478
  private handleRequest(req: Request): Response | Promise<Response> {
397
479
  try {
398
480
  const urlStr = req.url;
481
+ // Parse cookies
482
+ const cookieHeader = req.headers.get('cookie') || '';
483
+ (req as any).cookies = parseCookies(cookieHeader);
484
+
399
485
  // Fast path parsing
400
486
  let pathname = '/';
401
487
  let search = '';
@@ -416,7 +502,16 @@ export class CalyxApplication {
416
502
  return this.handleGraphQLRequest(req);
417
503
  }
418
504
 
419
- const matched = this.router.match(req.method, pathname);
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
+ }
420
515
  if (!matched) {
421
516
  if (this.globalMiddlewares.length > 0) {
422
517
  return this.handleGlobalMiddlewaresAnd404(req, pathname);
@@ -484,6 +579,8 @@ export class CalyxApplication {
484
579
  } catch {
485
580
  body = {};
486
581
  }
582
+ } else if (contentType.includes('multipart/form-data')) {
583
+ body = null;
487
584
  } else {
488
585
  try {
489
586
  body = await req.text();
@@ -576,39 +673,41 @@ export class CalyxApplication {
576
673
  query = {};
577
674
  }
578
675
 
579
- // Execute pipes for each argument
580
- const args: any[] = [];
581
- for (const config of paramsConfig) {
582
- let val: any;
583
- switch (config.type) {
584
- case 'req': val = req; break;
585
- case 'res': val = resWrapper; break;
586
- case 'param': val = config.name ? params[config.name] : params; break;
587
- case 'query': val = config.name ? query?.[config.name] : query; break;
588
- case 'body': val = config.name ? body?.[config.name] : body; break;
589
- case 'headers': val = config.name ? req.headers.get(config.name) : Object.fromEntries(req.headers.entries()); break;
590
- case 'custom': val = config.factory ? config.factory(config.name, context) : undefined; break;
591
- }
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
+ }
592
689
 
593
- // Apply pipes
594
- for (const pipe of config.pipesList) {
595
- const pipeInstance = pipe.isReqScoped
596
- ? this.container.resolveTokenInModuleContext(moduleClass, pipe.token, requestContext)
597
- : pipe.instance;
598
- const transformed = pipeInstance.transform(val, {
599
- type: config.type,
600
- metatype: config.paramType,
601
- data: config.name,
602
- });
603
- if (transformed instanceof Promise) {
604
- val = await transformed;
605
- } else {
606
- 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
+ }
607
705
  }
608
- }
609
706
 
610
- args[config.index] = val;
611
- }
707
+ args[config.index] = val;
708
+ }
709
+ return args;
710
+ };
612
711
 
613
712
  let result: any;
614
713
  if (handler.interceptorsList.length > 0) {
@@ -621,21 +720,23 @@ export class CalyxApplication {
621
720
  : interceptor.instance;
622
721
  return interceptorInstance.intercept(context, { handle: () => executeHandler() });
623
722
  }
723
+ const args = await resolveArgs();
624
724
  return instance[methodName](...args);
625
725
  };
626
726
  result = await executeHandler();
627
727
  } else {
728
+ const args = await resolveArgs();
628
729
  result = instance[methodName](...args);
629
730
  if (result instanceof Promise) {
630
731
  result = await result;
631
732
  }
632
733
  }
633
734
 
634
- if (this.isObservable(result)) {
735
+ if (this.isObservable(result) && !handler.isSse) {
635
736
  result = await this.resolveObservable(result);
636
737
  }
637
738
 
638
- 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);
639
740
  } catch (err: any) {
640
741
  return await this.handleLifecycleError(err, host, handler, requestContext);
641
742
  } finally {
@@ -775,6 +876,8 @@ export class CalyxApplication {
775
876
  } catch {
776
877
  body = {};
777
878
  }
879
+ } else if (contentType.includes('multipart/form-data')) {
880
+ body = null;
778
881
  } else {
779
882
  try {
780
883
  body = await req.text();
@@ -844,15 +947,54 @@ export class CalyxApplication {
844
947
 
845
948
  let result = instance[methodName](...args);
846
949
 
847
- if (this.isObservable(result)) {
950
+ if (this.isObservable(result) && !handler.isSse) {
848
951
  result = this.resolveObservable(result);
849
952
  }
850
953
 
851
954
  if (result instanceof Promise) {
852
- 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));
853
956
  }
854
957
 
855
- 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
+ }
856
998
  }
857
999
 
858
1000
  private processResult(
@@ -863,23 +1005,47 @@ export class CalyxApplication {
863
1005
  headers: { name: string; value: string }[],
864
1006
  redirect: { url: string; statusCode?: number } | undefined,
865
1007
  isPassthrough = false,
866
- hasResParam = false
867
- ): Response {
1008
+ hasResParam = false,
1009
+ renderTemplate?: string,
1010
+ isSse = false
1011
+ ): Response | Promise<Response> {
868
1012
  if (redirect) {
869
1013
  return Response.redirect(redirect.url, redirect.statusCode || 302);
870
1014
  }
871
1015
 
872
1016
  if (resWrapper) {
873
1017
  if (resWrapper.sent && !isPassthrough) {
874
- 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, {
875
1030
  status: resWrapper.statusCode,
876
- headers: resWrapper.headers,
1031
+ headers: responseHeaders,
877
1032
  });
878
1033
  }
879
1034
  if (hasResParam && !isPassthrough) {
880
- 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, {
881
1047
  status: resWrapper.statusCode,
882
- headers: resWrapper.headers,
1048
+ headers: responseHeaders,
883
1049
  });
884
1050
  }
885
1051
  }
@@ -889,12 +1055,73 @@ export class CalyxApplication {
889
1055
  : (httpCode ?? (req.method === 'POST' ? 201 : 200));
890
1056
 
891
1057
  // Build headers as a plain object Record<string, string>
892
- const responseHeaders: Record<string, string> = resWrapper
893
- ? { ...resWrapper.headers }
894
- : {};
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
+ }
895
1071
 
896
1072
  for (const header of headers) {
897
- 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 });
898
1125
  }
899
1126
 
900
1127
  if (result === undefined || result === null) {
@@ -902,24 +1129,46 @@ export class CalyxApplication {
902
1129
  }
903
1130
 
904
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
+ }
905
1140
  return result;
906
1141
  }
907
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
+
908
1152
  if (typeof result === 'object') {
909
- responseHeaders['content-type'] = 'application/json';
1153
+ responseHeaders.set('content-type', 'application/json');
910
1154
  const constructor = result.constructor;
911
1155
  if (constructor) {
912
1156
  const hasRules = Reflect.hasMetadata('calyx:validation_rules', constructor);
913
1157
  const hasExpose = Reflect.hasMetadata('calyx:expose_properties', constructor);
914
1158
  if (hasRules || hasExpose) {
915
1159
  const serialize = SerializationCompiler.compile(constructor);
916
- 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 });
917
1163
  }
918
1164
  }
919
- 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 });
920
1168
  }
921
1169
 
922
- 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 });
923
1172
  }
924
1173
 
925
1174
  private isObservable(obj: any): boolean {
@@ -1104,6 +1353,17 @@ export class CalyxApplication {
1104
1353
  }
1105
1354
  }
1106
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
+
1107
1367
  private registerEventListeners() {
1108
1368
  let eventEmitter: EventEmitter;
1109
1369
  try {
@@ -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';