From 49b5339bb357664a18b164968110b4fd06265dcc Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Mon, 20 Jan 2025 17:43:05 +0100 Subject: [PATCH 1/2] Added database models --- src/Portfolio.Api/App.razor | 58 ++++++++++++++++++++ src/Portfolio.Api/DatabaseContext.cs | 25 +++++++++ src/Portfolio.Api/Portfolio.Api.csproj | 2 + src/Portfolio.Api/Program.cs | 22 ++++++++ src/Portfolio.Host/Portfolio.Host.csproj | 1 + src/Portfolio.Host/Program.cs | 12 +++- src/Portfolio.Shared/Models/About.cs | 16 ++++++ src/Portfolio.Shared/Models/Language.cs | 20 +++++++ src/Portfolio.Shared/Models/Project.cs | 41 ++++++++++++++ src/Portfolio.Shared/Models/Technology.cs | 22 ++++++++ src/Portfolio.Shared/Models/TimelineEntry.cs | 25 +++++++++ 11 files changed, 242 insertions(+), 2 deletions(-) create mode 100644 src/Portfolio.Api/App.razor create mode 100644 src/Portfolio.Api/DatabaseContext.cs create mode 100644 src/Portfolio.Shared/Models/About.cs create mode 100644 src/Portfolio.Shared/Models/Language.cs create mode 100644 src/Portfolio.Shared/Models/Project.cs create mode 100644 src/Portfolio.Shared/Models/Technology.cs create mode 100644 src/Portfolio.Shared/Models/TimelineEntry.cs diff --git a/src/Portfolio.Api/App.razor b/src/Portfolio.Api/App.razor new file mode 100644 index 0000000..4124adc --- /dev/null +++ b/src/Portfolio.Api/App.razor @@ -0,0 +1,58 @@ +@using Microsoft.AspNetCore.Components.Web; +@using Microsoft.AspNetCore.Components.Routing + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Portfolio.Api/DatabaseContext.cs b/src/Portfolio.Api/DatabaseContext.cs new file mode 100644 index 0000000..2d46736 --- /dev/null +++ b/src/Portfolio.Api/DatabaseContext.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; +using Portfolio.Shared.Models; + +namespace Portfolio.Api; + +public class DatabaseContext(DbContextOptions options) : DbContext(options) { + + public DbSet Projects { get; set; } + + public DbSet Languages { get; set; } + + public DbSet Technologies { get; set; } + + public DbSet Timeline { get; set; } + + public DbSet About { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.Entity() + .HasMany(p => p.Languages) + .WithMany() + .UsingEntity("ProjectLanguage"); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/Portfolio.Api.csproj b/src/Portfolio.Api/Portfolio.Api.csproj index 35db154..f7751ce 100644 --- a/src/Portfolio.Api/Portfolio.Api.csproj +++ b/src/Portfolio.Api/Portfolio.Api.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/Portfolio.Api/Program.cs b/src/Portfolio.Api/Program.cs index e850aef..39a8398 100644 --- a/src/Portfolio.Api/Program.cs +++ b/src/Portfolio.Api/Program.cs @@ -1,3 +1,6 @@ +using HopFrame.Web; +using Portfolio.Api; + var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -5,6 +8,15 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.AddServiceDefaults(); +builder.AddNpgsqlDbContext("data"); + +builder.Services.AddHopFrame(options => { + options.DisplayUserInfo(false); + options.AddDbContext(); +}); +builder.Services.AddRazorComponents() + .AddInteractiveServerComponents(); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -12,7 +24,17 @@ if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } +await using (var scope = app.Services.CreateAsyncScope()) { + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.EnsureCreatedAsync(); +} + app.UseHttpsRedirection(); app.MapDefaultEndpoints(); +app.UseAntiforgery(); +app.MapStaticAssets(); +app.MapRazorComponents() + .MapHopFramePages(); + app.Run(); diff --git a/src/Portfolio.Host/Portfolio.Host.csproj b/src/Portfolio.Host/Portfolio.Host.csproj index 5f44a98..083deb5 100644 --- a/src/Portfolio.Host/Portfolio.Host.csproj +++ b/src/Portfolio.Host/Portfolio.Host.csproj @@ -13,6 +13,7 @@ + diff --git a/src/Portfolio.Host/Program.cs b/src/Portfolio.Host/Program.cs index e976287..ae405b9 100644 --- a/src/Portfolio.Host/Program.cs +++ b/src/Portfolio.Host/Program.cs @@ -1,9 +1,17 @@ var builder = DistributedApplication.CreateBuilder(args); -var api = builder.AddProject("api"); +var postgres = builder.AddPostgres("postgres") + .WithDataVolume("portfolio-postgres"); + +var db = postgres.AddDatabase("data"); + +var api = builder.AddProject("api") + .WithReference(db) + .WaitFor(db); builder.AddProject("web") .WithReference(api) - .WaitFor(api); + .WaitFor(api) + .WithExternalHttpEndpoints(); builder.Build().Run(); \ No newline at end of file diff --git a/src/Portfolio.Shared/Models/About.cs b/src/Portfolio.Shared/Models/About.cs new file mode 100644 index 0000000..b17cc31 --- /dev/null +++ b/src/Portfolio.Shared/Models/About.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Portfolio.Shared.Models; + +public class About { + + [Key] + public int Id { get; private set; } = 0; + + [MaxLength(5000)] + public required string AboutMe { get; set; } + + [MaxLength(5000)] + public required string Future { get; set; } + +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Models/Language.cs b/src/Portfolio.Shared/Models/Language.cs new file mode 100644 index 0000000..f6ee861 --- /dev/null +++ b/src/Portfolio.Shared/Models/Language.cs @@ -0,0 +1,20 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Portfolio.Shared.Models; + +public sealed class Language { + + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; init; } + + [MaxLength(255)] + public required string Label { get; set; } + + [MaxLength(255)] + public required string Identifier { get; set; } + + [MaxLength(255)] + public string? Suffix { get; set; } + +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Models/Project.cs b/src/Portfolio.Shared/Models/Project.cs new file mode 100644 index 0000000..fab23ce --- /dev/null +++ b/src/Portfolio.Shared/Models/Project.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Portfolio.Shared.Models; + +public sealed class Project { + + [Key] + public Guid Id { get; init; } = Guid.CreateVersion7(); + + [MaxLength(255)] + public required string Cover { get; set; } + + [MaxLength(255)] + public required string Name { get; set; } + + [MaxLength(1000)] + public string? Description { get; set; } + + [MaxLength(255)] + public required string SourceCode { get; set; } + + public bool Featured { get; set; } + + public int OrderIndex { get; set; } + + public ProjectStatus Status { get; set; } = ProjectStatus.Finished; + + [ForeignKey("languages")] + public List Languages { get; init; } = new(); + + public DateTime Created { get; init; } = DateTime.Now; + +} + +public enum ProjectStatus : byte { + Finished = 0, + Canceled = 1, + Paused = 2, + Development = 3 +} diff --git a/src/Portfolio.Shared/Models/Technology.cs b/src/Portfolio.Shared/Models/Technology.cs new file mode 100644 index 0000000..edd44dd --- /dev/null +++ b/src/Portfolio.Shared/Models/Technology.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Portfolio.Shared.Models; + +public sealed class Technology { + + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; init; } + + [MaxLength(255)] + public required string Name { get; set; } + + public TechnologyLevel Level { get; set; } = TechnologyLevel.Beginner; + +} + +public enum TechnologyLevel : byte { + Beginner = 0, + Intermediate = 1, + Professional = 2 +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Models/TimelineEntry.cs b/src/Portfolio.Shared/Models/TimelineEntry.cs new file mode 100644 index 0000000..f6cb8fa --- /dev/null +++ b/src/Portfolio.Shared/Models/TimelineEntry.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Portfolio.Shared.Models; + +public class TimelineEntry { + + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; init; } + + public DateOnly Date { get; init; } + + [MaxLength(1000)] + public required string Description { get; set; } + + public bool Featured { get; set; } + + public TimelineEntryType Type { get; init; } + +} + +public enum TimelineEntryType : byte { + Experience = 0, + Carrier = 1 +} -- 2.49.1 From 6ba7275e9299951badb785bd4a558ad99ce5dc46 Mon Sep 17 00:00:00 2001 From: Leon Hoppe Date: Mon, 20 Jan 2025 18:52:04 +0100 Subject: [PATCH 2/2] Added api endpoints and configured hopframe --- .../Controller/AboutController.cs | 15 ++++++ .../Controller/ProjectController.cs | 15 ++++++ .../Controller/TechnologyController.cs | 15 ++++++ .../Controller/TimelineController.cs | 16 ++++++ src/Portfolio.Api/DatabaseContext.cs | 2 +- src/Portfolio.Api/Program.cs | 50 ++++++++++++++++++- src/Portfolio.Api/Services/AboutRepository.cs | 14 ++++++ .../Services/ProjectRepository.cs | 15 ++++++ .../Services/TechnologyRepository.cs | 13 +++++ .../Services/TimelineRepository.cs | 15 ++++++ src/Portfolio.Shared/Models/About.cs | 2 +- src/Portfolio.Shared/Models/TimelineEntry.cs | 2 +- .../Services/IAboutRepository.cs | 9 ++++ .../Services/IProjectRepository.cs | 9 ++++ .../Services/ITechnologyRepository.cs | 9 ++++ .../Services/ITimelineRepository.cs | 9 ++++ 16 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 src/Portfolio.Api/Controller/AboutController.cs create mode 100644 src/Portfolio.Api/Controller/ProjectController.cs create mode 100644 src/Portfolio.Api/Controller/TechnologyController.cs create mode 100644 src/Portfolio.Api/Controller/TimelineController.cs create mode 100644 src/Portfolio.Api/Services/AboutRepository.cs create mode 100644 src/Portfolio.Api/Services/ProjectRepository.cs create mode 100644 src/Portfolio.Api/Services/TechnologyRepository.cs create mode 100644 src/Portfolio.Api/Services/TimelineRepository.cs create mode 100644 src/Portfolio.Shared/Services/IAboutRepository.cs create mode 100644 src/Portfolio.Shared/Services/IProjectRepository.cs create mode 100644 src/Portfolio.Shared/Services/ITechnologyRepository.cs create mode 100644 src/Portfolio.Shared/Services/ITimelineRepository.cs diff --git a/src/Portfolio.Api/Controller/AboutController.cs b/src/Portfolio.Api/Controller/AboutController.cs new file mode 100644 index 0000000..8a8f2b4 --- /dev/null +++ b/src/Portfolio.Api/Controller/AboutController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Controller; + +[ApiController, Route("api/about")] +public class AboutController(IAboutRepository repository) : ControllerBase { + + [HttpGet] + public async Task GetAbout(CancellationToken ct) { + var about = await repository.GetAbout(ct); + return Ok(about); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/Controller/ProjectController.cs b/src/Portfolio.Api/Controller/ProjectController.cs new file mode 100644 index 0000000..139228e --- /dev/null +++ b/src/Portfolio.Api/Controller/ProjectController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Controller; + +[ApiController, Route("api/projects")] +public class ProjectController(IProjectRepository repository) : ControllerBase { + + [HttpGet] + public async Task GetProjects(CancellationToken ct) { + var projects = await repository.GetProjects(ct); + return Ok(projects); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/Controller/TechnologyController.cs b/src/Portfolio.Api/Controller/TechnologyController.cs new file mode 100644 index 0000000..40de333 --- /dev/null +++ b/src/Portfolio.Api/Controller/TechnologyController.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Mvc; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Controller; + +[ApiController, Route("api/technologies")] +public class TechnologyController(ITechnologyRepository repository) : ControllerBase { + + [HttpGet] + public async Task GetTechnologies(CancellationToken ct) { + var technologies = await repository.GetTechnologies(ct); + return Ok(technologies); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/Controller/TimelineController.cs b/src/Portfolio.Api/Controller/TimelineController.cs new file mode 100644 index 0000000..4a405ce --- /dev/null +++ b/src/Portfolio.Api/Controller/TimelineController.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Controller; + +[ApiController, Route("api/timeline")] +public class TimelineController(ITimelineRepository repository) : ControllerBase { + + [HttpGet("{type}")] + public async Task GetTimeline(TimelineEntryType type, CancellationToken ct) { + var timeline = await repository.GetTimeline(type, ct); + return Ok(timeline); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/DatabaseContext.cs b/src/Portfolio.Api/DatabaseContext.cs index 2d46736..aaa7b5c 100644 --- a/src/Portfolio.Api/DatabaseContext.cs +++ b/src/Portfolio.Api/DatabaseContext.cs @@ -19,7 +19,7 @@ public class DatabaseContext(DbContextOptions options) : DbCont modelBuilder.Entity() .HasMany(p => p.Languages) .WithMany() - .UsingEntity("ProjectLanguage"); + .UsingEntity("LanguageProject"); } } \ No newline at end of file diff --git a/src/Portfolio.Api/Program.cs b/src/Portfolio.Api/Program.cs index 39a8398..e093629 100644 --- a/src/Portfolio.Api/Program.cs +++ b/src/Portfolio.Api/Program.cs @@ -1,5 +1,9 @@ +using HopFrame.Core.Config; using HopFrame.Web; using Portfolio.Api; +using Portfolio.Api.Services; +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; var builder = WebApplication.CreateBuilder(args); @@ -8,11 +12,53 @@ var builder = WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); builder.AddServiceDefaults(); +AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); builder.AddNpgsqlDbContext("data"); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddControllers(); + builder.Services.AddHopFrame(options => { options.DisplayUserInfo(false); - options.AddDbContext(); + options.AddDbContext(context => { + context.Table(table => { + var langConfig = table.InnerConfig.Properties + .Single(prop => prop.Name == nameof(Project.Languages)); + + langConfig + .GetType()! + .GetProperty(nameof(PropertyConfig.IsRelation))! + .SetValue(langConfig, true); + + langConfig + .GetType()! + .GetProperty(nameof(PropertyConfig.IsEnumerable))! + .SetValue(langConfig, true); + + langConfig + .GetType()! + .GetProperty(nameof(PropertyConfig.IsRequired))! + .SetValue(langConfig, true); + + table.Property(p => p.Languages) + .FormatEach((l, _) => l.Label) + .List(false); + + table.Property(p => p.Cover) + .List(false); + + table.Property(p => p.Description) + .List(false) + .IsTextArea(true); + + table.Property(p => p.SourceCode) + .List(false); + }); + }); }); builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); @@ -32,6 +78,8 @@ await using (var scope = app.Services.CreateAsyncScope()) { app.UseHttpsRedirection(); app.MapDefaultEndpoints(); +app.MapControllers(); + app.UseAntiforgery(); app.MapStaticAssets(); app.MapRazorComponents() diff --git a/src/Portfolio.Api/Services/AboutRepository.cs b/src/Portfolio.Api/Services/AboutRepository.cs new file mode 100644 index 0000000..c61a9ef --- /dev/null +++ b/src/Portfolio.Api/Services/AboutRepository.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Services; + +internal sealed class AboutRepository(DatabaseContext context) : IAboutRepository { + + public async Task GetAbout(CancellationToken ct) { + return await context.About + .SingleAsync(ct); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/Services/ProjectRepository.cs b/src/Portfolio.Api/Services/ProjectRepository.cs new file mode 100644 index 0000000..836afd9 --- /dev/null +++ b/src/Portfolio.Api/Services/ProjectRepository.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Services; + +internal sealed class ProjectRepository(DatabaseContext context) : IProjectRepository { + + public async Task> GetProjects(CancellationToken ct) { + return await context.Projects + .Include(p => p.Languages) + .ToArrayAsync(ct); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/Services/TechnologyRepository.cs b/src/Portfolio.Api/Services/TechnologyRepository.cs new file mode 100644 index 0000000..9f599ef --- /dev/null +++ b/src/Portfolio.Api/Services/TechnologyRepository.cs @@ -0,0 +1,13 @@ +using Microsoft.EntityFrameworkCore; +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Services; + +internal sealed class TechnologyRepository(DatabaseContext context) : ITechnologyRepository { + + public async Task> GetTechnologies(CancellationToken ct) { + return await context.Technologies.ToArrayAsync(ct); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Api/Services/TimelineRepository.cs b/src/Portfolio.Api/Services/TimelineRepository.cs new file mode 100644 index 0000000..2a8428c --- /dev/null +++ b/src/Portfolio.Api/Services/TimelineRepository.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore; +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Api.Services; + +internal sealed class TimelineRepository(DatabaseContext context) : ITimelineRepository { + + public async Task> GetTimeline(TimelineEntryType type, CancellationToken ct) { + return await context.Timeline + .Where(entry => entry.Type == type) + .ToArrayAsync(ct); + } + +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Models/About.cs b/src/Portfolio.Shared/Models/About.cs index b17cc31..baaabe8 100644 --- a/src/Portfolio.Shared/Models/About.cs +++ b/src/Portfolio.Shared/Models/About.cs @@ -2,7 +2,7 @@ namespace Portfolio.Shared.Models; -public class About { +public sealed class About { [Key] public int Id { get; private set; } = 0; diff --git a/src/Portfolio.Shared/Models/TimelineEntry.cs b/src/Portfolio.Shared/Models/TimelineEntry.cs index f6cb8fa..4c25ca1 100644 --- a/src/Portfolio.Shared/Models/TimelineEntry.cs +++ b/src/Portfolio.Shared/Models/TimelineEntry.cs @@ -3,7 +3,7 @@ using System.ComponentModel.DataAnnotations.Schema; namespace Portfolio.Shared.Models; -public class TimelineEntry { +public sealed class TimelineEntry { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; init; } diff --git a/src/Portfolio.Shared/Services/IAboutRepository.cs b/src/Portfolio.Shared/Services/IAboutRepository.cs new file mode 100644 index 0000000..8202a30 --- /dev/null +++ b/src/Portfolio.Shared/Services/IAboutRepository.cs @@ -0,0 +1,9 @@ +using Portfolio.Shared.Models; + +namespace Portfolio.Shared.Services; + +public interface IAboutRepository { + + Task GetAbout(CancellationToken ct); + +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Services/IProjectRepository.cs b/src/Portfolio.Shared/Services/IProjectRepository.cs new file mode 100644 index 0000000..11b17d1 --- /dev/null +++ b/src/Portfolio.Shared/Services/IProjectRepository.cs @@ -0,0 +1,9 @@ +using Portfolio.Shared.Models; + +namespace Portfolio.Shared.Services; + +public interface IProjectRepository { + + Task> GetProjects(CancellationToken ct); + +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Services/ITechnologyRepository.cs b/src/Portfolio.Shared/Services/ITechnologyRepository.cs new file mode 100644 index 0000000..fb34e75 --- /dev/null +++ b/src/Portfolio.Shared/Services/ITechnologyRepository.cs @@ -0,0 +1,9 @@ +using Portfolio.Shared.Models; + +namespace Portfolio.Shared.Services; + +public interface ITechnologyRepository { + + Task> GetTechnologies(CancellationToken ct); + +} \ No newline at end of file diff --git a/src/Portfolio.Shared/Services/ITimelineRepository.cs b/src/Portfolio.Shared/Services/ITimelineRepository.cs new file mode 100644 index 0000000..561f075 --- /dev/null +++ b/src/Portfolio.Shared/Services/ITimelineRepository.cs @@ -0,0 +1,9 @@ +using Portfolio.Shared.Models; + +namespace Portfolio.Shared.Services; + +public interface ITimelineRepository { + + Task> GetTimeline(TimelineEntryType type, CancellationToken ct); + +} \ No newline at end of file -- 2.49.1