diff --git a/SDG-Backend-Barracuda/Models/CardListModel.cs b/SDG-Backend-Barracuda/Models/CardListModel.cs new file mode 100644 index 0000000..9cf3d38 --- /dev/null +++ b/SDG-Backend-Barracuda/Models/CardListModel.cs @@ -0,0 +1,177 @@ +using Dapper; +using Npgsql; + +namespace SDG_Backend_Barracuda.Models; + +public record CardList +{ + public int Id { get; init; } + public IEnumerable? Cards { get; init; } + public IEnumerable? SubLists { get; init; } + + public CardList() { } + + public CardList(int id, IEnumerable? cards = null, IEnumerable? subLists = null) + { + Id = id; + Cards = cards; + SubLists = subLists; + } +} + +public record CardListInformation(IEnumerable? CardIds = null, IEnumerable? SubListIds = null); + +public interface ICardListModel : IDatabaseModel +{} + +public class CardListModel(NpgsqlDataSource dataSource) : ICardListModel, IEndpointRouteHandler +{ + public static void MapEndpoints(IEndpointRouteBuilder router) + { + var listEndpoint = router.MapGroup("/cardlist"); + + // Create + listEndpoint.MapPost("/", async (CardListInformation input, ICardListModel model) => + { + var newList = await model.Create(input); + return Results.Created($"/cardlist/{newList.Id}", newList); + }); + + // Get all + listEndpoint.MapGet("/", async (ICardListModel model) => + Results.Ok(await model.GetAll())); + + // Get one + listEndpoint.MapGet("/{id}", async (int id, ICardListModel model) => + await model.GetById(id) is { } l ? Results.Ok(l) : Results.NotFound()); + + // Update + listEndpoint.MapPut("/{id}", async (int id, CardListInformation input, ICardListModel model) => + await model.Update(id, input) is { } l ? Results.Ok(l) : Results.NotFound()); + + // Delete + listEndpoint.MapDelete("/{id}", async (int id, ICardListModel model) => + await model.Delete(id) ? Results.NoContent() : Results.NotFound()); + } + + public async Task> GetAll() + { + await using var connection = await dataSource.OpenConnectionAsync(); + var sql = """SELECT * FROM "CardList";"""; + var cardListIds = await connection.QueryAsync(sql); + + var result = new List(); + foreach (var cardListId in cardListIds) + { + result.Add(await GetById(cardListId.Id) ?? cardListId); + } + return result; + } + + public async Task GetById(int id) + { + // Check if it exists + await using var connection = await dataSource.OpenConnectionAsync(); + var listSql = """SELECT * FROM "CardList" WHERE "Id" = @Id;"""; + var cardListId = await connection.QueryFirstOrDefaultAsync(listSql, new { Id = id }); + if (cardListId == null) return null; + + // Get all associated cards + var cardsSql = """ + SELECT c."Id", c."Value" + FROM "Card" c + JOIN "CardListCard" clc ON c."Id" = clc."CardId" + WHERE clc."CardListId" = @Id; + """; + var cards = await connection.QueryAsync(cardsSql, new { Id = id }); + + // Get all child CardLists + var subListsSql = """ + SELECT sl."Id" + FROM "CardList" sl + JOIN "CardListSubList" clsl ON sl."Id" = clsl."SubCardListId" + WHERE clsl."CardListId" = @Id; + """; + var subLists = await connection.QueryAsync(subListsSql, new { Id = id }); + + return cardListId with { Cards = cards, SubLists = subLists }; + } + + public async Task CreateEmpty() + { + await using var connection = await dataSource.OpenConnectionAsync(); + var sql = """INSERT INTO "CardList" DEFAULT VALUES RETURNING "Id";"""; + return await connection.QuerySingleAsync(sql); + } + + public async Task Create(CardListInformation input) + { + await using var connection = await dataSource.OpenConnectionAsync(); + var newList = await CreateEmpty(); + + if (input.CardIds != null) + { + foreach (var cardId in input.CardIds) + { + await connection.ExecuteAsync( + """INSERT INTO "CardListCard" ("CardListId", "CardId") VALUES (@ListId, @CardId)""", + new { ListId = newList.Id, CardId = cardId }); + } + } + + if (input.SubListIds != null) + { + foreach (var subListId in input.SubListIds) + { + await connection.ExecuteAsync( + """INSERT INTO "CardListSubList" ("CardListId", "SubCardListId") VALUES (@ListId, @SubListId)""", + new { ListId = newList.Id, SubListId = subListId }); + } + } + + return (await GetById(newList.Id))!; + } + + public async Task Update(int id, CardListInformation input) + { + await using var connection = await dataSource.OpenConnectionAsync(); + + // Verify list exists + var exists = await connection.ExecuteScalarAsync("""SELECT EXISTS(SELECT 1 FROM "CardList" WHERE "Id" = @Id)""", new { Id = id }); + if (!exists) return null; + + // Sync Cards - Maybe think through a better way to do this that isn't "wipe it, then start over" + await connection.ExecuteAsync("""DELETE FROM "CardListCard" WHERE "CardListId" = @Id""", new { Id = id }); + if (input.CardIds != null) + { + foreach (var cardId in input.CardIds) + { + await connection.ExecuteAsync( + """INSERT INTO "CardListCard" ("CardListId", "CardId") VALUES (@Id, @CardId)""", + new { Id = id, CardId = cardId }); + } + } + + // Sync SubLists - Maybe think through a better way to do this that isn't "wipe it, then start over" + await connection.ExecuteAsync("""DELETE FROM "CardListSubList" WHERE "CardListId" = @Id""", new { Id = id }); + if (input.SubListIds != null) + { + foreach (var subListId in input.SubListIds) + { + await connection.ExecuteAsync( + """INSERT INTO "CardListSubList" ("CardListId", "SubCardListId") VALUES (@Id, @SubListId)""", + new { Id = id, SubListId = subListId }); + } + } + + return await GetById(id); + } + + public async Task Delete(int id) + { + await using var connection = await dataSource.OpenConnectionAsync(); + var sql = """DELETE FROM "CardList" WHERE "Id" = @Id;"""; + var rowsAffected = await connection.ExecuteAsync(sql, new { Id = id }); + return rowsAffected > 0; + } +} diff --git a/SDG-Backend-Barracuda/Models/CardModel.cs b/SDG-Backend-Barracuda/Models/CardModel.cs index c0f8797..0e5ec19 100644 --- a/SDG-Backend-Barracuda/Models/CardModel.cs +++ b/SDG-Backend-Barracuda/Models/CardModel.cs @@ -60,7 +60,7 @@ public class CardModel(NpgsqlDataSource dataSource) : ICardModel, IEndpointRoute { await using var connection = await dataSource.OpenConnectionAsync(); var sql = """UPDATE "Card" SET "Value" = @Value WHERE "Id" = @Id RETURNING "Id", "Value";"""; - return await connection.QueryFirstOrDefaultAsync(sql, new { Id = id, Value = input.Value }); + return await connection.QueryFirstOrDefaultAsync(sql, new { Id = id, input.Value }); } public async Task Delete(int id) diff --git a/SDG-Backend-Barracuda/Program.cs b/SDG-Backend-Barracuda/Program.cs index 3ee5b1e..14d9539 100644 --- a/SDG-Backend-Barracuda/Program.cs +++ b/SDG-Backend-Barracuda/Program.cs @@ -14,6 +14,7 @@ builder.Services.AddNpgsqlDataSource(connectionString!); // Register Data Tables builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); @@ -26,6 +27,19 @@ using (var scope = app.Services.CreateScope()) CREATE TABLE IF NOT EXISTS ""Card"" ( ""Id"" SERIAL PRIMARY KEY, ""Value"" TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS ""CardList"" ( + ""Id"" SERIAL PRIMARY KEY + ); + CREATE TABLE IF NOT EXISTS ""CardListCard"" ( + ""CardListId"" INTEGER NOT NULL REFERENCES ""CardList""(""Id"") ON DELETE CASCADE, + ""CardId"" INTEGER NOT NULL REFERENCES ""Card""(""Id"") ON DELETE CASCADE, + PRIMARY KEY (""CardListId"", ""CardId"") + ); + CREATE TABLE IF NOT EXISTS ""CardListSubList"" ( + ""CardListId"" INTEGER NOT NULL REFERENCES ""CardList""(""Id"") ON DELETE CASCADE, + ""SubCardListId"" INTEGER NOT NULL REFERENCES ""CardList""(""Id"") ON DELETE CASCADE, + PRIMARY KEY (""CardListId"", ""SubCardListId"") );"); } @@ -38,5 +52,6 @@ if (app.Environment.IsDevelopment()) app.UseHttpsRedirection(); CardModel.MapEndpoints(app); +CardListModel.MapEndpoints(app); app.Run(); \ No newline at end of file diff --git a/SDG-Backend-Barracuda/SDG-Backend-Barracuda.http b/SDG-Backend-Barracuda/SDG-Backend-Barracuda.http index 9e103ee..8d24470 100644 --- a/SDG-Backend-Barracuda/SDG-Backend-Barracuda.http +++ b/SDG-Backend-Barracuda/SDG-Backend-Barracuda.http @@ -26,3 +26,37 @@ Content-Type: application/json ### Delete a card DELETE {{SDG_Backend_Barracuda_HostAddress}}/card/1 + + +### Create a new empty card list +POST {{SDG_Backend_Barracuda_HostAddress}}/cardlist/ +Content-Type: application/json + +### Create a new card list with cards and sub-lists +POST {{SDG_Backend_Barracuda_HostAddress}}/cardlist +Content-Type: application/json + +{ + "cardIds": [3, 2], + "subListIds": [4] +} + +### Get all card lists +GET {{SDG_Backend_Barracuda_HostAddress}}/cardlist +Accept: application/json + +### Get a card list by ID (includes cards and sub-lists) +GET {{SDG_Backend_Barracuda_HostAddress}}/cardlist/1 +Accept: application/json + +### Update a card list +PUT {{SDG_Backend_Barracuda_HostAddress}}/cardlist/1 +Content-Type: application/json + +{ + "cardIds": [4], + "subListIds": [] +} + +### Delete a card list +DELETE {{SDG_Backend_Barracuda_HostAddress}}/cardlist/1