proteum 2.1.2 → 2.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/AGENTS.md +22 -14
  2. package/README.md +112 -17
  3. package/agents/project/AGENTS.md +188 -25
  4. package/agents/project/CODING_STYLE.md +1 -0
  5. package/agents/project/client/AGENTS.md +13 -8
  6. package/agents/project/client/pages/AGENTS.md +17 -9
  7. package/agents/project/diagnostics.md +52 -0
  8. package/agents/project/optimizations.md +48 -0
  9. package/agents/project/server/routes/AGENTS.md +9 -6
  10. package/agents/project/server/services/AGENTS.md +10 -6
  11. package/agents/project/tests/AGENTS.md +11 -5
  12. package/cli/app/config.ts +13 -14
  13. package/cli/app/index.ts +58 -0
  14. package/cli/commands/command.ts +8 -0
  15. package/cli/commands/connect.ts +45 -0
  16. package/cli/commands/dev.ts +26 -11
  17. package/cli/commands/diagnose.ts +286 -0
  18. package/cli/commands/doctor.ts +18 -5
  19. package/cli/commands/explain.ts +25 -0
  20. package/cli/commands/perf.ts +243 -0
  21. package/cli/commands/session.ts +254 -0
  22. package/cli/commands/sessionLocalRunner.js +188 -0
  23. package/cli/commands/trace.ts +17 -1
  24. package/cli/commands/verify.ts +281 -0
  25. package/cli/compiler/artifacts/connectedProjects.ts +453 -0
  26. package/cli/compiler/artifacts/controllers.ts +198 -49
  27. package/cli/compiler/artifacts/discovery.ts +0 -34
  28. package/cli/compiler/artifacts/manifest.ts +90 -6
  29. package/cli/compiler/artifacts/routing.ts +2 -2
  30. package/cli/compiler/artifacts/services.ts +277 -130
  31. package/cli/compiler/client/index.ts +3 -0
  32. package/cli/compiler/common/files/style.ts +52 -0
  33. package/cli/compiler/common/generatedRouteModules.ts +34 -5
  34. package/cli/compiler/common/scripts.ts +11 -5
  35. package/cli/compiler/index.ts +2 -1
  36. package/cli/compiler/server/index.ts +3 -0
  37. package/cli/presentation/commands.ts +136 -7
  38. package/cli/presentation/devSession.ts +32 -7
  39. package/cli/runtime/commands.ts +193 -6
  40. package/cli/scaffold/index.ts +14 -25
  41. package/cli/scaffold/templates.ts +41 -27
  42. package/cli/utils/agents.ts +4 -2
  43. package/cli/utils/keyboard.ts +8 -0
  44. package/client/dev/profiler/ApexChart.tsx +66 -0
  45. package/client/dev/profiler/index.tsx +2798 -417
  46. package/client/dev/profiler/runtime.noop.ts +12 -0
  47. package/client/dev/profiler/runtime.ts +195 -4
  48. package/client/services/router/request/api.ts +6 -1
  49. package/common/applicationConfig.ts +173 -0
  50. package/common/applicationConfigLoader.ts +102 -0
  51. package/common/connectedProjects.ts +113 -0
  52. package/common/dev/connect.ts +267 -0
  53. package/common/dev/console.ts +31 -0
  54. package/common/dev/contractsDoctor.ts +128 -0
  55. package/common/dev/diagnostics.ts +59 -15
  56. package/common/dev/inspection.ts +491 -0
  57. package/common/dev/performance.ts +809 -0
  58. package/common/dev/profiler.ts +3 -0
  59. package/common/dev/proteumManifest.ts +31 -6
  60. package/common/dev/requestTrace.ts +56 -1
  61. package/common/dev/session.ts +24 -0
  62. package/common/env/proteumEnv.ts +176 -50
  63. package/common/router/index.ts +1 -0
  64. package/common/router/request/api.ts +2 -0
  65. package/config.ts +5 -0
  66. package/docs/dev-commands.md +5 -1
  67. package/docs/dev-sessions.md +90 -0
  68. package/docs/diagnostics.md +74 -11
  69. package/docs/request-tracing.md +50 -3
  70. package/package.json +1 -1
  71. package/server/app/container/config.ts +16 -87
  72. package/server/app/container/console/index.ts +42 -8
  73. package/server/app/container/index.ts +3 -1
  74. package/server/app/container/trace/index.ts +153 -0
  75. package/server/app/devDiagnostics.ts +138 -0
  76. package/server/app/index.ts +18 -8
  77. package/server/app/service/container.ts +0 -12
  78. package/server/app/service/index.ts +0 -2
  79. package/server/services/prisma/index.ts +121 -4
  80. package/server/services/router/http/index.ts +352 -0
  81. package/server/services/router/index.ts +50 -47
  82. package/server/services/router/request/api.ts +160 -19
  83. package/server/services/router/request/index.ts +8 -0
  84. package/server/services/router/response/index.ts +24 -1
  85. package/server/services/router/response/page/document.tsx +5 -0
  86. package/server/services/router/response/page/index.tsx +10 -0
  87. package/agents/framework/AGENTS.md +0 -177
  88. package/server/services/auth/router/service.json +0 -6
  89. package/server/services/auth/service.json +0 -6
  90. package/server/services/cron/service.json +0 -6
  91. package/server/services/disks/drivers/local/service.json +0 -6
  92. package/server/services/disks/drivers/s3/service.json +0 -6
  93. package/server/services/disks/service.json +0 -6
  94. package/server/services/fetch/service.json +0 -7
  95. package/server/services/prisma/service.json +0 -6
  96. package/server/services/router/service.json +0 -6
  97. package/server/services/schema/router/service.json +0 -6
  98. package/server/services/schema/service.json +0 -6
  99. package/server/services/security/encrypt/aes/service.json +0 -6
