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
|
@@ -24,9 +24,17 @@ import type CronManager from '@server/services/cron';
|
|
|
24
24
|
import type CronTask from '@server/services/cron/CronTask';
|
|
25
25
|
import type { TBasicUser } from '@server/services/auth';
|
|
26
26
|
import type { TServerRouter } from '..';
|
|
27
|
+
import type { TDevConsoleLogLevel } from '@common/dev/console';
|
|
28
|
+
import type { TPerfGroupBy } from '@common/dev/performance';
|
|
27
29
|
import type { TDevSessionStartResponse, TDevSessionUserSummary } from '@common/dev/session';
|
|
28
30
|
import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
29
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';
|
|
30
38
|
|
|
31
39
|
// Middlewaees (core)
|
|
32
40
|
import { isMutipart, MiddlewareFormData } from './multipart';
|
|
@@ -84,6 +92,27 @@ const createContentSecurityPolicy = (config: Config['csp']): TContentSecurityPol
|
|
|
84
92
|
};
|
|
85
93
|
};
|
|
86
94
|
|
|
95
|
+
const immutablePublicAssetCacheControl = 'public, max-age=31536000, immutable';
|
|
96
|
+
const revalidatedPublicAssetCacheControl = 'public, max-age=0, must-revalidate';
|
|
97
|
+
const hashedPublicAssetPattern = /(^|[-_.])[a-f0-9]{6,}(?=(\.[^.]+)+$)/i;
|
|
98
|
+
const connectedProjectBootRetryCount = 10;
|
|
99
|
+
const connectedProjectBootRetryDelayMs = 5_000;
|
|
100
|
+
|
|
101
|
+
const isVersionedPublicAssetRequest = (res: express.Response, filePath: string) => {
|
|
102
|
+
const requestUrl = res.req?.originalUrl || res.req?.url || '';
|
|
103
|
+
const searchParams = new URL(requestUrl, 'http://proteum.local').searchParams;
|
|
104
|
+
if (searchParams.has('v')) return true;
|
|
105
|
+
|
|
106
|
+
return hashedPublicAssetPattern.test(path.basename(filePath));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const resolvePublicAssetCacheControl = (res: express.Response, filePath: string) =>
|
|
110
|
+
isVersionedPublicAssetRequest(res, filePath) ? immutablePublicAssetCacheControl : revalidatedPublicAssetCacheControl;
|
|
111
|
+
const wait = async (durationMs: number) =>
|
|
112
|
+
await new Promise<void>((resolve) => {
|
|
113
|
+
setTimeout(resolve, durationMs);
|
|
114
|
+
});
|
|
115
|
+
|
|
87
116
|
/*----------------------------------
|
|
88
117
|
- FUNCTION
|
|
89
118
|
----------------------------------*/
|
|
@@ -138,6 +167,143 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
138
167
|
};
|
|
139
168
|
}
|
|
140
169
|
|
|
170
|
+
private async verifyConnectedProjectsBeforeStart() {
|
|
171
|
+
for (const connectedProject of Object.values(this.app.connectedProjects || {})) {
|
|
172
|
+
const healthUrl = new URL(connectedProjectHealthPath, connectedProject.urlInternal).toString();
|
|
173
|
+
let lastError: Error | undefined;
|
|
174
|
+
|
|
175
|
+
for (let retryIndex = 0; retryIndex <= connectedProjectBootRetryCount; retryIndex++) {
|
|
176
|
+
try {
|
|
177
|
+
const response = await fetch(healthUrl, {
|
|
178
|
+
headers: { Accept: 'application/json' },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
throw new Error(
|
|
183
|
+
`Connected project "${connectedProject.namespace}" health check failed at ${healthUrl} with status ${response.status}.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
lastError = undefined;
|
|
188
|
+
break;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
lastError =
|
|
191
|
+
error instanceof Error && error.message.startsWith(`Connected project "${connectedProject.namespace}"`)
|
|
192
|
+
? error
|
|
193
|
+
: new Error(
|
|
194
|
+
`Connected project "${connectedProject.namespace}" is unreachable at ${connectedProject.urlInternal}. ${error instanceof Error ? error.message : String(error)}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!lastError) continue;
|
|
199
|
+
if (retryIndex === connectedProjectBootRetryCount) throw lastError;
|
|
200
|
+
|
|
201
|
+
console.warn(
|
|
202
|
+
`[connect] ${lastError.message} Retrying ${retryIndex + 1}/${connectedProjectBootRetryCount} in ${connectedProjectBootRetryDelayMs / 1000}s.`,
|
|
203
|
+
);
|
|
204
|
+
await wait(connectedProjectBootRetryDelayMs);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private registerConnectedProjectRoutes(routes: express.Express) {
|
|
210
|
+
routes.get(connectedProjectHealthPath, (_req, res) => {
|
|
211
|
+
res.json({
|
|
212
|
+
connectedProjects: Object.keys(this.app.connectedProjects || {}),
|
|
213
|
+
identifier: this.app.identity.identifier,
|
|
214
|
+
ok: true,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
routes.all(`${connectedProjectHealthPath}/*`, (_req, res) => {
|
|
219
|
+
res.status(404).json({ error: 'Unknown Proteum connected-project route.' });
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
routes.all(`${connectedProjectProxyPathPrefix}/:namespace/*`, async (req, res, next) => {
|
|
223
|
+
const parsed = parseConnectedProjectProxyPath(req.path);
|
|
224
|
+
if (!parsed) {
|
|
225
|
+
next();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const connectedProject = this.app.connectedProjects?.[parsed.namespace];
|
|
230
|
+
if (!connectedProject) {
|
|
231
|
+
res.status(404).json({ error: `Unknown connected project "${parsed.namespace}".` });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const search = new URL(req.originalUrl, 'http://proteum.local').search;
|
|
236
|
+
const targetUrl = new URL(`${parsed.httpPath}${search}`, connectedProject.urlInternal).toString();
|
|
237
|
+
const headers = new Headers();
|
|
238
|
+
|
|
239
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
240
|
+
if (!value) continue;
|
|
241
|
+
if (key === 'content-length' || key === 'host') continue;
|
|
242
|
+
headers.set(key, Array.isArray(value) ? value.join(', ') : String(value));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (!headers.has('accept')) headers.set('accept', 'application/json');
|
|
246
|
+
|
|
247
|
+
const init: RequestInit = {
|
|
248
|
+
method: req.method,
|
|
249
|
+
headers,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
253
|
+
if (req.files && Object.keys(req.files).length > 0) {
|
|
254
|
+
res.status(501).json({
|
|
255
|
+
error: `Connected project proxy does not support multipart payloads for ${parsed.namespace} yet.`,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const contentType = String(req.headers['content-type'] || '').toLowerCase();
|
|
261
|
+
|
|
262
|
+
if (contentType.includes('application/json')) {
|
|
263
|
+
headers.set('content-type', 'application/json');
|
|
264
|
+
init.body = JSON.stringify(req.body || {});
|
|
265
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
266
|
+
headers.set('content-type', 'application/x-www-form-urlencoded');
|
|
267
|
+
init.body = new URLSearchParams(req.body as Record<string, string>).toString();
|
|
268
|
+
} else {
|
|
269
|
+
headers.delete('content-type');
|
|
270
|
+
init.body = undefined;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let response: Response;
|
|
275
|
+
try {
|
|
276
|
+
response = await fetch(targetUrl, init);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
res.status(502).json({
|
|
279
|
+
error: `Failed to proxy connected request to ${connectedProject.namespace}. ${error instanceof Error ? error.message : String(error)}`,
|
|
280
|
+
});
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const traceRequestId = response.headers.get(profilerTraceRequestIdHeader);
|
|
285
|
+
if (traceRequestId) res.setHeader(profilerTraceRequestIdHeader, traceRequestId);
|
|
286
|
+
|
|
287
|
+
const setCookie = typeof (response.headers as Headers & { getSetCookie?: () => string[] }).getSetCookie === 'function'
|
|
288
|
+
? (response.headers as Headers & { getSetCookie: () => string[] }).getSetCookie()
|
|
289
|
+
: [];
|
|
290
|
+
if (setCookie.length > 0) {
|
|
291
|
+
res.setHeader('set-cookie', setCookie);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const contentType = response.headers.get('content-type') || '';
|
|
295
|
+
res.status(response.status);
|
|
296
|
+
if (contentType) res.setHeader('content-type', contentType);
|
|
297
|
+
|
|
298
|
+
if (contentType.includes('application/json')) {
|
|
299
|
+
res.json(await response.json());
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
res.send(await response.text());
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
141
307
|
/*----------------------------------
|
|
142
308
|
- HOOKS
|
|
143
309
|
----------------------------------*/
|
|
@@ -180,6 +346,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
180
346
|
);
|
|
181
347
|
routes.use(apiMultipartOnly(MiddlewareFormData));
|
|
182
348
|
if (this.config.cors !== undefined) routes.use(apiOnly(cors(this.config.cors)));
|
|
349
|
+
this.registerConnectedProjectRoutes(routes);
|
|
183
350
|
routes.use(apiOnly(routeRequest));
|
|
184
351
|
|
|
185
352
|
// Diverses protections (dont le disable x-powered-by)
|
|
@@ -199,17 +366,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
199
366
|
'/public',
|
|
200
367
|
express.static(path.join(Container.path.root, APP_OUTPUT_DIR, 'public'), {
|
|
201
368
|
dotfiles: 'deny',
|
|
202
|
-
setHeaders: function setCustomCacheControl(res,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
res.setHeader('Cache-Control', 'public, max-age=0');
|
|
206
|
-
|
|
207
|
-
// Set long term cache, except for non-hashed filenames
|
|
208
|
-
/*if (dontCache.some( p => path.startsWith( p ))) {
|
|
209
|
-
res.setHeader('Cache-Control', 'public, max-age=0');
|
|
210
|
-
} else {
|
|
211
|
-
res.setHeader('Cache-Control', 'public, max-age=604800000'); // 7 Days
|
|
212
|
-
}*/
|
|
369
|
+
setHeaders: function setCustomCacheControl(res, filePath) {
|
|
370
|
+
res.setHeader('Cache-Control', resolvePublicAssetCacheControl(res, filePath));
|
|
213
371
|
},
|
|
214
372
|
}),
|
|
215
373
|
(req, res) => {
|
|
@@ -268,6 +426,8 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
268
426
|
/*----------------------------------
|
|
269
427
|
- BOOT SERVICES
|
|
270
428
|
----------------------------------*/
|
|
429
|
+
await this.verifyConnectedProjectsBeforeStart();
|
|
430
|
+
|
|
271
431
|
this.http.listen(this.config.port, () => {
|
|
272
432
|
if (__DEV__ && typeof process.send === 'function') {
|
|
273
433
|
process.send({
|
|
@@ -348,6 +508,17 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
348
508
|
}
|
|
349
509
|
});
|
|
350
510
|
|
|
511
|
+
routes.get('/__proteum/explain/owner', (req, res) => {
|
|
512
|
+
const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
res.json(this.app.getDevDiagnostics().explainOwner(typeof query === 'string' ? query : ''));
|
|
516
|
+
} catch (error) {
|
|
517
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
518
|
+
res.status(message.includes('required') ? 400 : 500).json({ error: message });
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
351
522
|
routes.get('/__proteum/doctor', (req, res) => {
|
|
352
523
|
const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
|
|
353
524
|
const strict = rawStrict === '1' || rawStrict === 'true';
|
|
@@ -359,6 +530,129 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
359
530
|
}
|
|
360
531
|
});
|
|
361
532
|
|
|
533
|
+
routes.get('/__proteum/doctor/contracts', (req, res) => {
|
|
534
|
+
const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
|
|
535
|
+
const strict = rawStrict === '1' || rawStrict === 'true';
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
res.json(this.app.getDevDiagnostics().doctorContracts(strict));
|
|
539
|
+
} catch (error) {
|
|
540
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
routes.get('/__proteum/logs', (req, res) => {
|
|
545
|
+
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
546
|
+
const rawLevel = Array.isArray(req.query.level) ? req.query.level[0] : req.query.level;
|
|
547
|
+
const limit = Math.max(0, Math.min(500, Number(rawLimit) || 100));
|
|
548
|
+
const level = typeof rawLevel === 'string' ? (rawLevel as TDevConsoleLogLevel) : 'log';
|
|
549
|
+
|
|
550
|
+
try {
|
|
551
|
+
res.json(this.app.getDevDiagnostics().readLogs(limit, level));
|
|
552
|
+
} catch (error) {
|
|
553
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
routes.get('/__proteum/diagnose', (req, res) => {
|
|
558
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
559
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
560
|
+
const parsed = Number(readString(value));
|
|
561
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
res.json(
|
|
566
|
+
this.app.getDevDiagnostics().diagnose({
|
|
567
|
+
logsLevel:
|
|
568
|
+
typeof readString(req.query.logsLevel) === 'string'
|
|
569
|
+
? (readString(req.query.logsLevel) as TDevConsoleLogLevel)
|
|
570
|
+
: 'warn',
|
|
571
|
+
logsLimit: readNumber(req.query.logsLimit, 40),
|
|
572
|
+
path: typeof readString(req.query.path) === 'string' ? readString(req.query.path) : undefined,
|
|
573
|
+
query: typeof readString(req.query.query) === 'string' ? readString(req.query.query) : undefined,
|
|
574
|
+
requestId: typeof readString(req.query.requestId) === 'string' ? readString(req.query.requestId) : undefined,
|
|
575
|
+
strict: readString(req.query.strict) === '1' || readString(req.query.strict) === 'true',
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
580
|
+
res.status(message.includes('required') || message.includes('Diagnose requires') ? 400 : 500).json({ error: message });
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
routes.get('/__proteum/perf/top', (req, res) => {
|
|
585
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
586
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
587
|
+
const parsed = Number(readString(value));
|
|
588
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
res.json(
|
|
593
|
+
this.app.getDevDiagnostics().perfTop({
|
|
594
|
+
groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
|
|
595
|
+
limit: readNumber(req.query.limit, 12),
|
|
596
|
+
since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
|
|
597
|
+
}),
|
|
598
|
+
);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
routes.get('/__proteum/perf/compare', (req, res) => {
|
|
605
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
606
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
607
|
+
const parsed = Number(readString(value));
|
|
608
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
res.json(
|
|
613
|
+
this.app.getDevDiagnostics().perfCompare({
|
|
614
|
+
baseline: typeof readString(req.query.baseline) === 'string' ? readString(req.query.baseline) : undefined,
|
|
615
|
+
groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
|
|
616
|
+
limit: readNumber(req.query.limit, 12),
|
|
617
|
+
target: typeof readString(req.query.target) === 'string' ? readString(req.query.target) : undefined,
|
|
618
|
+
}),
|
|
619
|
+
);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
routes.get('/__proteum/perf/memory', (req, res) => {
|
|
626
|
+
const readString = (value: unknown) => (Array.isArray(value) ? value[0] : value);
|
|
627
|
+
const readNumber = (value: unknown, fallback: number) => {
|
|
628
|
+
const parsed = Number(readString(value));
|
|
629
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
res.json(
|
|
634
|
+
this.app.getDevDiagnostics().perfMemory({
|
|
635
|
+
groupBy: typeof readString(req.query.groupBy) === 'string' ? (readString(req.query.groupBy) as TPerfGroupBy) : undefined,
|
|
636
|
+
limit: readNumber(req.query.limit, 12),
|
|
637
|
+
since: typeof readString(req.query.since) === 'string' ? readString(req.query.since) : undefined,
|
|
638
|
+
}),
|
|
639
|
+
);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
res.status(400).json({ error: error instanceof Error ? error.message : String(error) });
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
routes.get('/__proteum/perf/request', (req, res) => {
|
|
646
|
+
const query = Array.isArray(req.query.query) ? req.query.query[0] : req.query.query;
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
res.json(this.app.getDevDiagnostics().perfRequest(typeof query === 'string' ? query : ''));
|
|
650
|
+
} catch (error) {
|
|
651
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
652
|
+
res.status(message.includes('Could not find') || message.includes('required') ? 404 : 400).json({ error: message });
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
|
|
362
656
|
routes.get('/__proteum/cron/tasks', (_req, res) => {
|
|
363
657
|
const cron = this.getCronManager();
|
|
364
658
|
res.json({
|