proteum 2.1.2 → 2.1.6

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 (99) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +112 -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/command.ts +8 -0
  15. package/cli/commands/connect.ts +45 -0
  16. package/cli/commands/dev.ts +26 -11
  17. package/cli/commands/diagnose.ts +286 -0
  18. package/cli/commands/doctor.ts +18 -5
  19. package/cli/commands/explain.ts +25 -0
  20. package/cli/commands/perf.ts +243 -0
  21. package/cli/commands/session.ts +254 -0
  22. package/cli/commands/sessionLocalRunner.js +188 -0
  23. package/cli/commands/trace.ts +17 -1
  24. package/cli/commands/verify.ts +281 -0
  25. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  26. package/cli/compiler/artifacts/controllers.ts +198 -49
  27. package/cli/compiler/artifacts/discovery.ts +0 -34
  28. package/cli/compiler/artifacts/manifest.ts +90 -6
  29. package/cli/compiler/artifacts/routing.ts +2 -2
  30. package/cli/compiler/artifacts/services.ts +277 -130
  31. package/cli/compiler/client/index.ts +3 -0
  32. package/cli/compiler/common/files/style.ts +52 -0
  33. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  34. package/cli/compiler/common/scripts.ts +11 -5
  35. package/cli/compiler/index.ts +2 -1
  36. package/cli/compiler/server/index.ts +3 -0
  37. package/cli/presentation/commands.ts +136 -7
  38. package/cli/presentation/devSession.ts +32 -7
  39. package/cli/runtime/commands.ts +193 -6
  40. package/cli/scaffold/index.ts +14 -25
  41. package/cli/scaffold/templates.ts +41 -27
  42. package/cli/utils/agents.ts +4 -2
  43. package/cli/utils/keyboard.ts +8 -0
  44. package/client/dev/profiler/ApexChart.tsx +66 -0
  45. package/client/dev/profiler/index.tsx +2798 -417
  46. package/client/dev/profiler/runtime.noop.ts +12 -0
  47. package/client/dev/profiler/runtime.ts +195 -4
  48. package/client/services/router/request/api.ts +6 -1
  49. package/common/applicationConfig.ts +173 -0
  50. package/common/applicationConfigLoader.ts +102 -0
  51. package/common/connectedProjects.ts +113 -0
  52. package/common/dev/connect.ts +267 -0
  53. package/common/dev/console.ts +31 -0
  54. package/common/dev/contractsDoctor.ts +128 -0
  55. package/common/dev/diagnostics.ts +59 -15
  56. package/common/dev/inspection.ts +491 -0
  57. package/common/dev/performance.ts +809 -0
  58. package/common/dev/profiler.ts +3 -0
  59. package/common/dev/proteumManifest.ts +31 -6
  60. package/common/dev/requestTrace.ts +56 -1
  61. package/common/dev/session.ts +24 -0
  62. package/common/env/proteumEnv.ts +176 -50
  63. package/common/router/index.ts +1 -0
  64. package/common/router/request/api.ts +2 -0
  65. package/config.ts +5 -0
  66. package/docs/dev-commands.md +5 -1
  67. package/docs/dev-sessions.md +90 -0
  68. package/docs/diagnostics.md +74 -11
  69. package/docs/request-tracing.md +50 -3
  70. package/package.json +1 -1
  71. package/server/app/container/config.ts +16 -87
  72. package/server/app/container/console/index.ts +42 -8
  73. package/server/app/container/index.ts +3 -1
  74. package/server/app/container/trace/index.ts +153 -0
  75. package/server/app/devDiagnostics.ts +138 -0
  76. package/server/app/index.ts +18 -8
  77. package/server/app/service/container.ts +0 -12
  78. package/server/app/service/index.ts +0 -2
  79. package/server/services/prisma/index.ts +121 -4
  80. package/server/services/router/http/index.ts +352 -0
  81. package/server/services/router/index.ts +50 -47
  82. package/server/services/router/request/api.ts +160 -19
  83. package/server/services/router/request/index.ts +8 -0
  84. package/server/services/router/response/index.ts +24 -1
  85. package/server/services/router/response/page/document.tsx +5 -0
  86. package/server/services/router/response/page/index.tsx +10 -0
  87. package/agents/framework/AGENTS.md +0 -177
  88. package/server/services/auth/router/service.json +0 -6
  89. package/server/services/auth/service.json +0 -6
  90. package/server/services/cron/service.json +0 -6
  91. package/server/services/disks/drivers/local/service.json +0 -6
  92. package/server/services/disks/drivers/s3/service.json +0 -6
  93. package/server/services/disks/service.json +0 -6
  94. package/server/services/fetch/service.json +0 -7
  95. package/server/services/prisma/service.json +0 -6
  96. package/server/services/router/service.json +0 -6
  97. package/server/services/schema/router/service.json +0 -6
  98. package/server/services/schema/service.json +0 -6
  99. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -22,9 +22,19 @@ import cookieParser from 'cookie-parser';
