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.
Files changed (95) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +109 -17
  3. package/agents/project/AGENTS.md +188 -25
  4. package/agents/project/CODING_STYLE.md +1 -0
  5. package/agents/project/client/AGENTS.md +13 -8
  6. package/agents/project/client/pages/AGENTS.md +17 -9
  7. package/agents/project/diagnostics.md +52 -0
  8. package/agents/project/optimizations.md +48 -0
  9. package/agents/project/server/routes/AGENTS.md +9 -6
  10. package/agents/project/server/services/AGENTS.md +10 -6
  11. package/agents/project/tests/AGENTS.md +11 -5
  12. package/cli/app/config.ts +13 -14
  13. package/cli/app/index.ts +58 -0
  14. package/cli/commands/connect.ts +45 -0
  15. package/cli/commands/dev.ts +37 -13
  16. package/cli/commands/diagnose.ts +286 -0
  17. package/cli/commands/doctor.ts +18 -5
  18. package/cli/commands/explain.ts +25 -0
  19. package/cli/commands/perf.ts +243 -0
  20. package/cli/commands/trace.ts +9 -1
  21. package/cli/commands/verify.ts +281 -0
  22. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  23. package/cli/compiler/artifacts/controllers.ts +198 -49
  24. package/cli/compiler/artifacts/discovery.ts +0 -34
  25. package/cli/compiler/artifacts/manifest.ts +95 -6
  26. package/cli/compiler/artifacts/routing.ts +2 -2
  27. package/cli/compiler/artifacts/services.ts +277 -130
  28. package/cli/compiler/client/index.ts +3 -0
  29. package/cli/compiler/common/files/style.ts +52 -0
  30. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  31. package/cli/compiler/common/scripts.ts +11 -5
  32. package/cli/compiler/index.ts +2 -1
  33. package/cli/compiler/server/index.ts +3 -0
  34. package/cli/presentation/commands.ts +110 -7
  35. package/cli/presentation/devSession.ts +32 -7
  36. package/cli/runtime/commands.ts +165 -6
  37. package/cli/scaffold/index.ts +18 -27
  38. package/cli/scaffold/templates.ts +48 -28
  39. package/cli/utils/agents.ts +106 -13
  40. package/cli/utils/keyboard.ts +8 -0
  41. package/client/dev/profiler/ApexChart.tsx +66 -0
  42. package/client/dev/profiler/index.tsx +2508 -302
  43. package/client/dev/profiler/runtime.noop.ts +12 -0
  44. package/client/dev/profiler/runtime.ts +195 -4
  45. package/client/services/router/request/api.ts +6 -1
  46. package/common/applicationConfig.ts +173 -0
  47. package/common/applicationConfigLoader.ts +102 -0
  48. package/common/connectedProjects.ts +113 -0
  49. package/common/dev/connect.ts +267 -0
  50. package/common/dev/console.ts +31 -0
  51. package/common/dev/contractsDoctor.ts +128 -0
  52. package/common/dev/diagnostics.ts +59 -15
  53. package/common/dev/inspection.ts +491 -0
  54. package/common/dev/performance.ts +809 -0
  55. package/common/dev/profiler.ts +3 -0
  56. package/common/dev/proteumManifest.ts +31 -6
  57. package/common/dev/requestTrace.ts +52 -1
  58. package/common/env/proteumEnv.ts +176 -50
  59. package/common/router/index.ts +1 -0
  60. package/common/router/request/api.ts +2 -0
  61. package/config.ts +5 -0
  62. package/docs/dev-commands.md +5 -1
  63. package/docs/dev-sessions.md +90 -0
  64. package/docs/diagnostics.md +74 -11
  65. package/docs/request-tracing.md +50 -3
  66. package/package.json +1 -1
  67. package/server/app/container/config.ts +16 -87
  68. package/server/app/container/console/index.ts +42 -8
  69. package/server/app/container/index.ts +10 -2
  70. package/server/app/container/trace/index.ts +105 -0
  71. package/server/app/devDiagnostics.ts +138 -0
  72. package/server/app/index.ts +18 -8
  73. package/server/app/service/container.ts +0 -12
  74. package/server/app/service/index.ts +0 -2
  75. package/server/services/prisma/index.ts +121 -4
  76. package/server/services/router/http/index.ts +305 -11
  77. package/server/services/router/index.ts +116 -57
  78. package/server/services/router/request/api.ts +160 -19
  79. package/server/services/router/request/index.ts +8 -0
  80. package/server/services/router/response/index.ts +23 -1
  81. package/server/services/router/response/page/document.tsx +31 -14
  82. package/server/services/router/response/page/index.tsx +10 -0
  83. package/agents/framework/AGENTS.md +0 -177
  84. package/server/services/auth/router/service.json +0 -6
  85. package/server/services/auth/service.json +0 -6
  86. package/server/services/cron/service.json +0 -6
  87. package/server/services/disks/drivers/local/service.json +0 -6
  88. package/server/services/disks/drivers/s3/service.json +0 -6
  89. package/server/services/disks/service.json +0 -6
  90. package/server/services/fetch/service.json +0 -7
  91. package/server/services/prisma/service.json +0 -6
  92. package/server/services/router/service.json +0 -6
  93. package/server/services/schema/router/service.json +0 -6
  94. package/server/services/schema/service.json +0 -6
  95. 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 = { ...defaultOptions, ...options };
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: { ...defaultOptions, ...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
- { cached: true, statusCode: response.statusCode, contentType: 'text/html' },
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
- // Set in cache
884
- if (response.request.path && route.options.static && route.options.static.urls.includes('*')) {
885
- console.log('[router] Set in cache', response.request.path);
886
- this.renderStatic(response.request.path, route.options.static, response.data);
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
- // TODO: use api.fetchSync instead
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 ('then' in fetcher) {
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
- // Create a children request to resolve the api data
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
- const response = await request.router.resolve(request);
95
- fetchedData[id] = response.data;
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: response.statusCode,
238
+ statusCode: 200,
98
239
  resultKeys:
99
- response.data && typeof response.data === 'object' && !Array.isArray(response.data)
100
- ? Object.keys(response.data as Record<string, unknown>)
240
+ fetchedData[id] && typeof fetchedData[id] === 'object' && !Array.isArray(fetchedData[id])
241
+ ? Object.keys(fetchedData[id] as Record<string, unknown>)
101
242
  : [],
102
- result: response.data,
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
- { optionKeys: Object.keys(options) },
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
  );