diff --git a/services/Directory/FilterLists.Directory.Api/Controllers/ListsController.cs b/services/Directory/FilterLists.Directory.Api/Controllers/ListsController.cs index daf295203..dd5a43845 100644 --- a/services/Directory/FilterLists.Directory.Api/Controllers/ListsController.cs +++ b/services/Directory/FilterLists.Directory.Api/Controllers/ListsController.cs @@ -1,4 +1,5 @@ using FilterLists.Directory.Api.Contracts.Models; +using FilterLists.Directory.Application.Commands; using FilterLists.Directory.Application.Queries; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -16,7 +17,7 @@ public ListsController(IMemoryCache cache, IMediator mediator) : base(cache) } /// - /// Gets the FilterLists.. + /// Gets the FilterLists. /// /// The cancellation token. /// The FilterLists. @@ -27,6 +28,18 @@ public Task Get(CancellationToken cancellationToken) return CacheGetOrCreateAsync(() => _mediator.Send(new GetLists.Query(), cancellationToken)); } + /// + /// Creates the FilterList. + /// + /// The command. + /// The cancellation token. + [HttpPost] + [ProducesResponseType(typeof(CreateList.Response), StatusCodes.Status200OK)] + public async Task Create(CreateList.Command command, CancellationToken cancellationToken) + { + return Ok(await _mediator.Send(command, cancellationToken)); + } + /// /// Gets the details of the FilterList. /// diff --git a/services/Directory/FilterLists.Directory.Application/Commands/CreateList.cs b/services/Directory/FilterLists.Directory.Application/Commands/CreateList.cs new file mode 100644 index 000000000..b7df3e9be --- /dev/null +++ b/services/Directory/FilterLists.Directory.Application/Commands/CreateList.cs @@ -0,0 +1,78 @@ +using FilterLists.Directory.Domain.Aggregates; +using FilterLists.Directory.Domain.Aggregates.Changes; +using FilterLists.Directory.Domain.Aggregates.Licenses; +using FluentValidation; +using MediatR; + +namespace FilterLists.Directory.Application.Commands; + +public static class CreateList +{ + public record Command(string Name, + string? Description = default, + int? LicenseId = default, + Uri? HomeUrl = default, + Uri? OnionUrl = default, + Uri? PolicyUrl = default, + Uri? SubmissionUrl = default, + Uri? IssuesUrl = default, + Uri? ForumUrl = default, + Uri? ChatUrl = default, + string? EmailAddress = default, + Uri? DonateUrl = default) : IRequest; + + internal class Validator : AbstractValidator + { + public Validator() + { + RuleFor(c => c.LicenseId) + .GreaterThanOrEqualTo(0) + .When(c => c.LicenseId != null); + } + } + + internal class Handler : IRequestHandler + { + private readonly IChangeRepository _changeRepo; + private readonly ILicenseRepository _licenseRepo; + + public Handler(IChangeRepository changeRepo, ILicenseRepository licenseRepo) + { + _changeRepo = changeRepo; + _licenseRepo = licenseRepo; + } + + public async Task Handle(Command request, CancellationToken cancellationToken) + { + var license = request.LicenseId != null + ? await _licenseRepo.GetByIdAsync(request.LicenseId.Value, cancellationToken) ?? + throw new ArgumentException($"LicenseId {request.LicenseId} not found.", + nameof(request.LicenseId)) + : null; + + var filterList = new FilterList(request.Name, + request.Description, + license, + request.HomeUrl, + request.OnionUrl, + request.PolicyUrl, + request.SubmissionUrl, + request.IssuesUrl, + request.ForumUrl, + request.ChatUrl, + request.EmailAddress, + request.DonateUrl); + + //Change change = null; + + //await _changeRepo.AddAsync(change, cancellationToken); + //return new Response(); + + throw new NotImplementedException(); + } + } + + public record Response + { + } +} diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/AggregateRoot.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/AggregateRoot.cs new file mode 100644 index 000000000..c884d1a83 --- /dev/null +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/AggregateRoot.cs @@ -0,0 +1,12 @@ +namespace FilterLists.Directory.Domain.Aggregates.Changes; + +public enum AggregateRoot +{ + FilterList, + Language, + License, + Maintainer, + Software, + Syntax, + Tag +} diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/Change.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/Change.cs new file mode 100644 index 000000000..9fef1081c --- /dev/null +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/Change.cs @@ -0,0 +1,27 @@ +using System.Text.Json; + +namespace FilterLists.Directory.Domain.Aggregates.Changes; + +public class Change +{ + private Change() + { + } + + public Change(ChangeType type, AggregateRoot aggregateRoot, int aggregateRootId, JsonDocument? aggregate) + { + Type = type; + AggregateRoot = aggregateRoot; + AggregateRootId = aggregateRootId; + Aggregate = aggregate; + } + + public ChangeType Type { get; private init; } + public AggregateRoot AggregateRoot { get; private init; } + public int AggregateRootId { get; private init; } + public JsonDocument? Aggregate { get; private init; } + public DateTime CreatedAt { get; private init; } + public DateTime? AppliedAt { get; private init; } + public DateTime? RejectedAt { get; private init; } + public string? RejectedReason { get; private init; } +} diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/ChangeType.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/ChangeType.cs new file mode 100644 index 000000000..a703aa664 --- /dev/null +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/ChangeType.cs @@ -0,0 +1,8 @@ +namespace FilterLists.Directory.Domain.Aggregates.Changes; + +public enum ChangeType +{ + Insert, + Update, + Delete +} diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/IChangeRepository.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/IChangeRepository.cs new file mode 100644 index 000000000..f066bf5c3 --- /dev/null +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Changes/IChangeRepository.cs @@ -0,0 +1,6 @@ +namespace FilterLists.Directory.Domain.Aggregates.Changes; + +public interface IChangeRepository +{ + Task AddAsync(Change change, CancellationToken cancellationToken); +} diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterList.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterList.cs index 9896531da..05e4858de 100644 --- a/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterList.cs +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/FilterList.cs @@ -1,18 +1,50 @@ -namespace FilterLists.Directory.Domain.Aggregates; +using FilterLists.Directory.Domain.Aggregates.Licenses; + +namespace FilterLists.Directory.Domain.Aggregates; public class FilterList { - public int Id { get; private set; } - public string Name { get; private set; } = null!; - public string? Description { get; private set; } - public int? LicenseId { get; private set; } - public Uri? HomeUrl { get; private set; } - public Uri? OnionUrl { get; private set; } - public Uri? PolicyUrl { get; private set; } - public Uri? SubmissionUrl { get; private set; } - public Uri? IssuesUrl { get; private set; } - public Uri? ForumUrl { get; private set; } - public Uri? ChatUrl { get; private set; } - public string? EmailAddress { get; private set; } - public Uri? DonateUrl { get; private set; } + private FilterList() + { + } + + public FilterList(string name, + string? description, + License? license, + Uri? homeUrl, + Uri? onionUrl, + Uri? policyUrl, + Uri? submissionUrl, + Uri? issuesUrl, + Uri? forumUrl, + Uri? chatUrl, + string? emailAddress, + Uri? donateUrl) + { + Name = name; + Description = description; + License = license; + HomeUrl = homeUrl; + OnionUrl = onionUrl; + PolicyUrl = policyUrl; + SubmissionUrl = submissionUrl; + IssuesUrl = issuesUrl; + ForumUrl = forumUrl; + ChatUrl = chatUrl; + EmailAddress = emailAddress; + DonateUrl = donateUrl; + } + + public string Name { get; private init; } = null!; + public string? Description { get; private init; } + public License? License { get; private init; } + public Uri? HomeUrl { get; private init; } + public Uri? OnionUrl { get; private init; } + public Uri? PolicyUrl { get; private init; } + public Uri? SubmissionUrl { get; private init; } + public Uri? IssuesUrl { get; private init; } + public Uri? ForumUrl { get; private init; } + public Uri? ChatUrl { get; private init; } + public string? EmailAddress { get; private init; } + public Uri? DonateUrl { get; private init; } } diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/ILicenseRepository.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/ILicenseRepository.cs new file mode 100644 index 000000000..cc663f0f7 --- /dev/null +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/ILicenseRepository.cs @@ -0,0 +1,6 @@ +namespace FilterLists.Directory.Domain.Aggregates.Licenses; + +public interface ILicenseRepository +{ + ValueTask GetByIdAsync(int id, CancellationToken cancellationToken); +} diff --git a/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/License.cs b/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/License.cs new file mode 100644 index 000000000..3fd3dc0bc --- /dev/null +++ b/services/Directory/FilterLists.Directory.Domain/Aggregates/Licenses/License.cs @@ -0,0 +1,27 @@ +namespace FilterLists.Directory.Domain.Aggregates.Licenses; + +public class License +{ + private License() + { + } + + public License(string name, + Uri? url, + bool? permitsModification, + bool? permitsDistribution, + bool? permitsCommercialUse) + { + Name = name; + Url = url; + PermitsModification = permitsModification ?? false; + PermitsDistribution = permitsDistribution ?? false; + PermitsCommercialUse = permitsCommercialUse ?? false; + } + + public string Name { get; private init; } = null!; + public Uri? Url { get; private init; } + public bool PermitsModification { get; private init; } + public bool PermitsDistribution { get; private init; } + public bool PermitsCommercialUse { get; private init; } +} diff --git a/services/Directory/FilterLists.Directory.Infrastructure/ConfigurationExtensions.cs b/services/Directory/FilterLists.Directory.Infrastructure/ConfigurationExtensions.cs index 0c678be27..d2e8355de 100644 --- a/services/Directory/FilterLists.Directory.Infrastructure/ConfigurationExtensions.cs +++ b/services/Directory/FilterLists.Directory.Infrastructure/ConfigurationExtensions.cs @@ -1,4 +1,5 @@ using FilterLists.Directory.Infrastructure.Persistence.Commands.Context; +using FilterLists.Directory.Infrastructure.Persistence.Commands.Repositories; using FilterLists.Directory.Infrastructure.Persistence.Queries.Context; using FilterLists.SharedKernel.Logging; using Microsoft.AspNetCore.Builder; @@ -32,10 +33,24 @@ public static void AddInfrastructure(this IServiceCollection services, IConfigur o.EnableSensitiveDataLogging(); #else ; +#endif + }); + services.AddDbContextPool(o => + { + o.UseNpgsql(configuration.GetConnectionString("DirectoryConnection")) +#if DEBUG + .LogTo(Console.WriteLine, LogLevel.Information); + o.EnableSensitiveDataLogging(); +#else + ; #endif }); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.Scan(s => s.FromAssembliesOf(typeof(ConfigurationExtensions)) + .AddClasses(f => f.InNamespaceOf(), false) + .AsImplementedInterfaces() + .WithScopedLifetime()); } public static void UseInfrastructure(this IApplicationBuilder app) diff --git a/services/Directory/FilterLists.Directory.Infrastructure/FilterLists.Directory.Infrastructure.csproj b/services/Directory/FilterLists.Directory.Infrastructure/FilterLists.Directory.Infrastructure.csproj index 7d82668d4..d345735da 100644 --- a/services/Directory/FilterLists.Directory.Infrastructure/FilterLists.Directory.Infrastructure.csproj +++ b/services/Directory/FilterLists.Directory.Infrastructure/FilterLists.Directory.Infrastructure.csproj @@ -20,6 +20,7 @@ + diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/CommandContext.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/CommandContext.cs deleted file mode 100644 index ab96858d9..000000000 --- a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/CommandContext.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context; - -internal class CommandContext : ICommandContext, IAsyncDisposable -{ - private readonly CommandDbContext _dbContext; - - public CommandContext(CommandDbContext dbContext) - { - _dbContext = dbContext; - } - - public async ValueTask DisposeAsync() - { - await _dbContext.DisposeAsync(); - } -} diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/CommandDbContext.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/CommandDbContext.cs index 72cf72ad5..fa1c79186 100644 --- a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/CommandDbContext.cs +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/CommandDbContext.cs @@ -1,15 +1,22 @@ -using Microsoft.EntityFrameworkCore; +using FilterLists.Directory.Domain.Aggregates; +using FilterLists.Directory.Domain.Aggregates.Licenses; +using FilterLists.Directory.Infrastructure.Persistence.Commands.EntityTypeConfigurations; +using Microsoft.EntityFrameworkCore; namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context; -public class CommandDbContext : DbContext +internal class CommandDbContext : DbContext, ICommandContext { public CommandDbContext(DbContextOptions options) : base(options) { } + public DbSet FilterLists => Set(); + public DbSet Licenses => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); + modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly, + type => type.Namespace == typeof(FilterListTypeConfiguration).Namespace); } } diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/ICommandContext.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/ICommandContext.cs index f18b4e0a2..53dd0453f 100644 --- a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/ICommandContext.cs +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Context/ICommandContext.cs @@ -1,5 +1,11 @@ -namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context; +using FilterLists.Directory.Domain.Aggregates; +using FilterLists.Directory.Domain.Aggregates.Licenses; +using Microsoft.EntityFrameworkCore; -public interface ICommandContext +namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Context; + +internal interface ICommandContext { + DbSet FilterLists { get; } + DbSet Licenses { get; } } diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/EntityTypeConfigurations/FilterListEntityTypeConfiguration.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/EntityTypeConfigurations/FilterListEntityTypeConfiguration.cs new file mode 100644 index 000000000..515f76cff --- /dev/null +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/EntityTypeConfigurations/FilterListEntityTypeConfiguration.cs @@ -0,0 +1,13 @@ +using FilterLists.Directory.Domain.Aggregates; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FilterLists.Directory.Infrastructure.Persistence.Commands.EntityTypeConfigurations; + +internal class FilterListTypeConfiguration : IEntityTypeConfiguration +{ + public virtual void Configure(EntityTypeBuilder builder) + { + builder.Property(nameof(Queries.Entities.FilterList.Id)); + } +} diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/EntityTypeConfigurations/LicenseEntityTypeConfiguration.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/EntityTypeConfigurations/LicenseEntityTypeConfiguration.cs new file mode 100644 index 000000000..ddff32b36 --- /dev/null +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/EntityTypeConfigurations/LicenseEntityTypeConfiguration.cs @@ -0,0 +1,13 @@ +using FilterLists.Directory.Domain.Aggregates.Licenses; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FilterLists.Directory.Infrastructure.Persistence.Commands.EntityTypeConfigurations; + +internal class LicenseTypeConfiguration : IEntityTypeConfiguration +{ + public virtual void Configure(EntityTypeBuilder builder) + { + builder.Property(nameof(Queries.Entities.License.Id)); + } +} diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Repositories/ChangeRepository.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Repositories/ChangeRepository.cs new file mode 100644 index 000000000..91423c64f --- /dev/null +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Repositories/ChangeRepository.cs @@ -0,0 +1,11 @@ +using FilterLists.Directory.Domain.Aggregates.Changes; + +namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Repositories; + +internal class ChangeRepository : IChangeRepository +{ + public Task AddAsync(Change change, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Repositories/LicenseRepository.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Repositories/LicenseRepository.cs new file mode 100644 index 000000000..c6ca752dd --- /dev/null +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Commands/Repositories/LicenseRepository.cs @@ -0,0 +1,19 @@ +using FilterLists.Directory.Domain.Aggregates.Licenses; +using FilterLists.Directory.Infrastructure.Persistence.Commands.Context; + +namespace FilterLists.Directory.Infrastructure.Persistence.Commands.Repositories; + +internal class LicenseRepository : ILicenseRepository +{ + private readonly ICommandContext _context; + + public LicenseRepository(ICommandContext context) + { + _context = context; + } + + public ValueTask GetByIdAsync(int id, CancellationToken cancellationToken) + { + return _context.Licenses.FindAsync(new object[] { id }, cancellationToken); + } +} diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Context/QueryDbContext.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Context/QueryDbContext.cs index 422092dce..28b8150a4 100644 --- a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Context/QueryDbContext.cs +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Context/QueryDbContext.cs @@ -1,6 +1,8 @@ -using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities; +using FilterLists.Directory.Domain.Aggregates.Changes; +using FilterLists.Directory.Infrastructure.Persistence.Queries.Entities; using Microsoft.EntityFrameworkCore; using Npgsql; +using Change = FilterLists.Directory.Infrastructure.Persistence.Queries.Entities.Change; namespace FilterLists.Directory.Infrastructure.Persistence.Queries.Context; @@ -39,7 +41,8 @@ public override Task SaveChangesAsync( protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); + modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly, + type => type.Namespace == typeof(FilterListTypeConfiguration).Namespace); modelBuilder.HasPostgresEnum(); modelBuilder.HasPostgresEnum(); } diff --git a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Entities/Change.cs b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Entities/Change.cs index 9ddd07966..67c41c8cf 100644 --- a/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Entities/Change.cs +++ b/services/Directory/FilterLists.Directory.Infrastructure/Persistence/Queries/Entities/Change.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using FilterLists.Directory.Domain.Aggregates.Changes; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -17,24 +18,6 @@ public class Change public string? RejectedReason { get; init; } } -public enum ChangeType -{ - Insert, - Update, - Delete -} - -public enum AggregateRoot -{ - FilterList, - Language, - License, - Maintainer, - Software, - Syntax, - Tag -} - internal class UpdateConfiguration : IEntityTypeConfiguration { public virtual void Configure(EntityTypeBuilder builder)