22
22
  import Container from '@server/app/container';
23
23
  import type CronManager from '@server/services/cron';
24
24
  import type CronTask from '@server/services/cron/CronTask';
25
+ import type { TBasicUser } from '@server/services/auth';
25
26
  import type { TServerRouter } from '..';
27
+ import type { TDevConsoleLogLevel } from '@common/dev/console';
28
+ import type { TPerfGroupBy } from '@common/dev/performance';
29
+ import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
26
30
  import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
27
31
  import { explainSectionNames } from '@common/dev/diagnostics';
32
+ import {
33
+ connectedProjectHealthPath,
34
+ connectedProjectProxyPathPrefix,
35
+ parseConnectedProjectProxyPath,
36
+ } from '@common/connectedProjects';
37
+ import { profilerTraceRequestIdHeader } from '@common/dev/profiler';
28
38
 
29
39
  // Middlewaees (core)
30
40
  import { isMutipart, MiddlewareFormData } from './multipart';
@@ -54,6 +64,15 @@ export type Hooks = {};
54
64
 
55
65
  type TContentSecurityPolicyOptions = NonNullable<Parameters<typeof helmet.contentSecurityPolicy>[0]>;
56
66
  type TContentSecurityPolicyDirectives = NonNullable<TContentSecurityPolicyOptions['directives']>;
67
+ type TDevSessionAuthService = {
68
+ config: {
69
+ jwt: {
70
+ expiration: number;
71
+ };
72
+ };
73
+ createSession: (session: { email: string }, request: { id: string; res: express.Response }) => string;
74
+ decodeSession: (session: { email: string }, req: express.Request) => Promise<TBasicUser | null>;
75
+ };
57
76
 
