@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.
- package/CHANGELOG.md +15 -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 +14 -0
- package/package.json +8 -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 +132 -0
- package/src/graphql/graphql.module.ts +316 -0
- package/src/graphql/index.ts +2 -0
- package/src/http/application.ts +380 -70
- package/src/http/factory.ts +1 -0
- package/src/http/router.ts +13 -0
- package/src/http-client/http-client.module.ts +124 -0
- package/src/http-client/index.ts +1 -0
- package/src/index.ts +15 -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 +203 -0
- package/src/openapi/index.ts +2 -0
- package/src/openapi/swagger.module.ts +326 -0
- 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 +176 -0
- package/tests/openapi.test.ts +162 -0
- 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 {
|
|
@@ -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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1031
|
+
headers: responseHeaders,
|
|
859
1032
|
});
|
|
860
1033
|
}
|
|
861
1034
|
if (hasResParam && !isPassthrough) {
|
|
862
|
-
|
|
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:
|
|
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
|
|
875
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/http/factory.ts
CHANGED
package/src/http/router.ts
CHANGED
|
@@ -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);
|