proteum 2.1.3-1 → 2.1.7
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/AGENTS.md +22 -14
- package/README.md +109 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +37 -13
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/trace.ts +9 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +95 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +110 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +165 -6
- package/cli/scaffold/index.ts +18 -27
- package/cli/scaffold/templates.ts +48 -28
- package/cli/utils/agents.ts +106 -13
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2508 -302
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +52 -1
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +10 -2
- package/server/app/container/trace/index.ts +105 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +305 -11
- package/server/services/router/index.ts +116 -57
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +23 -1
- package/server/services/router/response/page/document.tsx +31 -14
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
|
@@ -72,6 +72,8 @@ type TGeneratedRouteModule = { filepath: string; register?: TRouteModule['__regi
|
|
|
72
72
|
|
|
73
73
|
type TGeneratedControllerDefinition = {
|
|
74
74
|
path: string;
|
|
75
|
+
filepath: string;
|
|
76
|
+
sourceLocation: { line: number; column: number };
|
|
75
77
|
Controller: new (request: TRouterContext<TServerRouter>) => { [method: string]: () => any };
|
|
76
78
|
method: string;
|
|
77
79
|
};
|
|
@@ -93,6 +95,9 @@ export type TApiResponseData = { data: any; triggers?: { [cle: string]: any } };
|
|
|
93
95
|
|
|
94
96
|
export type HttpHeaders = { [cle: string]: string };
|
|
95
97
|
|
|
98
|
+
const dynamicHtmlCacheControl = 'no-store, no-cache, must-revalidate, proxy-revalidate';
|
|
99
|
+
const staticHtmlCacheControl = 'public, max-age=0, must-revalidate';
|
|
100
|
+
|
|
96
101
|
/*----------------------------------
|
|
97
102
|
- SERVICE CONFIG
|
|
98
103
|
----------------------------------*/
|
|
@@ -114,6 +119,7 @@ export type Config<
|
|
|
114
119
|
disk?: string; // Disk driver ID
|
|
115
120
|
|
|
116
121
|
currentDomain: string;
|
|
122
|
+
defaultRouteOptions?: Partial<TRouteOptions>;
|
|
117
123
|
|
|
118
124
|
http: HttpServiceConfig;
|
|
119
125
|
|
|
@@ -353,7 +359,7 @@ export default class ServerRouter<
|
|
|
353
359
|
const controller = new definition.Controller(requestContext);
|
|
354
360
|
return controller[definition.method]();
|
|
355
361
|
},
|
|
356
|
-
options: { ...defaultOptions },
|
|
362
|
+
options: { ...defaultOptions, filepath: definition.filepath, sourceLocation: definition.sourceLocation },
|
|
357
363
|
};
|
|
358
364
|
|
|
359
365
|
this.controllers[route.path] = route;
|
|
@@ -363,6 +369,14 @@ export default class ServerRouter<
|
|
|
363
369
|
public url = (path: string, params: {} = {}, absolute: boolean = true) =>
|
|
364
370
|
buildUrl(path, params, this.config.currentDomain, absolute);
|
|
365
371
|
|
|
372
|
+
private buildRouteOptions(options: Partial<TRouteOptions> = {}): TRouteOptions {
|
|
373
|
+
return {
|
|
374
|
+
...defaultOptions,
|
|
375
|
+
...(this.config.defaultRouteOptions || {}),
|
|
376
|
+
...options,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
366
380
|
/*----------------------------------
|
|
367
381
|
- REGISTER
|
|
368
382
|
----------------------------------*/
|
|
@@ -378,12 +392,11 @@ export default class ServerRouter<
|
|
|
378
392
|
regex,
|
|
379
393
|
keys,
|
|
380
394
|
controller: (context: TRouterContext<this>) => new Page(route, renderer, context, layout),
|
|
381
|
-
options: {
|
|
382
|
-
...defaultOptions,
|
|
395
|
+
options: this.buildRouteOptions({
|
|
383
396
|
accept: 'html', // Les pages retournent forcémment du html
|
|
384
397
|
setup,
|
|
385
398
|
...options,
|
|
386
|
-
},
|
|
399
|
+
}),
|
|
387
400
|
};
|
|
388
401
|
|
|
389
402
|
this.routes.push(route);
|
|
@@ -396,7 +409,7 @@ export default class ServerRouter<
|
|
|
396
409
|
options: Partial<TRouteOptions>,
|
|
397
410
|
renderer: TFrontRenderer<{}, { message: string }>,
|
|
398
411
|
) {
|
|
399
|
-
const finalOptions =
|
|
412
|
+
const finalOptions = this.buildRouteOptions(options);
|
|
400
413
|
|
|
401
414
|
// Automatic layout form the nearest _layout folder
|
|
402
415
|
const layout = getLayout('Error ' + code, finalOptions);
|
|
@@ -454,7 +467,7 @@ export default class ServerRouter<
|
|
|
454
467
|
path: path,
|
|
455
468
|
regex,
|
|
456
469
|
keys: keys,
|
|
457
|
-
options:
|
|
470
|
+
options: this.buildRouteOptions(options),
|
|
458
471
|
controller,
|
|
459
472
|
};
|
|
460
473
|
|
|
@@ -561,14 +574,10 @@ export default class ServerRouter<
|
|
|
561
574
|
- RESOLUTION
|
|
562
575
|
----------------------------------*/
|
|
563
576
|
public async middleware(req: express.Request, res: express.Response) {
|
|
564
|
-
// Don't cache HTML, because in case of update, assets file name will change (hash.ext)
|
|
565
|
-
// https://github.com/helmetjs/nocache/blob/main/index.ts
|
|
566
|
-
res.setHeader('Surrogate-Control', 'no-store');
|
|
567
|
-
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
568
|
-
|
|
569
577
|
// Create request
|
|
570
578
|
let requestId = uuid();
|
|
571
579
|
const cachedPage = req.headers['bypasscache'] ? undefined : this.cache[req.path];
|
|
580
|
+
this.applyHtmlCacheHeaders(res, Boolean(cachedPage));
|
|
572
581
|
const headers: HttpHeaders = Object.fromEntries(
|
|
573
582
|
Object.entries(req.headers).map(([key, value]) => [key, Array.isArray(value) ? value.join(', ') : value || '']),
|
|
574
583
|
);
|
|
@@ -628,10 +637,39 @@ export default class ServerRouter<
|
|
|
628
637
|
// Static pages
|
|
629
638
|
if (cachedPage) {
|
|
630
639
|
console.log('[router] Get static page from cache', req.path);
|
|
640
|
+
res.status(response.statusCode);
|
|
641
|
+
res.header(response.headers);
|
|
642
|
+
|
|
643
|
+
if (response.headers['Location']) {
|
|
644
|
+
res.send(response.data === undefined ? '' : response.data);
|
|
645
|
+
this.app.container.Trace.record(
|
|
646
|
+
request.id,
|
|
647
|
+
'response.send',
|
|
648
|
+
{
|
|
649
|
+
cached: true,
|
|
650
|
+
statusCode: response.statusCode,
|
|
651
|
+
contentType: response.headers['Content-Type'] || '',
|
|
652
|
+
headerKeys: Object.keys(response.headers),
|
|
653
|
+
redirected: true,
|
|
654
|
+
},
|
|
655
|
+
'summary',
|
|
656
|
+
);
|
|
657
|
+
this.app.container.Trace.finishRequest(request.id, {
|
|
658
|
+
statusCode: response.statusCode,
|
|
659
|
+
user: request.user?.email,
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
631
664
|
this.app.container.Trace.record(
|
|
632
665
|
request.id,
|
|
633
666
|
'response.send',
|
|
634
|
-
{
|
|
667
|
+
{
|
|
668
|
+
cached: true,
|
|
669
|
+
statusCode: response.statusCode,
|
|
670
|
+
contentType: response.headers['Content-Type'] || 'text/html',
|
|
671
|
+
headerKeys: Object.keys(response.headers),
|
|
672
|
+
},
|
|
635
673
|
'summary',
|
|
636
674
|
);
|
|
637
675
|
res.send(cachedPage.rendered);
|
|
@@ -711,6 +749,14 @@ export default class ServerRouter<
|
|
|
711
749
|
channelId: request.id,
|
|
712
750
|
method: request.method,
|
|
713
751
|
path: request.path,
|
|
752
|
+
...(request.traceCall
|
|
753
|
+
? {
|
|
754
|
+
traceCallFetcherId: request.traceCall.fetcherId,
|
|
755
|
+
traceCallId: request.traceCall.id,
|
|
756
|
+
traceCallLabel: request.traceCall.label,
|
|
757
|
+
traceCallOrigin: request.traceCall.origin,
|
|
758
|
+
}
|
|
759
|
+
: {}),
|
|
714
760
|
},
|
|
715
761
|
async () => {
|
|
716
762
|
const timeStart = Date.now();
|
|
@@ -745,6 +791,11 @@ export default class ServerRouter<
|
|
|
745
791
|
path: request.path,
|
|
746
792
|
accept: controllerRoute.options.accept || '',
|
|
747
793
|
filepath: controllerRoute.options.filepath || '',
|
|
794
|
+
source: {
|
|
795
|
+
filepath: controllerRoute.options.filepath || '',
|
|
796
|
+
line: controllerRoute.options.sourceLocation?.line || 0,
|
|
797
|
+
column: controllerRoute.options.sourceLocation?.column || 0,
|
|
798
|
+
},
|
|
748
799
|
},
|
|
749
800
|
'summary',
|
|
750
801
|
);
|
|
@@ -777,6 +828,11 @@ export default class ServerRouter<
|
|
|
777
828
|
routePath: route.path || '',
|
|
778
829
|
routeId: route.options.id || '',
|
|
779
830
|
filepath: route.options.filepath || '',
|
|
831
|
+
source: {
|
|
832
|
+
filepath: route.options.filepath || '',
|
|
833
|
+
line: route.options.sourceLocation?.line || 0,
|
|
834
|
+
column: route.options.sourceLocation?.column || 0,
|
|
835
|
+
},
|
|
780
836
|
},
|
|
781
837
|
'deep',
|
|
782
838
|
);
|
|
@@ -797,6 +853,11 @@ export default class ServerRouter<
|
|
|
797
853
|
routePath: route.path || '',
|
|
798
854
|
routeId: route.options.id || '',
|
|
799
855
|
filepath: route.options.filepath || '',
|
|
856
|
+
source: {
|
|
857
|
+
filepath: route.options.filepath || '',
|
|
858
|
+
line: route.options.sourceLocation?.line || 0,
|
|
859
|
+
column: route.options.sourceLocation?.column || 0,
|
|
860
|
+
},
|
|
800
861
|
},
|
|
801
862
|
'deep',
|
|
802
863
|
);
|
|
@@ -816,6 +877,11 @@ export default class ServerRouter<
|
|
|
816
877
|
routePath: route.path || '',
|
|
817
878
|
routeId: route.options.id || '',
|
|
818
879
|
filepath: route.options.filepath || '',
|
|
880
|
+
source: {
|
|
881
|
+
filepath: route.options.filepath || '',
|
|
882
|
+
line: route.options.sourceLocation?.line || 0,
|
|
883
|
+
column: route.options.sourceLocation?.column || 0,
|
|
884
|
+
},
|
|
819
885
|
},
|
|
820
886
|
'deep',
|
|
821
887
|
);
|
|
@@ -832,6 +898,12 @@ export default class ServerRouter<
|
|
|
832
898
|
}
|
|
833
899
|
|
|
834
900
|
this.app.container.Trace.record(request.id, 'resolve.routes-evaluated', routeStats, 'resolve');
|
|
901
|
+
|
|
902
|
+
if (isStatic) {
|
|
903
|
+
resolve(response);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
835
907
|
this.app.container.Trace.record(request.id, 'resolve.not-found', { path: request.path }, 'summary');
|
|
836
908
|
reject(new NotFound());
|
|
837
909
|
} catch (error) {
|
|
@@ -867,6 +939,11 @@ export default class ServerRouter<
|
|
|
867
939
|
routePath: route.path || '',
|
|
868
940
|
routeId: route.options.id || '',
|
|
869
941
|
filepath: route.options.filepath || '',
|
|
942
|
+
source: {
|
|
943
|
+
filepath: route.options.filepath || '',
|
|
944
|
+
line: route.options.sourceLocation?.line || 0,
|
|
945
|
+
column: route.options.sourceLocation?.column || 0,
|
|
946
|
+
},
|
|
870
947
|
accept: route.options.accept || '',
|
|
871
948
|
method: route.method,
|
|
872
949
|
},
|
|
@@ -880,10 +957,19 @@ export default class ServerRouter<
|
|
|
880
957
|
await response.runController(route);
|
|
881
958
|
if (!response.wasProvided) return;
|
|
882
959
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
960
|
+
if (response.request.path && route.options.static) {
|
|
961
|
+
const staticUrls = route.options.static.urls.includes('*') ? [response.request.path] : route.options.static.urls;
|
|
962
|
+
|
|
963
|
+
for (const staticUrl of staticUrls) {
|
|
964
|
+
if (!staticUrl) continue;
|
|
965
|
+
|
|
966
|
+
console.log('[router] Set in cache', staticUrl);
|
|
967
|
+
void this.renderStatic(
|
|
968
|
+
staticUrl,
|
|
969
|
+
route.options.static,
|
|
970
|
+
staticUrl === response.request.path ? response.data : undefined,
|
|
971
|
+
);
|
|
972
|
+
}
|
|
887
973
|
}
|
|
888
974
|
|
|
889
975
|
const timeEndResolving = Date.now();
|
|
@@ -901,47 +987,7 @@ export default class ServerRouter<
|
|
|
901
987
|
};
|
|
902
988
|
|
|
903
989
|
private async resolveApiBatch(fetchers: TFetcherList, request: ServerRequest<this>) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
const responseData: TObjetDonnees = {};
|
|
907
|
-
for (const id in fetchers) {
|
|
908
|
-
const fetcher = fetchers[id];
|
|
909
|
-
if (!fetcher || !('method' in fetcher)) continue;
|
|
910
|
-
|
|
911
|
-
const { method, path, data } = fetcher;
|
|
912
|
-
const callId = this.app.container.Trace.startCall(request.id, {
|
|
913
|
-
origin: 'api-batch-fetcher',
|
|
914
|
-
label: id,
|
|
915
|
-
method,
|
|
916
|
-
path,
|
|
917
|
-
fetcherId: id,
|
|
918
|
-
requestDataKeys: data && typeof data === 'object' ? Object.keys(data) : [],
|
|
919
|
-
requestData: data,
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
try {
|
|
923
|
-
const response = await this.resolve(request.children(method, path, data));
|
|
924
|
-
responseData[id] = response.data;
|
|
925
|
-
this.app.container.Trace.finishCall(request.id, callId, {
|
|
926
|
-
statusCode: response.statusCode,
|
|
927
|
-
resultKeys:
|
|
928
|
-
response.data && typeof response.data === 'object' && !Array.isArray(response.data)
|
|
929
|
-
? Object.keys(response.data as Record<string, unknown>)
|
|
930
|
-
: [],
|
|
931
|
-
result: response.data,
|
|
932
|
-
});
|
|
933
|
-
} catch (error) {
|
|
934
|
-
const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
|
935
|
-
const statusCode = 'http' in typedError ? Number((typedError as Error & { http?: number }).http) : undefined;
|
|
936
|
-
this.app.container.Trace.finishCall(request.id, callId, {
|
|
937
|
-
statusCode: Number.isFinite(statusCode) ? statusCode : undefined,
|
|
938
|
-
errorMessage: typedError.message,
|
|
939
|
-
});
|
|
940
|
-
throw error;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// TODO: merge response.headers ?
|
|
944
|
-
}
|
|
990
|
+
const responseData = await request.api.fetchSync(fetchers, {});
|
|
945
991
|
|
|
946
992
|
// Status
|
|
947
993
|
request.res.status(200);
|
|
@@ -1005,4 +1051,17 @@ export default class ServerRouter<
|
|
|
1005
1051
|
|
|
1006
1052
|
return response;
|
|
1007
1053
|
}
|
|
1054
|
+
|
|
1055
|
+
private applyHtmlCacheHeaders(res: express.Response, isStaticHtml: boolean) {
|
|
1056
|
+
if (isStaticHtml) {
|
|
1057
|
+
res.removeHeader('Surrogate-Control');
|
|
1058
|
+
res.setHeader('Cache-Control', staticHtmlCacheControl);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Don't cache dynamic HTML, because updated releases can change asset hashes.
|
|
1063
|
+
// https://github.com/helmetjs/nocache/blob/main/index.ts
|
|
1064
|
+
res.setHeader('Surrogate-Control', 'no-store');
|
|
1065
|
+
res.setHeader('Cache-Control', dynamicHtmlCacheControl);
|
|
1066
|
+
}
|
|
1008
1067
|
}
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
// Core
|
|
6
6
|
|
|
7
|
+
import { fromJson as errorFromJson } from '@common/errors';
|
|
8
|
+
import {
|
|
9
|
+
profilerOriginHeader,
|
|
10
|
+
profilerParentRequestIdHeader,
|
|
11
|
+
profilerSessionIdHeader,
|
|
12
|
+
} from '@common/dev/profiler';
|
|
7
13
|
import RequestService from './service';
|
|
8
14
|
|
|
9
15
|
import ApiClientService, {
|
|
@@ -21,6 +27,128 @@ import ApiClientService, {
|
|
|
21
27
|
- SERVICE
|
|
22
28
|
----------------------------------*/
|
|
23
29
|
export default class ApiClientRequest extends RequestService implements ApiClientService {
|
|
30
|
+
private isApiFetcher(fetcher: TFetcher | Promise<unknown>): fetcher is TFetcher {
|
|
31
|
+
return typeof fetcher === 'object' && fetcher !== null && 'method' in fetcher && 'path' in fetcher;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private toTraceInspectable(data: unknown) {
|
|
35
|
+
if (data === null || data === undefined) return data;
|
|
36
|
+
if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return data;
|
|
37
|
+
if (typeof data === 'bigint' || typeof data === 'symbol' || typeof data === 'function') return data;
|
|
38
|
+
if (typeof data === 'object') return data;
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private getTraceCallOrigin() {
|
|
44
|
+
return this.request.path === '/api' ? 'api-batch-fetcher' as const : 'ssr-fetcher' as const;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private createTraceCall({
|
|
48
|
+
fetcherId,
|
|
49
|
+
method,
|
|
50
|
+
path,
|
|
51
|
+
data,
|
|
52
|
+
options,
|
|
53
|
+
}: {
|
|
54
|
+
fetcherId: string;
|
|
55
|
+
method: string;
|
|
56
|
+
path: string;
|
|
57
|
+
data: unknown;
|
|
58
|
+
options?: TFetcher['options'];
|
|
59
|
+
}) {
|
|
60
|
+
return this.request.router.app.container.Trace.startCall(this.request.id, {
|
|
61
|
+
origin: this.getTraceCallOrigin(),
|
|
62
|
+
label: fetcherId,
|
|
63
|
+
method,
|
|
64
|
+
path,
|
|
65
|
+
fetcherId,
|
|
66
|
+
...(options?.connected
|
|
67
|
+
? {
|
|
68
|
+
connectedControllerAccessor: options.connected.controllerAccessor,
|
|
69
|
+
connectedProjectNamespace: options.connected.namespace,
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
72
|
+
requestDataKeys: data && typeof data === 'object' ? Object.keys(data as Record<string, unknown>) : [],
|
|
73
|
+
requestData: this.toTraceInspectable(data),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private buildConnectedRequestHeaders(fetcher: TFetcher) {
|
|
78
|
+
const headers = new Headers();
|
|
79
|
+
|
|
80
|
+
for (const [key, value] of Object.entries(this.request.headers)) {
|
|
81
|
+
if (!value) continue;
|
|
82
|
+
if (key === 'content-length' || key === 'host') continue;
|
|
83
|
+
headers.set(key, value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
headers.set('accept', 'application/json');
|
|
87
|
+
|
|
88
|
+
if (fetcher.options?.connected) {
|
|
89
|
+
headers.set(profilerOriginHeader, this.getTraceCallOrigin());
|
|
90
|
+
|
|
91
|
+
const profilerSessionId = this.request.headers[profilerSessionIdHeader];
|
|
92
|
+
if (profilerSessionId) headers.set(profilerSessionIdHeader, profilerSessionId);
|
|
93
|
+
headers.set(profilerParentRequestIdHeader, this.request.id);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return headers;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private async resolveConnectedFetcher<TData>(fetcher: TFetcher<TData>) {
|
|
100
|
+
const connected = fetcher.options?.connected;
|
|
101
|
+
if (!connected) throw new Error('Connected fetcher metadata is missing.');
|
|
102
|
+
|
|
103
|
+
const connectedProject = this.request.router.app.connectedProjects?.[connected.namespace];
|
|
104
|
+
if (!connectedProject) {
|
|
105
|
+
throw new Error(`Connected project "${connected.namespace}" is not registered on ${this.request.router.app.identity.identifier}.`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const headers = this.buildConnectedRequestHeaders(fetcher);
|
|
109
|
+
const url = new URL(fetcher.path, connectedProject.urlInternal).toString();
|
|
110
|
+
const init: RequestInit = {
|
|
111
|
+
method: fetcher.method,
|
|
112
|
+
headers,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (fetcher.data) {
|
|
116
|
+
if (fetcher.method === 'GET') {
|
|
117
|
+
const params = new URLSearchParams();
|
|
118
|
+
for (const [key, value] of Object.entries(fetcher.data)) {
|
|
119
|
+
if (value === undefined || value === null) continue;
|
|
120
|
+
params.set(key, String(value));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return this.fetchConnectedResponse<TData>(`${url}?${params.toString()}`, init);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
headers.set('content-type', 'application/json');
|
|
127
|
+
init.body = JSON.stringify(fetcher.data);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return this.fetchConnectedResponse<TData>(url, init);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async fetchConnectedResponse<TData>(url: string, init: RequestInit) {
|
|
134
|
+
const response = await fetch(url, init);
|
|
135
|
+
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
const contentType = response.headers.get('content-type') || '';
|
|
138
|
+
const errorPayload = contentType.includes('application/json') ? await response.json() : await response.text();
|
|
139
|
+
const typedError =
|
|
140
|
+
typeof errorPayload === 'object' && errorPayload && 'code' in (errorPayload as object)
|
|
141
|
+
? (errorFromJson(errorPayload as any) as Error & { http?: number })
|
|
142
|
+
: (new Error(typeof errorPayload === 'string' ? errorPayload : `Connected request failed with ${response.status}.`) as Error & {
|
|
143
|
+
http?: number;
|
|
144
|
+
});
|
|
145
|
+
typedError.http = response.status;
|
|
146
|
+
throw typedError;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return (await response.json()) as TData;
|
|
150
|
+
}
|
|
151
|
+
|
|
24
152
|
/*----------------------------------
|
|
25
153
|
- HIGH LEVEL
|
|
26
154
|
----------------------------------*/
|
|
@@ -67,39 +195,52 @@ export default class ApiClientRequest extends RequestService implements ApiClien
|
|
|
67
195
|
if (!fetcher) continue;
|
|
68
196
|
|
|
69
197
|
// Promise Fetcher (direct call from service method)
|
|
70
|
-
if (
|
|
198
|
+
if (!this.isApiFetcher(fetcher)) {
|
|
71
199
|
fetchedData[id] = await fetcher;
|
|
72
200
|
continue;
|
|
73
201
|
}
|
|
74
202
|
|
|
75
|
-
const { method, path, data } = fetcher;
|
|
203
|
+
const { method, path, data, options } = fetcher;
|
|
76
204
|
//this.router.config.debug && console.log(`[api] Resolving from internal api`, method, path, data);
|
|
77
205
|
|
|
78
206
|
// We don't fetch the already given data
|
|
79
207
|
if (id in fetchedData) continue;
|
|
80
208
|
|
|
81
|
-
|
|
82
|
-
const request = this.request.children(method, path, data);
|
|
83
|
-
const callId = this.request.router.app.container.Trace.startCall(this.request.id, {
|
|
84
|
-
origin: 'ssr-fetcher',
|
|
85
|
-
label: id,
|
|
86
|
-
method,
|
|
87
|
-
path,
|
|
88
|
-
fetcherId: id,
|
|
89
|
-
requestDataKeys: data && typeof data === 'object' ? Object.keys(data) : [],
|
|
90
|
-
requestData: data,
|
|
91
|
-
});
|
|
209
|
+
const callId = this.createTraceCall({ data, fetcherId: id, method, options, path });
|
|
92
210
|
|
|
93
211
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
212
|
+
if (options?.connected) {
|
|
213
|
+
fetchedData[id] = await this.resolveConnectedFetcher(fetcher);
|
|
214
|
+
} else {
|
|
215
|
+
const request = this.request.children(method, path, data);
|
|
216
|
+
if (callId)
|
|
217
|
+
request.traceCall = {
|
|
218
|
+
fetcherId: id,
|
|
219
|
+
id: callId,
|
|
220
|
+
label: id,
|
|
221
|
+
origin: this.getTraceCallOrigin(),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const response = await request.router.resolve(request);
|
|
225
|
+
fetchedData[id] = response.data;
|
|
226
|
+
this.request.router.app.container.Trace.finishCall(this.request.id, callId, {
|
|
227
|
+
statusCode: response.statusCode,
|
|
228
|
+
resultKeys:
|
|
229
|
+
response.data && typeof response.data === 'object' && !Array.isArray(response.data)
|
|
230
|
+
? Object.keys(response.data as Record<string, unknown>)
|
|
231
|
+
: [],
|
|
232
|
+
result: response.data as object | string | number | boolean | bigint | symbol | null | undefined,
|
|
233
|
+
});
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
96
237
|
this.request.router.app.container.Trace.finishCall(this.request.id, callId, {
|
|
97
|
-
statusCode:
|
|
238
|
+
statusCode: 200,
|
|
98
239
|
resultKeys:
|
|
99
|
-
|
|
100
|
-
? Object.keys(
|
|
240
|
+
fetchedData[id] && typeof fetchedData[id] === 'object' && !Array.isArray(fetchedData[id])
|
|
241
|
+
? Object.keys(fetchedData[id] as Record<string, unknown>)
|
|
101
242
|
: [],
|
|
102
|
-
result:
|
|
243
|
+
result: fetchedData[id] as object | string | number | boolean | bigint | symbol | null | undefined,
|
|
103
244
|
});
|
|
104
245
|
} catch (error) {
|
|
105
246
|
const typedError = error instanceof Error ? error : new Error(typeof error === 'string' ? error : 'Unknown error');
|
|
@@ -10,6 +10,7 @@ import Bowser from 'bowser';
|
|
|
10
10
|
|
|
11
11
|
// Core
|
|
12
12
|
import BaseRequest from '@common/router/request';
|
|
13
|
+
import type { TTraceCallOrigin } from '@common/dev/requestTrace';
|
|
13
14
|
|
|
14
15
|
// Specific
|
|
15
16
|
import type { HttpMethod, HttpHeaders } from '..';
|
|
@@ -35,6 +36,12 @@ const localeFilter = (input: any) => {
|
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
export type UploadedFile = File;
|
|
39
|
+
type TRequestTraceCallContext = {
|
|
40
|
+
fetcherId?: string;
|
|
41
|
+
id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
origin: TTraceCallOrigin;
|
|
44
|
+
};
|
|
38
45
|
|
|
39
46
|
/*----------------------------------
|
|
40
47
|
- CONTEXTE
|
|
@@ -66,6 +73,7 @@ export default class ServerRequest<TRouter extends TAnyRouter = TAnyRouter> exte
|
|
|
66
73
|
|
|
67
74
|
// Services
|
|
68
75
|
public api: ApiClient;
|
|
76
|
+
public traceCall?: TRequestTraceCallContext;
|
|
69
77
|
|
|
70
78
|
/*----------------------------------
|
|
71
79
|
- INITIALISATION
|
|
@@ -141,6 +141,11 @@ export default class ServerResponse<
|
|
|
141
141
|
target: getRouteTraceTarget(route as TAnyRoute<TRouterContext<TServerRouter>>),
|
|
142
142
|
routeId: route.options.id || '',
|
|
143
143
|
filepath: route.options.filepath || '',
|
|
144
|
+
source: {
|
|
145
|
+
filepath: route.options.filepath || '',
|
|
146
|
+
line: route.options.sourceLocation?.line || 0,
|
|
147
|
+
column: route.options.sourceLocation?.column || 0,
|
|
148
|
+
},
|
|
144
149
|
accept: route.options.accept || '',
|
|
145
150
|
},
|
|
146
151
|
'summary',
|
|
@@ -220,7 +225,14 @@ export default class ServerResponse<
|
|
|
220
225
|
this.app.container.Trace.record(
|
|
221
226
|
this.request.id,
|
|
222
227
|
'setup.options',
|
|
223
|
-
{
|
|
228
|
+
{
|
|
229
|
+
optionKeys: Object.keys(options),
|
|
230
|
+
source: {
|
|
231
|
+
filepath: route.options.filepath || '',
|
|
232
|
+
line: route.options.sourceLocation?.line || 0,
|
|
233
|
+
column: route.options.sourceLocation?.column || 0,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
224
236
|
'resolve',
|
|
225
237
|
);
|
|
226
238
|
|
|
@@ -260,6 +272,11 @@ export default class ServerResponse<
|
|
|
260
272
|
{
|
|
261
273
|
target: getRouteTraceTarget(route as TAnyRoute<TRouterContext<TServerRouter>>),
|
|
262
274
|
routeId: route.options.id || '',
|
|
275
|
+
source: {
|
|
276
|
+
filepath: route.options.filepath || '',
|
|
277
|
+
line: route.options.sourceLocation?.line || 0,
|
|
278
|
+
column: route.options.sourceLocation?.column || 0,
|
|
279
|
+
},
|
|
263
280
|
routerServiceKeys: Object.keys(contextServices),
|
|
264
281
|
controllerKeys: Object.keys(controllers),
|
|
265
282
|
customContextKeys: Object.keys(customSsrData as object),
|
|
@@ -318,6 +335,11 @@ export default class ServerResponse<
|
|
|
318
335
|
chunkId: page.chunkId || '',
|
|
319
336
|
dataKeys: Object.keys(page.data || {}),
|
|
320
337
|
data: page.data || {},
|
|
338
|
+
source: {
|
|
339
|
+
filepath: page.route.options.filepath || '',
|
|
340
|
+
line: page.route.options.sourceLocation?.line || 0,
|
|
341
|
+
column: page.route.options.sourceLocation?.column || 0,
|
|
342
|
+
},
|
|
321
343
|
},
|
|
322
344
|
'resolve',
|
|
323
345
|
);
|