58
77
  const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPolicyOptions => {
59
78
  const directives: TContentSecurityPolicyDirectives = {
@@ -101,6 +120,153 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
101
120
  this.app.on('cleanup', () => this.cleanup());
102
121
  }
103
122
 
123
+ private resolveDevSessionAuthService(): TDevSessionAuthService {
124
+ const plugins = Object.values(this.router.config.plugins || {}) as Array<{ users?: unknown }>;
125
+
126
+ for (const plugin of plugins) {
127
+ const users = plugin?.users as Partial<TDevSessionAuthService> | undefined;
128
+ if (!users) continue;
129
+ if (typeof users.createSession !== 'function') continue;
130
+ if (typeof users.decodeSession !== 'function') continue;
131
+ if (typeof users.config?.jwt?.expiration !== 'number') continue;
132
+
133
+ return users as TDevSessionAuthService;
134
+ }
135
+
136
+ throw new Error('No auth router plugin with a compatible users service is registered.');
137
+ }
138
+
139
+ private summarizeDevSessionUser(user: TBasicUser): TDevSessionUserSummary {
140
+ return {
141
+ email: user.email,
142
+ name: user.name,
143
+ type: user.type,
144
+ roles: [...user.roles],
145
+ locale: user.locale ?? null,
146
+ };
147
+ }
148
+
149
+ private async verifyConnectedProjectsBeforeStart() {
150
+ for (const connectedProject of Object.values(this.app.connectedProjects || {})) {
151
+ const healthUrl = new URL(connectedProjectHealthPath, connectedProject.urlInternal).toString();
152
+
153
+ let response: Response;
154
+ try {
155
+ response = await fetch(healthUrl, {
156
+ headers: { Accept: 'application/json' },
157
+ });
158
+ } catch (error) {
159
+ throw new Error(
160
+ `Connected project "${connectedProject.namespace}" is unreachable at ${connectedProject.urlInternal}. ${error instanceof Error ? error.message : String(error)}`,
161
+ );
162
+ }
163
+
164
+ if (!response.ok) {
165
+ throw new Error(
166
+ `Connected project "${connectedProject.namespace}" health check failed at ${healthUrl} with status ${response.status}.`,
167
+ );
168
+ }
169
+ }
170
+ }
171
+
172
+ private registerConnectedProjectRoutes(routes: express.Express) {
173
+ routes.get(connectedProjectHealthPath, (_req, res) => {
174
+ res.json({
175
+ connectedProjects: Object.keys(this.app.connectedProjects || {}),
176
+ identifier: this.app.identity.identifier,
177
+ ok: true,
178
+ });
179
+ });
180
+
181
+ routes.all(`${connectedProjectHealthPath}/*`, (_req, res) => {
182
+ res.status(404).json({ error: 'Unknown Proteum connected-project route.' });
183
+ });
184
+
185
+ routes.all(`${connectedProjectProxyPathPrefix}/:namespace/*`, async (req, res, next) => {
186
+ const parsed = parseConnectedProjectProxyPath(req.path);
187
+ if (!parsed) {
188
+ next();
189
+ return;
190
+ }
191
+
192
+ const connectedProject = this.app.connectedProjects?.[parsed.namespace];
193
+ if (!connectedProject) {
194
+ res.status(404).json({ error: `Unknown connected project "${parsed.namespace}".` });
195
+ return;
196
+ }
197
+
198
+ const search = new URL(req.originalUrl, 'http://proteum.local').search;
199
+ const targetUrl = new URL(`${parsed.httpPath}${search}`, connectedProject.urlInternal).toString();
200
+ const headers = new Headers();
201
+
202
+ for (const [key, value] of Object.entries(req.headers)) {
203
+ if (!value) continue;
204
+ if (key === 'content-length' || key === 'host') continue;
205
+ headers.set(key, Array.isArray(value) ? value.join(', ') : String(value));
206
+ }
207
+
208
+ if (!headers.has('accept')) headers.set('accept', 'application/json');
209
+
210
+ const init: RequestInit = {
211
+ method: req.method,
212
+ headers,
213
+ };
214
+
215
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
216
+ if (req.files && Object.keys(req.files).length > 0) {
217
+ res.status(501).json({
218
+ error: `Connected project proxy does not support multipart payloads for ${parsed.namespace} yet.`,
219
+ });
220
+ return;
221
+ }
222
+
223
+ const contentType = String(req.headers['content-type'] || '').toLowerCase();
224
+
225
+ if (contentType.includes('application/json')) {
226
+ headers.set('content-type', 'application/json');
227
+ init.body = JSON.stringify(req.body || {});
228
+ } else if (contentType.includes('application/x-www-form-urlencoded')) {
229
+ headers.set('content-type', 'application/x-www-form-urlencoded');
230
+ init.body = new URLSearchParams(req.body as Record<string, string>).toString();
231
+ } else {
232
+ headers.delete('content-type');
233
+ init.body = undefined;
234
+ }
235
+ }
236
+
237
+ let response: Response;
238
+ try {
239
+ response = await fetch(targetUrl, init);
240
+ } catch (error) {
241
+ res.status(502).json({
242
+ error: `Failed to proxy connected request to ${connectedProject.namespace}. ${error instanceof Error ? error.message : String(error)}`,
243
+ });
244
+ return;
245
+ }
246
+
247
+ const traceRequestId = response.headers.get(profilerTraceRequestIdHeader);
248
+ if (traceRequestId) res.setHeader(profilerTraceRequestIdHeader, traceRequestId);
249
+
250
+ const setCookie = typeof (response.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === 'function'
251
+ ? (response.headers as Headers & { getSetCookie: () => string[] }).getSetCookie()
252
+ : [];
253
+ if (setCookie.length > 0) {
254
+ res.setHeader('set-cookie', setCookie);
255
+ }
256
+
257
+ const contentType = response.headers.get('content-type') || '';
258
+ res.status(response.status);
259
+ if (contentType) res.setHeader('content-type', contentType);
260
+
261
+ if (contentType.includes('application/json')) {
262
+ res.json(await response.json());
263
+ return;
264
+ }
265
+
266
+ res.send(await response.text());
267
+ });
268
+ }
269
+
104
270
  /*----------------------------------
105
271
  - HOOKS
106
272
  ----------------------------------*/
@@ -143,6 +309,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
143
309
  );
