@memberjunction/server 2.50.0 → 2.51.0

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 (59) hide show
  1. package/README.md +133 -0
  2. package/dist/config.d.ts +264 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +24 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/generated/generated.d.ts +3 -0
  7. package/dist/generated/generated.d.ts.map +1 -1
  8. package/dist/generated/generated.js +532 -517
  9. package/dist/generated/generated.js.map +1 -1
  10. package/dist/generic/ResolverBase.d.ts +1 -1
  11. package/dist/generic/ResolverBase.d.ts.map +1 -1
  12. package/dist/generic/ResolverBase.js +13 -11
  13. package/dist/generic/ResolverBase.js.map +1 -1
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/orm.d.ts.map +1 -1
  19. package/dist/orm.js +6 -0
  20. package/dist/orm.js.map +1 -1
  21. package/dist/resolvers/ActionResolver.d.ts +3 -3
  22. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  23. package/dist/resolvers/ActionResolver.js +13 -10
  24. package/dist/resolvers/ActionResolver.js.map +1 -1
  25. package/dist/resolvers/FileResolver.js +1 -1
  26. package/dist/resolvers/FileResolver.js.map +1 -1
  27. package/dist/resolvers/RunAIAgentResolver.d.ts +49 -8
  28. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  29. package/dist/resolvers/RunAIAgentResolver.js +389 -106
  30. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  31. package/dist/resolvers/SqlLoggingConfigResolver.d.ts +61 -0
  32. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -0
  33. package/dist/resolvers/SqlLoggingConfigResolver.js +477 -0
  34. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -0
  35. package/dist/resolvers/UserFavoriteResolver.d.ts +3 -3
  36. package/dist/resolvers/UserFavoriteResolver.d.ts.map +1 -1
  37. package/dist/resolvers/UserFavoriteResolver.js +6 -6
  38. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  39. package/dist/resolvers/UserResolver.d.ts +3 -3
  40. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  41. package/dist/resolvers/UserResolver.js +6 -6
  42. package/dist/resolvers/UserResolver.js.map +1 -1
  43. package/dist/resolvers/UserViewResolver.d.ts +4 -4
  44. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  45. package/dist/resolvers/UserViewResolver.js +6 -6
  46. package/dist/resolvers/UserViewResolver.js.map +1 -1
  47. package/package.json +25 -25
  48. package/src/config.ts +28 -0
  49. package/src/generated/generated.ts +527 -518
  50. package/src/generic/ResolverBase.ts +17 -10
  51. package/src/index.ts +2 -1
  52. package/src/orm.ts +6 -0
  53. package/src/resolvers/ActionResolver.ts +21 -26
  54. package/src/resolvers/FileResolver.ts +1 -1
  55. package/src/resolvers/RunAIAgentResolver.ts +398 -100
  56. package/src/resolvers/SqlLoggingConfigResolver.ts +691 -0
  57. package/src/resolvers/UserFavoriteResolver.ts +6 -6
  58. package/src/resolvers/UserResolver.ts +6 -6
  59. package/src/resolvers/UserViewResolver.ts +6 -6
