proteum 2.1.0 → 2.1.1
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 +44 -98
- package/README.md +121 -7
- package/agents/framework/AGENTS.md +133 -886
- package/agents/project/AGENTS.md +70 -127
- package/agents/project/client/AGENTS.md +22 -93
- package/agents/project/client/pages/AGENTS.md +24 -26
- package/agents/project/server/routes/AGENTS.md +10 -8
- package/agents/project/server/services/AGENTS.md +22 -159
- package/agents/project/tests/AGENTS.md +11 -8
- package/cli/app/config.ts +7 -20
- package/cli/bin.js +8 -0
- package/cli/commands/command.ts +243 -0
- package/cli/commands/commandLocalRunner.js +198 -0
- package/cli/commands/deploy/web.ts +1 -2
- package/cli/commands/dev.ts +96 -1
- package/cli/commands/doctor.ts +8 -74
- package/cli/commands/explain.ts +8 -186
- package/cli/commands/trace.ts +228 -0
- package/cli/compiler/artifacts/commands.ts +217 -0
- package/cli/compiler/artifacts/manifest.ts +35 -21
- package/cli/compiler/artifacts/services.ts +300 -1
- package/cli/compiler/client/index.ts +43 -8
- package/cli/compiler/common/commands.ts +175 -0
- package/cli/compiler/common/index.ts +1 -1
- package/cli/compiler/common/proteumManifest.ts +15 -114
- package/cli/compiler/index.ts +25 -2
- package/cli/compiler/server/index.ts +31 -6
- package/cli/paths.ts +16 -1
- package/cli/presentation/commands.ts +59 -5
- package/cli/presentation/devSession.ts +5 -0
- package/cli/runtime/commands.ts +60 -1
- package/cli/tsconfig.json +4 -1
- package/cli/utils/check.ts +1 -1
- package/client/app/component.tsx +13 -9
- package/client/dev/profiler/index.tsx +1511 -0
- package/client/dev/profiler/noop.tsx +5 -0
- package/client/dev/profiler/runtime.noop.ts +116 -0
- package/client/dev/profiler/runtime.ts +840 -0
- package/client/services/router/components/router.tsx +30 -2
- package/client/services/router/index.tsx +27 -3
- package/client/services/router/request/api.ts +133 -17
- package/commands/proteum/diagnostics.ts +11 -0
- package/common/dev/commands.ts +50 -0
- package/common/dev/diagnostics.ts +298 -0
- package/common/dev/profiler.ts +91 -0
- package/common/dev/proteumManifest.ts +135 -0
- package/common/dev/requestTrace.ts +109 -0
- package/common/env/proteumEnv.ts +284 -0
- package/common/router/index.ts +4 -22
- package/docs/dev-commands.md +86 -0
- package/docs/request-tracing.md +122 -0
- package/package.json +1 -2
- package/server/app/commands.ts +35 -370
- package/server/app/commandsManager.ts +393 -0
- package/server/app/container/config.ts +11 -49
- package/server/app/container/console/index.ts +2 -3
- package/server/app/container/index.ts +5 -2
- package/server/app/container/trace/index.ts +364 -0
- package/server/app/devCommands.ts +192 -0
- package/server/app/devDiagnostics.ts +53 -0
- package/server/app/index.ts +27 -4
- package/server/services/cron/CronTask.ts +73 -5
- package/server/services/cron/index.ts +34 -11
- package/server/services/fetch/index.ts +3 -10
- package/server/services/prisma/index.ts +66 -4
- package/server/services/router/http/index.ts +151 -0
- package/server/services/router/index.ts +200 -12
- package/server/services/router/request/api.ts +30 -1
- package/server/services/router/response/index.ts +83 -10
- package/server/services/router/response/page/document.tsx +16 -0
- package/server/services/router/response/page/index.tsx +27 -1
- package/skills/clean-project-code/SKILL.md +7 -2
- package/test-results/.last-run.json +4 -0
- package/types/aliases.d.ts +6 -0
- package/types/global/utils.d.ts +7 -14
- package/Rte.zip +0 -0
- package/agents/project/agents.md.zip +0 -0
- package/doc/TODO.md +0 -71
- package/doc/front/router.md +0 -27
- package/doc/workspace/workspace.png +0 -0
- package/doc/workspace/workspace2.png +0 -0
- package/doc/workspace/workspace_26.01.22.png +0 -0
- package/server/services/router/http/session.ts.old +0 -40
|
@@ -10,16 +10,33 @@ import cronParser, { CronExpression } from 'cron-parser';
|
|
|
10
10
|
----------------------------------*/
|
|
11
11
|
|
|
12
12
|
import type CronManager from '.';
|
|
13
|
+
import type {
|
|
14
|
+
TProfilerCronTask,
|
|
15
|
+
TProfilerCronTaskFrequency,
|
|
16
|
+
TProfilerCronTaskRunStatus,
|
|
17
|
+
TProfilerCronTaskTrigger,
|
|
18
|
+
} from '@common/dev/profiler';
|
|
13
19
|
|
|
14
20
|
export type TFrequence = string | Date;
|
|
15
21
|
export type TRunner = () => Promise<any>;
|
|
22
|
+
const nowIso = () => new Date().toISOString();
|
|
16
23
|
|
|
17
24
|
/*----------------------------------
|
|
18
25
|
- CLASS
|
|
19
26
|
----------------------------------*/
|
|
20
27
|
export default class CronTask {
|
|
21
28
|
public cron?: CronExpression;
|
|
29
|
+
public frequency!: TProfilerCronTaskFrequency;
|
|
22
30
|
public nextInvocation?: Date;
|
|
31
|
+
public registeredAt = nowIso();
|
|
32
|
+
public running = false;
|
|
33
|
+
public lastTrigger?: TProfilerCronTaskTrigger;
|
|
34
|
+
public lastRunStartedAt?: string;
|
|
35
|
+
public lastRunFinishedAt?: string;
|
|
36
|
+
public lastRunDurationMs?: number;
|
|
37
|
+
public lastRunStatus?: TProfilerCronTaskRunStatus;
|
|
38
|
+
public lastErrorMessage?: string;
|
|
39
|
+
public runCount = 0;
|
|
23
40
|
|
|
24
41
|
public constructor(
|
|
25
42
|
private manager: CronManager,
|
|
@@ -35,6 +52,10 @@ export default class CronTask {
|
|
|
35
52
|
|
|
36
53
|
public schedule(next: TFrequence) {
|
|
37
54
|
this.cron = undefined;
|
|
55
|
+
this.frequency =
|
|
56
|
+
typeof next === 'string'
|
|
57
|
+
? { kind: 'cron', value: next }
|
|
58
|
+
: { kind: 'date', value: next.toISOString() };
|
|
38
59
|
|
|
39
60
|
// Cron expression
|
|
40
61
|
if (typeof next === 'string') {
|
|
@@ -58,16 +79,63 @@ export default class CronTask {
|
|
|
58
79
|
else this.nextInvocation = undefined;
|
|
59
80
|
}
|
|
60
81
|
|
|
61
|
-
public
|
|
82
|
+
public toProfilerTask(): TProfilerCronTask {
|
|
83
|
+
return {
|
|
84
|
+
name: this.nom,
|
|
85
|
+
registeredAt: this.registeredAt,
|
|
86
|
+
frequency: this.frequency,
|
|
87
|
+
autoexec: Boolean(this.autoexec),
|
|
88
|
+
automaticExecution: this.manager.isAutomaticExecutionEnabled(),
|
|
89
|
+
nextInvocation: this.nextInvocation?.toISOString(),
|
|
90
|
+
running: this.running,
|
|
91
|
+
lastTrigger: this.lastTrigger,
|
|
92
|
+
lastRunStartedAt: this.lastRunStartedAt,
|
|
93
|
+
lastRunFinishedAt: this.lastRunFinishedAt,
|
|
94
|
+
lastRunDurationMs: this.lastRunDurationMs,
|
|
95
|
+
lastRunStatus: this.lastRunStatus,
|
|
96
|
+
lastErrorMessage: this.lastErrorMessage,
|
|
97
|
+
runCount: this.runCount,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public async run(now: boolean = false, trigger: TProfilerCronTaskTrigger = 'scheduler') {
|
|
62
102
|
// Update invocation date
|
|
63
103
|
const maintenant = new Date();
|
|
64
104
|
const runnable = this.nextInvocation !== undefined && this.nextInvocation.valueOf() <= maintenant.valueOf();
|
|
65
105
|
if (runnable) this.scheduleNext();
|
|
66
|
-
else if (now === false) return;
|
|
106
|
+
else if (now === false) return false;
|
|
107
|
+
|
|
108
|
+
if (this.running) return false;
|
|
109
|
+
|
|
110
|
+
this.running = true;
|
|
111
|
+
this.lastTrigger = trigger;
|
|
112
|
+
const startedAt = nowIso();
|
|
113
|
+
this.lastRunStartedAt = startedAt;
|
|
114
|
+
this.lastRunFinishedAt = undefined;
|
|
115
|
+
this.lastRunDurationMs = undefined;
|
|
116
|
+
this.lastRunStatus = undefined;
|
|
117
|
+
this.lastErrorMessage = undefined;
|
|
67
118
|
|
|
68
119
|
// Execution
|
|
69
|
-
|
|
70
|
-
this.
|
|
71
|
-
|
|
120
|
+
try {
|
|
121
|
+
await this.runner();
|
|
122
|
+
this.runCount += 1;
|
|
123
|
+
const finishedAt = nowIso();
|
|
124
|
+
this.lastRunFinishedAt = finishedAt;
|
|
125
|
+
this.lastRunDurationMs = Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt));
|
|
126
|
+
this.lastRunStatus = 'completed';
|
|
127
|
+
this.manager.config.debug && console.info(`[cron][${this.nom}] Task completed.`);
|
|
128
|
+
return true;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.runCount += 1;
|
|
131
|
+
const finishedAt = nowIso();
|
|
132
|
+
this.lastRunFinishedAt = finishedAt;
|
|
133
|
+
this.lastRunDurationMs = Math.max(0, Date.parse(finishedAt) - Date.parse(startedAt));
|
|
134
|
+
this.lastRunStatus = 'error';
|
|
135
|
+
this.lastErrorMessage = error instanceof Error ? error.message : String(error);
|
|
136
|
+
throw error;
|
|
137
|
+
} finally {
|
|
138
|
+
this.running = false;
|
|
139
|
+
}
|
|
72
140
|
}
|
|
73
141
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import type { Application } from '@server/app/index';
|
|
7
7
|
import Service from '@server/app/service';
|
|
8
8
|
import { NotFound } from '@common/errors';
|
|
9
|
+
import type { TProfilerCronTaskTrigger } from '@common/dev/profiler';
|
|
9
10
|
import context from '@server/context';
|
|
10
11
|
|
|
11
12
|
/*----------------------------------
|
|
@@ -32,7 +33,7 @@ export type Services = {};
|
|
|
32
33
|
|
|
33
34
|
export default class CronManager extends Service<Config, Hooks, Application, Application> {
|
|
34
35
|
public static taches: { [nom: string]: CronTask } = {};
|
|
35
|
-
public static timer
|
|
36
|
+
public static timer?: NodeJS.Timeout;
|
|
36
37
|
|
|
37
38
|
/*----------------------------------
|
|
38
39
|
- LIFECICLE
|
|
@@ -40,8 +41,17 @@ export default class CronManager extends Service<Config, Hooks, Application, App
|
|
|
40
41
|
|
|
41
42
|
public async ready() {
|
|
42
43
|
clearInterval(CronManager.timer);
|
|
44
|
+
if (!this.isAutomaticExecutionEnabled()) {
|
|
45
|
+
this.config.debug && console.info('[cron] Automatic execution disabled in dev mode.');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
CronManager.timer = setInterval(() => {
|
|
44
|
-
for (const id in CronManager.taches)
|
|
50
|
+
for (const id in CronManager.taches) {
|
|
51
|
+
void this.runTask(id, false, 'scheduler').catch((error) => {
|
|
52
|
+
console.error(`[cron][${id}] Task failed.`, error);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
45
55
|
}, 10000);
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -59,16 +69,12 @@ export default class CronManager extends Service<Config, Hooks, Application, App
|
|
|
59
69
|
* @param autoexec true to execute the task immediatly
|
|
60
70
|
* @returns The CronTask that just have been created
|
|
61
71
|
*/
|
|
62
|
-
public task(nom: string, frequence: TFrequence, run: TRunner, autoexec?: boolean) {
|
|
63
|
-
|
|
64
|
-
context.run({ channelType: 'cron', channelId: nom }, async () => {
|
|
65
|
-
CronManager.taches[nom] = new CronTask(this, nom, frequence, run, autoexec);
|
|
72
|
+
public async task(nom: string, frequence: TFrequence, run: TRunner, autoexec?: boolean) {
|
|
73
|
+
CronManager.taches[nom] = new CronTask(this, nom, frequence, run, autoexec);
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
if (autoexec && this.isAutomaticExecutionEnabled()) await this.runTask(nom, true, 'autoexec');
|
|
68
76
|
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
});
|
|
77
|
+
return CronManager.taches[nom];
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
public async exec(nom: string) {
|
|
@@ -76,7 +82,8 @@ export default class CronManager extends Service<Config, Hooks, Application, App
|
|
|
76
82
|
|
|
77
83
|
if (tache === undefined) throw new NotFound('Tâche NotFound: ' + nom);
|
|
78
84
|
|
|
79
|
-
await
|
|
85
|
+
await this.runTask(nom, true, 'manual');
|
|
86
|
+
return tache;
|
|
80
87
|
}
|
|
81
88
|
public get(): typeof CronManager.taches;
|
|
82
89
|
public get(name: string): CronTask;
|
|
@@ -87,4 +94,20 @@ export default class CronManager extends Service<Config, Hooks, Application, App
|
|
|
87
94
|
if (cron === undefined) throw new Error(`L'instance de la tâche cron ${name} n'a pas été trouvée`);
|
|
88
95
|
return cron;
|
|
89
96
|
}
|
|
97
|
+
|
|
98
|
+
public isAutomaticExecutionEnabled() {
|
|
99
|
+
return !__DEV__;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public listTasks() {
|
|
103
|
+
return Object.values(CronManager.taches).map((task) => task.toProfilerTask());
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async runTask(name: string, now: boolean, trigger: TProfilerCronTaskTrigger) {
|
|
107
|
+
const task = this.get(name);
|
|
108
|
+
|
|
109
|
+
return context.run({ channelType: 'cron', channelId: name }, async () => {
|
|
110
|
+
return task.run(now, trigger);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
90
113
|
}
|
|
@@ -7,9 +7,6 @@ import type { default as sharp, Sharp } from 'sharp';
|
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import got, { Method, Options, Response as GotResponse } from 'got';
|
|
9
9
|
|
|
10
|
-
// Node
|
|
11
|
-
import request from 'request';
|
|
12
|
-
|
|
13
10
|
// Core: general
|
|
14
11
|
import type { Application } from '@server/app/index';
|
|
15
12
|
import Service from '@server/app/service';
|
|
@@ -109,14 +106,10 @@ export default class FetchService extends Service<Config, Hooks, Application, Ap
|
|
|
109
106
|
----------------------------------*/
|
|
110
107
|
|
|
111
108
|
public toBuffer(uri: string): Promise<Buffer> {
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
if (err) return reject(err);
|
|
115
|
-
|
|
116
|
-
if (!body) return reject(`Body is empty for ${uri}.`);
|
|
109
|
+
return got(uri, { responseType: 'buffer', throwHttpErrors: false }).then((response) => {
|
|
110
|
+
if (!response.body || response.body.length === 0) throw new Error(`Body is empty for ${uri}.`);
|
|
117
111
|
|
|
118
|
-
|
|
119
|
-
});
|
|
112
|
+
return response.body;
|
|
120
113
|
});
|
|
121
114
|
}
|
|
122
115
|
|
|
@@ -23,6 +23,62 @@ import { NotFound } from '@common/errors';
|
|
|
23
23
|
|
|
24
24
|
export type SqlQuery = ReturnType<ModelsManager['SQL']>;
|
|
25
25
|
|
|
26
|
+
type DecimalLike = {
|
|
27
|
+
constructor?: { name?: string };
|
|
28
|
+
equals: (value: number) => boolean;
|
|
29
|
+
toNumber: () => number;
|
|
30
|
+
toString: () => string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/*----------------------------------
|
|
34
|
+
- HELPERS
|
|
35
|
+
----------------------------------*/
|
|
36
|
+
|
|
37
|
+
const isDecimalLike = (value: object): value is DecimalLike =>
|
|
38
|
+
'constructor' in value &&
|
|
39
|
+
'equals' in value &&
|
|
40
|
+
'toNumber' in value &&
|
|
41
|
+
'toString' in value &&
|
|
42
|
+
typeof value.constructor === 'function' &&
|
|
43
|
+
value.constructor.name === 'Decimal' &&
|
|
44
|
+
typeof value.equals === 'function' &&
|
|
45
|
+
typeof value.toNumber === 'function' &&
|
|
46
|
+
typeof value.toString === 'function';
|
|
47
|
+
|
|
48
|
+
const isPlainObject = (value: object) => {
|
|
49
|
+
const prototype = Object.getPrototypeOf(value);
|
|
50
|
+
return prototype === Object.prototype || prototype === null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const normalizeBigInt = (value: bigint) => {
|
|
54
|
+
const number = Number(value);
|
|
55
|
+
return Number.isSafeInteger(number) ? number : value.toString();
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const normalizeDecimal = (value: DecimalLike) => {
|
|
59
|
+
const number = value.toNumber();
|
|
60
|
+
return Number.isFinite(number) && value.equals(number) ? number : value.toString();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const normalizeSqlScalar = (value: bigint | DecimalLike) =>
|
|
64
|
+
typeof value === 'bigint' ? normalizeBigInt(value) : normalizeDecimal(value);
|
|
65
|
+
|
|
66
|
+
const normalizeSqlResult = <T>(value: T): T => {
|
|
67
|
+
if (typeof value === 'bigint') return normalizeSqlScalar(value) as T;
|
|
68
|
+
|
|
69
|
+
if (Array.isArray(value)) return value.map((item) => normalizeSqlResult(item)) as T;
|
|
70
|
+
|
|
71
|
+
if (value === null || value === undefined || typeof value !== 'object' || value instanceof Date) return value;
|
|
72
|
+
|
|
73
|
+
if (isDecimalLike(value)) return normalizeSqlScalar(value) as T;
|
|
74
|
+
|
|
75
|
+
if (!isPlainObject(value)) return value;
|
|
76
|
+
|
|
77
|
+
return Object.fromEntries(
|
|
78
|
+
Object.entries(value).map(([key, nestedValue]) => [key, normalizeSqlResult(nestedValue)]),
|
|
79
|
+
) as T;
|
|
80
|
+
};
|
|
81
|
+
|
|
26
82
|
/*----------------------------------
|
|
27
83
|
- SERVICE CONFIG
|
|
28
84
|
----------------------------------*/
|
|
@@ -33,6 +89,11 @@ export type Hooks = {};
|
|
|
33
89
|
|
|
34
90
|
export type Services = {};
|
|
35
91
|
|
|
92
|
+
// Fix: Do not know how to serialize a BigInt
|
|
93
|
+
BigInt.prototype.toJSON = function () {
|
|
94
|
+
return normalizeBigInt(this.valueOf());
|
|
95
|
+
};
|
|
96
|
+
|
|
36
97
|
/*----------------------------------
|
|
37
98
|
- CLASSE
|
|
38
99
|
----------------------------------*/
|
|
@@ -43,7 +104,7 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
43
104
|
public constructor(...args: TServiceArgs<ModelsManager>) {
|
|
44
105
|
super(...args);
|
|
45
106
|
|
|
46
|
-
dotenv.config();
|
|
107
|
+
dotenv.config({ quiet: true });
|
|
47
108
|
|
|
48
109
|
const databaseUrl = process.env.DATABASE_URL;
|
|
49
110
|
if (!databaseUrl)
|
|
@@ -74,9 +135,10 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
74
135
|
public SQL<TRowData extends {} | number | string>(strings: TemplateStringsArray, ...data: any[]) {
|
|
75
136
|
const string = this.string(strings, ...data);
|
|
76
137
|
|
|
77
|
-
const query = () =>
|
|
78
|
-
|
|
79
|
-
|
|
138
|
+
const query = () =>
|
|
139
|
+
this.client.$queryRawUnsafe(string).then((resultatRequetes) => normalizeSqlResult(resultatRequetes)) as Promise<
|
|
140
|
+
TRowData[]
|
|
141
|
+
>;
|
|
80
142
|
|
|
81
143
|
query.all = query;
|
|
82
144
|
query.value = <TValue extends any = number>() =>
|
|
@@ -21,8 +21,11 @@ import * as csp from 'express-csp-header';
|
|
|
21
21
|
|
|
22
22
|
// Core
|
|
23
23
|
import Container from '@server/app/container';
|
|
24
|
+
import type CronManager from '@server/services/cron';
|
|
25
|
+
import type CronTask from '@server/services/cron/CronTask';
|
|
24
26
|
import type { TServerRouter } from '..';
|
|
25
27
|
import { serverHotReloadMessageType } from '@common/dev/serverHotReload';
|
|
28
|
+
import { explainSectionNames } from '@common/dev/diagnostics';
|
|
26
29
|
|
|
27
30
|
// Middlewaees (core)
|
|
28
31
|
import { isMutipart, MiddlewareFormData } from './multipart';
|
|
@@ -206,6 +209,7 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
206
209
|
}),
|
|
207
210
|
);
|
|
208
211
|
|
|
212
|
+
this.registerDevTraceRoutes(routes);
|
|
209
213
|
routes.use(routeRequest);
|
|
210
214
|
|
|
211
215
|
/*----------------------------------
|
|
@@ -227,4 +231,151 @@ export default class HttpServer<TRouter extends TServerRouter = TServerRouter> {
|
|
|
227
231
|
public async cleanup() {
|
|
228
232
|
this.http.close();
|
|
229
233
|
}
|
|
234
|
+
|
|
235
|
+
private registerDevTraceRoutes(routes: express.Express) {
|
|
236
|
+
if (!__DEV__ || this.app.env.profile !== 'dev') return;
|
|
237
|
+
|
|
238
|
+
if (this.app.container.Trace.isEnabled()) {
|
|
239
|
+
routes.get('/__proteum/trace/requests', (req, res) => {
|
|
240
|
+
const rawLimit = Array.isArray(req.query.limit) ? req.query.limit[0] : req.query.limit;
|
|
241
|
+
const parsedLimit = typeof rawLimit === 'string' ? Number.parseInt(rawLimit, 10) : NaN;
|
|
242
|
+
const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 20;
|
|
243
|
+
|
|
244
|
+
res.json({ requests: this.app.container.Trace.listRequests(limit) });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
routes.get('/__proteum/trace/latest', (_req, res) => {
|
|
248
|
+
const request = this.app.container.Trace.getLatestRequest();
|
|
249
|
+
if (!request) {
|
|
250
|
+
res.status(404).json({ error: 'No request trace is available yet.' });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
res.json({ request });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
routes.get('/__proteum/trace/requests/:id', (req, res) => {
|
|
258
|
+
const request = this.app.container.Trace.getRequest(req.params.id);
|
|
259
|
+
if (!request) {
|
|
260
|
+
res.status(404).json({ error: `Trace ${req.params.id} was not found.` });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
res.json({ request });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
routes.post('/__proteum/trace/arm', (req, res) => {
|
|
268
|
+
const rawCapture = typeof req.body.capture === 'string' ? req.body.capture : 'deep';
|
|
269
|
+
const capture = this.app.container.Trace.armNextRequest(rawCapture);
|
|
270
|
+
|
|
271
|
+
res.json({ armed: true, capture });
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
routes.get('/__proteum/explain', (req, res) => {
|
|
276
|
+
const rawSections = [
|
|
277
|
+
...(Array.isArray(req.query.section) ? req.query.section : req.query.section ? [req.query.section] : []),
|
|
278
|
+
...(Array.isArray(req.query.sections)
|
|
279
|
+
? req.query.sections.flatMap((value) => (typeof value === 'string' ? value.split(',') : []))
|
|
280
|
+
: typeof req.query.sections === 'string'
|
|
281
|
+
? req.query.sections.split(',')
|
|
282
|
+
: []),
|
|
283
|
+
]
|
|
284
|
+
.map((value) => (typeof value === 'string' ? value.trim() : ''))
|
|
285
|
+
.filter(Boolean);
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const diagnostics = this.app.getDevDiagnostics();
|
|
289
|
+
const sections = diagnostics.normalizeExplainSections(rawSections);
|
|
290
|
+
res.json(diagnostics.explain(sections));
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
293
|
+
const isBadRequest = explainSectionNames.some((sectionName) => message.includes(sectionName)) || message.includes('Unknown explain section');
|
|
294
|
+
res.status(isBadRequest ? 400 : 500).json({ error: message });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
routes.get('/__proteum/doctor', (req, res) => {
|
|
299
|
+
const rawStrict = Array.isArray(req.query.strict) ? req.query.strict[0] : req.query.strict;
|
|
300
|
+
const strict = rawStrict === '1' || rawStrict === 'true';
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
res.json(this.app.getDevDiagnostics().doctor(strict));
|
|
304
|
+
} catch (error) {
|
|
305
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
routes.get('/__proteum/cron/tasks', (_req, res) => {
|
|
310
|
+
const cron = this.getCronManager();
|
|
311
|
+
res.json({
|
|
312
|
+
automaticExecution: cron?.isAutomaticExecutionEnabled() ?? false,
|
|
313
|
+
tasks: cron?.listTasks() ?? [],
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
routes.get('/__proteum/commands', (_req, res) => {
|
|
318
|
+
res.json({ commands: this.app.getDevCommands().list() });
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
routes.post('/__proteum/commands/run', async (req, res) => {
|
|
322
|
+
const commandPath = typeof req.body?.path === 'string' ? req.body.path.trim() : '';
|
|
323
|
+
if (!commandPath) {
|
|
324
|
+
res.status(400).json({ error: 'Command path is required.' });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const execution = await this.app.getDevCommands().run(commandPath);
|
|
330
|
+
res.json({ execution });
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const execution =
|
|
333
|
+
error instanceof Error && 'execution' in error && typeof error.execution === 'object'
|
|
334
|
+
? error.execution
|
|
335
|
+
: undefined;
|
|
336
|
+
const statusCode = error instanceof Error && error.name === 'NotFound' ? 404 : 500;
|
|
337
|
+
|
|
338
|
+
res.status(statusCode).json({
|
|
339
|
+
error: error instanceof Error ? error.message : String(error),
|
|
340
|
+
execution,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
routes.post('/__proteum/cron/tasks/run', async (req, res) => {
|
|
346
|
+
const cron = this.getCronManager();
|
|
347
|
+
if (!cron) {
|
|
348
|
+
res.status(404).json({ error: 'Cron service is not registered for this app.' });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const name = typeof req.body?.name === 'string' ? req.body.name.trim() : '';
|
|
353
|
+
if (!name) {
|
|
354
|
+
res.status(400).json({ error: 'Cron task name is required.' });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let task: CronTask;
|
|
359
|
+
try {
|
|
360
|
+
task = cron.get(name);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
res.status(404).json({ error: error instanceof Error ? error.message : String(error) });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await cron.exec(name);
|
|
368
|
+
res.json({ task: task.toProfilerTask() });
|
|
369
|
+
} catch (error) {
|
|
370
|
+
res.status(500).json({
|
|
371
|
+
error: error instanceof Error ? error.message : String(error),
|
|
372
|
+
task: task.toProfilerTask(),
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private getCronManager() {
|
|
379
|
+
return (this.app as typeof this.app & { Cron?: CronManager }).Cron;
|
|
380
|
+
}
|
|
230
381
|
}
|