144
310
  routes.use(apiMultipartOnly(MiddlewareFormData));
145
311
  if (this.config.cors !== undefined) routes.use(apiOnly(cors(this.config.cors)));
312
+ this.registerConnectedProjectRoutes(routes);
146
313
  routes.use(apiOnly(routeRequest));
147
314
 
148
315
  // Diverses protections (dont le disable x-powered-by)
@@ -231,6 +398,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
231
398
  /*----------------------------------
232
399
  - BOOT SERVICES
233
400
  ----------------------------------*/
401
+ await this.verifyConnectedProjectsBeforeStart();
402
+
234
403
  this.http.listen(this.config.port, () => {
235
404
  if (__DEV__ && typeof process.send === 'function') {
236
405
  process.send({
@@ -311,6 +480,17 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
311
480
  }
312
481
  });
313
482
 
483
+ routes.get('/__proteum/explain/owner', (req, res) => {
484
+ const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
485
+
486
+ try {
487
+ res.json(this.app.getDevDiagnostics().explainOwner(typeof query === 'string' ? query : ''));
488
+ } catch (error) {
489
+ const message = error instanceof Error ? error.message : String(error);
490
+ res.status(message.includes('required') ? 400 : 500).json({ error: message });
491
+ }
492
+ });
493
+
314
494
  routes.get('/__proteum/doctor', (req, res) => {
315
495
  const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
316
496
  const strict = rawStrict === '1' || rawStrict === 'true';
@@ -322,6 +502,129 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
322
502
  }
323
503
  });
324
504
 
505
+ routes.get('/__proteum/doctor/contracts', (req, res) => {
506
+ const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
507
+ const strict = rawStrict === '1' || rawStrict === 'true';
508
+
509
+ try {
510
+ res.json(this.app.getDevDiagnostics().doctorContracts(strict));
511
+ } catch (error) {
512
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
513
+ }
514
+ });
515
+
516
+ routes.get('/__proteum/logs', (req, res) => {
517
+ const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
518
+ const rawLevel = Array.isArray(req.query.level) ? req.query.level[0] : req.query.level;
519
+ const limit = Math.max(0, Math.min(500, Number(rawLimit) || 100));
520
+ const level = typeof rawLevel === 'string' ? (rawLevel as TDevConsoleLogLevel) : 'log';
521
+
522
+ try {
523
+ res.json(this.app.getDevDiagnostics().readLogs(limit, level));
524
+ } catch (error) {
525
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
526
+ }
527
+ });
528
+
529
+ routes.get('/__proteum/diagnose', (req, res) => {
530
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
531
+ const readNumber = (value: unknown, fallback: number) => {
532
+ const parsed = Number(readString(value));
533
+ return Number.isFinite(parsed) ? parsed : fallback;
534
+ };
535
+
536
+ try {
537
+ res.json(
538
+ this.app.getDevDiagnostics().diagnose({
539
+ logsLevel:
540
+ typeof readString(req.query.logsLevel) === 'string'
541
+ ? (readString(req.query.logsLevel) as TDevConsoleLogLevel)
542
+ : 'warn',
543
+ logsLimit: readNumber(req.query.logsLimit, 40),
544
+ path: typeof readString(req.query.path) === 'string' ? readString(req.query.path) : undefined,
545
+ query: typeof readString(req.query.query) === 'string' ? readString(req.query.query) : undefined,
546
+ requestId: typeof readString(req.query.requestId) === 'string' ? readString(req.query.requestId) : undefined,
547
+ strict: readString(req.query.strict) === '1' || readString(req.query.strict) === 'true',
548
+ }),
549
+ );
550
+ } catch (error) {
551
+ const message = error instanceof Error ? error.message : String(error);
552
+ res.status(message.includes('required') || message.includes('Diagnose requires') ? 400 : 500).json({ error: message });
553
+ }
554
+ });
555
+
556
+ routes.get('/__proteum/perf/top', (req, res) => {
557
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
558
+ const readNumber = (value: unknown, fallback: number) => {
559
+ const parsed = Number(readString(value));
560
+ return Number.isFinite(parsed) ? parsed : fallback;
561
+ };
562
+
563
+ try {
564
+ res.json(
565
+ this.app.getDevDiagnostics().perfTop({
566
+ groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
567
+ limit: readNumber(req.query.limit, 12),
568
+ since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
569
+ }),
570
+ );
571
+ } catch (error) {
572
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
573
+ }
574
+ });
575
+
576
+ routes.get('/__proteum/perf/compare', (req, res) => {
577
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
578
+ const readNumber = (value: unknown, fallback: number) => {
579
+ const parsed = Number(readString(value));
580
+ return Number.isFinite(parsed) ? parsed : fallback;
581
+ };
582
+
583
+ try {
584
+ res.json(
585
+ this.app.getDevDiagnostics().perfCompare({
586
+ baseline: typeof readString(req.query.baseline) === 'string' ? readString(req.query.baseline) : undefined,
587
+ groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
588
+ limit: readNumber(req.query.limit, 12),
589
+ target: typeof readString(req.query.target) === 'string' ? readString(req.query.target) : undefined,
590
+ }),
591
+ );
592
+ } catch (error) {
593
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
594
+ }
595
+ });
596
+
597
+ routes.get('/__proteum/perf/memory', (req, res) => {
598
+ const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
599
+ const readNumber = (value: unknown, fallback: number) => {
600
+ const parsed = Number(readString(value));
601
+ return Number.isFinite(parsed) ? parsed : fallback;
602
+ };
603
+
604
+ try {
605
+ res.json(
606
+ this.app.getDevDiagnostics().perfMemory({
607
+ groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
608
+ limit: readNumber(req.query.limit, 12),
609
+ since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
610
+ }),
611
+ );
612
+ } catch (error) {
613
+ res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
614
+ }
615
+ });
616
+
617
+ routes.get('/__proteum/perf/request', (req, res) => {
618
+ const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
619
+
620
+ try {
621
+ res.json(this.app.getDevDiagnostics().perfRequest(typeof query === 'string' ? query : ''));
622
+ } catch (error) {
623
+ const message = error instanceof Error ? error.message : String(error);
624
+ res.status(message.includes('Could not find') || message.includes('required') ? 404 : 400).json({ error: message });
625
+ }
626
+ });
627
+
325
628
  routes.get('/__proteum/cron/tasks', (_req, res) => {
326
629
  const cron = this.getCronManager();
327
630
  res.json({
@@ -358,6 +661,55 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
358
661
  }
359
662
  });
360
663
 
664
+ routes.post('/__proteum/session/start', async (req, res) => {
665
+ const email = typeof req.body?.email === 'string' ? req.body.email.trim() : '';
666
+ const requiredRole = typeof req.body?.role === 'string' ? req.body.role.trim() : '';
667
+
668
+ if (!email) {
669
+ res.status(400).json({ error: 'Email is required.' });
670
+ return;
671
+ }
672
+
673
+ try {
674
+ const auth = this.resolveDevSessionAuthService();
675
+ const user = await auth.decodeSession({ email }, req);
676
+
677
+ if (!user) {
678
+ res.status(404).json({ error: `No user could be resolved for "${email}".` });
679
+ return;
680
+ }
681
+
682
+ if (requiredRole && !user.roles.includes(requiredRole)) {
683
+ res.status(403).json({ error: `User "${email}" does not have required role "${requiredRole}".` });
684
+ return;
685
+ }
686
+
687
+ const token = auth.createSession(
688
+ { email },
689
+ {
690
+ id: `proteum-session:${Date.now()}`,
691
+ res,
692
+ },
693
+ );
694
+ const issuedAt = new Date().toISOString();
695
+ const expiresAt = new Date(Date.now() + auth.config.jwt.expiration).toISOString();
696
+ const response: TDevSessionStartResponse = {
697
+ user: this.summarizeDevSessionUser(user),
698
+ session: {
699
+ token,
700
+ cookieName: 'authorization',
701
+ expiresInMs: auth.config.jwt.expiration,
702
+ issuedAt,
703
+ expiresAt,
704
+ },
705
+ };
706
+
707
+ res.json(response);
708
+ } catch (error) {
709
+ res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
710
+ }
711
+ });
712
+
361
713
  routes.post('/__proteum/cron/tasks/run', async (req, res) => {
362
714
  const cron = this.getCronManager();
363
715
  if (!cron) {
@@ -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
  };
@@ -114,6 +116,7 @@ export type Config<
114
116
  disk?: string; // Disk driver ID
115
117
 
116
118
  currentDomain: string;
119
+ defaultRouteOptions?: Partial<TRouteOptions>;
117
120
 
118
121
  http: HttpServiceConfig;
119
122
 
@@ -353,7 +356,7 @@ export default class ServerRouter<
353
356
  const controller = new definition.Controller(requestContext);
354
357
  return controller[definition.method]();
355
358
  },
356
- options: { ...defaultOptions },
359
+ options: { ...defaultOptions, filepath: definition.filepath, sourceLocation: definition.sourceLocation },
357
360
  };
358
361
 
359
362
  this.controllers[route.path] = route;
@@ -363,6 +366,14 @@ export default class ServerRouter<
363
366
  public url = (path: string, params: {} = {}, absolute: boolean = true) =>
364
367
  buildUrl(path, params, this.config.currentDomain, absolute);
365
368
 
369
+ private buildRouteOptions(options: Partial<TRouteOptions> = {}): TRouteOptions {
370
+ return {
371
+ ...defaultOptions,
372
+ ...(this.config.defaultRouteOptions || {}),
373
+ ...options,
374
+ };
375
+ }
376
+
366
377
  /*----------------------------------
367
378
  - REGISTER
368
379
  ----------------------------------*/
@@ -378,12 +389,11 @@ export default class ServerRouter<
378
389
  regex,
379
390
  keys,
380
391
  controller: (context: TRouterContext<this>) => new Page(route, renderer, context, layout),
381
- options: {
382
- ...defaultOptions,
392
+ options: this.buildRouteOptions({
383
393
  accept: 'html', // Les pages retournent forcémment du html
384
394
  setup,
385
395
  ...options,
386
- },
396
+ }),
387
397
  };
388
398
 
389
399
  this.routes.push(route);
@@ -396,7 +406,7 @@ export default class ServerRouter<
396
406
  options: Partial<TRouteOptions>,
397
407
  renderer: TFrontRenderer<{}, { message: string }>,
398
408
  ) {
399
- const finalOptions = { ...defaultOptions, ...options };
409
+ const finalOptions = this.buildRouteOptions(options);
400
410
 
401
411
  // Automatic layout form the nearest _layout folder
402
412
  const layout = getLayout('Error ' + code, finalOptions);
@@ -454,7 +464,7 @@ export default class ServerRouter<
454
464
  path: path,
455
465
  regex,
456
466
  keys: keys,
457
- options: { ...defaultOptions, ...options },
467
+ options: this.buildRouteOptions(options),
458
468
  controller,
459
469
  };
