Created database for user interests.
This commit is contained in:
parent
41d5d22b01
commit
54777c0a2d
@ -10,6 +10,7 @@ builder.Services.AddSwaggerGen();
|
|||||||
|
|
||||||
builder.Services.AddSingleton<IHistoricalMarketDataCache, PostgresHistoricalMarketDataCache>();
|
builder.Services.AddSingleton<IHistoricalMarketDataCache, PostgresHistoricalMarketDataCache>();
|
||||||
builder.Services.AddSingleton<MarketDataService>();
|
builder.Services.AddSingleton<MarketDataService>();
|
||||||
|
builder.Services.AddSingleton<IUserInterestStore, PostgresUserInterestStore>();
|
||||||
|
|
||||||
builder.Services.AddCors(options =>
|
builder.Services.AddCors(options =>
|
||||||
{
|
{
|
||||||
|
|||||||
66
Controller/UserInterestController.cs
Normal file
66
Controller/UserInterestController.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
DatabaseHandler/IUserInterestCache.cs
Normal file
8
DatabaseHandler/IUserInterestCache.cs
Normal 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);
|
||||||
|
}
|
||||||
141
DatabaseHandler/PostgresUserInterestStore.cs
Normal file
141
DatabaseHandler/PostgresUserInterestStore.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
UserInterests/UserInterests.cs
Normal file
24
UserInterests/UserInterests.cs
Normal 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; }
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user