@@ -0,0 +1,691 @@
1
+ import { Arg, Ctx, Field, InputType, Mutation, ObjectType, Query, Int, Resolver } from 'type-graphql';
2
+ import { AppContext } from '../types.js';
3
+ import { Metadata, UserInfo } from '@memberjunction/core';
4
+ import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
5
+ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
6
+ import { v4 as uuidv4 } from 'uuid';
7
+ import * as path from 'path';
8
+ import * as fs from 'fs/promises';
9
+ import { loadConfig } from '../config.js';
10
+ import { ResolverBase } from '../generic/ResolverBase.js';
11
+
12
+ /**
13
+ * Configuration options for SQL logging sessions.
14
+ * These options control how SQL statements are captured, formatted, and saved.
15
+ */
16
+ @ObjectType()
17
+ export class SqlLoggingOptions {
18
+ /** Whether to format SQL output as a database migration file with proper headers */
19
+ @Field(() => Boolean, { nullable: true })
20
+ formatAsMigration?: boolean;
21
+
22
+ /** Optional description or notes for this logging configuration */
23
+ @Field(() => String, { nullable: true })
24
+ description?: string;
25
+
26
+ /** Types of SQL statements to capture: 'queries', 'mutations', or 'both' */
27
+ @Field(() => String, { nullable: true })
28
+ statementTypes?: 'queries' | 'mutations' | 'both';
29
+
30
+ /** String separator to use between SQL statements (e.g., 'GO' for SQL Server) */
31
+ @Field(() => String, { nullable: true })
32
+ batchSeparator?: string;
33
+
34
+ /** Whether to format SQL with proper indentation and line breaks */
35
+ @Field(() => Boolean, { nullable: true })
36
+ prettyPrint?: boolean;
37
+
38
+ /** Whether to include metadata about record changes in the log output */
39
+ @Field(() => Boolean, { nullable: true })
40
+ logRecordChangeMetadata?: boolean;
41
+
42
+ /** Whether to keep log files even if they contain no SQL statements */
43
+ @Field(() => Boolean, { nullable: true })
44
+ retainEmptyLogFiles?: boolean;
45
+
46
+ /** Email address to filter SQL statements by user (when filtering is enabled) */
47
+ @Field(() => String, { nullable: true })
48
+ filterByUserId?: string;
49
+
50
+ /** Human-readable name for the logging session */
51
+ @Field(() => String, { nullable: true })
52
+ sessionName?: string;
53
+ }
54
+
55
+ @ObjectType()
56
+ export class SqlLoggingSession {
57
+ /** Unique identifier for this logging session */
58
+ @Field(() => String)
59
+ id: string;
60
+
61
+ /** Absolute file path where SQL statements are being logged */
62
+ @Field(() => String)
63
+ filePath: string;
64
+
65
+ /** Timestamp when this logging session was started */
66
+ @Field(() => Date)
67
+ startTime: Date;
68
+
69
+ /** Number of SQL statements captured so far in this session */
70
+ @Field(() => Int)
71
+ statementCount: number;
72
+
73
+ /** Configuration options applied to this logging session */
74
+ @Field(() => SqlLoggingOptions)
75
+ options: SqlLoggingOptions;
76
+
77
+ /** Human-readable name for this logging session */
78
+ @Field(() => String, { nullable: true })
79
+ sessionName?: string;
80
+
81
+ /** Email address of user whose SQL is being filtered (if filtering enabled) */
82
+ @Field(() => String, { nullable: true })
83
+ filterByUserId?: string;
84
+ }
85
+
86
+ @InputType()
87
+ export class SqlLoggingOptionsInput {
88
+ /** Whether to format SQL output as a database migration file with proper headers */
89
+ @Field(() => Boolean, { nullable: true })
90
+ formatAsMigration?: boolean;
91
+
92
+ /** Optional description or notes for this logging configuration */
93
+ @Field(() => String, { nullable: true })
94
+ description?: string;
95
+
96
+ /** Types of SQL statements to capture: 'queries', 'mutations', or 'both' */
97
+ @Field(() => String, { nullable: true })
98
+ statementTypes?: 'queries' | 'mutations' | 'both';
99
+
100
+ /** String separator to use between SQL statements (e.g., 'GO' for SQL Server) */
101
+ @Field(() => String, { nullable: true })
102
+ batchSeparator?: string;
103
+
104
+ /** Whether to format SQL with proper indentation and line breaks */
105
+ @Field(() => Boolean, { nullable: true })
106
+ prettyPrint?: boolean;
107
+
108
+ /** Whether to include metadata about record changes in the log output */
109
+ @Field(() => Boolean, { nullable: true })
110
+ logRecordChangeMetadata?: boolean;
111
+
112
+ /** Whether to keep log files even if they contain no SQL statements */
113
+ @Field(() => Boolean, { nullable: true })
114
+ retainEmptyLogFiles?: boolean;
115
+
116
+ /** Email address to filter SQL statements by user (when filtering is enabled) */
117
+ @Field(() => String, { nullable: true })
118
+ filterByUserId?: string;
119
+
120
+ /** Human-readable name for the logging session */
121
+ @Field(() => String, { nullable: true })
122
+ sessionName?: string;
123
+ }
124
+
125
+ @InputType()
126
+ export class StartSqlLoggingInput {
127
+ /** Optional custom filename for the SQL log file (auto-generated if not provided) */
128
+ @Field(() => String, { nullable: true })
129
+ fileName?: string;
130
+
131
+ /** Configuration options for the logging session (merged with server defaults) */
132
+ @Field(() => SqlLoggingOptionsInput, { nullable: true })
133
+ options?: SqlLoggingOptionsInput;
134
+
135
+ /** Whether to filter SQL statements to only those from the current user */
136
+ @Field(() => Boolean, { nullable: true })
137
+ filterToCurrentUser?: boolean;
138
+ }
139
+
140
+ @ObjectType()
141
+ export class SqlLoggingConfig {
142
+ /** Whether SQL logging is enabled in the server configuration */
143
+ @Field(() => Boolean)
144
+ enabled: boolean;
145
+
146
+ /** Default logging options applied to new sessions (can be overridden) */
147
+ @Field(() => SqlLoggingOptions)
148
+ defaultOptions: SqlLoggingOptions;
149
+
150
+ /** Directory path where SQL log files are allowed to be created */
151
+ @Field(() => String)
152
+ allowedLogDirectory: string;
153
+
154
+ /** Maximum number of concurrent SQL logging sessions allowed */
155
+ @Field(() => Int)
156
+ maxActiveSessions: number;
157
+
158
+ /** Whether to automatically delete log files that contain no SQL statements */
159
+ @Field(() => Boolean)
160
+ autoCleanupEmptyFiles: boolean;
161
+
162
+ /** Timeout in milliseconds after which inactive sessions are automatically stopped */
163
+ @Field(() => Int)
164
+ sessionTimeout: number;
165
+
166
+ /** Current number of active SQL logging sessions */
167
+ @Field(() => Int)
168
+ activeSessionCount: number;
169
+ }
170
+
171
+ /**
172
+ * GraphQL resolver for SQL logging configuration and session management.
173
+ * Provides queries and mutations for controlling SQL logging functionality.
174
+ *
175
+ * **Security**: All operations require Owner-level privileges.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * // Start a new logging session
180
+ * const session = await startSqlLogging({
181
+ * fileName: "my-session.sql",
182
+ * filterToCurrentUser: true,
183
+ * options: {
184
+ * prettyPrint: true,
185
+ * statementTypes: "both"
186
+ * }
187
+ * });
188
+ *
189
+ * // Get current configuration
190
+ * const config = await sqlLoggingConfig();
191
+ *
192
+ * // List active sessions
193
+ * const sessions = await activeSqlLoggingSessions();
194
+ *
195
+ * // Stop a session
196
+ * await stopSqlLogging(session.id);
197
+ * ```
198
+ */
199
+ @Resolver()
200
+ export class SqlLoggingConfigResolver extends ResolverBase {
201
+ /** Default prefix for auto-generated SQL log filenames */
202
+ private static readonly LOG_FILE_PREFIX = 'sql-log-';
203
+
204
+ /**
205
+ * Validates that the current user has Owner-level privileges required for SQL logging operations.
206
+ *
207
+ * This method performs authentication and authorization checks:
208
+ * - Verifies user is authenticated (has email in context)
209
+ * - Looks up user in UserCache by email (case-insensitive)
210
+ * - Checks that user Type field equals 'Owner' (trimmed for nchar padding)
211
+ *
212
+ * @param context - The GraphQL application context containing user authentication data
213
+ * @returns Promise resolving to the authenticated UserInfo object
214
+ * @throws Error if user is not authenticated, not found, or lacks Owner privileges
215
+ *
216
+ * @private
217
+ */
218
+ private async checkOwnerAccess(context: AppContext): Promise<UserInfo> {
219
+ const userEmail = context.userPayload?.email;
220
+ if (!userEmail) {
221
+ throw new Error('User not authenticated');
222
+ }
223
+
224
+ // Get the user from cache
225
+ const users = UserCache.Instance.Users;
226
+ const user = users.find(u => u.Email.toLowerCase() === userEmail.toLowerCase());
227
+ if (!user) {
228
+ throw new Error('User not found');
229
+ }
230
+
231
+ // Debug logging
232
+ console.log('SQL Logging access check:', {
233
+ email: user.Email,
234
+ type: user.Type,
235
+ typeLength: user.Type?.length,
236
+ typeTrimmed: user.Type?.trim(),
237
+ isOwner: user.Type?.trim().toLowerCase() === 'owner'
238
+ });
239
+
240
+ // Check if user has Type = 'Owner' (trim and case-insensitive for nchar fields)
241
+ if (user.Type?.trim().toLowerCase() !== 'owner') {
242
+ throw new Error('Access denied. This feature requires Owner privileges.');
243
+ }
244
+
245
+ return user;
246
+ }
247
+
248
+ /**
249
+ * Retrieves the current SQL logging configuration and status information.
250
+ *
251
+ * Returns comprehensive configuration details including:
252
+ * - Whether SQL logging is enabled in server config
253
+ * - Default logging options (formatting, statement types, etc.)
254
+ * - File system settings (log directory, cleanup options)
255
+ * - Session limits and timeout settings
256
+ * - Count of currently active logging sessions
257
+ *
258
+ * @param context - GraphQL context (requires Owner privileges)
259
+ * @returns Promise resolving to complete SQL logging configuration
260
+ * @throws Error if user lacks Owner privileges
261
+ *
262
+ * @example
263
+ * ```graphql
264
+ * query {
265
+ * sqlLoggingConfig {
266
+ * enabled
267
+ * activeSessionCount
268
+ * maxActiveSessions
269
+ * allowedLogDirectory
270
+ * defaultOptions {
271
+ * prettyPrint
272
+ * statementTypes
273
+ * }
274
+ * }
275
+ * }
276
+ * ```
277
+ */
278
+ @Query(() => SqlLoggingConfig)
279
+ async sqlLoggingConfig(@Ctx() context: AppContext): Promise<SqlLoggingConfig> {
280
+ await this.checkOwnerAccess(context);
281
+ const config = await loadConfig();
282
+ const provider = Metadata.Provider as SQLServerDataProvider;
283
+ const activeSessions = provider.GetActiveSqlLoggingSessions();
284
+
285
+ return {
286
+ enabled: config.sqlLogging?.enabled ?? false,
287
+ defaultOptions: config.sqlLogging?.defaultOptions ?? {
288
+ formatAsMigration: false,
289
+ statementTypes: 'both',
290
+ batchSeparator: 'GO',
291
+ prettyPrint: true,
292
+ logRecordChangeMetadata: false,
293
+ retainEmptyLogFiles: false
294
+ },
295
+ allowedLogDirectory: config.sqlLogging?.allowedLogDirectory ?? './logs/sql',
296
+ maxActiveSessions: config.sqlLogging?.maxActiveSessions ?? 5,
297
+ autoCleanupEmptyFiles: config.sqlLogging?.autoCleanupEmptyFiles ?? true,
298
+ sessionTimeout: config.sqlLogging?.sessionTimeout ?? 3600000,
299
+ activeSessionCount: activeSessions.length
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Retrieves a list of all currently active SQL logging sessions.
305
+ *
306
+ * Returns detailed information for each active session including:
307
+ * - Unique session identifier and file path
308
+ * - Start time and statement count
309
+ * - Session configuration options
310
+ * - User filtering settings
311
+ *
312
+ * @param context - GraphQL context (requires Owner privileges)
313
+ * @returns Promise resolving to array of active SqlLoggingSession objects
314
+ * @throws Error if user lacks Owner privileges
315
+ *
316
+ * @example
317
+ * ```graphql
318
+ * query {
319
+ * activeSqlLoggingSessions {
320
+ * id
321
+ * sessionName
322
+ * filePath
323
+ * startTime
324
+ * statementCount
325
+ * filterByUserId
326
+ * options {
327
+ * prettyPrint
328
+ * statementTypes
329
+ * }
330
+ * }
331
+ * }
332
+ * ```
333
+ */
334
+ @Query(() => [SqlLoggingSession])
335
+ async activeSqlLoggingSessions(@Ctx() context: AppContext): Promise<SqlLoggingSession[]> {
336
+ await this.checkOwnerAccess(context);
337
+ const provider = Metadata.Provider as SQLServerDataProvider;
338
+ const sessions = provider.GetActiveSqlLoggingSessions();
339
+
340
+ return sessions.map(session => ({
341
+ id: session.id,
342
+ filePath: session.filePath,
343
+ startTime: session.startTime,
344
+ statementCount: session.statementCount,
345
+ options: session.options,
346
+ sessionName: session.options.sessionName,
347
+ filterByUserId: session.options.filterByUserId
348
+ }));
349
+ }
350
+
351
+ /**
352
+ * Creates and starts a new SQL logging session with specified configuration.
353
+ *
354
+ * This mutation:
355
+ * - Validates SQL logging is enabled and session limits
356
+ * - Creates a secure file path within the allowed log directory
357
+ * - Configures session options (filtering, formatting, etc.)
358
+ * - Starts the logging session in SQLServerDataProvider
359
+ * - Sets up automatic cleanup after session timeout
360
+ *
361
+ * @param input - Configuration for the new logging session
362
+ * @param input.fileName - Optional custom filename for the log file
363
+ * @param input.filterToCurrentUser - Whether to filter SQL to current user only
364
+ * @param input.options - Logging options (formatting, statement types, etc.)
365
+ * @param context - GraphQL context (requires Owner privileges)
366
+ * @returns Promise resolving to the created SqlLoggingSession object
367
+ * @throws Error if logging disabled, session limit reached, or invalid file path
368
+ *
369
+ * @example
370
+ * ```graphql
371
+ * mutation {
372
+ * startSqlLogging(input: {
373
+ * fileName: "debug-session.sql"
374
+ * filterToCurrentUser: true
375
+ * options: {
376
+ * prettyPrint: true
377
+ * statementTypes: "both"
378
+ * sessionName: "Debug Session"
379
+ * }
380
+ * }) {
381
+ * id
382
+ * filePath
383
+ * sessionName
384
+ * }
385
+ * }
386
+ * ```
387
+ */
388
+ @Mutation(() => SqlLoggingSession)
389
+ async startSqlLogging(
390
+ @Arg('input', () => StartSqlLoggingInput) input: StartSqlLoggingInput,
391
+ @Ctx() context: AppContext
392
+ ): Promise<SqlLoggingSession> {
393
+ await this.checkOwnerAccess(context);
394
+ const config = await loadConfig();
395
+
396
+ // Check if SQL logging is enabled
397
+ if (!config.sqlLogging?.enabled) {
398
+ throw new Error('SQL logging is not enabled in the server configuration');
399
+ }
400
+
401
+ // Check max active sessions
402
+ const provider = Metadata.Provider as SQLServerDataProvider;
403
+ const activeSessions = provider.GetActiveSqlLoggingSessions();
404
+ if (activeSessions.length >= (config.sqlLogging.maxActiveSessions ?? 5)) {
405
+ throw new Error(`Maximum number of active SQL logging sessions (${config.sqlLogging.maxActiveSessions}) reached`);
406
+ }
407
+
408
+ // Prepare file path
409
+ const allowedDir = path.resolve(config.sqlLogging.allowedLogDirectory ?? './logs/sql');
410
+ await this.ensureDirectoryExists(allowedDir);
411
+
412
+ const fileName = input.fileName || `${SqlLoggingConfigResolver.LOG_FILE_PREFIX}${new Date().toISOString().replace(/[:.]/g, '-')}.sql`;
413
+ const filePath = path.join(allowedDir, fileName);
414
+
415
+ // Validate file path is within allowed directory
416
+ const resolvedPath = path.resolve(filePath);
417
+ if (!resolvedPath.startsWith(allowedDir)) {
418
+ throw new Error('Invalid file path - must be within allowed log directory');
419
+ }
420
+
421
+ // Merge options with defaults
422
+ const defaultOptions = config.sqlLogging.defaultOptions || {};
423
+ const userInfo = input.filterToCurrentUser ? this.GetUserFromPayload(context.userPayload) : undefined;
424
+ const sessionOptions = {
425
+ ...defaultOptions,
426
+ ...input.options,
427
+ sessionName: input.options?.sessionName || `Session started by ${context.userPayload.email}`,
428
+ filterByUserId: input.filterToCurrentUser ? userInfo?.ID : input.options?.filterByUserId
429
+ };
430
+
431
+ // Create the logging session
432
+ const session = await provider.CreateSqlLogger(filePath, sessionOptions);
433
+
434
+ // Set up auto-cleanup after timeout
435
+ if (config.sqlLogging.sessionTimeout > 0) {
436
+ setTimeout(async () => {
437
+ try {
438
+ await session.dispose();
439
+ } catch (e) {
440
+ // Session might already be disposed
441
+ }
442
+ }, config.sqlLogging.sessionTimeout);
443
+ }
444
+
445
+ return {
446
+ id: session.id,
447
+ filePath: session.filePath,
448
+ startTime: session.startTime,
449
+ statementCount: session.statementCount,
450
+ options: session.options,
451
+ sessionName: session.options.sessionName,
452
+ filterByUserId: session.options.filterByUserId
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Stops and disposes of a specific SQL logging session.
458
+ *
459
+ * This mutation:
460
+ * - Validates the session exists and user has access
461
+ * - Calls dispose() on the session to close file handles
462
+ * - Removes the session from the active sessions map
463
+ * - Performs any configured cleanup operations
464
+ *
465
+ * @param sessionId - Unique identifier of the session to stop
466
+ * @param context - GraphQL context (requires Owner privileges)
467
+ * @returns Promise resolving to true if session was successfully stopped
468
+ * @throws Error if session not found or user lacks Owner privileges
469
+ *
470
+ * @example
471
+ * ```graphql
472
+ * mutation {
473
+ * stopSqlLogging(sessionId: "session-123-456")
474
+ * }
475
+ * ```
476
+ */
477
+ @Mutation(() => Boolean)
478
+ async stopSqlLogging(
479
+ @Arg('sessionId', () => String) sessionId: string,
480
+ @Ctx() context: AppContext
481
+ ): Promise<boolean> {
482
+ await this.checkOwnerAccess(context);
483
+ const provider = Metadata.Provider as SQLServerDataProvider;
484
+
485
+ // Get the actual session from the private map to call dispose
486
+ const sessionMap = (provider as any)._sqlLoggingSessions as Map<string, any>;
487
+ const session = sessionMap.get(sessionId);
488
+
489
+ if (!session) {
490
+ throw new Error(`SQL logging session ${sessionId} not found`);
491
+ }
492
+
493
+ await session.dispose();
494
+ return true;
495
+ }
496
+
497
+ /**
498
+ * Stops and disposes of all currently active SQL logging sessions.
499
+ *
500
+ * This is a convenience method that:
501
+ * - Calls DisposeAllSqlLoggingSessions() on the data provider
502
+ * - Ensures all file handles are properly closed
503
+ * - Clears the active sessions map
504
+ * - Performs cleanup for all sessions at once
505
+ *
506
+ * @param context - GraphQL context (requires Owner privileges)
507
+ * @returns Promise resolving to true if all sessions were successfully stopped
508
+ * @throws Error if user lacks Owner privileges
509
+ *
510
+ * @example
511
+ * ```graphql
512
+ * mutation {
513
+ * stopAllSqlLogging
514
+ * }
515
+ * ```
516
+ */
517
+ @Mutation(() => Boolean)
518
+ async stopAllSqlLogging(@Ctx() context: AppContext): Promise<boolean> {
519
+ await this.checkOwnerAccess(context);
520
+ const provider = Metadata.Provider as SQLServerDataProvider;
521
+ await provider.DisposeAllSqlLoggingSessions();
522
+ return true;
523
+ }
524
+
525
+ /**
526
+ * Updates the default SQL logging options for new sessions.
527
+ *
528
+ * **Note**: This updates runtime configuration only, not the static config file.
529
+ * Changes apply to new sessions but do not persist across server restarts.
530
+ * In a production system, consider persisting changes to a database.
531
+ *
532
+ * @param options - New default options to apply (partial update supported)
533
+ * @param context - GraphQL context (requires Owner privileges)
534
+ * @returns Promise resolving to the updated SqlLoggingOptions object
535
+ * @throws Error if SQL logging not configured or user lacks Owner privileges
536
+ *
537
+ * @example
538
+ * ```graphql
539
+ * mutation {
540
+ * updateSqlLoggingDefaults(options: {
541
+ * prettyPrint: true
542
+ * statementTypes: "both"
543
+ * logRecordChangeMetadata: false
544
+ * }) {
545
+ * prettyPrint
546
+ * statementTypes
547
+ * formatAsMigration
548
+ * }
549
+ * }
550
+ * ```
551
+ */
552
+ @Mutation(() => SqlLoggingOptions)
553
+ async updateSqlLoggingDefaults(
554
+ @Arg('options', () => SqlLoggingOptionsInput) options: SqlLoggingOptionsInput,
555
+ @Ctx() context: AppContext
556
+ ): Promise<SqlLoggingOptions> {
557
+ await this.checkOwnerAccess(context);
558
+ // Note: This updates the runtime configuration only, not the file
559
+ // In a production system, you might want to persist this to a database
560
+ const config = await loadConfig();
561
+ if (!config.sqlLogging) {
562
+ throw new Error('SQL logging configuration not found');
563
+ }
564
+
565
+ config.sqlLogging.defaultOptions = {
566
+ ...config.sqlLogging.defaultOptions,
567
+ ...options
568
+ };
569
+
570
+ return config.sqlLogging.defaultOptions;
571
+ }
572
+
573
+ /**
574
+ * Reads the contents of a specific SQL log file.
575
+ *
576
+ * This method:
577
+ * - Validates the session exists and user has access
578
+ * - Ensures the file path is within the allowed log directory
579
+ * - Reads the file content with optional line limits
580
+ * - Returns the content as a string
581
+ *
582
+ * @param sessionId - Unique identifier of the logging session
583
+ * @param maxLines - Maximum number of lines to read (optional, defaults to all)
584
+ * @param context - GraphQL context (requires Owner privileges)
585
+ * @returns Promise resolving to the log file content
586
+ * @throws Error if session not found, file not accessible, or user lacks privileges
587
+ *
588
+ * @example
589
+ * ```graphql
590
+ * query {
591
+ * readSqlLogFile(sessionId: "session-123", maxLines: 100)
592
+ * }
593
+ * ```
594
+ */
595
+ @Query(() => String)
596
+ async readSqlLogFile(
597
+ @Arg('sessionId', () => String) sessionId: string,
598
+ @Arg('maxLines', () => Int, { nullable: true }) maxLines: number | null,
599
+ @Ctx() context: AppContext
600
+ ): Promise<string> {
601
+ await this.checkOwnerAccess(context);
602
+ const config = await loadConfig();
603
+
604
+ // Check if SQL logging is enabled
605
+ if (!config.sqlLogging?.enabled) {
606
+ throw new Error('SQL logging is not enabled in the server configuration');
607
+ }
608
+
609
+ // Find the session
610
+ const provider = Metadata.Provider as SQLServerDataProvider;
611
+ const sessions = provider.GetActiveSqlLoggingSessions();
612
+ const session = sessions.find(s => s.id === sessionId);
613
+
614
+ if (!session) {
615
+ throw new Error(`SQL logging session ${sessionId} not found`);
616
+ }
617
+
618
+ // Validate file path is within allowed directory
619
+ const allowedDir = path.resolve(config.sqlLogging.allowedLogDirectory ?? './logs/sql');
620
+ const resolvedPath = path.resolve(session.filePath);
621
+ if (!resolvedPath.startsWith(allowedDir)) {
622
+ throw new Error('Access denied - file path outside allowed directory');
623
+ }
624
+
625
+ try {
626
+ // Check if file exists
627
+ await fs.access(session.filePath);
628
+
629
+ // Read file content
630
+ const content = await fs.readFile(session.filePath, 'utf-8');
631
+
632
+ // Apply line limit if specified
633
+ if (maxLines && maxLines > 0) {
634
+ const lines = content.split('\n');
635
+ if (lines.length > maxLines) {
636
+ // Return the last N lines
637
+ return lines.slice(-maxLines).join('\n');
638
+ }
639
+ }
640
+
641
+ return content;
642
+ } catch (error: any) {
643
+ if (error.code === 'ENOENT') {
644
+ return '-- Log file not yet created or is empty --';
645
+ }
646
+ throw new Error(`Failed to read log file: ${error.message}`);
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Debug query to check what the current user email is in the SQL provider.
652
+ * This helps diagnose user filtering issues when SQL statements aren't being captured.
653
+ *
654
+ * Returns a comparison of the user email stored in the SQLServerDataProvider
655
+ * versus the user email from the GraphQL context, which helps identify mismatches
656
+ * that could prevent SQL filtering from working correctly.
657
+ *
658
+ * @param context - GraphQL context containing user information
659
+ * @returns Formatted string showing both email values and whether they match
660
+ * @throws Error if user doesn't have Owner privileges
661
+ */
662
+ @Query(() => String)
663
+ async debugCurrentUserEmail(@Ctx() context: AppContext): Promise<string> {
664
+ await this.checkOwnerAccess(context);
665
+
666
+ const contextUserEmail = context.userPayload?.email || 'NOT_SET';
667
+
668
+ return `Context User Email: "${contextUserEmail}" | Note: Provider no longer stores user email - uses contextUser parameter for SQL logging`;
669
+ }
670
+
671
+ /**
672
+ * Ensures the specified log directory exists, creating it if necessary.
673
+ *
674
+ * This method:
675
+ * - Attempts to access the directory to check if it exists
676
+ * - Creates the directory recursively if it doesn't exist
677
+ * - Handles permission and file system errors gracefully
678
+ *
679
+ * @param dir - Absolute path to the directory to ensure exists
680
+ * @throws Error if directory cannot be created due to permissions or other issues
681
+ *
682
+ * @private
683
+ */
684
+ private async ensureDirectoryExists(dir: string): Promise<void> {
685
+ try {
686
+ await fs.access(dir);
687
+ } catch {
688
+ await fs.mkdir(dir, { recursive: true });
689
+ }
690
+ }
691
+ }