460
470
 
@@ -711,6 +721,14 @@ export default class ServerRouter<
711
721
  channelId: request.id,
712
722
  method: request.method,
713
723
  path: request.path,
724
+ ...(request.traceCall
725
+ ? {
726
+ traceCallFetcherId: request.traceCall.fetcherId,
727
+ traceCallId: request.traceCall.id,
728
+ traceCallLabel: request.traceCall.label,
729
+ traceCallOrigin: request.traceCall.origin,
730
+ }
731
+ : {}),
714
732
  },
715
733
  async () => {
716
734
  const timeStart = Date.now();
@@ -745,6 +763,11 @@ export default class ServerRouter<
745
763
  path: request.path,
746
764
  accept: controllerRoute.options.accept || '',
747
765
  filepath: controllerRoute.options.filepath || '',
766
+ source: {
767
+ filepath: controllerRoute.options.filepath || '',
768
+ line: controllerRoute.options.sourceLocation?.line || 0,
769
+ column: controllerRoute.options.sourceLocation?.column || 0,
770
+ },
748
771
  },
749
772
  'summary',
750
773
  );
@@ -777,6 +800,11 @@ export default class ServerRouter<
777
800
  routePath: route.path || '',
778
801
  routeId: route.options.id || '',
779
802
  filepath: route.options.filepath || '',
