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
|
@@ -9,8 +9,12 @@ import {
|
|
|
9
9
|
type TTraceCallOrigin,
|
|
10
10
|
type TTraceEvent,
|
|
11
11
|
type TTraceEventType,
|
|
12
|
+
type TTraceSqlQuery,
|
|
13
|
+
type TTraceSqlQueryCallerOrigin,
|
|
14
|
+
type TTraceSqlQueryKind,
|
|
12
15
|
type TTraceSummaryValue,
|
|
13
16
|
type TRequestTrace,
|
|
17
|
+
type TTraceMemorySnapshot,
|
|
14
18
|
type TRequestTraceListItem,
|
|
15
19
|
} from '@common/dev/requestTrace';
|
|
16
20
|
|
|
@@ -156,11 +160,29 @@ const summarizeCaptureValue = (value: TTraceInspectable, capture: TTraceCaptureM
|
|
|
156
160
|
summarizeValue(value, capture === 'deep' ? 3 : 1, new WeakSet<object>(), [key]);
|
|
157
161
|
|
|
158
162
|
const nowIso = () => new Date().toISOString();
|
|
163
|
+
const snapshotMemory = (): TTraceMemorySnapshot => {
|
|
164
|
+
const usage = process.memoryUsage();
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
arrayBuffers: typeof usage.arrayBuffers === 'number' ? usage.arrayBuffers : 0,
|
|
168
|
+
external: usage.external,
|
|
169
|
+
heapTotal: usage.heapTotal,
|
|
170
|
+
heapUsed: usage.heapUsed,
|
|
171
|
+
rss: usage.rss,
|
|
172
|
+
};
|
|
173
|
+
};
|
|
159
174
|
|
|
160
175
|
export default class Trace {
|
|
161
176
|
private requests = new Map<string, TRequestTrace>();
|
|
162
177
|
private order: string[] = [];
|
|
163
178
|
private armedCapture?: TTraceCaptureMode;
|
|
179
|
+
private activeMeasurements = new Map<
|
|
180
|
+
string,
|
|
181
|
+
{
|
|
182
|
+
cpu: ReturnType<typeof process.cpuUsage>;
|
|
183
|
+
memory: TTraceMemorySnapshot;
|
|
184
|
+
}
|
|
185
|
+
>();
|
|
164
186
|
|
|
165
187
|
public constructor(
|
|
166
188
|
private container: typeof ApplicationContainer,
|
|
@@ -209,10 +231,12 @@ export default class Trace {
|
|
|
209
231
|
droppedEvents: 0,
|
|
210
232
|
requestDataJson: serializeCaptureValue(input.data, 'requestData'),
|
|
211
233
|
calls: [],
|
|
234
|
+
sqlQueries: [],
|
|
212
235
|
events: [],
|
|
213
236
|
};
|
|
214
237
|
|
|
215
238
|
this.requests.set(trace.id, trace);
|
|
239
|
+
this.activeMeasurements.set(trace.id, { cpu: process.cpuUsage(), memory: snapshotMemory() });
|
|
216
240
|
this.order.push(trace.id);
|
|
217
241
|
this.trimRequestBuffer();
|
|
218
242
|
|
|
@@ -265,6 +289,21 @@ export default class Trace {
|
|
|
265
289
|
if (output.user) trace.user = output.user;
|
|
266
290
|
trace.statusCode = output.statusCode;
|
|
267
291
|
trace.errorMessage = output.errorMessage;
|
|
292
|
+
const measurement = this.activeMeasurements.get(requestId);
|
|
293
|
+
if (measurement) {
|
|
294
|
+
const cpu = process.cpuUsage(measurement.cpu);
|
|
295
|
+
trace.performance = {
|
|
296
|
+
cpu: {
|
|
297
|
+
systemMicros: cpu.system,
|
|
298
|
+
userMicros: cpu.user,
|
|
299
|
+
},
|
|
300
|
+
memory: {
|
|
301
|
+
after: snapshotMemory(),
|
|
302
|
+
before: measurement.memory,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
this.activeMeasurements.delete(requestId);
|
|
306
|
+
}
|
|
268
307
|
|
|
269
308
|
this.record(
|
|
270
309
|
requestId,
|
|
@@ -289,6 +328,8 @@ export default class Trace {
|
|
|
289
328
|
method?: string;
|
|
290
329
|
path?: string;
|
|
291
330
|
fetcherId?: string;
|
|
331
|
+
connectedProjectNamespace?: string;
|
|
332
|
+
connectedControllerAccessor?: string;
|
|
292
333
|
parentId?: string;
|
|
293
334
|
requestDataKeys?: string[];
|
|
294
335
|
requestData?: TTraceInspectable;
|
|
@@ -305,6 +346,8 @@ export default class Trace {
|
|
|
305
346
|
method: input.method || '',
|
|
306
347
|
path: input.path || '',
|
|
307
348
|
fetcherId: input.fetcherId,
|
|
349
|
+
connectedProjectNamespace: input.connectedProjectNamespace,
|
|
350
|
+
connectedControllerAccessor: input.connectedControllerAccessor,
|
|
308
351
|
startedAt: nowIso(),
|
|
309
352
|
requestDataKeys: input.requestDataKeys || [],
|
|
310
353
|
requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
|
|
@@ -348,6 +391,58 @@ export default class Trace {
|
|
|
348
391
|
trace.resultJson = serializeCaptureValue(result, 'result');
|
|
349
392
|
}
|
|
350
393
|
|
|
394
|
+
public recordSqlQuery(
|
|
395
|
+
requestId: string,
|
|
396
|
+
input: {
|
|
397
|
+
callerCallId?: string;
|
|
398
|
+
callerFetcherId?: string;
|
|
399
|
+
callerLabel?: string;
|
|
400
|
+
callerMethod?: string;
|
|
401
|
+
callerOrigin?: TTraceSqlQueryCallerOrigin;
|
|
402
|
+
callerPath?: string;
|
|
403
|
+
durationMs?: number;
|
|
404
|
+
finishedAt?: string;
|
|
405
|
+
kind: TTraceSqlQueryKind;
|
|
406
|
+
model?: string;
|
|
407
|
+
operation: string;
|
|
408
|
+
paramsJson?: unknown;
|
|
409
|
+
paramsText?: string;
|
|
410
|
+
query: string;
|
|
411
|
+
target?: string;
|
|
412
|
+
},
|
|
413
|
+
) {
|
|
414
|
+
const trace = this.requests.get(requestId);
|
|
415
|
+
if (!trace) return;
|
|
416
|
+
|
|
417
|
+
const durationMs = Math.max(0, input.durationMs || 0);
|
|
418
|
+
const finishedAt = input.finishedAt || nowIso();
|
|
419
|
+
const finishedAtMs = Date.parse(finishedAt);
|
|
420
|
+
const startedAt =
|
|
421
|
+
Number.isFinite(finishedAtMs) && durationMs > 0 ? new Date(finishedAtMs - durationMs).toISOString() : finishedAt;
|
|
422
|
+
|
|
423
|
+
const sqlQuery: TTraceSqlQuery = {
|
|
424
|
+
id: `${requestId}:sql:${trace.sqlQueries.length}`,
|
|
425
|
+
callerCallId: input.callerCallId,
|
|
426
|
+
callerFetcherId: input.callerFetcherId,
|
|
427
|
+
callerLabel: input.callerLabel,
|
|
428
|
+
callerMethod: input.callerMethod || '',
|
|
429
|
+
callerOrigin: input.callerOrigin || 'request',
|
|
430
|
+
callerPath: input.callerPath || '',
|
|
431
|
+
durationMs,
|
|
432
|
+
finishedAt,
|
|
433
|
+
kind: input.kind,
|
|
434
|
+
model: input.model,
|
|
435
|
+
operation: input.operation,
|
|
436
|
+
paramsJson: input.paramsJson,
|
|
437
|
+
paramsText: input.paramsText,
|
|
438
|
+
query: input.query.trim(),
|
|
439
|
+
startedAt,
|
|
440
|
+
target: input.target,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
trace.sqlQueries.push(sqlQuery);
|
|
444
|
+
}
|
|
445
|
+
|
|
351
446
|
public listRequests(limit = 20): TRequestTraceListItem[] {
|
|
352
447
|
return [...this.order]
|
|
353
448
|
.reverse()
|
|
@@ -373,9 +468,18 @@ export default class Trace {
|
|
|
373
468
|
profilerParentRequestId: trace.profilerParentRequestId,
|
|
374
469
|
eventCount: trace.events.length,
|
|
375
470
|
callCount: trace.calls.length,
|
|
471
|
+
sqlQueryCount: trace.sqlQueries.length,
|
|
376
472
|
}));
|
|
377
473
|
}
|
|
378
474
|
|
|
475
|
+
public listTraceRequests(limit = this.config.requestsLimit) {
|
|
476
|
+
return [...this.order]
|
|
477
|
+
.reverse()
|
|
478
|
+
.slice(0, Math.max(1, limit))
|
|
479
|
+
.map((requestId) => this.requests.get(requestId))
|
|
480
|
+
.filter((trace): trace is TRequestTrace => trace !== undefined);
|
|
481
|
+
}
|
|
482
|
+
|
|
379
483
|
public getLatestRequest() {
|
|
380
484
|
const latestRequestId = this.order[this.order.length - 1];
|
|
381
485
|
return latestRequestId ? this.requests.get(latestRequestId) : undefined;
|
|
@@ -407,6 +511,7 @@ export default class Trace {
|
|
|
407
511
|
|
|
408
512
|
for (const requestId of this.order.splice(0, overflow)) {
|
|
409
513
|
this.requests.delete(requestId);
|
|
514
|
+
this.activeMeasurements.delete(requestId);
|
|
410
515
|
}
|
|
411
516
|
}
|
|
412
517
|
}
|
|
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
|
|
4
4
|
import type { Application } from './index';
|
|
5
|
+
import type { TDevConsoleLogLevel, TDevConsoleLogsResponse } from '@common/dev/console';
|
|
5
6
|
import {
|
|
6
7
|
buildDoctorResponse,
|
|
7
8
|
explainSectionNames,
|
|
@@ -9,10 +10,31 @@ import {
|
|
|
9
10
|
type TDoctorResponse,
|
|
10
11
|
type TExplainSectionName,
|
|
11
12
|
} from '@common/dev/diagnostics';
|
|
13
|
+
import { buildContractsDoctorResponse } from '@common/dev/contractsDoctor';
|
|
14
|
+
import {
|
|
15
|
+
buildPerfCompareResponse,
|
|
16
|
+
buildPerfMemoryResponse,
|
|
17
|
+
buildPerfTopResponse,
|
|
18
|
+
resolvePerfRequest,
|
|
19
|
+
type TPerfCompareResponse,
|
|
20
|
+
type TPerfGroupBy,
|
|
21
|
+
type TPerfMemoryResponse,
|
|
22
|
+
type TPerfRequestResponse,
|
|
23
|
+
type TPerfTopResponse,
|
|
24
|
+
} from '@common/dev/performance';
|
|
25
|
+
import {
|
|
26
|
+
buildDiagnoseResponse,
|
|
27
|
+
explainOwner,
|
|
28
|
+
type TDiagnoseResponse,
|
|
29
|
+
type TExplainOwnerResponse,
|
|
30
|
+
} from '@common/dev/inspection';
|
|
12
31
|
import type { TProteumManifest } from '@common/dev/proteumManifest';
|
|
32
|
+
import type { TRequestTrace } from '@common/dev/requestTrace';
|
|
13
33
|
|
|
14
34
|
const isExplainSectionName = (value: string): value is TExplainSectionName =>
|
|
15
35
|
explainSectionNames.includes(value as TExplainSectionName);
|
|
36
|
+
const isConsoleLogLevel = (value: string): value is TDevConsoleLogLevel =>
|
|
37
|
+
['silly', 'log', 'info', 'warn', 'error'].includes(value);
|
|
16
38
|
|
|
17
39
|
export default class DevDiagnosticsRegistry<TApplication extends Application = Application> {
|
|
18
40
|
public constructor(private app: TApplication) {}
|
|
@@ -50,4 +72,120 @@ export default class DevDiagnosticsRegistry<TApplication extends Application = A
|
|
|
50
72
|
public doctor(strict = false): TDoctorResponse {
|
|
51
73
|
return buildDoctorResponse(this.readManifest(), strict);
|
|
52
74
|
}
|
|
75
|
+
|
|
76
|
+
public doctorContracts(strict = false): TDoctorResponse {
|
|
77
|
+
return buildContractsDoctorResponse(this.readManifest(), strict);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public explainOwner(query: string): TExplainOwnerResponse {
|
|
81
|
+
const normalizedQuery = query.trim();
|
|
82
|
+
if (!normalizedQuery) throw new Error('Owner query is required.');
|
|
83
|
+
|
|
84
|
+
return explainOwner(this.readManifest(), normalizedQuery);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public readLogs(limit = 100, minimumLevel: TDevConsoleLogLevel = 'log'): TDevConsoleLogsResponse {
|
|
88
|
+
return { logs: this.app.container.Console.listLogs(limit, isConsoleLogLevel(minimumLevel) ? minimumLevel : 'log') };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private resolveRequestTrace({ path, requestId }: { path?: string; requestId?: string }): TRequestTrace | undefined {
|
|
92
|
+
if (requestId) return this.app.container.Trace.getRequest(requestId);
|
|
93
|
+
if (!path) return this.app.container.Trace.getLatestRequest();
|
|
94
|
+
|
|
95
|
+
const match = this.app.container.Trace.listRequests(200).find((request) => request.path === path);
|
|
96
|
+
return match ? this.app.container.Trace.getRequest(match.id) : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private readPerfRequests() {
|
|
100
|
+
return this.app.container.Trace.listTraceRequests(Number.MAX_SAFE_INTEGER);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public diagnose({
|
|
104
|
+
logsLevel = 'warn',
|
|
105
|
+
logsLimit = 40,
|
|
106
|
+
path,
|
|
107
|
+
query,
|
|
108
|
+
requestId,
|
|
109
|
+
strict = false,
|
|
110
|
+
}: {
|
|
111
|
+
logsLevel?: TDevConsoleLogLevel;
|
|
112
|
+
logsLimit?: number;
|
|
113
|
+
path?: string;
|
|
114
|
+
query?: string;
|
|
115
|
+
requestId?: string;
|
|
116
|
+
strict?: boolean;
|
|
117
|
+
} = {}): TDiagnoseResponse {
|
|
118
|
+
const manifest = this.readManifest();
|
|
119
|
+
const request = this.resolveRequestTrace({ path, requestId });
|
|
120
|
+
const resolvedQuery = query?.trim() || path?.trim() || request?.path || requestId?.trim() || '';
|
|
121
|
+
|
|
122
|
+
if (!resolvedQuery) throw new Error('Diagnose requires a query, path, request id, or an existing latest request trace.');
|
|
123
|
+
|
|
124
|
+
return buildDiagnoseResponse({
|
|
125
|
+
contracts: buildContractsDoctorResponse(manifest, strict),
|
|
126
|
+
doctor: buildDoctorResponse(manifest, strict),
|
|
127
|
+
manifest,
|
|
128
|
+
query: resolvedQuery,
|
|
129
|
+
request,
|
|
130
|
+
serverLogs: this.readLogs(logsLimit, logsLevel),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public perfTop({
|
|
135
|
+
groupBy = 'path',
|
|
136
|
+
limit = 12,
|
|
137
|
+
since = 'today',
|
|
138
|
+
}: {
|
|
139
|
+
groupBy?: TPerfGroupBy;
|
|
140
|
+
limit?: number;
|
|
141
|
+
since?: string;
|
|
142
|
+
} = {}): TPerfTopResponse {
|
|
143
|
+
return buildPerfTopResponse({
|
|
144
|
+
groupBy,
|
|
145
|
+
limit,
|
|
146
|
+
requests: this.readPerfRequests(),
|
|
147
|
+
since,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public perfCompare({
|
|
152
|
+
baseline = 'yesterday',
|
|
153
|
+
groupBy = 'path',
|
|
154
|
+
limit = 12,
|
|
155
|
+
target = 'today',
|
|
156
|
+
}: {
|
|
157
|
+
baseline?: string;
|
|
158
|
+
groupBy?: TPerfGroupBy;
|
|
159
|
+
limit?: number;
|
|
160
|
+
target?: string;
|
|
161
|
+
} = {}): TPerfCompareResponse {
|
|
162
|
+
return buildPerfCompareResponse({
|
|
163
|
+
baseline,
|
|
164
|
+
groupBy,
|
|
165
|
+
limit,
|
|
166
|
+
requests: this.readPerfRequests(),
|
|
167
|
+
target,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public perfMemory({
|
|
172
|
+
groupBy = 'path',
|
|
173
|
+
limit = 12,
|
|
174
|
+
since = 'today',
|
|
175
|
+
}: {
|
|
176
|
+
groupBy?: TPerfGroupBy;
|
|
177
|
+
limit?: number;
|
|
178
|
+
since?: string;
|
|
179
|
+
} = {}): TPerfMemoryResponse {
|
|
180
|
+
return buildPerfMemoryResponse({
|
|
181
|
+
groupBy,
|
|
182
|
+
limit,
|
|
183
|
+
requests: this.readPerfRequests(),
|
|
184
|
+
since,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public perfRequest(requestIdOrPath: string): TPerfRequestResponse {
|
|
189
|
+
return { request: resolvePerfRequest(this.readPerfRequests(), requestIdOrPath) };
|
|
190
|
+
}
|
|
53
191
|
}
|
package/server/app/index.ts
CHANGED
|
@@ -10,12 +10,13 @@ import ApplicationService, { AnyService } from './service';
|
|
|
10
10
|
import CommandsManager from './commandsManager';
|
|
11
11
|
import DevCommandsRegistry from './devCommands';
|
|
12
12
|
import DevDiagnosticsRegistry from './devDiagnostics';
|
|
13
|
-
import ServicesContainer, { ServicesContainer as ServicesContainerClass
|
|
13
|
+
import ServicesContainer, { ServicesContainer as ServicesContainerClass } from './service/container';
|
|
14
14
|
|
|
15
15
|
// Built-in
|
|
16
16
|
import type { TServerRouter, Request as ServerRequest } from '@server/services/router';
|
|
17
17
|
import { Anomaly } from '@common/errors';
|
|
18
18
|
import { TBasicUser } from '@server/services/auth';
|
|
19
|
+
import { Application as ConfigApplication } from '@common/applicationConfig';
|
|
19
20
|
|
|
20
21
|
export { default as Services } from './service/container';
|
|
21
22
|
export type { ServiceConfig } from './service/container';
|
|
@@ -62,6 +63,9 @@ export abstract class Application<
|
|
|
62
63
|
TServicesContainer extends ServicesContainerClass = ServicesContainerClass,
|
|
63
64
|
TUser extends TBasicUser = TBasicUser,
|
|
64
65
|
> extends ApplicationService<Config, Hooks, Application, Application> {
|
|
66
|
+
public static identity = ConfigApplication.identity;
|
|
67
|
+
public static setup = ConfigApplication.setup;
|
|
68
|
+
|
|
65
69
|
public app!: this;
|
|
66
70
|
public servicesContainer!: TServicesContainer;
|
|
67
71
|
public userType!: TUser;
|
|
@@ -71,18 +75,13 @@ export abstract class Application<
|
|
|
71
75
|
----------------------------------*/
|
|
72
76
|
|
|
73
77
|
public side = 'server' as 'server';
|
|
74
|
-
public metas: TServiceMetas = {
|
|
75
|
-
id: 'application',
|
|
76
|
-
name: 'Application',
|
|
77
|
-
parent: 'root',
|
|
78
|
-
dependences: [],
|
|
79
|
-
class: () => ({ default: Application }),
|
|
80
|
-
};
|
|
81
78
|
|
|
82
79
|
// Shortcuts to ApplicationContainer
|
|
83
80
|
public container = AppContainer;
|
|
84
81
|
public env = AppContainer.Environment;
|
|
85
82
|
public identity = AppContainer.Identity;
|
|
83
|
+
public setup = AppContainer.Setup;
|
|
84
|
+
public connectedProjects = AppContainer.Environment.connectedProjects;
|
|
86
85
|
|
|
87
86
|
// Status
|
|
88
87
|
public debug: boolean = false;
|
|
@@ -195,6 +194,17 @@ export abstract class Application<
|
|
|
195
194
|
return rootServices[serviceName];
|
|
196
195
|
}
|
|
197
196
|
|
|
197
|
+
public getConnectedProject(namespace: string) {
|
|
198
|
+
return this.connectedProjects[namespace];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
public requireConnectedProject(namespace: string) {
|
|
202
|
+
const connectedProject = this.getConnectedProject(namespace);
|
|
203
|
+
if (connectedProject) return connectedProject;
|
|
204
|
+
|
|
205
|
+
throw new Error(`Connected project "${namespace}" is not configured on ${this.identity.identifier}.`);
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
public register(service: AnyService) {
|
|
199
209
|
return (service as AnyService & { ready: () => Promise<any> }).ready();
|
|
200
210
|
}
|
|
@@ -9,15 +9,6 @@ import type { AnyService, AnyServiceClass, StartedServicesIndex } from '.';
|
|
|
9
9
|
- TYPES
|
|
10
10
|
----------------------------------*/
|
|
11
11
|
|
|
12
|
-
// From service/service.json
|
|
13
|
-
export type TServiceMetas<TServiceClass extends AnyService = AnyService> = {
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
parent: string;
|
|
17
|
-
dependences: string[];
|
|
18
|
-
class: () => { default: ClassType<TServiceClass> };
|
|
19
|
-
};
|
|
20
|
-
|
|
21
12
|
export type ServiceConfig<TServiceClass extends AnyServiceClass> = NonNullable<ConstructorParameters<TServiceClass>[1]>;
|
|
22
13
|
|
|
23
14
|
type ExactConfig<TValue, TShape> = TValue extends TShape
|
|
@@ -40,9 +31,6 @@ type ExactConfig<TValue, TShape> = TValue extends TShape
|
|
|
40
31
|
- CLASS
|
|
41
32
|
----------------------------------*/
|
|
42
33
|
export class ServicesContainer<TServicesIndex extends StartedServicesIndex = StartedServicesIndex> {
|
|
43
|
-
// All service instances by service id
|
|
44
|
-
public allServices: TServicesIndex = {} as TServicesIndex;
|
|
45
|
-
|
|
46
34
|
public config<TServiceClass extends AnyServiceClass, const TConfig extends ServiceConfig<TServiceClass>>(
|
|
47
35
|
_serviceClass: TServiceClass,
|
|
48
36
|
config: TConfig & ExactConfig<TConfig, ServiceConfig<TServiceClass>>,
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// Specific
|
|
6
6
|
import type { Application } from '../index';
|
|
7
7
|
import type { Command } from '../commands';
|
|
8
|
-
import type { TServiceMetas } from './container';
|
|
9
8
|
import type { TRouterContext, TAnyRouter } from '../../services/router';
|
|
10
9
|
|
|
11
10
|
export { schema } from '../../services/router/request/validation/zod';
|
|
@@ -95,7 +94,6 @@ export default abstract class Service<
|
|
|
95
94
|
public status: 'stopped' | 'starting' | 'running' | 'paused' = 'starting';
|
|
96
95
|
|
|
97
96
|
public commands?: Command[];
|
|
98
|
-
public metas!: TServiceMetas;
|
|
99
97
|
public bindings: string[] = [];
|
|
100
98
|
|
|
101
99
|
public parent: TParent;
|
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
// Npm
|
|
6
6
|
import dotenv from 'dotenv';
|
|
7
|
-
import { PrismaClient } from '@generated/server/models';
|
|
7
|
+
import { Prisma, PrismaClient } from '@generated/server/models';
|
|
8
8
|
import mysql from 'mysql2/promise';
|
|
9
9
|
const safeStringify = require('fast-safe-stringify'); // remplace les références circulairs par un [Circular]
|
|
10
10
|
|
|
11
11
|
// Core
|
|
12
12
|
import type { Application } from '@server/app/index';
|
|
13
|
+
import type { ChannelInfos } from '@server/app/container/console';
|
|
13
14
|
import Service, { TServiceArgs } from '@server/app/service';
|
|
15
|
+
import context from '@server/context';
|
|
14
16
|
|
|
15
17
|
// Specific
|
|
16
18
|
import Facet, { TDelegate, TSubset, Transform } from './Facet';
|
|
@@ -29,6 +31,20 @@ type DecimalLike = {
|
|
|
29
31
|
toNumber: () => number;
|
|
30
32
|
toString: () => string;
|
|
31
33
|
};
|
|
34
|
+
type TPrismaOperationContext = { kind: 'orm' | 'raw'; model?: string; operation: string };
|
|
35
|
+
type TPrismaQueryEvent = {
|
|
36
|
+
duration: number;
|
|
37
|
+
params: string;
|
|
38
|
+
query: string;
|
|
39
|
+
target: string;
|
|
40
|
+
timestamp: Date;
|
|
41
|
+
};
|
|
42
|
+
type TPrismaExtensionOperation = {
|
|
43
|
+
args: unknown;
|
|
44
|
+
model?: string;
|
|
45
|
+
operation: string;
|
|
46
|
+
query: (args: unknown) => Promise<unknown>;
|
|
47
|
+
};
|
|
32
48
|
|
|
33
49
|
/*----------------------------------
|
|
34
50
|
- HELPERS
|
|
@@ -78,6 +94,51 @@ const normalizeSqlResult = <T>(value: T): T => {
|
|
|
78
94
|
Object.entries(value).map(([key, nestedValue]) => [key, normalizeSqlResult(nestedValue)]),
|
|
79
95
|
) as T;
|
|
80
96
|
};
|
|
97
|
+
const rawOperationNames = new Set([
|
|
98
|
+
'$executeRaw',
|
|
99
|
+
'$executeRawUnsafe',
|
|
100
|
+
'$queryRaw',
|
|
101
|
+
'$queryRawUnsafe',
|
|
102
|
+
'aggregateRaw',
|
|
103
|
+
'executeRaw',
|
|
104
|
+
'findRaw',
|
|
105
|
+
'queryRaw',
|
|
106
|
+
'runCommandRaw',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
const inferPrismaOperationKind = (model: string | undefined, operation: string): TPrismaOperationContext['kind'] =>
|
|
110
|
+
rawOperationNames.has(operation) || operation.toLowerCase().includes('raw') ? 'raw' : model ? 'orm' : 'raw';
|
|
111
|
+
|
|
112
|
+
const parseQueryParams = (value: string) => {
|
|
113
|
+
if (!value) return undefined;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(value);
|
|
117
|
+
} catch (_error) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const withPrismaOperationContext = async <T>(meta: TPrismaOperationContext, execute: () => Promise<T>) => {
|
|
123
|
+
const store = context.getStore() as ChannelInfos | undefined;
|
|
124
|
+
if (!store) return execute();
|
|
125
|
+
|
|
126
|
+
const operations = store.prismaOperations || (store.prismaOperations = []);
|
|
127
|
+
operations.push(meta);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
return await execute();
|
|
131
|
+
} finally {
|
|
132
|
+
const lastOperation = operations[operations.length - 1];
|
|
133
|
+
if (lastOperation === meta) operations.pop();
|
|
134
|
+
else {
|
|
135
|
+
const operationIndex = operations.lastIndexOf(meta);
|
|
136
|
+
if (operationIndex !== -1) operations.splice(operationIndex, 1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (operations.length === 0) delete store.prismaOperations;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
81
142
|
|
|
82
143
|
/*----------------------------------
|
|
83
144
|
- SERVICE CONFIG
|
|
@@ -112,9 +173,40 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
112
173
|
'DATABASE_URL is required before starting the Models service. Prisma 7 no longer auto-loads runtime env files.',
|
|
113
174
|
);
|
|
114
175
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
176
|
+
const shouldTraceQueries = this.app.container.Trace.isEnabled();
|
|
177
|
+
const prismaClient = shouldTraceQueries
|
|
178
|
+
? new PrismaClient({
|
|
179
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
180
|
+
log: [{ emit: 'event', level: 'query' }],
|
|
181
|
+
})
|
|
182
|
+
: new PrismaClient({
|
|
183
|
+
adapter: createMariaDbAdapter(databaseUrl),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (!shouldTraceQueries) {
|
|
187
|
+
this.client = prismaClient;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
prismaClient.$on('query', (event: TPrismaQueryEvent) => this.traceQuery(event));
|
|
192
|
+
|
|
193
|
+
this.client = prismaClient.$extends(
|
|
194
|
+
Prisma.defineExtension({
|
|
195
|
+
query: {
|
|
196
|
+
async $allOperations({ args, model, operation, query }: TPrismaExtensionOperation) {
|
|
197
|
+
const normalizedModel = typeof model === 'string' ? model : undefined;
|
|
198
|
+
return withPrismaOperationContext(
|
|
199
|
+
{
|
|
200
|
+
kind: inferPrismaOperationKind(normalizedModel, operation),
|
|
201
|
+
model: normalizedModel,
|
|
202
|
+
operation,
|
|
203
|
+
},
|
|
204
|
+
() => query(args),
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
) as PrismaClient;
|
|
118
210
|
}
|
|
119
211
|
|
|
120
212
|
public async ready() {
|
|
@@ -241,4 +333,29 @@ export default class ModelsManager extends Service<Config, Hooks, Application, A
|
|
|
241
333
|
public equalities = (data: TObjetDonnees, forStorage: boolean = false) => {
|
|
242
334
|
return Object.keys(data).map((k) => '' + k + ' = ' + this.esc(data[k], forStorage));
|
|
243
335
|
};
|
|
336
|
+
|
|
337
|
+
private traceQuery(event: TPrismaQueryEvent) {
|
|
338
|
+
const store = context.getStore() as ChannelInfos | undefined;
|
|
339
|
+
if (!store || store.channelType !== 'request' || !store.channelId) return;
|
|
340
|
+
|
|
341
|
+
const operation = store.prismaOperations?.[store.prismaOperations.length - 1];
|
|
342
|
+
|
|
343
|
+
this.app.container.Trace.recordSqlQuery(store.channelId, {
|
|
344
|
+
callerCallId: store.traceCallId,
|
|
345
|
+
callerFetcherId: store.traceCallFetcherId,
|
|
346
|
+
callerLabel: store.traceCallLabel,
|
|
347
|
+
callerMethod: store.method,
|
|
348
|
+
callerOrigin: store.traceCallOrigin || 'request',
|
|
349
|
+
callerPath: store.path,
|
|
350
|
+
durationMs: event.duration,
|
|
351
|
+
finishedAt: event.timestamp.toISOString(),
|
|
352
|
+
kind: operation?.kind || 'orm',
|
|
353
|
+
model: operation?.model,
|
|
354
|
+
operation: operation?.operation || 'query',
|
|
355
|
+
paramsJson: parseQueryParams(event.params),
|
|
356
|
+
paramsText: event.params,
|
|
357
|
+
query: event.query,
|
|
358
|
+
target: event.target,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
244
361
|
}
|