Created database for user interests.

This commit is contained in:
caspar 2026-03-19 03:32:45 -04:00
parent 41d5d22b01
commit 54777c0a2d
7 changed files with 240 additions and 0 deletions

View File

@ -10,6 +10,7 @@ builder.Services.AddSwaggerGen();
builder.Services.AddSingleton<IHistoricalMarketDataCache, PostgresHistoricalMarketDataCache>();
builder.Services.AddSingleton<MarketDataService>();
builder.Services.AddSingleton<IUserInterestStore, PostgresUserInterestStore>();
builder.Services.AddCors(options =>
{

View File

@ -0,0 +1,66 @@
using Backend.DatabaseHandler;
using Backend.UserInterests;
using Microsoft.AspNetCore.Mvc;
namespace Backend.Controllers;
[ApiController]
[Route("api/user-interest")]
public sealed class UserInterestController : ControllerBase
{
private readonly IUserInterestStore _store;
public UserInterestController(IUserInterestStore store)
{
_store = store;
}
[HttpGet("fetch")]
public async Task<ActionResult<UserInterestFetchResponse>> FetchAll(
[FromQuery] string user,
[FromQuery] string type)
{
try
{
List<string> items = await _store.FetchAllAsync(user, type);
return Ok(new UserInterestFetchResponse
{
Items = items
});
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
[HttpPost("add")]
public async Task<ActionResult<BoolResponse>> Add(
[FromBody] UserInterestModifyRequest request)
{
try
{
bool success = await _store.AddAsync(request.User, request.Type, request.Value);
return Ok(new BoolResponse { Success = success });
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
[HttpDelete("remove")]
public async Task<ActionResult<BoolResponse>> Remove(
[FromBody] UserInterestModifyRequest request)
{
try
{
bool success = await _store.RemoveAsync(request.User, request.Type, request.Value);
return Ok(new BoolResponse { Success = success });
}
catch (ArgumentException ex)
{
return BadRequest(ex.Message);
}
}
}

View File

@ -0,0 +1,8 @@
namespace Backend.DatabaseHandler;
public interface IUserInterestStore
{
Task<List<string>> FetchAllAsync(string user, string type);
Task<bool> AddAsync(string user, string type, string value);
Task<bool> RemoveAsync(string user, string type, string value);
}

View File

@ -0,0 +1,141 @@
using Npgsql;
namespace Backend.DatabaseHandler;
public sealed class PostgresUserInterestStore : IUserInterestStore
{
private readonly string _connectionString;
public PostgresUserInterestStore(IConfiguration configuration)
{
_connectionString = configuration.GetConnectionString("MarketDataDb")
?? throw new InvalidOperationException("Missing connection string: MarketDataDb");
}
public async Task<List<string>> FetchAllAsync(string user, string type)
{
ValidateUser(user);
string normalizedType = NormalizeType(type);
string sql = normalizedType == "symbol"
? """
SELECT symbol
FROM user_interest_symbols
WHERE username = @user
ORDER BY symbol ASC;
"""
: """
SELECT topic
FROM user_interest_topics
WHERE username = @user
ORDER BY topic ASC;
""";
var results = new List<string>();
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("user", user.Trim());
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
results.Add(reader.GetString(0));
}
return results;
}
public async Task<bool> AddAsync(string user, string type, string value)
{
ValidateUser(user);
ValidateValue(value);
string normalizedType = NormalizeType(type);
string normalizedValue = normalizedType == "symbol"
? value.Trim().ToUpperInvariant()
: value.Trim();
string sql = normalizedType == "symbol"
? """
INSERT INTO user_interest_symbols (username, symbol)
VALUES (@user, @value)
ON CONFLICT (username, symbol) DO NOTHING;
"""
: """
INSERT INTO user_interest_topics (username, topic)
VALUES (@user, @value)
ON CONFLICT (username, topic) DO NOTHING;
""";
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("user", user.Trim());
cmd.Parameters.AddWithValue("value", normalizedValue);
int rows = await cmd.ExecuteNonQueryAsync();
return rows > 0;
}
public async Task<bool> RemoveAsync(string user, string type, string value)
{
ValidateUser(user);
ValidateValue(value);
string normalizedType = NormalizeType(type);
string normalizedValue = normalizedType == "symbol"
? value.Trim().ToUpperInvariant()
: value.Trim();
string sql = normalizedType == "symbol"
? """
DELETE FROM user_interest_symbols
WHERE username = @user AND symbol = @value;
"""
: """
DELETE FROM user_interest_topics
WHERE username = @user AND topic = @value;
""";
await using var conn = new NpgsqlConnection(_connectionString);
await conn.OpenAsync();
await using var cmd = new NpgsqlCommand(sql, conn);
cmd.Parameters.AddWithValue("user", user.Trim());
cmd.Parameters.AddWithValue("value", normalizedValue);
int rows = await cmd.ExecuteNonQueryAsync();
return rows > 0;
}
private static string NormalizeType(string type)
{
string normalized = type.Trim().ToLowerInvariant();
return normalized switch
{
"symbol" => "symbol",
"topic" => "topic",
_ => throw new ArgumentException("Type must be either 'symbol' or 'topic'.")
};
}
private static void ValidateUser(string user)
{
if (string.IsNullOrWhiteSpace(user))
{
throw new ArgumentException("User cannot be empty.");
}
}
private static void ValidateValue(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value cannot be empty.");
}
}
}

View File

@ -0,0 +1,24 @@
namespace Backend.UserInterests;
public sealed class UserInterestFetchRequest
{
public string User { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; // "symbol" or "topic"
}
public sealed class UserInterestModifyRequest
{
public string User { get; set; } = string.Empty;
public string Type { get; set; } = string.Empty; // "symbol" or "topic"
public string Value { get; set; } = string.Empty; // symbol or topic
}
public sealed class UserInterestFetchResponse
{
public List<string> Items { get; set; } = new();
}
public sealed class BoolResponse
{
public bool Success { get; set; }
}