@@ -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
 
@@ -38,6 +42,44 @@ const isSensitiveKeyPath = (keyPath: string[]) => sensitiveKeyPattern.test(keyPa
38
42
  const summarizeString = (value: string) =>
39
43
  value.length <= maxStringLength ? value : `${value.slice(0, maxStringLength)}…`;
40
44
 
45
+ const serializeJsonValue = (value: unknown, keyPath: string[], seen: WeakSet<object>): unknown => {
46
+ if (isSensitiveKeyPath(keyPath)) return `[redacted: Sensitive key ${keyPath[keyPath.length - 1] || 'value'}]`;
47
+ if (value === undefined || value === null) return value;
48
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
49
+ if (typeof value === 'bigint') return `${value.toString()}n`;
50
+ if (typeof value === 'symbol') return value.toString();
51
+ if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
52
+
53
+ if (value instanceof Date) return value.toISOString();
54
+ if (value instanceof Error) return { name: value.name, message: value.message, stack: value.stack };
55
+ if (Buffer.isBuffer(value)) return `[Buffer ${value.byteLength} bytes]`;
56
+ if (value instanceof Map) return Array.from(value.entries()).map(([entryKey, entryValue], index) =>
57
+ serializeJsonValue([entryKey, entryValue], [...keyPath, `[${index}]`], seen),
58
+ );
59
+ if (value instanceof Set) {
60
+ return Array.from(value.values()).map((entryValue, index) => serializeJsonValue(entryValue, [...keyPath, `[${index}]`], seen));
61
+ }
62
+
63
+ if (typeof value !== 'object') return String(value);
64
+ if (seen.has(value)) return `[Circular ${value.constructor?.name || 'Object'}]`;
65
+
66
+ seen.add(value);
67
+
68
+ if (Array.isArray(value)) {
69
+ return value.map((item, index) => serializeJsonValue(item, [...keyPath, `[${index}]`], seen));
70
+ }
71
+
72
+ const serialized: Record<string, unknown> = {};
73
+ for (const [entryKey, entryValue] of Object.entries(value)) {
74
+ const nextValue = serializeJsonValue(entryValue, [...keyPath, entryKey], seen);
75
+ if (nextValue !== undefined) serialized[entryKey] = nextValue;
76
+ }
77
+
78
+ return serialized;
79
+ };
80
+
81
+ const serializeCaptureValue = (value: TTraceInspectable, key: string) => serializeJsonValue(value, [key], new WeakSet<object>());
82
+
41
83
  const summarizeError = (error: Error): TTraceSummaryValue => ({
42
84
  kind: 'error',
43
85
  name: error.name,
@@ -118,11 +160,29 @@ const summarizeCaptureValue = (value: TTraceInspectable, capture: TTraceCaptureM
118
160
  summarizeValue(value, capture === 'deep' ? 3 : 1, new WeakSet<object>(), [key]);
119
161
 
120
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
+ };
121
174
 
122
175
  export default class Trace {
123
176
  private requests = new Map<string, TRequestTrace>();
124
177
  private order: string[] = [];
125
178
  private armedCapture?: TTraceCaptureMode;
179
+ private activeMeasurements = new Map<
180
+ string,
181
+ {
182
+ cpu: ReturnType<typeof process.cpuUsage>;
183
+ memory: TTraceMemorySnapshot;
184
+ }
185
+ >();
126
186
 
127
187
  public constructor(
128
188
  private container: typeof ApplicationContainer,
@@ -169,11 +229,14 @@ export default class Trace {
169
229
  profilerParentRequestId: input.profilerParentRequestId,
170
230
  startedAt: nowIso(),
171
231
  droppedEvents: 0,
232
+ requestDataJson: serializeCaptureValue(input.data, 'requestData'),
172
233
  calls: [],
234
+ sqlQueries: [],
173
235
  events: [],
174
236
  };
175
237
 
176
238
  this.requests.set(trace.id, trace);
239
+ this.activeMeasurements.set(trace.id, { cpu: process.cpuUsage(), memory: snapshotMemory() });
177
240
  this.order.push(trace.id);
178
241
  this.trimRequestBuffer();
179
242
 
@@ -226,6 +289,21 @@ export default class Trace {
226
289
  if (output.user) trace.user = output.user;
227
290
  trace.statusCode = output.statusCode;
228
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
+ }
229
307
 
230
308
  this.record(
231
309
  requestId,
@@ -250,6 +328,8 @@ export default class Trace {
250
328
  method?: string;
251
329
  path?: string;
252
330
  fetcherId?: string;
331
+ connectedProjectNamespace?: string;
332
+ connectedControllerAccessor?: string;
253
333
  parentId?: string;
254
334
  requestDataKeys?: string[];
255
335
  requestData?: TTraceInspectable;
@@ -266,9 +346,12 @@ export default class Trace {
266
346
  method: input.method || '',
267
347
  path: input.path || '',
268
348
  fetcherId: input.fetcherId,
349
+ connectedProjectNamespace: input.connectedProjectNamespace,
350
+ connectedControllerAccessor: input.connectedControllerAccessor,
269
351
  startedAt: nowIso(),
270
352
  requestDataKeys: input.requestDataKeys || [],
271
353
  requestData: input.requestData !== undefined ? summarizeCaptureValue(input.requestData, trace.capture, 'requestData') : undefined,
354
+ requestDataJson: input.requestData !== undefined ? serializeCaptureValue(input.requestData, 'requestData') : undefined,
272
355
  resultKeys: [],
273
356
  };
274
357
 
@@ -298,6 +381,66 @@ export default class Trace {
298
381
  call.errorMessage = output.errorMessage;
299
382
  call.resultKeys = output.resultKeys || [];
300
383
  call.result = output.result !== undefined ? summarizeCaptureValue(output.result, trace.capture, 'result') : undefined;
384
+ call.resultJson = output.result !== undefined ? serializeCaptureValue(output.result, 'result') : undefined;
385
+ }
386
+
387
+ public setRequestResult(requestId: string, result: TTraceInspectable) {
388
+ const trace = this.requests.get(requestId);
389
+ if (!trace) return;
390
+
391
+ trace.resultJson = serializeCaptureValue(result, 'result');
392
+ }
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);
301
444
  }
302
445
 
303
446
  public listRequests(limit = 20): TRequestTraceListItem[] {
@@ -325,9 +468,18 @@ export default class Trace {
325
468
  profilerParentRequestId: trace.profilerParentRequestId,
326
469
  eventCount: trace.events.length,
327
470
  callCount: trace.calls.length,
471
+ sqlQueryCount: trace.sqlQueries.length,
328
472
  }));
329
473
  }
330
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
+
331
483
  public getLatestRequest() {
332
484
  const latestRequestId = this.order[this.order.length - 1];
333
485
  return latestRequestId ? this.requests.get(latestRequestId) : undefined;
@@ -359,6 +511,7 @@ export default class Trace {
359
511
 
360
512
  for (const requestId of this.order.splice(0, overflow)) {
361
513
  this.requests.delete(requestId);
514
+ this.activeMeasurements.delete(requestId);
362
515
  }
363
516
  }
364
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
  }