@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.
- package/CHANGELOG.md +8 -0
- package/README.md +71 -27
- package/benchmarks/graphql-benchmark.ts +81 -0
- package/benchmarks/index.ts +32 -0
- package/benchmarks/openapi-benchmark.ts +168 -0
- package/benchmarks/serialization-benchmark.ts +52 -0
- package/benchmarks/techniques-benchmark.ts +84 -0
- package/benchmarks/validation-benchmark.ts +74 -0
- package/bun.lock +11 -0
- package/package.json +7 -6
- package/src/cli/index.ts +19 -3
- package/src/compression/compression.middleware.ts +7 -0
- package/src/cookies/cookies.ts +69 -0
- package/src/database/mongoose.module.ts +250 -0
- package/src/database/typeorm.module.ts +276 -0
- package/src/file-upload/file-upload.interceptor.ts +93 -0
- package/src/file-upload/index.ts +1 -0
- package/src/graphql/decorators.ts +70 -0
- package/src/graphql/graphql.module.ts +197 -47
- package/src/http/application.ts +330 -70
- package/src/http-client/http-client.module.ts +124 -0
- package/src/http-client/index.ts +1 -0
- package/src/index.ts +14 -0
- package/src/logger/index.ts +1 -0
- package/src/logger/logger.service.ts +118 -0
- package/src/mvc/index.ts +1 -0
- package/src/mvc/mvc.ts +22 -0
- package/src/openapi/decorators.ts +154 -0
- package/src/openapi/swagger.module.ts +172 -20
- package/src/queue/queue.module.ts +174 -0
- package/src/session/index.ts +1 -0
- package/src/session/session.middleware.ts +82 -0
- package/src/sse/index.ts +1 -0
- package/src/sse/sse.ts +18 -0
- package/src/streaming/index.ts +1 -0
- package/src/streaming/streamable-file.ts +32 -0
- package/src/validation/pipe.ts +79 -10
- package/src/versioning/versioning.ts +46 -0
- package/tests/graphql.test.ts +68 -4
- package/tests/openapi.test.ts +78 -11
- package/tests/techniques.test.ts +471 -0
package/src/http/application.ts
CHANGED
|
@@ -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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1031
|
+
headers: responseHeaders,
|
|
877
1032
|
});
|
|
878
1033
|
}
|
|
879
1034
|
if (hasResParam && !isPassthrough) {
|
|
880
|
-
|
|
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:
|
|
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
|
|
893
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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';
|