Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 26 additions & 23 deletions src/LightQueryProfiler.JsonRpc/JsonRpcServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,12 @@ await Task.Run(() => profilerService.PauseProfiling(request.SessionName), cancel
}

/// <summary>
/// Returns all saved recent connections sorted by most recent first.
/// Returns all saved connections sorted by most recent first.
/// Passwords in the returned DTOs are already decrypted by the repository layer.
/// </summary>
[JsonRpcMethod("GetRecentConnectionsAsync", UseSingleObjectParameterDeserialization = true)]
public async Task<List<RecentConnectionDto>> GetRecentConnectionsAsync(
GetRecentConnectionsRequest request,
[JsonRpcMethod("GetConnectionsAsync", UseSingleObjectParameterDeserialization = true)]
public async Task<List<ConnectionDto>> GetConnectionsAsync(
GetConnectionsRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expand All @@ -319,7 +319,7 @@ public async Task<List<RecentConnectionDto>> GetRecentConnectionsAsync(

return connections
.OrderByDescending(c => c.CreationDate)
.Select(c => new RecentConnectionDto
.Select(c => new ConnectionDto
{
Id = c.Id,
DataSource = c.DataSource,
Expand All @@ -330,24 +330,25 @@ public async Task<List<RecentConnectionDto>> GetRecentConnectionsAsync(
EngineType = c.EngineType.HasValue ? (int)c.EngineType.Value : null,
AuthenticationMode = (int)c.AuthenticationMode,
ConnectionString = c.ConnectionString,
ProfileName = c.ProfileName,
})
.ToList();
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to retrieve recent connections");
_logger.LogError(ex, "Failed to retrieve connections");
throw;
}
}

/// <summary>
/// Saves (upserts) a connection. If the same DataSource+UserId+InitialCatalog already
/// Saves (upserts) a connection profile. If the same DataSource+UserId+InitialCatalog already
/// exists the row is updated; otherwise a new row is inserted.
/// Passwords are encrypted by the repository layer before storage.
/// </summary>
[JsonRpcMethod("SaveRecentConnectionAsync", UseSingleObjectParameterDeserialization = true)]
public async Task SaveRecentConnectionAsync(
SaveRecentConnectionRequest request,
[JsonRpcMethod("SaveConnectionAsync", UseSingleObjectParameterDeserialization = true)]
public async Task SaveConnectionAsync(
SaveConnectionRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expand All @@ -361,7 +362,7 @@ public async Task SaveRecentConnectionAsync(

var builder = new SqlConnectionStringBuilder(request.ConnectionString);
var connection = new Connection(
id: 0,
id: request.Id.HasValue ? request.Id.Value : 0,
initialCatalog: builder.InitialCatalog,
creationDate: DateTime.UtcNow,
dataSource: builder.DataSource,
Expand All @@ -370,14 +371,15 @@ public async Task SaveRecentConnectionAsync(
userId: string.IsNullOrEmpty(builder.UserID) ? null : builder.UserID,
engineType: null,
authenticationMode: AuthenticationMode.ConnectionString,
connectionString: request.ConnectionString);
connectionString: request.ConnectionString,
profileName: request.ProfileName);

await _connectionRepository.UpsertAsync(connection).ConfigureAwait(false);

if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation(
"Recent connection saved (ConnString mode): DataSource={DataSource}, InitialCatalog={InitialCatalog}",
"Connection saved (ConnString mode): DataSource={DataSource}, InitialCatalog={InitialCatalog}",
builder.DataSource,
builder.InitialCatalog);
}
Expand All @@ -399,7 +401,7 @@ public async Task SaveRecentConnectionAsync(
try
{
var connection = new Connection(
id: 0,
id: request.Id.HasValue ? request.Id.Value : 0,
initialCatalog: request.InitialCatalog,
creationDate: DateTime.UtcNow,
dataSource: request.DataSource,
Expand All @@ -411,35 +413,36 @@ public async Task SaveRecentConnectionAsync(
: null,
authenticationMode: request.AuthenticationMode.HasValue
? (AuthenticationMode)request.AuthenticationMode.Value
: AuthenticationMode.WindowsAuth);
: AuthenticationMode.WindowsAuth,
profileName: request.ProfileName);

await _connectionRepository.UpsertAsync(connection).ConfigureAwait(false);

if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation(
"Recent connection saved: DataSource={DataSource}, InitialCatalog={InitialCatalog}",
"Connection saved: DataSource={DataSource}, InitialCatalog={InitialCatalog}",
request.DataSource,
request.InitialCatalog);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save recent connection: {DataSource}", request.DataSource);
_logger.LogError(ex, "Failed to save connection: {DataSource}", request.DataSource);
throw;
}
}

/// <summary>
/// Deletes a recent connection by its unique identifier.
/// Deletes a connection by its unique identifier.
/// </summary>
/// <remarks>
/// If no row with the given <paramref name="request"/> Id exists the operation
/// completes silently — SQLite DELETE is a no-op when no rows match.
/// </remarks>
[JsonRpcMethod("DeleteRecentConnectionAsync", UseSingleObjectParameterDeserialization = true)]
public async Task DeleteRecentConnectionAsync(
DeleteRecentConnectionRequest request,
[JsonRpcMethod("DeleteConnectionAsync", UseSingleObjectParameterDeserialization = true)]
public async Task DeleteConnectionAsync(
DeleteConnectionRequest request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(request);
Expand All @@ -451,12 +454,12 @@ public async Task DeleteRecentConnectionAsync(

if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Recent connection deleted: Id={Id}", request.Id);
_logger.LogInformation("Connection deleted: Id={Id}", request.Id);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to delete recent connection: {Id}", request.Id);
_logger.LogError(ex, "Failed to delete connection: {Id}", request.Id);
throw;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
namespace LightQueryProfiler.JsonRpc.Models
{
/// <summary>
/// Data transfer object representing a recent connection entry.
/// Data transfer object representing a saved connection profile.
/// Passwords are returned in plain-text — decryption has already been applied by the repository.
/// </summary>
public record RecentConnectionDto
public record ConnectionDto
{
public required int Id { get; init; }
public required string DataSource { get; init; }
Expand All @@ -25,5 +25,8 @@ public record RecentConnectionDto
/// Never log this value — it may contain credentials.
/// </remarks>
public string? ConnectionString { get; init; }

/// <summary>User-assigned descriptive name for this connection profile.</summary>
public string? ProfileName { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
namespace LightQueryProfiler.JsonRpc.Models;

/// <summary>
/// Request model for deleting a recent connection by its unique identifier.
/// Request model for deleting a connection by its unique identifier.
/// </summary>
public record DeleteRecentConnectionRequest
public record DeleteConnectionRequest
{
/// <summary>Gets the unique identifier of the connection to delete.</summary>
public required int Id { get; init; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
namespace LightQueryProfiler.JsonRpc.Models
{
/// <summary>
/// Request model for retrieving all recent connections.
/// Request model for retrieving all saved connections.
/// No parameters needed — returns all connections sorted by most recent first.
/// Kept as a record for consistency with the existing pattern and future extensibility.
/// </summary>
public record GetRecentConnectionsRequest
public record GetConnectionsRequest
{
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
namespace LightQueryProfiler.JsonRpc.Models
{
/// <summary>
/// Request model for saving (upserting) a recent connection.
/// Request model for saving (upserting) a connection profile.
/// Passwords are accepted in plain-text and encrypted by the repository layer.
/// </summary>
public record SaveRecentConnectionRequest
public record SaveConnectionRequest
{
/// <summary>
/// Gets the unique identifier of the connection to update.
/// When null or 0, a new connection is created.
/// </summary>
public int? Id { get; init; }

public required string DataSource { get; init; }
public required string InitialCatalog { get; init; }
public string? UserId { get; init; }
Expand All @@ -24,5 +30,8 @@ public record SaveRecentConnectionRequest
/// The repository layer encrypts this value before storage. Never log this value.
/// </remarks>
public string? ConnectionString { get; init; }

/// <summary>User-assigned descriptive name for this connection profile.</summary>
public string? ProfileName { get; init; }
}
}
16 changes: 16 additions & 0 deletions src/LightQueryProfiler.Shared/Data/SqliteContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ FROM pragma_table_info('Connections')
SqliteCommand alterTableCmd = new(alterTableConnString, db);
await alterTableCmd.ExecuteNonQueryAsync();
}

// Migration: Add ProfileName column if it doesn't exist (for existing databases)
const string addProfileNameColumnCheck = @"
SELECT COUNT(*) as ColumnExists
FROM pragma_table_info('Connections')
WHERE name='ProfileName'";

SqliteCommand checkProfileNameColumn = new(addProfileNameColumnCheck, db);
var profileNameResult = await checkProfileNameColumn.ExecuteScalarAsync();

if (profileNameResult != null && Convert.ToInt32(profileNameResult) == 0)
{
const string alterTableProfileName = "ALTER TABLE Connections ADD COLUMN ProfileName NVARCHAR(200) NULL";
SqliteCommand alterTableCmd = new(alterTableProfileName, db);
await alterTableCmd.ExecuteNonQueryAsync();
}
}
}
}
9 changes: 8 additions & 1 deletion src/LightQueryProfiler.Shared/Models/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public class Connection
/// <param name="engineType">Detected or specified engine type. <see langword="null"/> when using <see cref="AuthenticationMode.ConnectionString"/> (detected at start-profiling time).</param>
/// <param name="authenticationMode">Authentication method used.</param>
/// <param name="connectionString">Raw ADO.NET connection string. Only set when <paramref name="authenticationMode"/> is <see cref="AuthenticationMode.ConnectionString"/>. Plain-text — the repository layer decrypts it before passing here. Never log this value.</param>
public Connection(int id, string initialCatalog, DateTime creationDate, string dataSource, bool integratedSecurity, string? password, string? userId, DatabaseEngineType? engineType = null, AuthenticationMode authenticationMode = AuthenticationMode.WindowsAuth, string? connectionString = null)
/// <param name="profileName">User-assigned descriptive name for this connection profile (e.g., "Production", "Dev Local").</param>
public Connection(int id, string initialCatalog, DateTime creationDate, string dataSource, bool integratedSecurity, string? password, string? userId, DatabaseEngineType? engineType = null, AuthenticationMode authenticationMode = AuthenticationMode.WindowsAuth, string? connectionString = null, string? profileName = null)
{
Id = id;
InitialCatalog = initialCatalog;
Expand All @@ -29,6 +30,7 @@ public Connection(int id, string initialCatalog, DateTime creationDate, string d
EngineType = engineType;
AuthenticationMode = authenticationMode;
ConnectionString = connectionString;
ProfileName = profileName;
}

public int Id { get; }
Expand Down Expand Up @@ -59,5 +61,10 @@ public Connection(int id, string initialCatalog, DateTime creationDate, string d
/// </summary>
/// <remarks>Never log this value — it may contain credentials.</remarks>
public string? ConnectionString { get; }

/// <summary>
/// Gets the user-assigned descriptive name for this connection profile (e.g., "Production", "Dev Local").
/// </summary>
public string? ProfileName { get; }
}
}
Loading
Loading