803
+ source: {
804
+ filepath: route.options.filepath || '',
805
+ line: route.options.sourceLocation?.line || 0,
806
+ column: route.options.sourceLocation?.column || 0,
807
+ },
780
808
  },
781
809
  'deep',
782
810
  );
@@ -797,6 +825,11 @@ export default class ServerRouter<
797
825
  routePath: route.path || '',
798
826
  routeId: route.options.id || '',
799
827
  filepath: route.options.filepath || '',
828
+ source: {
829
+ filepath: route.options.filepath || '',
830
+ line: route.options.sourceLocation?.line || 0,
831
+ column: route.options.sourceLocation?.column || 0,
832
+ },
800
833
  },
801
834
  'deep',
802
835
  );
@@ -816,6 +849,11 @@ export default class ServerRouter<
816
849
  routePath: route.path || '',
817
850
  routeId: route.options.id || '',
818
851
  filepath: route.options.filepath || '',
852
+ source: {
853
+ filepath: route.options.filepath || '',
854
+ line: route.options.sourceLocation?.line || 0,
855
+ column: route.options.sourceLocation?.column || 0,
856
+ },
819
857
  },
820
858
  'deep',
821
859
  );
@@ -867,6 +905,11 @@ export default class ServerRouter<
867
905
  routePath: route.path || '',
868
906
  routeId: route.options.id || '',
869
907
  filepath: route.options.filepath || '',
908
+ source: {
909
+ filepath: route.options.filepath || '',
910
+ line: route.options.sourceLocation?.line || 0,
911
+ column: route.options.sourceLocation?.column || 0,
912
+ },
870
913
  accept: route.options.accept || '',
871
914
  method: route.method,
872
915
  },
@@ -901,47 +944,7 @@ export default class ServerRouter<
901
944
  };
902
945
 
903
946
  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
- }
947
+ const responseData = await request.api.fetchSync(fetchers, {});
945
948
 
946
949
  // Status
947
950
  request.res.status(200);