diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..defe95e --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +dev: + docker compose --profile hot-reload up --build --watch + +no-reload: + docker compose --profile without-hot-reload up --build + +db: + docker compose up db -d + +db_down: + docker compose down db + +apply_migrations: + @cd scripts && pwsh apply_migrations.ps1 + @cd ../ + +.PHONY: dev no-reload db db_down apply_migrations +.DEFAULT_GOAL := dev diff --git a/README.md b/README.md index 8507806..1e2cc6e 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,16 @@ Directory `scripts` contains some helpful scripts which automate some parts of w ## Development -The application is started using the following command: +The application is started using the following command (with hot-reload feature): +```bash +make +``` +or without the hot-reload feature: +```bash +make no-reload +``` +If you want to run the application manually you can use the following command: ```bash docker compose --profile hot-reload up --build --watch ``` @@ -65,3 +73,8 @@ docker compose --profile without-hot-reload up --build --watch The `--watch` parameter starts containers with the `hot-reload` feature. This enables the auto-reload functionality, meaning that the container will be automatically reloaded when the code for either the frontend or backend changes. > The `hot-reload` feature for backend applications uses `dotnet watch`, which only detects changes to existing files. It will not restart the container if new files are added (dotnet watch [issue](https://github.com/dotnet/aspnetcore/issues/8321)). + +### DB Migrations +When working with migrations remember to add parameter for project and startup project. +For example when generating a new migration while in backend directory: +`dotnet ef migrations add -p ./src/infrastructure -s ./src/api "Example"` diff --git a/backend/bruno/KSummarized/ToDo/Items/Create.bru b/backend/bruno/KSummarized/ToDo/Items/Create.bru new file mode 100644 index 0000000..a40a8b9 --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Create.bru @@ -0,0 +1,54 @@ +meta { + name: Create + type: http + seq: 1 +} + +post { + url: https://{{api_base_url}}/api/todo/items + body: json + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "id": 1, + "name": "Demo", + "completed": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there", + "tags": [{ + "id":1, + "name": "star wars" + }], + "subtasks": [ + { + "id": 2, + "name": "Demo", + "completed": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there2", + "tags": [], + "subtasks": [] + }, + { + "id": 3, + "name": "Demo3", + "completed": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there 3", + "tags": [], + "subtasks": [] + } + ], + "listId": {{listId}} + } +} + +vars:post-response { + itemId: res.body.id +} diff --git a/backend/bruno/KSummarized/ToDo/Items/Delete.bru b/backend/bruno/KSummarized/ToDo/Items/Delete.bru new file mode 100644 index 0000000..8adb760 --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Delete.bru @@ -0,0 +1,15 @@ +meta { + name: Delete + type: http + seq: 5 +} + +delete { + url: https://{{api_base_url}}/api/todo/items/{{itemId}} + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} diff --git a/backend/bruno/KSummarized/ToDo/Items/Get.bru b/backend/bruno/KSummarized/ToDo/Items/Get.bru new file mode 100644 index 0000000..7e903e8 --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Get.bru @@ -0,0 +1,15 @@ +meta { + name: Get + type: http + seq: 3 +} + +get { + url: https://{{api_base_url}}/api/todo/items/{{itemId}} + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} diff --git a/backend/bruno/KSummarized/ToDo/Items/List.bru b/backend/bruno/KSummarized/ToDo/Items/List.bru new file mode 100644 index 0000000..51fddeb --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/List.bru @@ -0,0 +1,15 @@ +meta { + name: List + type: http + seq: 4 +} + +get { + url: https://{{api_base_url}}/api/todo/items + body: none + auth: bearer +} + +auth:bearer { + token: {{token}} +} diff --git a/backend/bruno/KSummarized/ToDo/Items/Update.bru b/backend/bruno/KSummarized/ToDo/Items/Update.bru new file mode 100644 index 0000000..5fd77ee --- /dev/null +++ b/backend/bruno/KSummarized/ToDo/Items/Update.bru @@ -0,0 +1,47 @@ +meta { + name: Update + type: http + seq: 2 +} + +put { + url: https://{{api_base_url}}/api/todo/items/{{itemId}} + body: json + auth: bearer +} + +auth:bearer { + token: {{token}} +} + +body:json { + { + "id": {{itemId}}, + "name": "Demo-Updated", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there", + "tags": [], + "subtasks": [ + { + "id": 2, + "name": "Demo2", + "compleated": true, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there2", + "tags": [], + "subtasks": [] + }, + { + "id": 3, + "name": "Demo22", + "compleated": false, + "deadline": "2024-09-20T18:27:39.149Z", + "notes": "hello there 3", + "tags": [], + "subtasks": [] + } + ], + "listId": 2 + } +} diff --git a/backend/ksummarized.sln b/backend/ksummarized.sln index 20d82c5..1b0a635 100644 --- a/backend/ksummarized.sln +++ b/backend/ksummarized.sln @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "unit", "test\unit\unit.cspr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "infrastructure", "src\infrastructure\infrastructure.csproj", "{9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "core", "src\core\core.csproj", "{75F10B5B-9DD4-4B97-B571-DC9F517B839F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -34,10 +36,15 @@ Global {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB}.Release|Any CPU.Build.0 = Release|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75F10B5B-9DD4-4B97-B571-DC9F517B839F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {949171D5-4018-4C40-AB2B-687B39F20974} = {DE2804D2-3D3D-4A41-91C7-4FEEF71A4434} {B930D2FA-7FC6-4772-89EA-6B925EDD2EBC} = {4CA15B69-5E75-48E2-BE26-EA818B675CFC} {9D4BB729-24FB-4B6E-9EFD-B8C2E0A2B1EB} = {DE2804D2-3D3D-4A41-91C7-4FEEF71A4434} + {75F10B5B-9DD4-4B97-B571-DC9F517B839F} = {DE2804D2-3D3D-4A41-91C7-4FEEF71A4434} EndGlobalSection EndGlobal diff --git a/backend/src/api/Controllers/TodoController.cs b/backend/src/api/Controllers/TodoController.cs index 0a37197..b047ccb 100644 --- a/backend/src/api/Controllers/TodoController.cs +++ b/backend/src/api/Controllers/TodoController.cs @@ -1,56 +1,48 @@ using core.Ports; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using api.Responses; +using api.Filters; +using core; namespace api.Controllers; [Authorize] +[UserIdFilter] [Route("/api/todo")] [ApiController] -public class TodoController : ControllerBase +public class TodoController(ITodoService service, ILogger logger) : ControllerBase { - private readonly ITodoService _service; - private readonly ILogger _logger; + private readonly ITodoService _service = service; + private readonly ILogger _logger = logger; - public TodoController(ITodoService service, ILogger logger) - { - _service = service; - _logger = logger; - } + //We are sure that this is not null because of the [UserIdFilter] + private Guid UserId => (Guid)HttpContext.Items["UserId"]!; [HttpGet("lists")] public IActionResult GetLists() { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} requested his lists", userId); - return userId switch - { - null => Unauthorized(), - var user => Ok(_service.GetLists(user)), - }; + _logger.LogDebug("User: {user} requested his lists", UserId); + return Ok(_service.GetLists(UserId).Select(l => l.ToResponse())); } [HttpGet("lists/{id}")] public IActionResult GetList([FromRoute] int id) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} requested his list: {id}", userId, id); - if (userId is null) { return Unauthorized(); } - var list = _service.GetList(userId, id); + _logger.LogDebug("User: {user} requested his list: {id}", UserId, id); + var list = _service.GetList(UserId, id)?.ToResponse(); return list switch { null => NotFound(), - var user => Ok(list), + var l => Ok(l), }; } [HttpDelete("lists/{id}")] public IActionResult DeleteList([FromRoute] int id) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} deleted his list: {id}", userId, id); - if (userId is null) { return Unauthorized(); } - var success = _service.DeleteList(userId, id); + _logger.LogDebug("User: {user} deleted his list: {id}", UserId, id); + var success = _service.DeleteList(UserId, id); if (success) { return Ok(); @@ -59,49 +51,95 @@ public IActionResult DeleteList([FromRoute] int id) } [HttpPost("lists")] - public async Task CreateLists([FromBody] Request request) + public async Task CreateLists([FromBody] ListCreationRequest request) + { + _logger.LogDebug("User: {user} created: {list}", UserId, request.Name); + var list = await _service.CreateList(UserId, request.Name); + return Created(HttpContext.Request.Path.Add(new PathString($"/{list.Id}")), list.ToResponse()); + } + + [HttpPut("lists/{id}")] + public async Task RenameList(ListRenameRequest request) { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} created: {list}", userId, request.Name); - return userId switch + _logger.LogDebug("User: {user} renamed: {id} to: {list}", UserId, request.Id, request.Body.Name); + var list = await _service.RenameList(UserId, request.Id, request.Body.Name); + if (list) { - null => Unauthorized(), - var user => await Create(user, request.Name), - }; + return Ok(); + } + return BadRequest(); + } - async Task Create(string user, string name) + [HttpPost("items")] + public async Task CreateItem([FromBody] TodoItem request) + { + _logger.LogDebug("User: {user} created: {item}", UserId, request.Name); + var newItem = await _service.CreateItem(UserId, request); + if (newItem is null) { - //TODO: return DTO instead of DAO - var list = await _service.CreateList(user, name); - return Created(HttpContext.Request.Path.Add(new PathString($"/{list.Id}")), list); + return BadRequest(); } + return Created(HttpContext.Request.Path.Add(new PathString($"/{newItem.Id}")), newItem); } - [HttpPut("lists/{id}")] - public async Task RenameList([FromRoute] int id, [FromBody] Request request) + [HttpGet("items")] + public IActionResult ListItems() { - var userId = Request.UserId(); - _logger.LogDebug("User: {user} renamed: {id} to: {list}", userId, id, request.Name); - return userId switch + _logger.LogDebug("User: {user} requested his items", UserId); + return Ok(_service.ListItems(UserId)); + } + + [HttpGet("items/{id}")] + public async Task GetItem([FromRoute] int id) + { + _logger.LogDebug("User: {user} requested his item: {id}", UserId, id); + var item = await _service.GetItem(UserId, id); + if (item is null) { - null => Unauthorized(), - var user => await Rename(user, id, request.Name), - }; + return NotFound(); + } + return Ok(item); + } - async Task Rename(string user, int id, string name) + [HttpDelete("items/{id}")] + public async Task DeleteItem([FromRoute] int id) + { + _logger.LogDebug("User: {user} deleted his item: {id}", UserId, id); + var success = await _service.DeleteItem(UserId, id); + if (success) { - //TODO: return DTO instead of DAO - var list = await _service.RenameList(user, id, name); - if (list) - { - return Ok(); - } - return BadRequest(); + return Ok(); } + return BadRequest(); } -} -public class Request -{ - public required string Name { get; set; } + [HttpPut("items/{id}")] + public async Task UpdateItem([FromRoute] int id, [FromBody] TodoItem request) + { + _logger.LogDebug("User: {user} updated his item: {id}", UserId, id); + var success = await _service.UpdateItem(UserId, request); + if (success) + { + return Ok(); + } + return BadRequest(); + } + + public class ListCreationRequest + { + public required string Name { get; set; } + } + + public class ListRenameRequest + { + [FromRoute] + public required int Id { get; set; } + [FromBody] + public required Payload Body { get; set; } + + public class Payload + { + public required string Name { get; set; } + } + } } diff --git a/backend/src/api/Data/DTO/TodoListDTO.cs b/backend/src/api/Data/DTO/TodoListDTO.cs deleted file mode 100644 index 275e8d4..0000000 --- a/backend/src/api/Data/DTO/TodoListDTO.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace api.Data.DTO; - -public record TodoListDTO(int Id, string Name); diff --git a/backend/src/api/Filters/UserIdFilter.cs b/backend/src/api/Filters/UserIdFilter.cs new file mode 100644 index 0000000..d7b5506 --- /dev/null +++ b/backend/src/api/Filters/UserIdFilter.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc; + +namespace api.Filters; + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class UserIdFilter : Attribute, IAuthorizationFilter +{ + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext?.User?.Identity?.IsAuthenticated ?? false) + { + context.Result = new UnauthorizedResult(); + } + var userId = context.HttpContext?.Request.UserId(); + if (userId == null) + { + context.Result = new UnauthorizedResult(); + } + var valid = Guid.TryParse(userId, out Guid id); + if (!valid) + { + context.Result = new UnauthorizedResult(); + } + context.HttpContext!.Items["UserId"] = id; + } +} diff --git a/backend/src/api/Program.cs b/backend/src/api/Program.cs index 4e0557e..1df442b 100644 --- a/backend/src/api/Program.cs +++ b/backend/src/api/Program.cs @@ -4,10 +4,11 @@ using Microsoft.OpenApi.Models; using Serilog; using System.Security.Cryptography; -using api; +using api.Filters; +using core.Ports; using infrastructure.Data; using infrastructure.Keycloak; -using core.Ports; +using infrastructure.Logging; const string logFormat = "[{Timestamp:HH:mm:ss} {Level:u3}] {CorelationId} | {Message:lj}{NewLine}{Exception}"; Log.Logger = new LoggerConfiguration().Enrich.WithCorrelationId() @@ -57,7 +58,7 @@ options.TokenValidationParameters = tokenValidationParameters; }); - builder.Services.AddControllers(); + builder.Services.AddControllers(o => o.Filters.Add(typeof(UserIdFilter))); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "api", Version = "v1" }); diff --git a/backend/src/api/RequestExtensions.cs b/backend/src/api/RequestExtensions.cs index 2d45f01..5bb893b 100644 --- a/backend/src/api/RequestExtensions.cs +++ b/backend/src/api/RequestExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.JsonWebTokens; namespace api; diff --git a/backend/src/api/Responses/TodoList.cs b/backend/src/api/Responses/TodoList.cs new file mode 100644 index 0000000..ba73c85 --- /dev/null +++ b/backend/src/api/Responses/TodoList.cs @@ -0,0 +1,10 @@ +using core; + +namespace api.Responses; + +public record TodoList(int Id, string Name, IEnumerable Items); + +public static class MapExtensions +{ + public static TodoList ToResponse(this core.TodoList list) => new TodoList(list.Id, list.Name, list.Items); +} diff --git a/backend/src/core/Ports/ITodoService.cs b/backend/src/core/Ports/ITodoService.cs index 21ef07a..65d6704 100644 --- a/backend/src/core/Ports/ITodoService.cs +++ b/backend/src/core/Ports/ITodoService.cs @@ -2,9 +2,14 @@ namespace core.Ports; public interface ITodoService { - Task CreateList(string user, string name); - IEnumerable GetLists(string userId); - TodoList? GetList(string userId, int id); - bool DeleteList(string userId, int id); - Task RenameList(string user, int id, string name); + Task CreateList(Guid user, string name); + IEnumerable GetLists(Guid user); + TodoList? GetList(Guid user, int id); + bool DeleteList(Guid user, int id); + Task RenameList(Guid user, int id, string name); + Task CreateItem(Guid user, TodoItem item); + Task GetItem(Guid user, int id); + IEnumerable ListItems(Guid user); + Task DeleteItem(Guid user, int id); + Task UpdateItem(Guid user, TodoItem item); } diff --git a/backend/src/core/Tag.cs b/backend/src/core/Tag.cs new file mode 100644 index 0000000..c25435f --- /dev/null +++ b/backend/src/core/Tag.cs @@ -0,0 +1,5 @@ +namespace core; +public class Tag{ + public int Id {get; set;} + public required string Name {get; set;} +} \ No newline at end of file diff --git a/backend/src/core/TodoItem.cs b/backend/src/core/TodoItem.cs new file mode 100644 index 0000000..082b46d --- /dev/null +++ b/backend/src/core/TodoItem.cs @@ -0,0 +1,13 @@ +namespace core; + +public record TodoItem +{ + public int? Id { get; set; } + public required string Name { get; set; } + public bool Completed { get; set; } + public DateTime Deadline { get; set; } + public string Notes { get; set; } = null!; + public required IEnumerable Tags { get; set; } + public required IEnumerable Subtasks { get; set; } + public int ListId { get; set; } +} diff --git a/backend/src/core/TodoList.cs b/backend/src/core/TodoList.cs index 8392f04..6fe7c76 100644 --- a/backend/src/core/TodoList.cs +++ b/backend/src/core/TodoList.cs @@ -5,6 +5,5 @@ public class TodoList public int Id { get; set; } public required string Name { get; set; } public required Guid Owner { get; set; } - - public bool IsAuthorized(Guid person) => Owner.Equals(person); + public required IEnumerable Items { get; set; } } diff --git a/backend/src/infrastructure/Data/ApplicationDbContext.cs b/backend/src/infrastructure/Data/ApplicationDbContext.cs index df8dfd3..383126f 100644 --- a/backend/src/infrastructure/Data/ApplicationDbContext.cs +++ b/backend/src/infrastructure/Data/ApplicationDbContext.cs @@ -1,11 +1,10 @@ -using Microsoft.EntityFrameworkCore; - -namespace infrastructure.Data; - -public class ApplicationDbContext : DbContext -{ - public ApplicationDbContext(DbContextOptions options) : base(options) - { - } - public DbSet TodoLists { get; set; } -} +using Microsoft.EntityFrameworkCore; + +namespace infrastructure.Data; + +public class ApplicationDbContext(DbContextOptions options) : DbContext(options) +{ + public required DbSet TodoLists { get; set; } + public required DbSet TodoItems { get; set; } + public required DbSet Tags { get; set; } +} diff --git a/backend/src/infrastructure/Data/TagModel.cs b/backend/src/infrastructure/Data/TagModel.cs new file mode 100644 index 0000000..48f2c3a --- /dev/null +++ b/backend/src/infrastructure/Data/TagModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace infrastructure.Data; + +[Table("Tags")] +public class TagModel +{ + [Key] + public int Id { get; set; } + [MaxLength(512)] + public required string Name { get; set; } + public required Guid Owner { get; set; } + public ICollection Items { get; set; } = null!; +} diff --git a/backend/src/infrastructure/Data/TodoItemModel.cs b/backend/src/infrastructure/Data/TodoItemModel.cs new file mode 100644 index 0000000..7ac8d76 --- /dev/null +++ b/backend/src/infrastructure/Data/TodoItemModel.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace infrastructure.Data; + +[Table("TodoItems")] +public class TodoItemModel +{ + [Key] + public int Id { get; set; } + [MaxLength(512)] + public required string Name { get; set; } + public required Guid Owner { get; set; } + public bool Completed { get; set; } + public DateTime Deadline { get; set; } + [MaxLength(4096)] + public string Notes { get; set; } = null!; + public required ICollection Tags { get; set; } + public required ICollection Subtasks { get; set; } + public int? MainTaskId { get; set; } + public TodoItemModel? MainTask { get; set; } + public int ListId { get; set; } + public TodoListModel? List { get; set; } +} diff --git a/backend/src/infrastructure/Data/TodoListModel.cs b/backend/src/infrastructure/Data/TodoListModel.cs index d838b5b..70c3764 100644 --- a/backend/src/infrastructure/Data/TodoListModel.cs +++ b/backend/src/infrastructure/Data/TodoListModel.cs @@ -3,7 +3,7 @@ namespace infrastructure.Data; -[Table("todo_lists")] +[Table("TodoLists")] public class TodoListModel { [Key] @@ -11,4 +11,5 @@ public class TodoListModel [MaxLength(512)] public required string Name { get; set; } public required Guid Owner { get; set; } + public required ICollection Items { get; set; } } diff --git a/backend/src/infrastructure/Data/TodoService.cs b/backend/src/infrastructure/Data/TodoService.cs index 3d2dcc0..f72adf1 100644 --- a/backend/src/infrastructure/Data/TodoService.cs +++ b/backend/src/infrastructure/Data/TodoService.cs @@ -1,6 +1,9 @@ using Microsoft.EntityFrameworkCore; using core.Ports; using core; +using Serilog; +using System.Text.Json; +using System.Text.Json.Serialization; namespace infrastructure.Data; @@ -14,49 +17,240 @@ public TodoService(ApplicationDbContext context) _context = context; } - public IEnumerable GetLists(string userId) + public IEnumerable GetLists(Guid user) { + var empty = Enumerable.Empty(); return _context.TodoLists.AsNoTracking() - .Where(list => list.Owner.Equals(Guid.Parse(userId))) - .Select(list => new TodoList { Id = list.Id, Name = list.Name, Owner = list.Owner }) + .Where(list => list.Owner.Equals(user)) + .Select(list => new TodoList + { + Id = list.Id, + Name = list.Name, + Owner = list.Owner, + Items = empty + }) .AsEnumerable(); } - public TodoList? GetList(string userId, int id) + public TodoList? GetList(Guid user, int id) { var list = _context.TodoLists.AsNoTracking() - .SingleOrDefault(l => l.Owner.Equals(Guid.Parse(userId)) && l.Id == id); - //TODO: Consider creating a mapper instead of this manual new - if (list is not null) { return new() { Id = list.Id, Name = list.Name, Owner = list.Owner }; } + .Include(l => l.Items) + .SingleOrDefault(l => l.Owner.Equals(user) && l.Id == id); + if (list is not null) + { + return new() + { + Id = list.Id, + Name = list.Name, + Owner = list.Owner, + Items = list.Items.Select(i => MapTodoItem(i)).ToList() + }; + }; return null; } - public async Task CreateList(string user, string name) + public async Task CreateList(Guid user, string name) { - var newList = new TodoListModel() { Name = name, Owner = Guid.Parse(user) }; + var newList = new TodoListModel() { Name = name, Owner = user, Items = [] }; _context.TodoLists.Add(newList); await _context.SaveChangesAsync(); - return new TodoList() { Id = newList.Id, Name = newList.Name, Owner = newList.Owner }; + return new TodoList() { Id = newList.Id, Name = newList.Name, Owner = newList.Owner, Items = [] }; } - public bool DeleteList(string userId, int id) + public bool DeleteList(Guid user, int id) { var list = _context.TodoLists - .SingleOrDefault(l => l.Owner.Equals(Guid.Parse(userId)) && l.Id == id); + .SingleOrDefault(l => l.Owner.Equals(user) && l.Id == id); if (list is null) { return false; } _context.TodoLists.Remove(list); _context.SaveChanges(); return true; } - public async Task RenameList(string userId, int id, string name) + public async Task RenameList(Guid user, int id, string name) { var list = _context.TodoLists - .SingleOrDefault(l => l.Owner.Equals(Guid.Parse(userId)) && l.Id == id); + .SingleOrDefault(l => l.Owner.Equals(user) && l.Id == id); if (list is null) { return false; } list.Name = name; await _context.SaveChangesAsync(); return true; } + + public async Task CreateItem(Guid user, TodoItem item) + { + var newItem = new TodoItemModel() + { + Name = item.Name, + Owner = user, + Completed = false, + Deadline = item.Deadline, + Notes = item.Notes, + Subtasks = [], + Tags = [], + ListId = item.ListId + }; + foreach (var st in item.Subtasks) + { + var newSubtask = new TodoItemModel() + { + Name = st.Name, + Owner = user, + Completed = false, + Deadline = st.Deadline, + Notes = st.Notes, + Tags = [], + Subtasks = [], + ListId = item.ListId + }; + newItem.Subtasks.Add(newSubtask); + } + foreach (var tag in item.Tags) + { + var t = _context.Tags.FirstOrDefault(t => t.Id == tag.Id && t.Owner.Equals(user)); + if (t is null) + { + newItem.Tags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = user }); + } + else + { + t.Name = tag.Name; + } + } + await _context.TodoItems.AddAsync(newItem); + await _context.SaveChangesAsync(); + return item with { Id = newItem.Id }; + } + + public async Task GetItem(Guid user, int id) + { + var item = await _context.TodoItems + .AsNoTracking() + .Include(i => i.Subtasks) + .Include(i => i.Tags) + .AsSplitQuery() + .SingleOrDefaultAsync(i => i.Owner.Equals(user) && i.Id == id); + if (item is null) { return null; } + + return MapTodoItem(item); + } + + public IEnumerable ListItems(Guid user) + { + return _context.TodoItems + .AsNoTracking() + .Include(i => i.Subtasks) + .Include(i => i.Tags) + .Where(i => i.Owner.Equals(user) && i.MainTaskId == null) + .AsSplitQuery() + .Select(i => MapTodoItem(i)) + .AsEnumerable(); + } + + public async Task DeleteItem(Guid user, int id) + { + var item = _context.TodoItems + .Include(i => i.Subtasks) + .SingleOrDefault(i => i.Owner.Equals(user) && i.Id == id); + if (item is null) { return false; } + if (item.Subtasks.Any()) + { + _context.TodoItems.RemoveRange(item.Subtasks); + } + _context.TodoItems.Remove(item); + await _context.SaveChangesAsync(); + return true; + } + + public async Task UpdateItem(Guid user, TodoItem item) + { + var existingItem = _context.TodoItems + .Include(i => i.Subtasks) + .Include(i => i.Tags) + .AsSplitQuery() + .SingleOrDefault(i => i.Owner.Equals(user) && i.Id == item.Id); + if (existingItem is null) { return false; } + existingItem.Name = item.Name; + existingItem.Deadline = item.Deadline; + existingItem.Notes = item.Notes; + existingItem.Completed = item.Completed; + existingItem.ListId = item.ListId; + foreach (var st in item.Subtasks) + { + var existingSubtask = existingItem.Subtasks.FirstOrDefault(t => t.Id == st.Id); + if (existingSubtask is null) + { + var newSubtask = new TodoItemModel() + { + Name = st.Name, + Owner = user, + Completed = st.Completed, + Deadline = st.Deadline, + Notes = st.Notes, + Tags = [], + Subtasks = [], + ListId = item.ListId + }; + existingItem.Subtasks.Add(newSubtask); + } + else + { + existingSubtask.Name = st.Name; + existingSubtask.Deadline = st.Deadline; + existingSubtask.Notes = st.Notes; + existingSubtask.Completed = st.Completed; + existingSubtask.ListId = item.ListId; + } + } + var existingTags = existingItem.Tags.ToList(); + foreach (var tag in item.Tags) + { + var t = existingTags.FirstOrDefault(t => t.Id == tag.Id); + if (t is null) + { + existingTags.Add(new() { Id = tag.Id, Name = tag.Name, Owner = user }); + } + else + { + t.Name = tag.Name; + } + } + existingItem.Tags = existingTags; + + await _context.SaveChangesAsync(); + return true; + } + + private static TodoItem MapTodoItem(infrastructure.Data.TodoItemModel item) + { + Log.Debug("Mapped {item}", JsonSerializer.Serialize(item, new JsonSerializerOptions() { ReferenceHandler = ReferenceHandler.Preserve })); + return new TodoItem() + { + Id = item.Id, + Name = item.Name, + Deadline = item.Deadline, + Notes = item.Notes, + Subtasks = item.Subtasks?.Select(st => MapSubtask(st)).ToList() ?? [], + Tags = item.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [], + ListId = item.ListId, + Completed = item.Completed + }; + } + + private static TodoItem MapSubtask(infrastructure.Data.TodoItemModel subtask) + { + return new TodoItem() + { + Id = subtask.Id, + Name = subtask.Name, + Deadline = subtask.Deadline, + Notes = subtask.Notes, + Subtasks = [], + Tags = subtask.Tags?.Select(t => new core.Tag() { Id = t.Id, Name = t.Name }).ToList() ?? [], + ListId = subtask.ListId, + Completed = subtask.Completed + }; + } } diff --git a/backend/src/api/LoggingExtensions.cs b/backend/src/infrastructure/Logging/LoggingExtensions.cs similarity index 94% rename from backend/src/api/LoggingExtensions.cs rename to backend/src/infrastructure/Logging/LoggingExtensions.cs index a359a02..792c2f2 100644 --- a/backend/src/api/LoggingExtensions.cs +++ b/backend/src/infrastructure/Logging/LoggingExtensions.cs @@ -1,9 +1,10 @@ +using Microsoft.AspNetCore.Http; using Serilog; using Serilog.Configuration; using Serilog.Core; using Serilog.Events; -namespace api; +namespace infrastructure.Logging; public static class LoggingExtensions { diff --git a/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs b/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs index ba9fe02..b18a79b 100644 --- a/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs +++ b/backend/src/infrastructure/Migrations/20240330212015_TodoList.Designer.cs @@ -43,7 +43,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("todo_lists"); + b.ToTable("TodoLists"); }); #pragma warning restore 612, 618 } diff --git a/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs b/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs index 13b4cef..040f2a7 100644 --- a/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs +++ b/backend/src/infrastructure/Migrations/20240330212015_TodoList.cs @@ -13,7 +13,7 @@ public partial class TodoList : Migration protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( - name: "todo_lists", + name: "TodoLists", columns: table => new { Id = table.Column(type: "integer", nullable: false) @@ -23,7 +23,7 @@ protected override void Up(MigrationBuilder migrationBuilder) }, constraints: table => { - table.PrimaryKey("PK_todo_lists", x => x.Id); + table.PrimaryKey("PK_TodoLists", x => x.Id); }); } @@ -31,7 +31,7 @@ protected override void Up(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( - name: "todo_lists"); + name: "TodoLists"); } } } diff --git a/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs b/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs new file mode 100644 index 0000000..8060e54 --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240920175610_Add items.Designer.cs @@ -0,0 +1,153 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using infrastructure.Data; + +#nullable disable + +namespace infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240920175610_Add items")] + partial class Additems + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TagTodoItemModel", b => + { + b.Property("ItemsId") + .HasColumnType("integer"); + + b.Property("TagsId") + .HasColumnType("integer"); + + b.HasKey("ItemsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("TagTodoItemModel"); + }); + + modelBuilder.Entity("infrastructure.Data.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("Deadline") + .HasColumnType("timestamp with time zone"); + + b.Property("MainTaskId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MainTaskId"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("TodoLists"); + }); + + modelBuilder.Entity("TagTodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.Tag", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", "MainTask") + .WithMany("Subtasks") + .HasForeignKey("MainTaskId"); + + b.Navigation("MainTask"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Navigation("Subtasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/infrastructure/Migrations/20240920175610_Add items.cs b/backend/src/infrastructure/Migrations/20240920175610_Add items.cs new file mode 100644 index 0000000..c995dc9 --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240920175610_Add items.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace infrastructure.Migrations +{ + /// + public partial class Additems : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Owner = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "TodoItems", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Name = table.Column(type: "character varying(512)", maxLength: 512, nullable: false), + Owner = table.Column(type: "uuid", nullable: false), + Completed = table.Column(type: "boolean", nullable: false), + Deadline = table.Column(type: "timestamp with time zone", nullable: false), + Notes = table.Column(type: "character varying(4096)", maxLength: 4096, nullable: false), + MainTaskId = table.Column(type: "integer", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Todos", x => x.Id); + table.ForeignKey( + name: "FK_Todos_Todos_MainTaskId", + column: x => x.MainTaskId, + principalTable: "TodoItems", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "TagTodoItemModel", + columns: table => new + { + ItemsId = table.Column(type: "integer", nullable: false), + TagsId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TagTodoItemModel", x => new { x.ItemsId, x.TagsId }); + table.ForeignKey( + name: "FK_TagTodoItemModel_Tags_TagsId", + column: x => x.TagsId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TagTodoItemModel_Todos_ItemsId", + column: x => x.ItemsId, + principalTable: "TodoItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TagTodoItemModel_TagsId", + table: "TagTodoItemModel", + column: "TagsId"); + + migrationBuilder.CreateIndex( + name: "IX_Todos_MainTaskId", + table: "TodoItems", + column: "MainTaskId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TagTodoItemModel"); + + migrationBuilder.DropTable( + name: "Tags"); + + migrationBuilder.DropTable( + name: "TodoItems"); + } + } +} diff --git a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs new file mode 100644 index 0000000..fa3a895 --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using infrastructure.Data; + +#nullable disable + +namespace infrastructure.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240927182157_Introduce list mapping")] + partial class Introducelistmapping + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TagModelTodoItemModel", b => + { + b.Property("ItemsId") + .HasColumnType("integer"); + + b.Property("TagsId") + .HasColumnType("integer"); + + b.HasKey("ItemsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("TagModelTodoItemModel"); + }); + + modelBuilder.Entity("infrastructure.Data.TagModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("Deadline") + .HasColumnType("timestamp with time zone"); + + b.Property("ListId") + .HasColumnType("integer"); + + b.Property("MainTaskId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ListId"); + + b.HasIndex("MainTaskId"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("TodoLists"); + }); + + modelBuilder.Entity("TagModelTodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.TagModel", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoListModel", "List") + .WithMany("Items") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.TodoItemModel", "MainTask") + .WithMany("Subtasks") + .HasForeignKey("MainTaskId"); + + b.Navigation("List"); + + b.Navigation("MainTask"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Navigation("Subtasks"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Navigation("Items"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs new file mode 100644 index 0000000..29f0354 --- /dev/null +++ b/backend/src/infrastructure/Migrations/20240927182157_Introduce list mapping.cs @@ -0,0 +1,114 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace infrastructure.Migrations +{ + /// + public partial class Introducelistmapping : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TagTodoItemModel"); + + migrationBuilder.AddColumn( + name: "ListId", + table: "TodoItems", + type: "integer", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateTable( + name: "TagModelTodoItemModel", + columns: table => new + { + ItemsId = table.Column(type: "integer", nullable: false), + TagsId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TagModelTodoItemModel", x => new { x.ItemsId, x.TagsId }); + table.ForeignKey( + name: "FK_TagModelTodoItemModel_Tags_TagsId", + column: x => x.TagsId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TagModelTodoItemModel_Todos_ItemsId", + column: x => x.ItemsId, + principalTable: "TodoItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Todos_ListId", + table: "TodoItems", + column: "ListId"); + + migrationBuilder.CreateIndex( + name: "IX_TagModelTodoItemModel_TagsId", + table: "TagModelTodoItemModel", + column: "TagsId"); + + migrationBuilder.AddForeignKey( + name: "FK_Todos_TodoLists_ListId", + table: "TodoItems", + column: "ListId", + principalTable: "TodoLists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Todos_TodoLists_ListId", + table: "TodoItems"); + + migrationBuilder.DropTable( + name: "TagModelTodoItemModel"); + + migrationBuilder.DropIndex( + name: "IX_Todos_ListId", + table: "TodoItems"); + + migrationBuilder.DropColumn( + name: "ListId", + table: "TodoItems"); + + migrationBuilder.CreateTable( + name: "TagTodoItemModel", + columns: table => new + { + ItemsId = table.Column(type: "integer", nullable: false), + TagsId = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TagTodoItemModel", x => new { x.ItemsId, x.TagsId }); + table.ForeignKey( + name: "FK_TagTodoItemModel_Tags_TagsId", + column: x => x.TagsId, + principalTable: "Tags", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TagTodoItemModel_Todos_ItemsId", + column: x => x.ItemsId, + principalTable: "TodoItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TagTodoItemModel_TagsId", + table: "TagTodoItemModel", + column: "TagsId"); + } + } +} diff --git a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs index 42e6371..11821ce 100644 --- a/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/backend/src/infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs @@ -17,12 +17,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.3") + .HasAnnotation("ProductVersion", "8.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - modelBuilder.Entity("api.Data.DAO.TodoListModel", b => + modelBuilder.Entity("TagModelTodoItemModel", b => + { + b.Property("ItemsId") + .HasColumnType("integer"); + + b.Property("TagsId") + .HasColumnType("integer"); + + b.HasKey("ItemsId", "TagsId"); + + b.HasIndex("TagsId"); + + b.ToTable("TagModelTodoItemModel"); + }); + + modelBuilder.Entity("infrastructure.Data.TagModel", b => { b.Property("Id") .ValueGeneratedOnAdd() @@ -40,7 +55,112 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.ToTable("todo_lists"); + b.ToTable("Tags"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Completed") + .HasColumnType("boolean"); + + b.Property("Deadline") + .HasColumnType("timestamp with time zone"); + + b.Property("ListId") + .HasColumnType("integer"); + + b.Property("MainTaskId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Notes") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ListId"); + + b.HasIndex("MainTaskId"); + + b.ToTable("TodoItems"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Owner") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("TodoLists"); + }); + + modelBuilder.Entity("TagModelTodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoItemModel", null) + .WithMany() + .HasForeignKey("ItemsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.TagModel", null) + .WithMany() + .HasForeignKey("TagsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.HasOne("infrastructure.Data.TodoListModel", "List") + .WithMany("Items") + .HasForeignKey("ListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("infrastructure.Data.TodoItemModel", "MainTask") + .WithMany("Subtasks") + .HasForeignKey("MainTaskId"); + + b.Navigation("List"); + + b.Navigation("MainTask"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoItemModel", b => + { + b.Navigation("Subtasks"); + }); + + modelBuilder.Entity("infrastructure.Data.TodoListModel", b => + { + b.Navigation("Items"); }); #pragma warning restore 612, 618 } diff --git a/backend/src/infrastructure/infrastructure.csproj b/backend/src/infrastructure/infrastructure.csproj index 7d93446..92b153b 100644 --- a/backend/src/infrastructure/infrastructure.csproj +++ b/backend/src/infrastructure/infrastructure.csproj @@ -13,6 +13,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all +