proteum 2.1.3-1 → 2.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +109 -17
  3. package/agents/project/AGENTS.md +188 -25
  4. package/agents/project/CODING_STYLE.md +1 -0
  5. package/agents/project/client/AGENTS.md +13 -8
  6. package/agents/project/client/pages/AGENTS.md +17 -9
  7. package/agents/project/diagnostics.md +52 -0
  8. package/agents/project/optimizations.md +48 -0
  9. package/agents/project/server/routes/AGENTS.md +9 -6
  10. package/agents/project/server/services/AGENTS.md +10 -6
  11. package/agents/project/tests/AGENTS.md +11 -5
  12. package/cli/app/config.ts +13 -14
  13. package/cli/app/index.ts +58 -0
  14. package/cli/commands/connect.ts +45 -0
  15. package/cli/commands/dev.ts +37 -13
  16. package/cli/commands/diagnose.ts +286 -0
  17. package/cli/commands/doctor.ts +18 -5
  18. package/cli/commands/explain.ts +25 -0
  19. package/cli/commands/perf.ts +243 -0
  20. package/cli/commands/trace.ts +9 -1
  21. package/cli/commands/verify.ts +281 -0
  22. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  23. package/cli/compiler/artifacts/controllers.ts +198 -49
  24. package/cli/compiler/artifacts/discovery.ts +0 -34
  25. package/cli/compiler/artifacts/manifest.ts +95 -6
  26. package/cli/compiler/artifacts/routing.ts +2 -2
  27. package/cli/compiler/artifacts/services.ts +277 -130
  28. package/cli/compiler/client/index.ts +3 -0
  29. package/cli/compiler/common/files/style.ts +52 -0
  30. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  31. package/cli/compiler/common/scripts.ts +11 -5
  32. package/cli/compiler/index.ts +2 -1
  33. package/cli/compiler/server/index.ts +3 -0
  34. package/cli/presentation/commands.ts +110 -7
  35. package/cli/presentation/devSession.ts +32 -7
  36. package/cli/runtime/commands.ts +165 -6
  37. package/cli/scaffold/index.ts +18 -27
  38. package/cli/scaffold/templates.ts +48 -28
  39. package/cli/utils/agents.ts +106 -13
  40. package/cli/utils/keyboard.ts +8 -0
  41. package/client/dev/profiler/ApexChart.tsx +66 -0
  42. package/client/dev/profiler/index.tsx +2508 -302
  43. package/client/dev/profiler/runtime.noop.ts +12 -0
  44. package/client/dev/profiler/runtime.ts +195 -4
  45. package/client/services/router/request/api.ts +6 -1
  46. package/common/applicationConfig.ts +173 -0
  47. package/common/applicationConfigLoader.ts +102 -0
  48. package/common/connectedProjects.ts +113 -0
  49. package/common/dev/connect.ts +267 -0
  50. package/common/dev/console.ts +31 -0
  51. package/common/dev/contractsDoctor.ts +128 -0
  52. package/common/dev/diagnostics.ts +59 -15
  53. package/common/dev/inspection.ts +491 -0
  54. package/common/dev/performance.ts +809 -0
  55. package/common/dev/profiler.ts +3 -0
  56. package/common/dev/proteumManifest.ts +31 -6
  57. package/common/dev/requestTrace.ts +52 -1
  58. package/common/env/proteumEnv.ts +176 -50
  59. package/common/router/index.ts +1 -0
  60. package/common/router/request/api.ts +2 -0
  61. package/config.ts +5 -0
  62. package/docs/dev-commands.md +5 -1
  63. package/docs/dev-sessions.md +90 -0
  64. package/docs/diagnostics.md +74 -11
  65. package/docs/request-tracing.md +50 -3
  66. package/package.json +1 -1
  67. package/server/app/container/config.ts +16 -87
  68. package/server/app/container/console/index.ts +42 -8
  69. package/server/app/container/index.ts +10 -2
  70. package/server/app/container/trace/index.ts +105 -0
  71. package/server/app/devDiagnostics.ts +138 -0
  72. package/server/app/index.ts +18 -8
  73. package/server/app/service/container.ts +0 -12
  74. package/server/app/service/index.ts +0 -2
  75. package/server/services/prisma/index.ts +121 -4
  76. package/server/services/router/http/index.ts +305 -11
  77. package/server/services/router/index.ts +116 -57
  78. package/server/services/router/request/api.ts +160 -19
  79. package/server/services/router/request/index.ts +8 -0
  80. package/server/services/router/response/index.ts +23 -1
  81. package/server/services/router/response/page/document.tsx +31 -14
  82. package/server/services/router/response/page/index.tsx +10 -0
  83. package/agents/framework/AGENTS.md +0 -177
  84. package/server/services/auth/router/service.json +0 -6
  85. package/server/services/auth/service.json +0 -6
  86. package/server/services/cron/service.json +0 -6
  87. package/server/services/disks/drivers/local/service.json +0 -6
  88. package/server/services/disks/drivers/s3/service.json +0 -6
  89. package/server/services/disks/service.json +0 -6
  90. package/server/services/fetch/service.json +0 -7
  91. package/server/services/prisma/service.json +0 -6
  92. package/server/services/router/service.json +0 -6
  93. package/server/services/schema/router/service.json +0 -6
  94. package/server/services/schema/service.json +0 -6
  95. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -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
  }
@@ -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, TServiceMetas } from './service/container';
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
- this.client = new PrismaClient({
116
- adapter: createMariaDbAdapter(databaseUrl),
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
  }