From 3118bff54712d9d350653087668487484df6b083 Mon Sep 17 00:00:00 2001 From: kirinnee Date: Mon, 23 Oct 2023 16:16:42 +0800 Subject: [PATCH] feat: all registry resources --- App/App.csproj | 1 + App/Config/settings.pichu.yaml | 4 +- App/Error/V1/LikeConflict.cs | 38 + App/Error/V1/LikeRaceCondition.cs | 38 + App/Error/V1/MultipleEntityNotFound.cs | 41 + ...3072338_DomainRegistryEntities.Designer.cs | 716 ++++++++++++++++++ .../20231023072338_DomainRegistryEntities.cs | 458 +++++++++++ App/Migrations/MainDbContextModelSnapshot.cs | 622 +++++++++++++++ App/Modules/Common/BaseController.cs | 7 +- .../API/V1/Controllers/PluginController.cs | 259 +++++++ .../API/V1/Controllers/ProcessorController.cs | 258 +++++++ .../API/V1/Controllers/TemplateController.cs | 267 +++++++ .../Cyan/API/V1/Mappers/PluginMapper.cs | 76 ++ .../Cyan/API/V1/Mappers/ProcessorMapper.cs | 76 ++ .../Cyan/API/V1/Mappers/TemplateMapper.cs | 87 +++ App/Modules/Cyan/API/V1/Models/PluginModel.cs | 25 + .../Cyan/API/V1/Models/PluginVersionModel.cs | 16 + .../Cyan/API/V1/Models/ProcessorModel.cs | 25 + .../API/V1/Models/ProcessorVersionModel.cs | 16 + .../Cyan/API/V1/Models/TemplateModel.cs | 25 + .../API/V1/Models/TemplateVersionModel.cs | 31 + .../Cyan/API/V1/Validators/PluginValidator.cs | 61 ++ .../V1/Validators/PluginVersionValidator.cs | 36 + .../API/V1/Validators/ProcessorValidator.cs | 61 ++ .../Validators/ProcessorVersionValidator.cs | 36 + .../API/V1/Validators/TemplateValidator.cs | 61 ++ .../V1/Validators/TemplateVersionValidator.cs | 40 + App/Modules/Cyan/Data/Mappers/PluginMapper.cs | 76 ++ .../Cyan/Data/Mappers/ProcessorMapper.cs | 76 ++ .../Cyan/Data/Mappers/TemplateMapper.cs | 99 +++ App/Modules/Cyan/Data/Models/LikeData.cs | 36 + App/Modules/Cyan/Data/Models/PluginData.cs | 35 + .../Cyan/Data/Models/PluginVersionData.cs | 24 + App/Modules/Cyan/Data/Models/ProcessorData.cs | 37 + .../Cyan/Data/Models/ProcessorVersionData.cs | 23 + App/Modules/Cyan/Data/Models/TemplateData.cs | 35 + .../Cyan/Data/Models/TemplatePluginData.cs | 12 + .../Cyan/Data/Models/TemplateProcessorData.cs | 13 + .../Cyan/Data/Models/TemplateVersionData.cs | 30 + .../Data/Repositories/PluginRepository.cs | 647 ++++++++++++++++ .../Data/Repositories/ProcessorRepository.cs | 652 ++++++++++++++++ .../Data/Repositories/TemplateRepository.cs | 658 ++++++++++++++++ App/Modules/DomainServices.cs | 16 + App/Modules/Users/API/V1/UserController.cs | 3 +- App/Modules/Users/Data/TokenData.cs | 1 + App/Modules/Users/Data/TokenRepository.cs | 3 - App/Modules/Users/Data/UserData.cs | 18 +- App/StartUp/Database/MainDbContext.cs | 136 ++++ App/Utility/ValidationUtility.cs | 21 +- App/packages.lock.json | 15 + Domain/Model/Plugin.cs | 60 ++ Domain/Model/PluginVersion.cs | 47 ++ Domain/Model/Processor.cs | 56 ++ Domain/Model/ProcessorVersion.cs | 44 ++ Domain/Model/Template.cs | 42 +- Domain/Model/TemplateVersion.cs | 50 ++ Domain/Repository/IPluginRepository.cs | 47 ++ Domain/Repository/IProcessorRepository.cs | 46 ++ Domain/Repository/ITemplateRepository.cs | 41 + Domain/Service/IPluginService.cs | 40 + Domain/Service/IProcessorService.cs | 42 + Domain/Service/ITemplateService.cs | 48 ++ Domain/Service/PluginService.cs | 121 +++ Domain/Service/ProcessorService.cs | 114 +++ Domain/Service/TemplateService.cs | 157 ++++ Taskfile.yml | 4 + config/dev.yaml | 2 +- infra/api_chart/app/settings.pichu.yaml | 4 +- infra/migration_chart/app/settings.pichu.yaml | 4 +- infra/root_chart/values.pichu.yaml | 23 +- scripts/local/exec.sh | 20 + tasks/Taskfile.stop.yml | 4 +- 72 files changed, 7024 insertions(+), 39 deletions(-) create mode 100644 App/Error/V1/LikeConflict.cs create mode 100644 App/Error/V1/LikeRaceCondition.cs create mode 100644 App/Error/V1/MultipleEntityNotFound.cs create mode 100644 App/Migrations/20231023072338_DomainRegistryEntities.Designer.cs create mode 100644 App/Migrations/20231023072338_DomainRegistryEntities.cs create mode 100644 App/Modules/Cyan/API/V1/Controllers/PluginController.cs create mode 100644 App/Modules/Cyan/API/V1/Controllers/ProcessorController.cs create mode 100644 App/Modules/Cyan/API/V1/Controllers/TemplateController.cs create mode 100644 App/Modules/Cyan/API/V1/Mappers/PluginMapper.cs create mode 100644 App/Modules/Cyan/API/V1/Mappers/ProcessorMapper.cs create mode 100644 App/Modules/Cyan/API/V1/Mappers/TemplateMapper.cs create mode 100644 App/Modules/Cyan/API/V1/Models/PluginModel.cs create mode 100644 App/Modules/Cyan/API/V1/Models/PluginVersionModel.cs create mode 100644 App/Modules/Cyan/API/V1/Models/ProcessorModel.cs create mode 100644 App/Modules/Cyan/API/V1/Models/ProcessorVersionModel.cs create mode 100644 App/Modules/Cyan/API/V1/Models/TemplateModel.cs create mode 100644 App/Modules/Cyan/API/V1/Models/TemplateVersionModel.cs create mode 100644 App/Modules/Cyan/API/V1/Validators/PluginValidator.cs create mode 100644 App/Modules/Cyan/API/V1/Validators/PluginVersionValidator.cs create mode 100644 App/Modules/Cyan/API/V1/Validators/ProcessorValidator.cs create mode 100644 App/Modules/Cyan/API/V1/Validators/ProcessorVersionValidator.cs create mode 100644 App/Modules/Cyan/API/V1/Validators/TemplateValidator.cs create mode 100644 App/Modules/Cyan/API/V1/Validators/TemplateVersionValidator.cs create mode 100644 App/Modules/Cyan/Data/Mappers/PluginMapper.cs create mode 100644 App/Modules/Cyan/Data/Mappers/ProcessorMapper.cs create mode 100644 App/Modules/Cyan/Data/Mappers/TemplateMapper.cs create mode 100644 App/Modules/Cyan/Data/Models/LikeData.cs create mode 100644 App/Modules/Cyan/Data/Models/PluginData.cs create mode 100644 App/Modules/Cyan/Data/Models/PluginVersionData.cs create mode 100644 App/Modules/Cyan/Data/Models/ProcessorData.cs create mode 100644 App/Modules/Cyan/Data/Models/ProcessorVersionData.cs create mode 100644 App/Modules/Cyan/Data/Models/TemplateData.cs create mode 100644 App/Modules/Cyan/Data/Models/TemplatePluginData.cs create mode 100644 App/Modules/Cyan/Data/Models/TemplateProcessorData.cs create mode 100644 App/Modules/Cyan/Data/Models/TemplateVersionData.cs create mode 100644 App/Modules/Cyan/Data/Repositories/PluginRepository.cs create mode 100644 App/Modules/Cyan/Data/Repositories/ProcessorRepository.cs create mode 100644 App/Modules/Cyan/Data/Repositories/TemplateRepository.cs create mode 100644 Domain/Model/Plugin.cs create mode 100644 Domain/Model/PluginVersion.cs create mode 100644 Domain/Model/Processor.cs create mode 100644 Domain/Model/ProcessorVersion.cs create mode 100644 Domain/Model/TemplateVersion.cs create mode 100644 Domain/Repository/IPluginRepository.cs create mode 100644 Domain/Repository/IProcessorRepository.cs create mode 100644 Domain/Service/IPluginService.cs create mode 100644 Domain/Service/IProcessorService.cs create mode 100644 Domain/Service/ITemplateService.cs create mode 100644 Domain/Service/PluginService.cs create mode 100644 Domain/Service/ProcessorService.cs create mode 100644 Domain/Service/TemplateService.cs create mode 100755 scripts/local/exec.sh diff --git a/App/App.csproj b/App/App.csproj index 87ca844..3d96f90 100644 --- a/App/App.csproj +++ b/App/App.csproj @@ -20,6 +20,7 @@ + diff --git a/App/Config/settings.pichu.yaml b/App/Config/settings.pichu.yaml index 2907619..d40d4e8 100644 --- a/App/Config/settings.pichu.yaml +++ b/App/Config/settings.pichu.yaml @@ -24,7 +24,9 @@ Metrics: Enabled: true # Infra-based -Database: {} +Database: + MAIN: + AutoMigrate: false Cache: {} BlockStorage: {} # external diff --git a/App/Error/V1/LikeConflict.cs b/App/Error/V1/LikeConflict.cs new file mode 100644 index 0000000..af92b69 --- /dev/null +++ b/App/Error/V1/LikeConflict.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using App.Modules.Common; +using NJsonSchema.Annotations; + +namespace App.Error.V1; + +[ + Description( + "Like conflict error occurs when a user tries to like a template, plugin or processor that they have already liked, or unlike a template, plugin or processor that they have not liked.") +] +internal class LikeConflictError : IDomainProblem +{ + public LikeConflictError() { } + + public LikeConflictError(string detail, string resourceId, string resourceType, string conflictType) + { + this.Detail = detail; + this.ResourceId = resourceId; + this.ResourceType = resourceType; + this.ConflictType = conflictType; + } + + [JsonIgnore, JsonSchemaIgnore] public string Id { get; } = "like_conflict"; + [JsonIgnore, JsonSchemaIgnore] public string Title { get; } = "Like Conflict"; + [JsonIgnore, JsonSchemaIgnore] public string Version { get; } = "v1"; + [JsonIgnore, JsonSchemaIgnore] public string Detail { get; } = string.Empty; + + [Description("Type of Resource that like conflicted. Can be either 'template', 'plugin' or 'processor'")] + public string ResourceType { get; } = string.Empty; + + [Description("ID of the resource that like conflicted")] + public string ResourceId { get; } = string.Empty; + + [Description("Conflict type of the like. Can be either 'like' or 'unlike'")] + public string ConflictType { get; } = string.Empty; +} diff --git a/App/Error/V1/LikeRaceCondition.cs b/App/Error/V1/LikeRaceCondition.cs new file mode 100644 index 0000000..2ba964c --- /dev/null +++ b/App/Error/V1/LikeRaceCondition.cs @@ -0,0 +1,38 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using App.Modules.Common; +using NJsonSchema.Annotations; + +namespace App.Error.V1; + +[ + Description( + "Like race condition error occurs when a user tries to like a template, plugin or processor, when the start of the like query, the like exist and at the end of the like query, the like disappeared.") +] +internal class LikeRaceConditionError : IDomainProblem +{ + public LikeRaceConditionError() { } + + public LikeRaceConditionError(string detail, string resourceId, string resourceType, string conflictType) + { + this.Detail = detail; + this.ResourceId = resourceId; + this.ResourceType = resourceType; + this.ConflictType = conflictType; + } + + [JsonIgnore, JsonSchemaIgnore] public string Id { get; } = "like_race_condition"; + [JsonIgnore, JsonSchemaIgnore] public string Title { get; } = "Like Race Condition"; + [JsonIgnore, JsonSchemaIgnore] public string Version { get; } = "v1"; + [JsonIgnore, JsonSchemaIgnore] public string Detail { get; } = string.Empty; + + [Description("Type of Resource that like have race condition. Can be either 'template', 'plugin' or 'processor'")] + public string ResourceType { get; } = string.Empty; + + [Description("ID of the resource that like have race condition")] + public string ResourceId { get; } = string.Empty; + + [Description("Type of the like of the race condition. Can be either 'like' or 'unlike'")] + public string ConflictType { get; } = string.Empty; +} diff --git a/App/Error/V1/MultipleEntityNotFound.cs b/App/Error/V1/MultipleEntityNotFound.cs new file mode 100644 index 0000000..63c9b26 --- /dev/null +++ b/App/Error/V1/MultipleEntityNotFound.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; +using Newtonsoft.Json; +using NJsonSchema.Annotations; + +namespace App.Error.V1; + +[Description("This error represents an error that multiple entities could not be found during a batch request")] +public class MultipleEntityNotFound : IDomainProblem +{ + public MultipleEntityNotFound() { } + + public MultipleEntityNotFound(string detail, Type type, string[] notfound, string[] found) + { + this.Detail = detail; + this.AssemblyQualifiedName = type.AssemblyQualifiedName ?? "Unknown"; + this.TypeName = type.FullName ?? "Unknown"; + this.RequestIdentifiers = notfound; + this.FoundRequestIdentifiers = found; + } + + [JsonIgnore, JsonSchemaIgnore] public string Id { get; } = "multiple_entity_not_found"; + + [JsonIgnore, JsonSchemaIgnore] public string Title { get; } = "Multiple Entity Not Found"; + + [JsonIgnore, JsonSchemaIgnore] public string Version { get; } = "v1"; + + [JsonIgnore, JsonSchemaIgnore] public string Detail { get; } = string.Empty; + + + [Description("All identifiers of the requested entity, that could not be found")] + public string[] RequestIdentifiers { get; } = Array.Empty(); + + [Description("All identifiers of the requested entity, that could be found")] + public string[] FoundRequestIdentifiers { get; } = Array.Empty(); + + [Description("The Full Name of the type of entities that could not be found")] + public string TypeName { get; } = string.Empty; + + [Description("The AssemblyQualifiedName of the entities that could not be found")] + public string AssemblyQualifiedName { get; } = string.Empty; +} diff --git a/App/Migrations/20231023072338_DomainRegistryEntities.Designer.cs b/App/Migrations/20231023072338_DomainRegistryEntities.Designer.cs new file mode 100644 index 0000000..4e61d04 --- /dev/null +++ b/App/Migrations/20231023072338_DomainRegistryEntities.Designer.cs @@ -0,0 +1,716 @@ +// +using System; +using App.StartUp.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace App.Migrations +{ + [DbContext(typeof(MainDbContext))] + [Migration("20231023072338_DomainRegistryEntities")] + partial class DomainRegistryEntities + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0-preview.4.23259.3") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downloads") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Project") + .IsRequired() + .HasColumnType("text"); + + b.Property("Readme") + .IsRequired() + .HasColumnType("text"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("Plugins"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginLikeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PluginId") + .HasColumnType("uuid"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PluginId"); + + b.HasIndex("UserId", "PluginId") + .IsUnique(); + + b.ToTable("PluginLikes"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("PluginId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("PluginId"); + + b.HasIndex("Id", "Version") + .IsUnique(); + + b.ToTable("PluginVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downloads") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Project") + .IsRequired() + .HasColumnType("text"); + + b.Property("Readme") + .IsRequired() + .HasColumnType("text"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("Processors"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorLikeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ProcessorId") + .HasColumnType("uuid"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProcessorId"); + + b.HasIndex("UserId", "ProcessorId") + .IsUnique(); + + b.ToTable("ProcessorLikes"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProcessorId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("ProcessorId"); + + b.HasIndex("Id", "Version") + .IsUnique(); + + b.ToTable("ProcessorVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downloads") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Project") + .IsRequired() + .HasColumnType("text"); + + b.Property("Readme") + .IsRequired() + .HasColumnType("text"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateLikeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId"); + + b.HasIndex("UserId", "TemplateId") + .IsUnique(); + + b.ToTable("TemplateLikes"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplatePluginVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PluginId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PluginId"); + + b.HasIndex("TemplateId"); + + b.ToTable("TemplatePluginVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateProcessorVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ProcessorId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProcessorId"); + + b.HasIndex("TemplateId"); + + b.ToTable("TemplateProcessorVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlobDockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("BlobDockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateDockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateDockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId"); + + b.HasIndex("Id", "Version") + .IsUnique(); + + b.ToTable("TemplateVersions"); + }); + + modelBuilder.Entity("App.Modules.Users.Data.TokenData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ApiToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Revoked") + .HasColumnType("boolean"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ApiToken") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Tokens"); + }); + + modelBuilder.Entity("App.Modules.Users.Data.UserData", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("Plugins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginLikeData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.PluginData", "Plugin") + .WithMany("Likes") + .HasForeignKey("PluginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("PluginLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plugin"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.PluginData", "Plugin") + .WithMany("Versions") + .HasForeignKey("PluginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plugin"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("Processors") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorLikeData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.ProcessorData", "Processor") + .WithMany("Likes") + .HasForeignKey("ProcessorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("ProcessorLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Processor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.ProcessorData", "Processor") + .WithMany("Versions") + .HasForeignKey("ProcessorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Processor"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("Templates") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateLikeData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.TemplateData", "Template") + .WithMany("Likes") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("TemplateLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplatePluginVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.PluginVersionData", "Plugin") + .WithMany("Templates") + .HasForeignKey("PluginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Cyan.Data.Models.TemplateVersionData", "Template") + .WithMany("Plugins") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plugin"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateProcessorVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.ProcessorVersionData", "Processor") + .WithMany("Templates") + .HasForeignKey("ProcessorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Cyan.Data.Models.TemplateVersionData", "Template") + .WithMany("Processors") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Processor"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.TemplateData", "Template") + .WithMany("Versions") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("App.Modules.Users.Data.TokenData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("Tokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginData", b => + { + b.Navigation("Likes"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginVersionData", b => + { + b.Navigation("Templates"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorData", b => + { + b.Navigation("Likes"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorVersionData", b => + { + b.Navigation("Templates"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateData", b => + { + b.Navigation("Likes"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateVersionData", b => + { + b.Navigation("Plugins"); + + b.Navigation("Processors"); + }); + + modelBuilder.Entity("App.Modules.Users.Data.UserData", b => + { + b.Navigation("PluginLikes"); + + b.Navigation("Plugins"); + + b.Navigation("ProcessorLikes"); + + b.Navigation("Processors"); + + b.Navigation("TemplateLikes"); + + b.Navigation("Templates"); + + b.Navigation("Tokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/App/Migrations/20231023072338_DomainRegistryEntities.cs b/App/Migrations/20231023072338_DomainRegistryEntities.cs new file mode 100644 index 0000000..642b549 --- /dev/null +++ b/App/Migrations/20231023072338_DomainRegistryEntities.cs @@ -0,0 +1,458 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using NpgsqlTypes; + +#nullable disable + +namespace App.Migrations +{ + /// + public partial class DomainRegistryEntities : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Plugins", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Downloads = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: false), + Project = table.Column(type: "text", nullable: false), + Source = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Tags = table.Column(type: "text[]", nullable: false), + Description = table.Column(type: "text", nullable: false), + Readme = table.Column(type: "text", nullable: false), + SearchVector = table.Column(type: "tsvector", nullable: false) + .Annotation("Npgsql:TsVectorConfig", "english") + .Annotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Plugins", x => x.Id); + table.ForeignKey( + name: "FK_Plugins_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Processors", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Downloads = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: false), + Project = table.Column(type: "text", nullable: false), + Source = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Tags = table.Column(type: "text[]", nullable: false), + Description = table.Column(type: "text", nullable: false), + Readme = table.Column(type: "text", nullable: false), + SearchVector = table.Column(type: "tsvector", nullable: false) + .Annotation("Npgsql:TsVectorConfig", "english") + .Annotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Processors", x => x.Id); + table.ForeignKey( + name: "FK_Processors_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Templates", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Downloads = table.Column(type: "bigint", nullable: false), + Name = table.Column(type: "text", nullable: false), + Project = table.Column(type: "text", nullable: false), + Source = table.Column(type: "text", nullable: false), + Email = table.Column(type: "text", nullable: false), + Tags = table.Column(type: "text[]", nullable: false), + Description = table.Column(type: "text", nullable: false), + Readme = table.Column(type: "text", nullable: false), + SearchVector = table.Column(type: "tsvector", nullable: false) + .Annotation("Npgsql:TsVectorConfig", "english") + .Annotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Templates", x => x.Id); + table.ForeignKey( + name: "FK_Templates_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PluginLikes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PluginId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PluginLikes", x => x.Id); + table.ForeignKey( + name: "FK_PluginLikes_Plugins_PluginId", + column: x => x.PluginId, + principalTable: "Plugins", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PluginLikes_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PluginVersions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Version = table.Column(type: "numeric(20,0)", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Description = table.Column(type: "text", nullable: false), + DockerReference = table.Column(type: "text", nullable: false), + DockerSha = table.Column(type: "text", nullable: false), + PluginId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PluginVersions", x => x.Id); + table.ForeignKey( + name: "FK_PluginVersions_Plugins_PluginId", + column: x => x.PluginId, + principalTable: "Plugins", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProcessorLikes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ProcessorId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProcessorLikes", x => x.Id); + table.ForeignKey( + name: "FK_ProcessorLikes_Processors_ProcessorId", + column: x => x.ProcessorId, + principalTable: "Processors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_ProcessorLikes_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "ProcessorVersions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Version = table.Column(type: "numeric(20,0)", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Description = table.Column(type: "text", nullable: false), + DockerReference = table.Column(type: "text", nullable: false), + DockerSha = table.Column(type: "text", nullable: false), + ProcessorId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProcessorVersions", x => x.Id); + table.ForeignKey( + name: "FK_ProcessorVersions_Processors_ProcessorId", + column: x => x.ProcessorId, + principalTable: "Processors", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TemplateLikes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TemplateId = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TemplateLikes", x => x.Id); + table.ForeignKey( + name: "FK_TemplateLikes_Templates_TemplateId", + column: x => x.TemplateId, + principalTable: "Templates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TemplateLikes_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TemplateVersions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Version = table.Column(type: "numeric(20,0)", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + Description = table.Column(type: "text", nullable: false), + BlobDockerReference = table.Column(type: "text", nullable: false), + BlobDockerSha = table.Column(type: "text", nullable: false), + TemplateDockerReference = table.Column(type: "text", nullable: false), + TemplateDockerSha = table.Column(type: "text", nullable: false), + TemplateId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TemplateVersions", x => x.Id); + table.ForeignKey( + name: "FK_TemplateVersions_Templates_TemplateId", + column: x => x.TemplateId, + principalTable: "Templates", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TemplatePluginVersions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TemplateId = table.Column(type: "uuid", nullable: false), + PluginId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TemplatePluginVersions", x => x.Id); + table.ForeignKey( + name: "FK_TemplatePluginVersions_PluginVersions_PluginId", + column: x => x.PluginId, + principalTable: "PluginVersions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TemplatePluginVersions_TemplateVersions_TemplateId", + column: x => x.TemplateId, + principalTable: "TemplateVersions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "TemplateProcessorVersions", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + TemplateId = table.Column(type: "uuid", nullable: false), + ProcessorId = table.Column(type: "uuid", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TemplateProcessorVersions", x => x.Id); + table.ForeignKey( + name: "FK_TemplateProcessorVersions_ProcessorVersions_ProcessorId", + column: x => x.ProcessorId, + principalTable: "ProcessorVersions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TemplateProcessorVersions_TemplateVersions_TemplateId", + column: x => x.TemplateId, + principalTable: "TemplateVersions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_PluginLikes_PluginId", + table: "PluginLikes", + column: "PluginId"); + + migrationBuilder.CreateIndex( + name: "IX_PluginLikes_UserId_PluginId", + table: "PluginLikes", + columns: new[] { "UserId", "PluginId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PluginVersions_Id_Version", + table: "PluginVersions", + columns: new[] { "Id", "Version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_PluginVersions_PluginId", + table: "PluginVersions", + column: "PluginId"); + + migrationBuilder.CreateIndex( + name: "IX_Plugins_SearchVector", + table: "Plugins", + column: "SearchVector") + .Annotation("Npgsql:IndexMethod", "GIN"); + + migrationBuilder.CreateIndex( + name: "IX_Plugins_UserId_Name", + table: "Plugins", + columns: new[] { "UserId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProcessorLikes_ProcessorId", + table: "ProcessorLikes", + column: "ProcessorId"); + + migrationBuilder.CreateIndex( + name: "IX_ProcessorLikes_UserId_ProcessorId", + table: "ProcessorLikes", + columns: new[] { "UserId", "ProcessorId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProcessorVersions_Id_Version", + table: "ProcessorVersions", + columns: new[] { "Id", "Version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ProcessorVersions_ProcessorId", + table: "ProcessorVersions", + column: "ProcessorId"); + + migrationBuilder.CreateIndex( + name: "IX_Processors_SearchVector", + table: "Processors", + column: "SearchVector") + .Annotation("Npgsql:IndexMethod", "GIN"); + + migrationBuilder.CreateIndex( + name: "IX_Processors_UserId_Name", + table: "Processors", + columns: new[] { "UserId", "Name" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TemplateLikes_TemplateId", + table: "TemplateLikes", + column: "TemplateId"); + + migrationBuilder.CreateIndex( + name: "IX_TemplateLikes_UserId_TemplateId", + table: "TemplateLikes", + columns: new[] { "UserId", "TemplateId" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TemplatePluginVersions_PluginId", + table: "TemplatePluginVersions", + column: "PluginId"); + + migrationBuilder.CreateIndex( + name: "IX_TemplatePluginVersions_TemplateId", + table: "TemplatePluginVersions", + column: "TemplateId"); + + migrationBuilder.CreateIndex( + name: "IX_TemplateProcessorVersions_ProcessorId", + table: "TemplateProcessorVersions", + column: "ProcessorId"); + + migrationBuilder.CreateIndex( + name: "IX_TemplateProcessorVersions_TemplateId", + table: "TemplateProcessorVersions", + column: "TemplateId"); + + migrationBuilder.CreateIndex( + name: "IX_TemplateVersions_Id_Version", + table: "TemplateVersions", + columns: new[] { "Id", "Version" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TemplateVersions_TemplateId", + table: "TemplateVersions", + column: "TemplateId"); + + migrationBuilder.CreateIndex( + name: "IX_Templates_SearchVector", + table: "Templates", + column: "SearchVector") + .Annotation("Npgsql:IndexMethod", "GIN"); + + migrationBuilder.CreateIndex( + name: "IX_Templates_UserId_Name", + table: "Templates", + columns: new[] { "UserId", "Name" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "PluginLikes"); + + migrationBuilder.DropTable( + name: "ProcessorLikes"); + + migrationBuilder.DropTable( + name: "TemplateLikes"); + + migrationBuilder.DropTable( + name: "TemplatePluginVersions"); + + migrationBuilder.DropTable( + name: "TemplateProcessorVersions"); + + migrationBuilder.DropTable( + name: "PluginVersions"); + + migrationBuilder.DropTable( + name: "ProcessorVersions"); + + migrationBuilder.DropTable( + name: "TemplateVersions"); + + migrationBuilder.DropTable( + name: "Plugins"); + + migrationBuilder.DropTable( + name: "Processors"); + + migrationBuilder.DropTable( + name: "Templates"); + } + } +} diff --git a/App/Migrations/MainDbContextModelSnapshot.cs b/App/Migrations/MainDbContextModelSnapshot.cs index 81ebf3c..075b174 100644 --- a/App/Migrations/MainDbContextModelSnapshot.cs +++ b/App/Migrations/MainDbContextModelSnapshot.cs @@ -5,6 +5,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; #nullable disable @@ -22,6 +23,416 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downloads") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Project") + .IsRequired() + .HasColumnType("text"); + + b.Property("Readme") + .IsRequired() + .HasColumnType("text"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("Plugins"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginLikeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PluginId") + .HasColumnType("uuid"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PluginId"); + + b.HasIndex("UserId", "PluginId") + .IsUnique(); + + b.ToTable("PluginLikes"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("PluginId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("PluginId"); + + b.HasIndex("Id", "Version") + .IsUnique(); + + b.ToTable("PluginVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downloads") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Project") + .IsRequired() + .HasColumnType("text"); + + b.Property("Readme") + .IsRequired() + .HasColumnType("text"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("Processors"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorLikeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ProcessorId") + .HasColumnType("uuid"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ProcessorId"); + + b.HasIndex("UserId", "ProcessorId") + .IsUnique(); + + b.ToTable("ProcessorLikes"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("DockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProcessorId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("ProcessorId"); + + b.HasIndex("Id", "Version") + .IsUnique(); + + b.ToTable("ProcessorVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("Downloads") + .HasColumnType("bigint"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Project") + .IsRequired() + .HasColumnType("text"); + + b.Property("Readme") + .IsRequired() + .HasColumnType("text"); + + b.Property("SearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "Name", "Description" }); + + b.Property("Source") + .IsRequired() + .HasColumnType("text"); + + b.Property("Tags") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("SearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("SearchVector"), "GIN"); + + b.HasIndex("UserId", "Name") + .IsUnique(); + + b.ToTable("Templates"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateLikeData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId"); + + b.HasIndex("UserId", "TemplateId") + .IsUnique(); + + b.ToTable("TemplateLikes"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplatePluginVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("PluginId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PluginId"); + + b.HasIndex("TemplateId"); + + b.ToTable("TemplatePluginVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateProcessorVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ProcessorId") + .HasColumnType("uuid"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProcessorId"); + + b.HasIndex("TemplateId"); + + b.ToTable("TemplateProcessorVersions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateVersionData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BlobDockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("BlobDockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateDockerReference") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateDockerSha") + .IsRequired() + .HasColumnType("text"); + + b.Property("TemplateId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasColumnType("numeric(20,0)"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId"); + + b.HasIndex("Id", "Version") + .IsUnique(); + + b.ToTable("TemplateVersions"); + }); + modelBuilder.Entity("App.Modules.Users.Data.TokenData", b => { b.Property("Id") @@ -70,6 +481,167 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("Plugins") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginLikeData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.PluginData", "Plugin") + .WithMany("Likes") + .HasForeignKey("PluginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("PluginLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plugin"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.PluginData", "Plugin") + .WithMany("Versions") + .HasForeignKey("PluginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plugin"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("Processors") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorLikeData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.ProcessorData", "Processor") + .WithMany("Likes") + .HasForeignKey("ProcessorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("ProcessorLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Processor"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.ProcessorData", "Processor") + .WithMany("Versions") + .HasForeignKey("ProcessorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Processor"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateData", b => + { + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("Templates") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateLikeData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.TemplateData", "Template") + .WithMany("Likes") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Users.Data.UserData", "User") + .WithMany("TemplateLikes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplatePluginVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.PluginVersionData", "Plugin") + .WithMany("Templates") + .HasForeignKey("PluginId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Cyan.Data.Models.TemplateVersionData", "Template") + .WithMany("Plugins") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plugin"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateProcessorVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.ProcessorVersionData", "Processor") + .WithMany("Templates") + .HasForeignKey("ProcessorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("App.Modules.Cyan.Data.Models.TemplateVersionData", "Template") + .WithMany("Processors") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Processor"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateVersionData", b => + { + b.HasOne("App.Modules.Cyan.Data.Models.TemplateData", "Template") + .WithMany("Versions") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + modelBuilder.Entity("App.Modules.Users.Data.TokenData", b => { b.HasOne("App.Modules.Users.Data.UserData", "User") @@ -81,8 +653,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginData", b => + { + b.Navigation("Likes"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.PluginVersionData", b => + { + b.Navigation("Templates"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorData", b => + { + b.Navigation("Likes"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.ProcessorVersionData", b => + { + b.Navigation("Templates"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateData", b => + { + b.Navigation("Likes"); + + b.Navigation("Versions"); + }); + + modelBuilder.Entity("App.Modules.Cyan.Data.Models.TemplateVersionData", b => + { + b.Navigation("Plugins"); + + b.Navigation("Processors"); + }); + modelBuilder.Entity("App.Modules.Users.Data.UserData", b => { + b.Navigation("PluginLikes"); + + b.Navigation("Plugins"); + + b.Navigation("ProcessorLikes"); + + b.Navigation("Processors"); + + b.Navigation("TemplateLikes"); + + b.Navigation("Templates"); + b.Navigation("Tokens"); }); #pragma warning restore 612, 618 diff --git a/App/Modules/Common/BaseController.cs b/App/Modules/Common/BaseController.cs index 4d41338..de3c9b6 100644 --- a/App/Modules/Common/BaseController.cs +++ b/App/Modules/Common/BaseController.cs @@ -35,11 +35,14 @@ private ActionResult MapException(Exception e) ValidationError validationError => this.Error(HttpStatusCode.BadRequest, validationError), Unauthorized unauthorizedError => this.Error(HttpStatusCode.Unauthorized, unauthorizedError), EntityConflict entityConflict => this.Error(HttpStatusCode.Conflict, entityConflict), - _ => throw d + MultipleEntityNotFound multipleEntityNotFound => this.Error(HttpStatusCode.NotFound, multipleEntityNotFound), + LikeConflictError likeConflictError => this.Error(HttpStatusCode.Conflict, likeConflictError), + LikeRaceConditionError likeRaceConditionError => this.Error(HttpStatusCode.Conflict, likeRaceConditionError), + _ => this.Error(HttpStatusCode.BadRequest, d.Problem), }, AlreadyExistException aee => this.Error(HttpStatusCode.Conflict, new EntityConflict(aee.Message, aee.t)), - _ => throw e + _ => throw new AggregateException("Unhandled Exception", e), }; } diff --git a/App/Modules/Cyan/API/V1/Controllers/PluginController.cs b/App/Modules/Cyan/API/V1/Controllers/PluginController.cs new file mode 100644 index 0000000..5f8fc1b --- /dev/null +++ b/App/Modules/Cyan/API/V1/Controllers/PluginController.cs @@ -0,0 +1,259 @@ +using System.Net.Mime; +using App.Error.V1; +using App.Modules.Common; +using App.Modules.Cyan.API.V1.Mappers; +using App.Modules.Cyan.API.V1.Models; +using App.Modules.Cyan.API.V1.Validators; +using App.StartUp.Registry; +using App.Utility; +using Asp.Versioning; +using CSharp_Result; +using Domain.Model; +using Domain.Service; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace App.Modules.Cyan.API.V1.Controllers; + +/// +/// V1 controller +/// +[ApiVersion(1.0)] +[ApiController] +[Consumes(MediaTypeNames.Application.Json)] +[Route("api/v{version:apiVersion}/[controller]")] +public class PluginController : AtomiControllerBase +{ + private readonly IPluginService _service; + private readonly IUserService _userService; + private readonly CreatePluginReqValidator _createPluginReqValidator; + private readonly UpdatePluginReqValidator _updatePluginReqValidator; + private readonly SearchPluginQueryValidator _searchPluginQueryValidator; + private readonly CreatePluginVersionReqValidator _createPluginVersionReqValidator; + private readonly UpdatePluginVersionReqValidator _updatePluginVersionReqValidator; + private readonly SearchPluginVersionQueryValidator _searchPluginVersionQueryValidator; + + + public PluginController(IPluginService service, + CreatePluginReqValidator createPluginReqValidator, UpdatePluginReqValidator updatePluginReqValidator, + SearchPluginQueryValidator searchPluginQueryValidator, + CreatePluginVersionReqValidator createPluginVersionReqValidator, + UpdatePluginVersionReqValidator updatePluginVersionReqValidator, + SearchPluginVersionQueryValidator searchPluginVersionQueryValidator, IUserService userService) + { + this._service = service; + this._createPluginReqValidator = createPluginReqValidator; + this._updatePluginReqValidator = updatePluginReqValidator; + this._searchPluginQueryValidator = searchPluginQueryValidator; + this._createPluginVersionReqValidator = createPluginVersionReqValidator; + this._updatePluginVersionReqValidator = updatePluginVersionReqValidator; + this._searchPluginVersionQueryValidator = searchPluginVersionQueryValidator; + this._userService = userService; + } + + [HttpGet] + public async Task>> Search([FromQuery] SearchPluginQuery query) + { + var plugins = await this._searchPluginQueryValidator + .ValidateAsyncResult(query, "Invalid SearchPluginQuery") + .ThenAwait(x => this._service.Search(x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + + return this.ReturnResult(plugins); + } + + + [HttpGet("{id:guid}")] + public async Task> Get(Guid id) + { + var plugin = await this._service.Get(id) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(plugin, + new EntityNotFound("Plugin not found", typeof(PluginPrincipal), id.ToString())); + } + + [HttpGet("{username}/{name}")] + public async Task> Get(string username, string name) + { + var plugin = await this._service.Get(username, name) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(plugin, + new EntityNotFound("Plugin not found", typeof(PluginPrincipal), $"{username}/{name}")); + } + + [Authorize, HttpPost("{userId}")] + public async Task> Create(string userId, [FromBody] CreatePluginReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a plugin for this user") + .ToException(); + return this.ReturnResult(e); + } + + var plugin = await this._createPluginReqValidator + .ValidateAsyncResult(req, "Invalid CreatePluginReq") + .ThenAwait(x => this._service.Create(userId, x.ToDomain().Item1, x.ToDomain().Item2)) + .Then(x => x.ToResp(), Errors.MapAll); + return this.ReturnResult(plugin); + } + + + [Authorize, HttpPut("{userId}/{pluginId}")] + public async Task> Update(string userId, Guid pluginId, + [FromBody] UpdatePluginReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a plugin for this user") + .ToException(); + return this.ReturnResult(e); + } + + var plugin = await this._updatePluginReqValidator + .ValidateAsyncResult(req, "Invalid UpdatePluginReq") + .ThenAwait(x => this._service.Update(userId, pluginId, x.ToDomain())) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(plugin, new EntityNotFound("Plugin not found", typeof(PluginPrincipal), pluginId.ToString())); + } + + [Authorize, HttpPost("{username}/{pluginName}/like/{likerId}/{like}")] + public async Task> Like(string username, string pluginName, string likerId, bool like) + { + var sub = this.Sub(); + if (sub != likerId) + { + Result e = new Unauthorized("You are not authorized to like this plugin") + .ToException(); + return this.ReturnResult(e); + } + + var plugin = await this._service.Like(likerId, username, pluginName, like) + .Then(x => x.ToResult()); + return this.ReturnUnitNullableResult(plugin, + new EntityNotFound("Plugin not found", typeof(PluginPrincipal), $"{username}/{pluginName}")); + } + + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpDelete("{userId}/{pluginId:guid}")] + public async Task> Delete(string userId, Guid pluginId) + { + var plugin = await this._service.Delete(userId, pluginId) + .Then(x => x.ToResult()); + return this.ReturnUnitNullableResult(plugin, + new EntityNotFound("Plugin not found", typeof(PluginPrincipal), $"{userId}/{pluginId}")); + } + + [HttpGet("{username}/{pluginName}/versions")] + public async Task>> SearchVersion(string username, + string pluginName, [FromQuery] SearchPluginVersionQuery query) + { + var plugins = await this._searchPluginVersionQueryValidator + .ValidateAsyncResult(query, "Invalid SearchPluginVersionQuery") + .ThenAwait(x => this._service.SearchVersion(username, pluginName, x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + return this.ReturnResult(plugins); + } + + [HttpGet("{userId}/{pluginId:guid}/versions")] + public async Task>> SearchVersion(string userId, Guid pluginId, + [FromQuery] SearchPluginVersionQuery query) + { + var plugins = await this._searchPluginVersionQueryValidator + .ValidateAsyncResult(query, "Invalid SearchPluginVersionQuery") + .ThenAwait(x => this._service.SearchVersion(userId, pluginId, x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + return this.ReturnResult(plugins); + } + + [HttpGet("{username}/{pluginName}/versions/{ver}")] + public async Task> GetVersion(string username, string pluginName, ulong ver, + bool bumpDownload) + { + var plugin = await this._service.GetVersion(username, pluginName, ver, bumpDownload) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(plugin, + new EntityNotFound("Plugin not found", typeof(PluginVersionPrincipal), $"{username}/{pluginName}:{ver}")); + } + + [HttpGet("{userId}/{pluginId:guid}/versions/{ver}")] + public async Task> GetVersion(string userId, Guid pluginId, ulong ver) + { + var plugin = await this._service.GetVersion(userId, pluginId, ver) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(plugin, + new EntityNotFound("Plugin not found", typeof(PluginVersionPrincipal), $"{userId}/{pluginId}:{ver}")); + } + + [Authorize, HttpPost("{username}/{pluginName}/versions")] + public async Task> CreateVersion(string username, string pluginName, + [FromBody] CreatePluginVersionReq req) + { + var sub = this.Sub(); + var version = await this._userService + .GetByUsername(username) + .ThenAwait(x => Task.FromResult(x?.Principal.Id == sub), Errors.MapAll) + .ThenAwait(async x => + { + if (x) + { + return await this._createPluginVersionReqValidator + .ValidateAsyncResult(req, "Invalid CreatePluginVersionReq") + .ThenAwait(c => this._service.CreateVersion(username, pluginName, c.ToDomain().Item2, c.ToDomain().Item1)) + .Then(c => c?.ToResp(), Errors.MapAll); + } + + return new Unauthorized("You are not authorized to create a plugin for this user") + .ToException(); + }); + return this.ReturnNullableResult(version, + new EntityNotFound("Plugin not found", typeof(PluginPrincipal), $"{username}/{pluginName}")); + } + + [Authorize, HttpPost("{userId}/{pluginId:guid}/versions")] + public async Task> CreateVersion(string userId, Guid pluginId, + [FromBody] CreatePluginVersionReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a plugin for this user") + .ToException(); + return this.ReturnResult(e); + } + + var version = await this._createPluginVersionReqValidator + .ValidateAsyncResult(req, "Invalid CreatePluginVersionReq") + .ThenAwait(x => this._service.CreateVersion(userId, pluginId, x.ToDomain().Item2, x.ToDomain().Item1)) + .Then(x => x?.ToResp(), Errors.MapAll); + + return this.ReturnNullableResult(version, + new EntityNotFound("Plugin not found", typeof(PluginPrincipal), $"{userId}/{pluginId}")); + } + + [Authorize, HttpPut("{userId}/{pluginId:guid}/versions/{ver}")] + public async Task> UpdateVersion(string userId, Guid pluginId, ulong ver, + [FromBody] UpdatePluginVersionReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a plugin for this user") + .ToException(); + return this.ReturnResult(e); + } + + var version = await this._updatePluginVersionReqValidator + .ValidateAsyncResult(req, "Invalid UpdatePluginVersionReq") + .ThenAwait(x => this._service.UpdateVersion(userId, pluginId, ver, x.ToDomain())) + .Then(x => x?.ToResp(), Errors.MapAll); + + return this.ReturnNullableResult(version, + new EntityNotFound("Plugin not found", typeof(PluginPrincipal), $"{userId}/{pluginId}")); + } + +} diff --git a/App/Modules/Cyan/API/V1/Controllers/ProcessorController.cs b/App/Modules/Cyan/API/V1/Controllers/ProcessorController.cs new file mode 100644 index 0000000..4a26533 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Controllers/ProcessorController.cs @@ -0,0 +1,258 @@ +using System.Net.Mime; +using App.Error.V1; +using App.Modules.Common; +using App.Modules.Cyan.API.V1.Mappers; +using App.Modules.Cyan.API.V1.Models; +using App.Modules.Cyan.API.V1.Validators; +using App.StartUp.Registry; +using App.Utility; +using Asp.Versioning; +using CSharp_Result; +using Domain.Model; +using Domain.Service; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace App.Modules.Cyan.API.V1.Controllers; + +/// +/// V1 controller +/// +[ApiVersion(1.0)] +[ApiController] +[Consumes(MediaTypeNames.Application.Json)] +[Route("api/v{version:apiVersion}/[controller]")] +public class ProcessorController : AtomiControllerBase +{ + private readonly IProcessorService _service; + private readonly IUserService _userService; + private readonly CreateProcessorReqValidator _createProcessorReqValidator; + private readonly UpdateProcessorReqValidator _updateProcessorReqValidator; + private readonly SearchProcessorQueryValidator _searchProcessorQueryValidator; + private readonly CreateProcessorVersionReqValidator _createProcessorVersionReqValidator; + private readonly UpdateProcessorVersionReqValidator _updateProcessorVersionReqValidator; + private readonly SearchProcessorVersionQueryValidator _searchProcessorVersionQueryValidator; + + + public ProcessorController(IProcessorService service, + CreateProcessorReqValidator createProcessorReqValidator, UpdateProcessorReqValidator updateProcessorReqValidator, + SearchProcessorQueryValidator searchProcessorQueryValidator, + CreateProcessorVersionReqValidator createProcessorVersionReqValidator, + UpdateProcessorVersionReqValidator updateProcessorVersionReqValidator, + SearchProcessorVersionQueryValidator searchProcessorVersionQueryValidator, IUserService userService) + { + this._service = service; + this._createProcessorReqValidator = createProcessorReqValidator; + this._updateProcessorReqValidator = updateProcessorReqValidator; + this._searchProcessorQueryValidator = searchProcessorQueryValidator; + this._createProcessorVersionReqValidator = createProcessorVersionReqValidator; + this._updateProcessorVersionReqValidator = updateProcessorVersionReqValidator; + this._searchProcessorVersionQueryValidator = searchProcessorVersionQueryValidator; + this._userService = userService; + } + + [HttpGet] + public async Task>> Search([FromQuery] SearchProcessorQuery query) + { + var processors = await this._searchProcessorQueryValidator + .ValidateAsyncResult(query, "Invalid SearchProcessorQuery") + .ThenAwait(x => this._service.Search(x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + + return this.ReturnResult(processors); + } + + [HttpGet("{id:guid}")] + public async Task> Get(Guid id) + { + var processor = await this._service.Get(id) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(processor, + new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), id.ToString())); + } + + [HttpGet("{username}/{name}")] + public async Task> Get(string username, string name) + { + var processor = await this._service.Get(username, name) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(processor, + new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), $"{username}/{name}")); + } + + [Authorize, HttpPost("{userId}")] + public async Task> Create(string userId, [FromBody] CreateProcessorReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a processor for this user") + .ToException(); + return this.ReturnResult(e); + } + + var processor = await this._createProcessorReqValidator + .ValidateAsyncResult(req, "Invalid CreateProcessorReq") + .ThenAwait(x => this._service.Create(userId, x.ToDomain().Item1, x.ToDomain().Item2)) + .Then(x => x.ToResp(), Errors.MapAll); + return this.ReturnResult(processor); + } + + + [Authorize, HttpPut("{userId}/{processorId}")] + public async Task> Update(string userId, Guid processorId, + [FromBody] UpdateProcessorReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a processor for this user") + .ToException(); + return this.ReturnResult(e); + } + + var processor = await this._updateProcessorReqValidator + .ValidateAsyncResult(req, "Invalid UpdateProcessorReq") + .ThenAwait(x => this._service.Update(userId, processorId, x.ToDomain())) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(processor, new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), processorId.ToString())); + } + + [Authorize, HttpPost("{username}/{processorName}/like/{likerId}/{like}")] + public async Task> Like(string username, string processorName, string likerId, bool like) + { + var sub = this.Sub(); + if (sub != likerId) + { + Result e = new Unauthorized("You are not authorized to like this processor") + .ToException(); + return this.ReturnResult(e); + } + + var processor = await this._service.Like(likerId, username, processorName, like) + .Then(x => x.ToResult()); + return this.ReturnUnitNullableResult(processor, + new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), $"{username}/{processorName}")); + } + + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpDelete("{userId}/{processorId:guid}")] + public async Task> Delete(string userId, Guid processorId) + { + var processor = await this._service.Delete(userId, processorId) + .Then(x => x.ToResult()); + return this.ReturnUnitNullableResult(processor, + new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), $"{userId}/{processorId}")); + } + + [HttpGet("{username}/{processorName}/versions")] + public async Task>> SearchVersion(string username, + string processorName, [FromQuery] SearchProcessorVersionQuery query) + { + var processors = await this._searchProcessorVersionQueryValidator + .ValidateAsyncResult(query, "Invalid SearchProcessorVersionQuery") + .ThenAwait(x => this._service.SearchVersion(username, processorName, x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + return this.ReturnResult(processors); + } + + [HttpGet("{userId}/{processorId:guid}/versions")] + public async Task>> SearchVersion(string userId, Guid processorId, + [FromQuery] SearchProcessorVersionQuery query) + { + var processors = await this._searchProcessorVersionQueryValidator + .ValidateAsyncResult(query, "Invalid SearchProcessorVersionQuery") + .ThenAwait(x => this._service.SearchVersion(userId, processorId, x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + return this.ReturnResult(processors); + } + + [HttpGet("{username}/{processorName}/versions/{ver}")] + public async Task> GetVersion(string username, string processorName, ulong ver, + bool bumpDownload) + { + var processor = await this._service.GetVersion(username, processorName, ver, bumpDownload) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(processor, + new EntityNotFound("Processor not found", typeof(ProcessorVersionPrincipal), $"{username}/{processorName}:{ver}")); + } + + [HttpGet("{userId}/{processorId:guid}/versions/{ver}")] + public async Task> GetVersion(string userId, Guid processorId, ulong ver) + { + var processor = await this._service.GetVersion(userId, processorId, ver) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(processor, + new EntityNotFound("Processor not found", typeof(ProcessorVersionPrincipal), $"{userId}/{processorId}:{ver}")); + } + + [Authorize, HttpPost("{username}/{processorName}/versions")] + public async Task> CreateVersion(string username, string processorName, + [FromBody] CreateProcessorVersionReq req) + { + var sub = this.Sub(); + var version = await this._userService + .GetByUsername(username) + .ThenAwait(x => Task.FromResult(x?.Principal.Id == sub), Errors.MapAll) + .ThenAwait(async x => + { + if (x) + { + return await this._createProcessorVersionReqValidator + .ValidateAsyncResult(req, "Invalid CreateProcessorVersionReq") + .ThenAwait(c => this._service.CreateVersion(username, processorName, c.ToDomain().Item2, c.ToDomain().Item1)) + .Then(c => c?.ToResp(), Errors.MapAll); + } + + return new Unauthorized("You are not authorized to create a processor for this user") + .ToException(); + }); + return this.ReturnNullableResult(version, + new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), $"{username}/{processorName}")); + } + + [Authorize, HttpPost("{userId}/{processorId:guid}/versions")] + public async Task> CreateVersion(string userId, Guid processorId, + [FromBody] CreateProcessorVersionReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a processor for this user") + .ToException(); + return this.ReturnResult(e); + } + + var version = await this._createProcessorVersionReqValidator + .ValidateAsyncResult(req, "Invalid CreateProcessorVersionReq") + .ThenAwait(x => this._service.CreateVersion(userId, processorId, x.ToDomain().Item2, x.ToDomain().Item1)) + .Then(x => x?.ToResp(), Errors.MapAll); + + return this.ReturnNullableResult(version, + new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), $"{userId}/{processorId}")); + } + + [Authorize, HttpPut("{userId}/{processorId:guid}/versions/{ver}")] + public async Task> UpdateVersion(string userId, Guid processorId, ulong ver, + [FromBody] UpdateProcessorVersionReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a processor for this user") + .ToException(); + return this.ReturnResult(e); + } + + var version = await this._updateProcessorVersionReqValidator + .ValidateAsyncResult(req, "Invalid UpdateProcessorVersionReq") + .ThenAwait(x => this._service.UpdateVersion(userId, processorId, ver, x.ToDomain())) + .Then(x => x?.ToResp(), Errors.MapAll); + + return this.ReturnNullableResult(version, + new EntityNotFound("Processor not found", typeof(ProcessorPrincipal), $"{userId}/{processorId}")); + } + +} diff --git a/App/Modules/Cyan/API/V1/Controllers/TemplateController.cs b/App/Modules/Cyan/API/V1/Controllers/TemplateController.cs new file mode 100644 index 0000000..fa653b7 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Controllers/TemplateController.cs @@ -0,0 +1,267 @@ +using System.Net.Mime; +using App.Error.V1; +using App.Modules.Common; +using App.Modules.Cyan.API.V1.Mappers; +using App.Modules.Cyan.API.V1.Models; +using App.Modules.Cyan.API.V1.Validators; +using App.StartUp.Registry; +using App.Utility; +using Asp.Versioning; +using CSharp_Result; +using Domain.Model; +using Domain.Service; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace App.Modules.Cyan.API.V1.Controllers; + +/// +/// V1 controller +/// +[ApiVersion(1.0)] +[ApiController] +[Consumes(MediaTypeNames.Application.Json)] +[Route("api/v{version:apiVersion}/[controller]")] +public class TemplateController : AtomiControllerBase +{ + private readonly ITemplateService _service; + private readonly IUserService _userService; + private readonly CreateTemplateReqValidator _createTemplateReqValidator; + private readonly UpdateTemplateReqValidator _updateTemplateReqValidator; + private readonly SearchTemplateQueryValidator _searchTemplateQueryValidator; + private readonly CreateTemplateVersionReqValidator _createTemplateVersionReqValidator; + private readonly UpdateTemplateVersionReqValidator _updateTemplateVersionReqValidator; + private readonly SearchTemplateVersionQueryValidator _searchTemplateVersionQueryValidator; + + + public TemplateController(ITemplateService service, + CreateTemplateReqValidator createTemplateReqValidator, UpdateTemplateReqValidator updateTemplateReqValidator, + SearchTemplateQueryValidator searchTemplateQueryValidator, + CreateTemplateVersionReqValidator createTemplateVersionReqValidator, + UpdateTemplateVersionReqValidator updateTemplateVersionReqValidator, + SearchTemplateVersionQueryValidator searchTemplateVersionQueryValidator, IUserService userService) + { + this._service = service; + this._createTemplateReqValidator = createTemplateReqValidator; + this._updateTemplateReqValidator = updateTemplateReqValidator; + this._searchTemplateQueryValidator = searchTemplateQueryValidator; + this._createTemplateVersionReqValidator = createTemplateVersionReqValidator; + this._updateTemplateVersionReqValidator = updateTemplateVersionReqValidator; + this._searchTemplateVersionQueryValidator = searchTemplateVersionQueryValidator; + this._userService = userService; + } + + [HttpGet] + public async Task>> Search([FromQuery] SearchTemplateQuery query) + { + var templates = await this._searchTemplateQueryValidator + .ValidateAsyncResult(query, "Invalid SearchTemplateQuery") + .ThenAwait(x => this._service.Search(x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + + return this.ReturnResult(templates); + } + + [HttpGet("{id:guid}")] + public async Task> Get(Guid id) + { + var template = await this._service.Get(id) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(template, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), id.ToString())); + } + + [HttpGet("{username}/{name}")] + public async Task> Get(string username, string name) + { + var template = await this._service.Get(username, name) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(template, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), $"{username}/{name}")); + } + + [Authorize, HttpPost("{userId}")] + public async Task> Create(string userId, [FromBody] CreateTemplateReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a template for this user") + .ToException(); + return this.ReturnResult(e); + } + + var template = await this._createTemplateReqValidator + .ValidateAsyncResult(req, "Invalid CreateTemplateReq") + .ThenAwait(x => this._service.Create(userId, x.ToDomain().Item1, x.ToDomain().Item2)) + .Then(x => x.ToResp(), Errors.MapAll); + return this.ReturnResult(template); + } + + + [Authorize, HttpPut("{userId}/{templateId:guid}")] + public async Task> Update(string userId, Guid templateId, + [FromBody] UpdateTemplateReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = new Unauthorized("You are not authorized to create a template for this user") + .ToException(); + return this.ReturnResult(e); + } + + var template = await this._updateTemplateReqValidator + .ValidateAsyncResult(req, "Invalid UpdateTemplateReq") + .ThenAwait(x => this._service.Update(userId, templateId, x.ToDomain())) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(template, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), templateId.ToString())); + } + + [Authorize, HttpPost("{username}/{templateName}/like/{likerId}/{like:bool}")] + public async Task> Like(string username, string templateName, string likerId, bool like) + { + var sub = this.Sub(); + if (sub != likerId) + { + Result e = new Unauthorized("You are not authorized to like this template") + .ToException(); + return this.ReturnResult(e); + } + + var template = await this._service.Like(likerId, username, templateName, like) + .Then(x => x.ToResult()); + return this.ReturnUnitNullableResult(template, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), $"{username}/{templateName}")); + } + + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpDelete("{userId}/{templateId:guid}")] + public async Task> Delete(string userId, Guid templateId) + { + var template = await this._service.Delete(userId, templateId) + .Then(x => x.ToResult()); + return this.ReturnUnitNullableResult(template, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), $"{userId}/{templateId}")); + } + + [HttpGet("{username}/{templateName}/versions")] + public async Task>> SearchVersion(string username, + string templateName, [FromQuery] SearchTemplateVersionQuery query) + { + var templates = await this._searchTemplateVersionQueryValidator + .ValidateAsyncResult(query, "Invalid SearchTemplateVersionQuery") + .ThenAwait(x => this._service.SearchVersion(username, templateName, x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + return this.ReturnResult(templates); + } + + [HttpGet("{userId}/{templateId:guid}/versions")] + public async Task>> SearchVersion(string userId, + Guid templateId, + [FromQuery] SearchTemplateVersionQuery query) + { + var templates = await this._searchTemplateVersionQueryValidator + .ValidateAsyncResult(query, "Invalid SearchTemplateVersionQuery") + .ThenAwait(x => this._service.SearchVersion(userId, templateId, x.ToDomain())) + .Then(x => x.Select(u => u.ToResp()) + .ToResult()); + return this.ReturnResult(templates); + } + + [HttpGet("{username}/{templateName}/versions/{ver}")] + public async Task> GetVersion(string username, string templateName, + ulong ver, + bool bumpDownload) + { + var template = await this._service.GetVersion(username, templateName, ver, bumpDownload) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(template, + new EntityNotFound("Template not found", typeof(TemplateVersionResp), $"{username}/{templateName}:{ver}")); + } + + [HttpGet("{userId}/{templateId:guid}/versions/{ver}")] + public async Task> GetVersion(string userId, Guid templateId, ulong ver) + { + var template = await this._service.GetVersion(userId, templateId, ver) + .Then(x => x?.ToResp(), Errors.MapAll); + return this.ReturnNullableResult(template, + new EntityNotFound("Template not found", typeof(TemplateVersionPrincipal), $"{userId}/{templateId}:{ver}")); + } + + [Authorize, HttpPost("{username}/{templateName}/versions")] + public async Task> CreateVersion(string username, string templateName, + [FromBody] CreateTemplateVersionReq req) + { + var sub = this.Sub(); + var version = await this._userService + .GetByUsername(username) + .ThenAwait(x => Task.FromResult(x?.Principal.Id == sub), Errors.MapAll) + .ThenAwait(async x => + { + if (x) + { + return await this._createTemplateVersionReqValidator + .ValidateAsyncResult(req, "Invalid CreateTemplateVersionReq") + .ThenAwait(c => this._service.CreateVersion(username, templateName, c.ToRecord(), c.ToProperty(), + c.Processors.Select(p => p.ToDomain()), + c.Plugins.Select(p => p.ToDomain()))) + .Then(c => c?.ToResp(), Errors.MapAll); + } + + return new Unauthorized("You are not authorized to create a template for this user") + .ToException(); + }); + return this.ReturnNullableResult(version, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), $"{username}/{templateName}")); + } + + [Authorize, HttpPost("{userId}/{templateId:guid}/versions")] + public async Task> CreateVersion(string userId, Guid templateId, + [FromBody] CreateTemplateVersionReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = + new Unauthorized("You are not authorized to create a template for this user") + .ToException(); + return this.ReturnResult(e); + } + + var version = await this._createTemplateVersionReqValidator + .ValidateAsyncResult(req, "Invalid CreateTemplateVersionReq") + .ThenAwait(c => this._service.CreateVersion(userId, templateId, c.ToRecord(), c.ToProperty(), + c.Processors.Select(p => p.ToDomain()), + c.Plugins.Select(p => p.ToDomain()) + )) + .Then(x => x?.ToResp(), Errors.MapAll); + + return this.ReturnNullableResult(version, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), $"{userId}/{templateId}")); + } + + [Authorize, HttpPut("{userId}/{templateId:guid}/versions/{ver}")] + public async Task> UpdateVersion(string userId, Guid templateId, ulong ver, + [FromBody] UpdateTemplateVersionReq req) + { + var sub = this.Sub(); + if (sub != userId) + { + Result e = + new Unauthorized("You are not authorized to create a template for this user") + .ToException(); + return this.ReturnResult(e); + } + + var version = await this._updateTemplateVersionReqValidator + .ValidateAsyncResult(req, "Invalid UpdateTemplateVersionReq") + .ThenAwait(x => this._service.UpdateVersion(userId, templateId, ver, x.ToDomain())) + .Then(x => x?.ToResp(), Errors.MapAll); + + return this.ReturnNullableResult(version, + new EntityNotFound("Template not found", typeof(TemplatePrincipal), $"{userId}/{templateId}")); + } +} diff --git a/App/Modules/Cyan/API/V1/Mappers/PluginMapper.cs b/App/Modules/Cyan/API/V1/Mappers/PluginMapper.cs new file mode 100644 index 0000000..8c436dd --- /dev/null +++ b/App/Modules/Cyan/API/V1/Mappers/PluginMapper.cs @@ -0,0 +1,76 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Modules.Users.API.V1; +using Domain.Model; + +namespace App.Modules.Cyan.API.V1.Mappers; + +public static class PluginMapper +{ + public static (PluginRecord, PluginMetadata) ToDomain(this CreatePluginReq req) => + (new PluginRecord { Name = req.Name }, + new PluginMetadata + { + Project = req.Project, + Source = req.Source, + Email = req.Email, + Tags = req.Tags, + Description = req.Description, + Readme = req.Readme + }); + + public static PluginMetadata ToDomain(this UpdatePluginReq req) => + new() + { + Project = req.Project, + Source = req.Source, + Email = req.Email, + Tags = req.Tags, + Description = req.Description, + Readme = req.Readme + }; + + public static PluginSearch ToDomain(this SearchPluginQuery query) => + new() + { + Owner = query.Owner, + Search = query.Search, + Limit = query.Limit ?? 20, + Skip = query.Skip ?? 0, + }; + + public static PluginInfoResp ToResp(this PluginInfo info) => new(info.Downloads, info.Dependencies, info.Stars); + + public static PluginPrincipalResp ToResp(this PluginPrincipal principal) => + new(principal.Id, principal.Record.Name, principal.Metadata.Project, + principal.Metadata.Source, principal.Metadata.Email, principal.Metadata.Tags, + principal.Metadata.Description, principal.Metadata.Readme); + + public static PluginResp ToResp(this Plugin plugin) => + new(plugin.Principal.ToResp(), plugin.Info.ToResp(), plugin.User.ToResp(), + plugin.Versions.Select(v => v.ToResp())); +} + +public static class PluginVersionMapper +{ + public static (PluginVersionProperty, PluginVersionRecord) ToDomain(this CreatePluginVersionReq req) => + (new PluginVersionProperty { DockerReference = req.DockerReference, DockerSha = req.DockerSha }, + new PluginVersionRecord { Description = req.Description }); + + public static PluginVersionRecord ToDomain(this UpdatePluginVersionReq req) => new() { Description = req.Description }; + + public static PluginVersionSearch ToDomain(this SearchPluginVersionQuery query) => + new() + { + Search = query.Search, + Limit = query.Limit ?? 20, + Skip = query.Skip ?? 0, + }; + + public static PluginVersionPrincipalResp ToResp(this PluginVersionPrincipal principal) => + new(principal.Id, principal.Version, principal.CreatedAt, + principal.Record.Description, principal.Property.DockerReference, + principal.Property.DockerSha); + + public static PluginVersionResp ToResp(this PluginVersion version) => + new(version.Principal.ToResp(), version.PluginPrincipal.ToResp()); +} diff --git a/App/Modules/Cyan/API/V1/Mappers/ProcessorMapper.cs b/App/Modules/Cyan/API/V1/Mappers/ProcessorMapper.cs new file mode 100644 index 0000000..f77b517 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Mappers/ProcessorMapper.cs @@ -0,0 +1,76 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Modules.Users.API.V1; +using Domain.Model; + +namespace App.Modules.Cyan.API.V1.Mappers; + +public static class ProcessorMapper +{ + public static (ProcessorRecord, ProcessorMetadata) ToDomain(this CreateProcessorReq req) => + (new ProcessorRecord { Name = req.Name }, + new ProcessorMetadata + { + Project = req.Project, + Source = req.Source, + Email = req.Email, + Tags = req.Tags, + Description = req.Description, + Readme = req.Readme + }); + + public static ProcessorMetadata ToDomain(this UpdateProcessorReq req) => + new() + { + Project = req.Project, + Source = req.Source, + Email = req.Email, + Tags = req.Tags, + Description = req.Description, + Readme = req.Readme + }; + + public static ProcessorSearch ToDomain(this SearchProcessorQuery query) => + new() + { + Owner = query.Owner, + Search = query.Search, + Limit = query.Limit ?? 20, + Skip = query.Skip ?? 0, + }; + + public static ProcessorInfoResp ToResp(this ProcessorInfo info) => new(info.Downloads, info.Dependencies, info.Stars); + + public static ProcessorPrincipalResp ToResp(this ProcessorPrincipal principal) => + new(principal.Id, principal.Record.Name, principal.Metadata.Project, + principal.Metadata.Source, principal.Metadata.Email, principal.Metadata.Tags, + principal.Metadata.Description, principal.Metadata.Readme); + + public static ProcessorResp ToResp(this Processor processor) => + new(processor.Principal.ToResp(), processor.Info.ToResp(), processor.User.ToResp(), + processor.Versions.Select(v => v.ToResp())); +} + +public static class ProcessorVersionMapper +{ + public static (ProcessorVersionProperty, ProcessorVersionRecord) ToDomain(this CreateProcessorVersionReq req) => + (new ProcessorVersionProperty { DockerReference = req.DockerReference, DockerSha = req.DockerSha }, + new ProcessorVersionRecord { Description = req.Description }); + + public static ProcessorVersionRecord ToDomain(this UpdateProcessorVersionReq req) => new() { Description = req.Description }; + + public static ProcessorVersionSearch ToDomain(this SearchProcessorVersionQuery query) => + new() + { + Search = query.Search, + Limit = query.Limit ?? 20, + Skip = query.Skip ?? 0, + }; + + public static ProcessorVersionPrincipalResp ToResp(this ProcessorVersionPrincipal principal) => + new(principal.Id, principal.Version, principal.CreatedAt, + principal.Record.Description, principal.Property.DockerReference, + principal.Property.DockerSha); + + public static ProcessorVersionResp ToResp(this ProcessorVersion version) => + new(version.Principal.ToResp(), version.ProcessorPrincipal.ToResp()); +} diff --git a/App/Modules/Cyan/API/V1/Mappers/TemplateMapper.cs b/App/Modules/Cyan/API/V1/Mappers/TemplateMapper.cs new file mode 100644 index 0000000..076e7f6 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Mappers/TemplateMapper.cs @@ -0,0 +1,87 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Modules.Users.API.V1; +using Domain.Model; + +namespace App.Modules.Cyan.API.V1.Mappers; + +public static class TemplateMapper +{ + public static (TemplateRecord, TemplateMetadata) ToDomain(this CreateTemplateReq req) => + (new TemplateRecord { Name = req.Name }, + new TemplateMetadata + { + Project = req.Project, + Source = req.Source, + Email = req.Email, + Tags = req.Tags, + Description = req.Description, + Readme = req.Readme + }); + + public static TemplateMetadata ToDomain(this UpdateTemplateReq req) => + new() + { + Project = req.Project, + Source = req.Source, + Email = req.Email, + Tags = req.Tags, + Description = req.Description, + Readme = req.Readme + }; + + public static TemplateSearch ToDomain(this SearchTemplateQuery query) => + new() + { + Owner = query.Owner, + Search = query.Search, + Limit = query.Limit ?? 20, + Skip = query.Skip ?? 0, + }; + + public static TemplateInfoResp ToResp(this TemplateInfo info) => new(info.Downloads, info.Stars); + + public static TemplatePrincipalResp ToResp(this TemplatePrincipal principal) => + new(principal.Id, principal.Record.Name, principal.Metadata.Project, + principal.Metadata.Source, principal.Metadata.Email, principal.Metadata.Tags, + principal.Metadata.Description, principal.Metadata.Readme); + + public static TemplateResp ToResp(this Template template) => + new(template.Principal.ToResp(), template.Info.ToResp(), template.User.ToResp(), + template.Versions.Select(v => v.ToResp())); +} + +public static class TemplateVersionMapper +{ + public static TemplateVersionRecord ToRecord(this CreateTemplateVersionReq req) => new() { Description = req.Description }; + + public static TemplateVersionProperty ToProperty(this CreateTemplateVersionReq req) => + new() + { + BlobDockerReference = req.BlobDockerReference, + BlobDockerSha = req.BlobDockerSha, + TemplateDockerReference = req.TemplateDockerReference, + TemplateDockerSha = req.TemplateDockerSha + }; + + public static TemplateVersionRecord ToDomain(this UpdateTemplateVersionReq req) => new() { Description = req.Description }; + + public static TemplateVersionSearch ToDomain(this SearchTemplateVersionQuery query) => + new() { Search = query.Search, Limit = query.Limit ?? 20, Skip = query.Skip ?? 0, }; + + public static PluginVersionRef ToDomain(this PluginReferenceReq req) => new(req.Username, req.Name, req.Version); + + public static ProcessorVersionRef ToDomain(this ProcessorReferenceReq req) => new(req.Username, req.Name, req.Version); + + public static TemplateVersionPrincipalResp ToResp(this TemplateVersionPrincipal principal) => + new(principal.Id, principal.Version, principal.CreatedAt, + principal.Record.Description, principal.Property.BlobDockerReference, + principal.Property.BlobDockerSha, principal.Property.TemplateDockerReference, + principal.Property.TemplateDockerSha); + + public static TemplateVersionResp ToResp(this TemplateVersion version) => + new( + version.Principal.ToResp(), version.TemplatePrincipal.ToResp(), + version.Plugins.Select(x => x.ToResp()), + version.Processors.Select(x => x.ToResp()) + ); +} diff --git a/App/Modules/Cyan/API/V1/Models/PluginModel.cs b/App/Modules/Cyan/API/V1/Models/PluginModel.cs new file mode 100644 index 0000000..bbbf510 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Models/PluginModel.cs @@ -0,0 +1,25 @@ +using App.Modules.Users.API.V1; + +namespace App.Modules.Cyan.API.V1.Models; + +public record SearchPluginQuery(string? Owner, string? Search, int? Limit, int? Skip); + +public record CreatePluginReq(string Name, string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record UpdatePluginReq(string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record PluginPrincipalResp( + Guid Id, string Name, string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record PluginInfoResp( + uint Downloads, uint Dependencies, uint Stars); + +public record PluginResp( + PluginPrincipalResp Principal, PluginInfoResp Info, + UserPrincipalResp User, + IEnumerable Versions); + + diff --git a/App/Modules/Cyan/API/V1/Models/PluginVersionModel.cs b/App/Modules/Cyan/API/V1/Models/PluginVersionModel.cs new file mode 100644 index 0000000..f609d29 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Models/PluginVersionModel.cs @@ -0,0 +1,16 @@ +namespace App.Modules.Cyan.API.V1.Models; + +public record SearchPluginVersionQuery(string? Search, int? Limit, int? Skip); + +public record CreatePluginVersionReq( + string Description, string DockerReference, string DockerSha); + +public record UpdatePluginVersionReq(string Description); + +public record PluginVersionPrincipalResp( + Guid Id, ulong Version, DateTime CreatedAt, + string Description, string DockerReference, string DockerSha); + +public record PluginVersionResp( + PluginVersionPrincipalResp Principal, + PluginPrincipalResp Plugin); diff --git a/App/Modules/Cyan/API/V1/Models/ProcessorModel.cs b/App/Modules/Cyan/API/V1/Models/ProcessorModel.cs new file mode 100644 index 0000000..c1f1e02 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Models/ProcessorModel.cs @@ -0,0 +1,25 @@ +using App.Modules.Users.API.V1; + +namespace App.Modules.Cyan.API.V1.Models; + +public record SearchProcessorQuery(string? Owner, string? Search, int? Limit, int? Skip); + +public record CreateProcessorReq(string Name, string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record UpdateProcessorReq(string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record ProcessorPrincipalResp( + Guid Id, string Name, string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record ProcessorInfoResp( + uint Downloads, uint Dependencies, uint Stars); + +public record ProcessorResp( + ProcessorPrincipalResp Principal, ProcessorInfoResp Info, + UserPrincipalResp User, + IEnumerable Versions); + + diff --git a/App/Modules/Cyan/API/V1/Models/ProcessorVersionModel.cs b/App/Modules/Cyan/API/V1/Models/ProcessorVersionModel.cs new file mode 100644 index 0000000..6d14937 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Models/ProcessorVersionModel.cs @@ -0,0 +1,16 @@ +namespace App.Modules.Cyan.API.V1.Models; + +public record SearchProcessorVersionQuery(string? Search, int? Limit, int? Skip); + +public record CreateProcessorVersionReq( + string Description, string DockerReference, string DockerSha); + +public record UpdateProcessorVersionReq(string Description); + +public record ProcessorVersionPrincipalResp( + Guid Id, ulong Version, DateTime CreatedAt, + string Description, string DockerReference, string DockerSha); + +public record ProcessorVersionResp( + ProcessorVersionPrincipalResp Principal, + ProcessorPrincipalResp Processor); diff --git a/App/Modules/Cyan/API/V1/Models/TemplateModel.cs b/App/Modules/Cyan/API/V1/Models/TemplateModel.cs new file mode 100644 index 0000000..0c85849 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Models/TemplateModel.cs @@ -0,0 +1,25 @@ +using App.Modules.Users.API.V1; + +namespace App.Modules.Cyan.API.V1.Models; + +public record SearchTemplateQuery(string? Owner, string? Search, int? Limit, int? Skip); + +public record CreateTemplateReq(string Name, string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record UpdateTemplateReq(string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record TemplatePrincipalResp( + Guid Id, string Name, string Project, string Source, + string Email, string[] Tags, string Description, string Readme); + +public record TemplateInfoResp( + uint Downloads, uint Stars); + +public record TemplateResp( + TemplatePrincipalResp Principal, TemplateInfoResp Info, + UserPrincipalResp User, + IEnumerable Versions); + + diff --git a/App/Modules/Cyan/API/V1/Models/TemplateVersionModel.cs b/App/Modules/Cyan/API/V1/Models/TemplateVersionModel.cs new file mode 100644 index 0000000..fa5f708 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Models/TemplateVersionModel.cs @@ -0,0 +1,31 @@ +namespace App.Modules.Cyan.API.V1.Models; + +public record SearchTemplateVersionQuery(string? Search, int? Limit, int? Skip); + +public record CreateTemplateVersionReq( + string Description, + string BlobDockerReference, + string BlobDockerSha, + string TemplateDockerReference, + string TemplateDockerSha, + PluginReferenceReq[] Plugins, + ProcessorReferenceReq[] Processors +); + +public record PluginReferenceReq(string Username, string Name, uint Version); +public record ProcessorReferenceReq(string Username, string Name, uint Version); + +public record UpdateTemplateVersionReq(string Description); + +public record TemplateVersionPrincipalResp( + Guid Id, ulong Version, DateTime CreatedAt, + string Description, + string BlobDockerReference, string BlobDockerSha, + string TemplateDockerReference, string TemplateDockerSha +); + +public record TemplateVersionResp( + TemplateVersionPrincipalResp Principal, + TemplatePrincipalResp Template, + IEnumerable Plugins, + IEnumerable Processors); diff --git a/App/Modules/Cyan/API/V1/Validators/PluginValidator.cs b/App/Modules/Cyan/API/V1/Validators/PluginValidator.cs new file mode 100644 index 0000000..107102e --- /dev/null +++ b/App/Modules/Cyan/API/V1/Validators/PluginValidator.cs @@ -0,0 +1,61 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Utility; +using FluentValidation; + +namespace App.Modules.Cyan.API.V1.Validators; + +public class SearchPluginQueryValidator : AbstractValidator +{ + public SearchPluginQueryValidator() + { + this.RuleFor(x => x.Skip).Skip(); + this.RuleFor(x => x.Limit).Limit(); + } +} + +public class CreatePluginReqValidator : AbstractValidator +{ + public CreatePluginReqValidator() + { + this.RuleFor(x => x.Name) + .NotNull() + .UsernameValid(); + this.RuleFor(x => x.Project) + .UrlValid(); + this.RuleFor(x => x.Source) + .UrlValid(); + this.RuleFor(x => x.Email) + .EmailAddress(); + this.RuleForEach(x => x.Tags) + .UsernameValid() + .NotNull(); + this.RuleFor(x => x.Tags) + .NotNull(); + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.Readme) + .NotNull(); + } +} + +public class UpdatePluginReqValidator : AbstractValidator +{ + public UpdatePluginReqValidator() + { + this.RuleFor(x => x.Project) + .UrlValid(); + this.RuleFor(x => x.Source) + .UrlValid(); + this.RuleFor(x => x.Email) + .EmailAddress(); + this.RuleForEach(x => x.Tags) + .UsernameValid() + .NotNull(); + this.RuleFor(x => x.Tags) + .NotNull(); + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.Readme) + .NotNull(); + } +} diff --git a/App/Modules/Cyan/API/V1/Validators/PluginVersionValidator.cs b/App/Modules/Cyan/API/V1/Validators/PluginVersionValidator.cs new file mode 100644 index 0000000..3efe229 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Validators/PluginVersionValidator.cs @@ -0,0 +1,36 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Utility; +using FluentValidation; + +namespace App.Modules.Cyan.API.V1.Validators; + +public class SearchPluginVersionQueryValidator : AbstractValidator +{ + public SearchPluginVersionQueryValidator() + { + this.RuleFor(x => x.Skip).Skip(); + this.RuleFor(x => x.Limit).Limit(); + } +} + +public class CreatePluginVersionReqValidator : AbstractValidator +{ + public CreatePluginVersionReqValidator() + { + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.DockerReference) + .DockerReferenceValid(); + this.RuleFor(x => x.DockerSha) + .ShaValid(); + } +} + +public class UpdatePluginVersionReqValidator : AbstractValidator +{ + public UpdatePluginVersionReqValidator() + { + this.RuleFor(x => x.Description) + .DescriptionValid(); + } +} diff --git a/App/Modules/Cyan/API/V1/Validators/ProcessorValidator.cs b/App/Modules/Cyan/API/V1/Validators/ProcessorValidator.cs new file mode 100644 index 0000000..7d00e5b --- /dev/null +++ b/App/Modules/Cyan/API/V1/Validators/ProcessorValidator.cs @@ -0,0 +1,61 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Utility; +using FluentValidation; + +namespace App.Modules.Cyan.API.V1.Validators; + +public class SearchProcessorQueryValidator : AbstractValidator +{ + public SearchProcessorQueryValidator() + { + this.RuleFor(x => x.Skip).Skip(); + this.RuleFor(x => x.Limit).Limit(); + } +} + +public class CreateProcessorReqValidator : AbstractValidator +{ + public CreateProcessorReqValidator() + { + this.RuleFor(x => x.Name) + .NotNull() + .UsernameValid(); + this.RuleFor(x => x.Project) + .UrlValid(); + this.RuleFor(x => x.Source) + .UrlValid(); + this.RuleFor(x => x.Email) + .EmailAddress(); + this.RuleForEach(x => x.Tags) + .UsernameValid() + .NotNull(); + this.RuleFor(x => x.Tags) + .NotNull(); + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.Readme) + .NotNull(); + } +} + +public class UpdateProcessorReqValidator : AbstractValidator +{ + public UpdateProcessorReqValidator() + { + this.RuleFor(x => x.Project) + .UrlValid(); + this.RuleFor(x => x.Source) + .UrlValid(); + this.RuleFor(x => x.Email) + .EmailAddress(); + this.RuleForEach(x => x.Tags) + .UsernameValid() + .NotNull(); + this.RuleFor(x => x.Tags) + .NotNull(); + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.Readme) + .NotNull(); + } +} diff --git a/App/Modules/Cyan/API/V1/Validators/ProcessorVersionValidator.cs b/App/Modules/Cyan/API/V1/Validators/ProcessorVersionValidator.cs new file mode 100644 index 0000000..808e98a --- /dev/null +++ b/App/Modules/Cyan/API/V1/Validators/ProcessorVersionValidator.cs @@ -0,0 +1,36 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Utility; +using FluentValidation; + +namespace App.Modules.Cyan.API.V1.Validators; + +public class SearchProcessorVersionQueryValidator : AbstractValidator +{ + public SearchProcessorVersionQueryValidator() + { + this.RuleFor(x => x.Skip).Skip(); + this.RuleFor(x => x.Limit).Limit(); + } +} + +public class CreateProcessorVersionReqValidator : AbstractValidator +{ + public CreateProcessorVersionReqValidator() + { + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.DockerReference) + .DockerReferenceValid(); + this.RuleFor(x => x.DockerSha) + .ShaValid(); + } +} + +public class UpdateProcessorVersionReqValidator : AbstractValidator +{ + public UpdateProcessorVersionReqValidator() + { + this.RuleFor(x => x.Description) + .DescriptionValid(); + } +} diff --git a/App/Modules/Cyan/API/V1/Validators/TemplateValidator.cs b/App/Modules/Cyan/API/V1/Validators/TemplateValidator.cs new file mode 100644 index 0000000..3655e6f --- /dev/null +++ b/App/Modules/Cyan/API/V1/Validators/TemplateValidator.cs @@ -0,0 +1,61 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Utility; +using FluentValidation; + +namespace App.Modules.Cyan.API.V1.Validators; + +public class SearchTemplateQueryValidator : AbstractValidator +{ + public SearchTemplateQueryValidator() + { + this.RuleFor(x => x.Skip).Skip(); + this.RuleFor(x => x.Limit).Limit(); + } +} + +public class CreateTemplateReqValidator : AbstractValidator +{ + public CreateTemplateReqValidator() + { + this.RuleFor(x => x.Name) + .NotNull() + .UsernameValid(); + this.RuleFor(x => x.Project) + .UrlValid(); + this.RuleFor(x => x.Source) + .UrlValid(); + this.RuleFor(x => x.Email) + .EmailAddress(); + this.RuleForEach(x => x.Tags) + .UsernameValid() + .NotNull(); + this.RuleFor(x => x.Tags) + .NotNull(); + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.Readme) + .NotNull(); + } +} + +public class UpdateTemplateReqValidator : AbstractValidator +{ + public UpdateTemplateReqValidator() + { + this.RuleFor(x => x.Project) + .UrlValid(); + this.RuleFor(x => x.Source) + .UrlValid(); + this.RuleFor(x => x.Email) + .EmailAddress(); + this.RuleForEach(x => x.Tags) + .UsernameValid() + .NotNull(); + this.RuleFor(x => x.Tags) + .NotNull(); + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.Readme) + .NotNull(); + } +} diff --git a/App/Modules/Cyan/API/V1/Validators/TemplateVersionValidator.cs b/App/Modules/Cyan/API/V1/Validators/TemplateVersionValidator.cs new file mode 100644 index 0000000..d2bca24 --- /dev/null +++ b/App/Modules/Cyan/API/V1/Validators/TemplateVersionValidator.cs @@ -0,0 +1,40 @@ +using App.Modules.Cyan.API.V1.Models; +using App.Utility; +using FluentValidation; + +namespace App.Modules.Cyan.API.V1.Validators; + +public class SearchTemplateVersionQueryValidator : AbstractValidator +{ + public SearchTemplateVersionQueryValidator() + { + this.RuleFor(x => x.Skip).Skip(); + this.RuleFor(x => x.Limit).Limit(); + } +} + +public class CreateTemplateVersionReqValidator : AbstractValidator +{ + public CreateTemplateVersionReqValidator() + { + this.RuleFor(x => x.Description) + .DescriptionValid(); + this.RuleFor(x => x.BlobDockerReference) + .DockerReferenceValid(); + this.RuleFor(x => x.BlobDockerSha) + .ShaValid(); + this.RuleFor(x => x.TemplateDockerReference) + .DockerReferenceValid(); + this.RuleFor(x => x.TemplateDockerSha) + .ShaValid(); + } +} + +public class UpdateTemplateVersionReqValidator : AbstractValidator +{ + public UpdateTemplateVersionReqValidator() + { + this.RuleFor(x => x.Description) + .DescriptionValid(); + } +} diff --git a/App/Modules/Cyan/Data/Mappers/PluginMapper.cs b/App/Modules/Cyan/Data/Mappers/PluginMapper.cs new file mode 100644 index 0000000..34480cd --- /dev/null +++ b/App/Modules/Cyan/Data/Mappers/PluginMapper.cs @@ -0,0 +1,76 @@ +using App.Modules.Cyan.Data.Models; +using App.Modules.Users.Data; +using Domain.Model; + +namespace App.Modules.Cyan.Data.Mappers; + +public static class PluginMapper +{ + public static PluginData HydrateData(this PluginData data, PluginMetadata metadata) => + data with + { + Project = metadata.Project, + Source = metadata.Source, + Email = metadata.Email, + Tags = metadata.Tags, + Description = metadata.Description, + Readme = metadata.Readme, + }; + + public static PluginData HydrateData(this PluginData data, PluginRecord record) => + data with { Name = record.Name }; + + public static PluginMetadata ToMetadata(this PluginData data) => + new() + { + Project = data.Project, + Source = data.Source, + Email = data.Email, + Tags = data.Tags, + Description = data.Description, + Readme = data.Readme, + }; + + public static PluginRecord ToRecord(this PluginData data) => + new() { Name = data.Name }; + + public static PluginPrincipal ToPrincipal(this PluginData data) => + new() { Id = data.Id, Metadata = data.ToMetadata(), Record = data.ToRecord(), }; + + public static Plugin ToDomain(this PluginData data, PluginInfo info) => + new() + { + Principal = data.ToPrincipal(), + User = data.User.ToPrincipal(), + Versions = data.Versions.Select(x => x.ToPrincipal()).ToList(), + Info = info + }; +} + +public static class PluginVersionMapper +{ + public static PluginVersionData HydrateData(this PluginVersionData data, PluginVersionRecord record) => + data with { Description = record.Description, }; + + public static PluginVersionData HydrateData(this PluginVersionData data, PluginVersionProperty record) => + data with { DockerReference = record.DockerReference, DockerSha = record.DockerSha, }; + + public static PluginVersionProperty ToProperty(this PluginVersionData data) => + new() { DockerReference = data.DockerReference, DockerSha = data.DockerSha, }; + + public static PluginVersionRecord ToRecord(this PluginVersionData data) => + new() { Description = data.Description, }; + + public static PluginVersionPrincipal ToPrincipal(this PluginVersionData data) => + new() + { + Id = data.Id, + Version = data.Version, + CreatedAt = data.CreatedAt, + Record = data.ToRecord(), + Property = data.ToProperty(), + }; + + public static PluginVersion ToDomain(this PluginVersionData data) => + new() { Principal = data.ToPrincipal(), PluginPrincipal = data.Plugin.ToPrincipal(), }; +} diff --git a/App/Modules/Cyan/Data/Mappers/ProcessorMapper.cs b/App/Modules/Cyan/Data/Mappers/ProcessorMapper.cs new file mode 100644 index 0000000..3591588 --- /dev/null +++ b/App/Modules/Cyan/Data/Mappers/ProcessorMapper.cs @@ -0,0 +1,76 @@ +using App.Modules.Cyan.Data.Models; +using App.Modules.Users.Data; +using Domain.Model; + +namespace App.Modules.Cyan.Data.Mappers; + +public static class ProcessorMapper +{ + public static ProcessorData HydrateData(this ProcessorData data, ProcessorMetadata metadata) => + data with + { + Project = metadata.Project, + Source = metadata.Source, + Email = metadata.Email, + Tags = metadata.Tags, + Description = metadata.Description, + Readme = metadata.Readme, + }; + + public static ProcessorData HydrateData(this ProcessorData data, ProcessorRecord record) => + data with { Name = record.Name }; + + public static ProcessorMetadata ToMetadata(this ProcessorData data) => + new() + { + Project = data.Project, + Source = data.Source, + Email = data.Email, + Tags = data.Tags, + Description = data.Description, + Readme = data.Readme, + }; + + public static ProcessorRecord ToRecord(this ProcessorData data) => + new() { Name = data.Name }; + + public static ProcessorPrincipal ToPrincipal(this ProcessorData data) => + new() { Id = data.Id, Metadata = data.ToMetadata(), Record = data.ToRecord(), }; + + public static Processor ToDomain(this ProcessorData data, ProcessorInfo info) => + new() + { + Principal = data.ToPrincipal(), + User = data.User.ToPrincipal(), + Versions = data.Versions.Select(x => x.ToPrincipal()).ToList(), + Info = info, + }; +} + +public static class ProcessorVersionMapper +{ + public static ProcessorVersionData HydrateData(this ProcessorVersionData data, ProcessorVersionRecord record) => + data with { Description = record.Description, }; + + public static ProcessorVersionData HydrateData(this ProcessorVersionData data, ProcessorVersionProperty record) => + data with { DockerReference = record.DockerReference, DockerSha = record.DockerSha, }; + + public static ProcessorVersionProperty ToProperty(this ProcessorVersionData data) => + new() { DockerReference = data.DockerReference, DockerSha = data.DockerSha, }; + + public static ProcessorVersionRecord ToRecord(this ProcessorVersionData data) => + new() { Description = data.Description, }; + + public static ProcessorVersionPrincipal ToPrincipal(this ProcessorVersionData data) => + new() + { + Id = data.Id, + Version = data.Version, + CreatedAt = data.CreatedAt, + Record = data.ToRecord(), + Property = data.ToProperty(), + }; + + public static ProcessorVersion ToDomain(this ProcessorVersionData data) => + new() { Principal = data.ToPrincipal(), ProcessorPrincipal = data.Processor.ToPrincipal(), }; +} diff --git a/App/Modules/Cyan/Data/Mappers/TemplateMapper.cs b/App/Modules/Cyan/Data/Mappers/TemplateMapper.cs new file mode 100644 index 0000000..2d1dcd8 --- /dev/null +++ b/App/Modules/Cyan/Data/Mappers/TemplateMapper.cs @@ -0,0 +1,99 @@ +using App.Modules.Cyan.Data.Models; +using App.Modules.Users.Data; +using Domain.Model; + +namespace App.Modules.Cyan.Data.Mappers; + +public static class TemplateMapper +{ + public static TemplateData HydrateData(this TemplateData data, TemplateMetadata metadata) => + data with + { + Project = metadata.Project, + Source = metadata.Source, + Email = metadata.Email, + Tags = metadata.Tags, + Description = metadata.Description, + Readme = metadata.Readme, + }; + + public static TemplateData HydrateData(this TemplateData data, TemplateRecord record) => + data with { Name = record.Name }; + + public static TemplateMetadata ToMetadata(this TemplateData data) => + new() + { + Project = data.Project, + Source = data.Source, + Email = data.Email, + Tags = data.Tags, + Description = data.Description, + Readme = data.Readme, + }; + + public static TemplateRecord ToRecord(this TemplateData data) => + new() { Name = data.Name }; + + public static TemplatePrincipal ToPrincipal(this TemplateData data) => + new() + { + Id = data.Id, + Metadata = data.ToMetadata(), + Record = data.ToRecord(), + }; + + public static Template ToDomain(this TemplateData data, TemplateInfo info) => + new() + { + Principal = data.ToPrincipal(), + User = data.User.ToPrincipal(), + Versions = data.Versions.Select(x => x.ToPrincipal()).ToList(), + Info = info, + }; +} + +public static class TemplateVersionMapper +{ + public static TemplateVersionData HydrateData(this TemplateVersionData data, TemplateVersionRecord record) => + data with { Description = record.Description, }; + + public static TemplateVersionData HydrateData(this TemplateVersionData data, TemplateVersionProperty record) => + data with + { + BlobDockerReference = record.BlobDockerReference, + BlobDockerSha = record.BlobDockerSha, + TemplateDockerReference = record.TemplateDockerReference, + TemplateDockerSha = record.TemplateDockerSha, + }; + + public static TemplateVersionProperty ToProperty(this TemplateVersionData data) => + new() + { + BlobDockerReference = data.BlobDockerReference, + BlobDockerSha = data.BlobDockerSha, + TemplateDockerReference = data.TemplateDockerReference, + TemplateDockerSha = data.TemplateDockerSha, + }; + + public static TemplateVersionRecord ToRecord(this TemplateVersionData data) => + new() { Description = data.Description, }; + + public static TemplateVersionPrincipal ToPrincipal(this TemplateVersionData data) => + new() + { + Id = data.Id, + Version = data.Version, + CreatedAt = data.CreatedAt, + Record = data.ToRecord(), + Property = data.ToProperty(), + }; + + public static TemplateVersion ToDomain(this TemplateVersionData data) => + new() + { + Principal = data.ToPrincipal(), + TemplatePrincipal = data.Template.ToPrincipal(), + Plugins = data.Plugins.Select(x => x.Plugin.ToPrincipal()).ToList(), + Processors = data.Processors.Select(x => x.Processor.ToPrincipal()).ToList(), + }; +} diff --git a/App/Modules/Cyan/Data/Models/LikeData.cs b/App/Modules/Cyan/Data/Models/LikeData.cs new file mode 100644 index 0000000..aa339d2 --- /dev/null +++ b/App/Modules/Cyan/Data/Models/LikeData.cs @@ -0,0 +1,36 @@ +using App.Modules.Users.Data; + +namespace App.Modules.Cyan.Data.Models; + +public record TemplateLikeData +{ + public Guid Id { get; set; } + + public Guid TemplateId { get; set; } + public TemplateData Template { get; set; } = null!; + + public string UserId { get; set; } = string.Empty; + public UserData User { get; set; } = null!; +} + +public record PluginLikeData +{ + public Guid Id { get; set; } + + public Guid PluginId { get; set; } + public PluginData Plugin { get; set; } = null!; + + public string UserId { get; set; } = string.Empty; + public UserData User { get; set; } = null!; +} + +public record ProcessorLikeData +{ + public Guid Id { get; set; } + + public Guid ProcessorId { get; set; } + public ProcessorData Processor { get; set; } = null!; + + public string UserId { get; set; } = string.Empty; + public UserData User { get; set; } = null!; +} diff --git a/App/Modules/Cyan/Data/Models/PluginData.cs b/App/Modules/Cyan/Data/Models/PluginData.cs new file mode 100644 index 0000000..8ad8999 --- /dev/null +++ b/App/Modules/Cyan/Data/Models/PluginData.cs @@ -0,0 +1,35 @@ +using App.Modules.Users.Data; +using NpgsqlTypes; + +namespace App.Modules.Cyan.Data.Models; + +public record PluginData +{ + public Guid Id { get; set; } + + public uint Downloads { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Project { get; set; } = string.Empty; + + public string Source { get; set; } = string.Empty; + + public string Email { get; set; } = string.Empty; + + public string[] Tags { get; set; } = Array.Empty(); + + public string Description { get; set; } = string.Empty; + + public string Readme { get; set; } = string.Empty; + + public NpgsqlTsVector SearchVector { get; set; } = null!; + // Foreign Keys + public string UserId { get; set; } = string.Empty; + + public UserData User { get; set; } = null!; + + public IEnumerable Versions { get; set; } = null!; + + public IEnumerable Likes { get; set; } = null!; +} diff --git a/App/Modules/Cyan/Data/Models/PluginVersionData.cs b/App/Modules/Cyan/Data/Models/PluginVersionData.cs new file mode 100644 index 0000000..491f9cf --- /dev/null +++ b/App/Modules/Cyan/Data/Models/PluginVersionData.cs @@ -0,0 +1,24 @@ +namespace App.Modules.Cyan.Data.Models; + +public record PluginVersionData +{ + public Guid Id { get; set; } + + public ulong Version { get; set; } + + public DateTime CreatedAt { get; set; } + + public string Description { get; set; } = string.Empty; + + public string DockerReference { get; set; } = string.Empty; + + public string DockerSha { get; set; } = string.Empty; + + // Foreign Keys + public Guid PluginId { get; set; } + + public PluginData Plugin { get; set; } = null!; + + public IEnumerable Templates { get; set; } = null!; + +}; diff --git a/App/Modules/Cyan/Data/Models/ProcessorData.cs b/App/Modules/Cyan/Data/Models/ProcessorData.cs new file mode 100644 index 0000000..e99dfbc --- /dev/null +++ b/App/Modules/Cyan/Data/Models/ProcessorData.cs @@ -0,0 +1,37 @@ +using App.Modules.Users.Data; +using NpgsqlTypes; + +namespace App.Modules.Cyan.Data.Models; + +public record ProcessorData +{ + public Guid Id { get; set; } + + public uint Downloads { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Project { get; set; } = string.Empty; + + public string Source { get; set; } = string.Empty; + + public string Email { get; set; } = string.Empty; + + public string[] Tags { get; set; } = Array.Empty(); + + public string Description { get; set; } = string.Empty; + + public string Readme { get; set; } = string.Empty; + + public NpgsqlTsVector SearchVector { get; set; } = null!; + + // Foreign Keys + public string UserId { get; set; } = string.Empty; + + public UserData User { get; set; } = null!; + + public IEnumerable Versions { get; set; } = null!; + + public IEnumerable Likes { get; set; } = null!; + +} diff --git a/App/Modules/Cyan/Data/Models/ProcessorVersionData.cs b/App/Modules/Cyan/Data/Models/ProcessorVersionData.cs new file mode 100644 index 0000000..377e8f0 --- /dev/null +++ b/App/Modules/Cyan/Data/Models/ProcessorVersionData.cs @@ -0,0 +1,23 @@ +namespace App.Modules.Cyan.Data.Models; + +public record ProcessorVersionData +{ + public Guid Id { get; set; } + + public ulong Version { get; set; } + + public DateTime CreatedAt { get; set; } + + public string Description { get; set; } = string.Empty; + + public string DockerReference { get; set; } = string.Empty; + + public string DockerSha { get; set; } = string.Empty; + + // Foreign Keys + public Guid ProcessorId { get; set; } + + public ProcessorData Processor { get; set; } = null!; + + public IEnumerable Templates { get; set; } = null!; +} diff --git a/App/Modules/Cyan/Data/Models/TemplateData.cs b/App/Modules/Cyan/Data/Models/TemplateData.cs new file mode 100644 index 0000000..04c576d --- /dev/null +++ b/App/Modules/Cyan/Data/Models/TemplateData.cs @@ -0,0 +1,35 @@ +using App.Modules.Users.Data; +using NpgsqlTypes; + +namespace App.Modules.Cyan.Data.Models; + +public record TemplateData +{ + public Guid Id { get; set; } + + public uint Downloads { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Project { get; set; } = string.Empty; + + public string Source { get; set; } = string.Empty; + + public string Email { get; set; } = string.Empty; + + public string[] Tags { get; set; } = Array.Empty(); + + public string Description { get; set; } = string.Empty; + + public string Readme { get; set; } = string.Empty; + + public NpgsqlTsVector SearchVector { get; set; } = null!; + + // Foreign Keys + public string UserId { get; set; } = string.Empty; + + public UserData User { get; set; } = null!; + + public IEnumerable Versions { get; set; } = null!; + public IEnumerable Likes { get; set; } = null!; +} diff --git a/App/Modules/Cyan/Data/Models/TemplatePluginData.cs b/App/Modules/Cyan/Data/Models/TemplatePluginData.cs new file mode 100644 index 0000000..3b27350 --- /dev/null +++ b/App/Modules/Cyan/Data/Models/TemplatePluginData.cs @@ -0,0 +1,12 @@ +namespace App.Modules.Cyan.Data.Models; + +public record TemplatePluginVersionData +{ + public Guid Id { get; set; } + + public Guid TemplateId { get; set; } + public TemplateVersionData Template { get; set; } = null!; + + public Guid PluginId { get; set; } + public PluginVersionData Plugin { get; set; } = null!; +} diff --git a/App/Modules/Cyan/Data/Models/TemplateProcessorData.cs b/App/Modules/Cyan/Data/Models/TemplateProcessorData.cs new file mode 100644 index 0000000..b5a61b8 --- /dev/null +++ b/App/Modules/Cyan/Data/Models/TemplateProcessorData.cs @@ -0,0 +1,13 @@ +namespace App.Modules.Cyan.Data.Models; + +public record TemplateProcessorVersionData +{ + public Guid Id { get; set; } + + public Guid TemplateId { get; set; } + public TemplateVersionData Template { get; set; } = null!; + + public Guid ProcessorId { get; set; } + public ProcessorVersionData Processor { get; set; } = null!; + +} diff --git a/App/Modules/Cyan/Data/Models/TemplateVersionData.cs b/App/Modules/Cyan/Data/Models/TemplateVersionData.cs new file mode 100644 index 0000000..93c58c6 --- /dev/null +++ b/App/Modules/Cyan/Data/Models/TemplateVersionData.cs @@ -0,0 +1,30 @@ +namespace App.Modules.Cyan.Data.Models; + +public record TemplateVersionData +{ + public Guid Id { get; set; } + + public ulong Version { get; set; } + + public DateTime CreatedAt { get; set; } + + public string Description { get; set; } = string.Empty; + + public string BlobDockerReference { get; set; } = string.Empty; + + public string BlobDockerSha { get; set; } = string.Empty; + + public string TemplateDockerReference { get; set; } = string.Empty; + + public string TemplateDockerSha { get; set; } = string.Empty; + + // Foreign Keys + public Guid TemplateId { get; set; } + + public TemplateData Template { get; set; } = null!; + + public IEnumerable Processors { get; set; } = null!; + + public IEnumerable Plugins { get; set; } = null!; + +} diff --git a/App/Modules/Cyan/Data/Repositories/PluginRepository.cs b/App/Modules/Cyan/Data/Repositories/PluginRepository.cs new file mode 100644 index 0000000..859ab4e --- /dev/null +++ b/App/Modules/Cyan/Data/Repositories/PluginRepository.cs @@ -0,0 +1,647 @@ +using App.Error.V1; +using App.Modules.Cyan.Data.Mappers; +using App.Modules.Cyan.Data.Models; +using App.StartUp.Database; +using App.Utility; +using CSharp_Result; +using Domain.Error; +using Domain.Model; +using Domain.Repository; +using EntityFramework.Exceptions.Common; +using LinqKit; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using NpgsqlTypes; + +namespace App.Modules.Cyan.Data.Repositories; + +public class PluginRepository : IPluginRepository +{ + private readonly MainDbContext _db; + private readonly ILogger _logger; + + public PluginRepository(MainDbContext db, ILogger logger) + { + this._db = db; + this._logger = logger; + } + + public async Task>> Search(PluginSearch search) + { + try + { + this._logger.LogInformation("Searching for plugins with Search Params '{@SearchParams}'", search.ToJson()); + var plugins = this._db.Plugins.AsQueryable(); + + if (search.Owner != null) + plugins = plugins + .Include(x => x.User) + .Where(x => x.User.Username == search.Owner); + + if (search.Search != null) + plugins = plugins + .Include(x => x.User) + .Where(x => + // Full text search + x.SearchVector + .Concat( + EF.Functions.ToTsVector("english", x.User.Username) + ) + .Concat( + EF.Functions.ArrayToTsVector(x.Tags) + ) + .Matches(EF.Functions.PlainToTsQuery("english", search.Search.Replace("/", " "))) || + EF.Functions.ILike(x.Name, $"%{search.Search}%") || + EF.Functions.ILike(x.User.Username, $"%{search.Search}%") + ) + // Rank with full text search + .OrderBy(x => + x.SearchVector + .Concat( + EF.Functions.ToTsVector(x.User.Username) + ) + .Concat( + EF.Functions.ArrayToTsVector(x.Tags) + ) + .Rank(EF.Functions.PlainToTsQuery("english", search.Search.Replace("/", " ")))); + + + plugins = plugins.Skip(search.Skip).Take(search.Limit); + var a = await plugins + .ToArrayAsync(); + return a.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting plugins with Search Params '{@SearchParams}'", search.ToJson()); + return e; + } + } + + public async Task> Get(Guid id) + { + try + { + this._logger.LogInformation("Getting plugin with '{ID}'", id); + var plugin = await this._db.Plugins + .Where(x => x.Id == id) + .Include(x => x.Likes) + .Include(x => x.User) + .Include(x => x.Versions) + .ThenInclude(x => x.Templates) + .FirstOrDefaultAsync(); + + if (plugin == null) return (Plugin?)null; + + var info = new PluginInfo + { + Downloads = plugin.Downloads, + Dependencies = (uint)plugin.Versions.Sum(x => x.Templates.Count()), + Stars = (uint)plugin.Likes.Count() + }; + return plugin?.ToDomain(info); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting plugin '{PluginId}'", id); + return e; + } + } + + public async Task> Get(string username, string name) + { + try + { + this._logger.LogInformation("Getting plugin '{Username}/{Name}'", username, name); + var plugin = await this._db.Plugins + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .Include(x => x.Likes) + .Include(x => x.User) + .Include(x => x.Versions) + .ThenInclude(x => x.Templates) + .FirstOrDefaultAsync(); + + if (plugin == null) return (Plugin?)null; + + var info = new PluginInfo + { + Downloads = plugin.Downloads, + Dependencies = (uint)plugin.Versions.Sum(x => x.Templates.Count()), + Stars = (uint)plugin.Likes.Count() + }; + return plugin?.ToDomain(info); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting plugin '{Username}/{Name}'", username, name); + return e; + } + } + + public async Task> Create(string userId, PluginRecord record, PluginMetadata metadata) + { + try + { + var data = new PluginData(); + data = data + .HydrateData(record) + .HydrateData(metadata) with + { + Downloads = 0, + User = null!, + UserId = userId, + }; + this._logger.LogInformation("Creating plugin {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + + var r = this._db.Plugins.Add(data); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (UniqueConstraintException e) + { + this._logger.LogError(e, + "Failed to create plugin due to conflict: {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + return new AlreadyExistException("Failed to create Plugin due to conflicting with existing record", e, + typeof(PluginPrincipal)); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed updating plugin {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + return e; + } + } + + public async Task> Update(string userId, Guid id, PluginMetadata v2) + { + try + { + var v1 = await this._db.Plugins + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + if (v1 == null) return (PluginPrincipal?)null; + + var v3 = v1.HydrateData(v2) with { User = null!, }; + var updated = this._db.Plugins.Update(v3); + await this._db.SaveChangesAsync(); + return updated.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to update Plugin with User ID '{UserID}' and Plugin ID '{PluginID}': {@Record}", + userId, id, v2.ToJson()); + return e; + } + } + + public async Task> Update(string username, string name, PluginMetadata v2) + { + try + { + var v1 = await this._db.Plugins + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + if (v1 == null) return (PluginPrincipal?)null; + + var v3 = v1.HydrateData(v2) with { User = null!, }; + var updated = this._db.Plugins.Update(v3); + await this._db.SaveChangesAsync(); + return updated.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to update Plugin '{Username}/{Name}': {@Record}", + username, name, v2.ToJson()); + return e; + } + } + + public async Task> Delete(string userId, Guid id) + { + try + { + var a = await this._db.Plugins + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + if (a == null) return (Unit?)null; + + this._db.Plugins.Remove(a); + await this._db.SaveChangesAsync(); + return new Unit(); + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to delete Plugin '{PluginId}' for User '{UserId}", id, userId); + return e; + } + } + + public async Task> Like(string likerId, string username, string name, bool like) + { + try + { + // check if like already exist + var likeExist = await this._db.PluginLikes + .Include(x => x.Plugin) + .ThenInclude(x => x.User) + .AnyAsync(x => x.UserId == likerId + && x.Plugin.User.Username == username + && x.Plugin.Name == name + ); + + if (like == likeExist) + return new LikeConflictError("Failed to like plugins", $"{username}/{name}", "plugin", + like ? "like" : "unlike").ToException(); + + var p = await this._db.Plugins + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.User.Username == username && x.Name == name); + + if (p == null) return (Unit?)null; + + if (like) + { + // if like, check for conflict + var l = new PluginLikeData { UserId = likerId, User = null!, PluginId = p.Id, Plugin = null! }; + var r = this._db.PluginLikes.Add(l); + await this._db.SaveChangesAsync(); + return new Unit(); + } + else + { + var l = await this._db.PluginLikes + .FirstOrDefaultAsync(x => x.UserId == likerId && x.PluginId == p.Id); + if (l == null) + { + this._logger.LogError( + "Race Condition, Failed to unlike Plugin '{Username}/{Name}' for User '{UserId}'. User-Plugin-Like entry does not exist even though it exist at the start of the query", + username, name, likerId); + + return new LikeRaceConditionError("Failed to like plugins", $"{username}/{name}", "plugin", + like ? "like" : "unlike").ToException(); + } + + this._db.Remove(l with { User = null!, Plugin = null! }); + await this._db.SaveChangesAsync(); + return new Unit(); + } + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to {Like} Plugin '{Username}/{Name}' for User '{UserId}", + like ? "like" : "unlike", username, name, likerId); + return e; + } + } + + public async Task> IncrementDownload(string username, string name) + { + try + { + var plugin = await this._db.Plugins + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + if (plugin == null) return (uint?)null; + + plugin = plugin with { Downloads = plugin.Downloads + 1, User = null! }; + + var updated = this._db.Plugins.Update(plugin); + await this._db.SaveChangesAsync(); + return updated.Entity.Downloads; + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to increment download count for Plugin '{Username}/{Name}'", + username, name); + return e; + } + } + + public async Task>> SearchVersion(string username, string name, + PluginVersionSearch version) + { + try + { + var query = this._db.PluginVersions + .Include(x => x.Plugin) + .ThenInclude(x => x.User) + .Where(x => x.Plugin.User.Username == username && x.Plugin.Name == name) + .AsQueryable(); + + if (version.Search != null) + query = query.Where(x => + EF.Functions.ILike(x.Description, $"%{version.Search}%") || + version.Search.Contains(version.Search) + ); + + var plugins = await query + .Skip(version.Skip) + .Take(version.Limit) + .ToArrayAsync(); + + return plugins.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed searching plugin version of Plugin '{Username}/{Name}' with {@Params}", + username, name, version.ToJson()); + return e; + } + } + + public async Task>> SearchVersion(string userId, Guid id, + PluginVersionSearch version) + { + try + { + var query = this._db.PluginVersions + .Include(x => x.Plugin) + .Where(x => x.Plugin.UserId == userId && x.Plugin.Id == id) + .AsQueryable(); + + if (version.Search != null) + query = query.Where(x => + EF.Functions.ILike(x.Description, $"%{version.Search}%") || + version.Search.Contains(version.Search) + ); + + var plugins = await query + .Skip(version.Skip) + .Take(version.Limit) + .ToArrayAsync(); + + return plugins.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed searching plugin version of Plugin '{PluginId}' of User '{UserId}' with {@Params}", + id, userId, version.ToJson()); + return e; + } + } + + public async Task>> GetAllVersion(IEnumerable references) + { + var pluginRefs = references as PluginVersionRef[] ?? references.ToArray(); + try + { + this._logger.LogInformation("Getting all plugin versions {@References}", pluginRefs.ToJson()); + if (pluginRefs.IsNullOrEmpty()) return Array.Empty(); + var query = this._db.PluginVersions + .Include(x => x.Plugin) + .ThenInclude(x => x.User) + .AsQueryable(); + + var predicate = PredicateBuilder.New(true); + + predicate = pluginRefs.Aggregate(predicate, (c, r) => + c.Or(x => x.Version == r.Version && x.Plugin.Name == r.Name && x.Plugin.User.Username == r.Username)); + + query = query.Where(predicate); + + var pluginReferences = await query.ToArrayAsync(); + this._logger.LogInformation("Plugin References: {@PluginReferences}", pluginReferences.Select(x => x.Id)); + if (pluginReferences.Length != pluginRefs.Length) + { + var found = pluginReferences.Select(x => $"{x.Plugin.User.Username}/{x.Plugin.Name}:{x.Version}").ToArray(); + var search = pluginRefs.Select(x => $"{x.Username}/{x.Name}:{x.Version}"); + var notFound = search.Except(found); + return new MultipleEntityNotFound("Plugins not found", typeof(PluginPrincipal), notFound.ToArray(), + found.ToArray()).ToException(); + } + + return pluginReferences.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed searching plugin versions '{@References}'", pluginRefs.ToJson()); + return e; + } + } + + public async Task> GetVersion(string username, string name, ulong version) + { + try + { + this._logger.LogInformation("Getting plugin version '{Username}/{Name}:{Version}'", username, name, version); + var plugin = await this._db.PluginVersions + .Include(x => x.Plugin) + .ThenInclude(x => x.User) + .Where(x => x.Plugin.User.Username == username && x.Plugin.Name == name && x.Version == version) + .FirstOrDefaultAsync(); + + return plugin?.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed to get plugin version '{Username}/{Name}:{Version}'", username, name, version); + return e; + } + } + + public async Task> GetVersion(string userId, Guid id, ulong version) + { + try + { + this._logger.LogInformation( + "Getting plugin version for User '{UserId}', Plugin: '{PluginId}', Version: {Version}'", userId, id, version); + var plugin = await this._db.PluginVersions + .Include(x => x.Plugin) + .Where(x => x.Plugin.UserId == userId && x.Plugin.Id == id && x.Version == version) + .FirstOrDefaultAsync(); + + return plugin?.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed to get plugin version: User '{UserId}', Plugin '{Name}', Version {Version}'", userId, id, + version); + return e; + } + } + + public async Task> CreateVersion(string username, string name, + PluginVersionRecord record, + PluginVersionProperty property) + { + try + { + this._logger.LogInformation( + "Creating plugin version for '{Username}/{Name}' with Record {@Record} and Property {@Property} ", + username, name, record.ToJson(), property.ToJson()); + + var plugin = await this._db.Plugins + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + + if (plugin == null) return (PluginVersionPrincipal?)null; + + + this._logger.LogInformation("Getting latest version for '{Username}/{Name}'", username, name); + var latest = this._db.PluginVersions + .Where(x => x.PluginId == plugin.Id) + .Max(x => x.Version as ulong?) ?? 0; + + this._logger.LogInformation("Latest version for '{Username}/{Name}' is {Version}", username, name, latest); + + var data = new PluginVersionData(); + data = data + .HydrateData(record) + .HydrateData(property) + with + { + PluginId = plugin.Id, + Plugin = null!, + Version = latest + 1, + CreatedAt = DateTime.UtcNow, + }; + + var r = this._db.PluginVersions.Add(data); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to create plugin version for '{Username}/{Name}' with Record {@Record} and Property {@Property} ", + username, name, record.ToJson(), property.ToJson()); + return e; + } + } + + public async Task> CreateVersion(string userId, Guid id, PluginVersionRecord record, + PluginVersionProperty property) + { + try + { + this._logger.LogInformation( + "Creating plugin version for User '{UserId}', Plugin '{Id}' with Record {@Record} and Property {@Property} ", + userId, id, record.ToJson(), property.ToJson()); + + var plugin = await this._db.Plugins + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + + if (plugin == null) return (PluginVersionPrincipal?)null; + + var latest = this._db.PluginVersions + .Where(x => x.PluginId == plugin.Id) + .Max(x => x.Version); + + var data = new PluginVersionData(); + data = data + .HydrateData(record) + .HydrateData(property) + with + { + PluginId = plugin.Id, + Plugin = null!, + Version = latest + 1, + CreatedAt = DateTime.Now, + }; + + var r = this._db.PluginVersions.Add(data); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to create plugin version for User '{UserId}', Plugin '{Id}' with Record {@Record} and Property {@Property}", + userId, id, record.ToJson(), property.ToJson()); + return e; + } + } + + public async Task> UpdateVersion(string username, string name, ulong version, + PluginVersionRecord v2) + { + try + { + this._logger.LogInformation( + "Updating plugin '{Username}/{Name}:{Version}' with Record {@Record}", + username, name, version, v2.ToJson()); + + + var v1 = await this._db.PluginVersions + .Include(x => x.Plugin) + .ThenInclude(x => x.User) + .Where(x => x.Version == version && x.Plugin.Name == name && x.Plugin.User.Username == username) + .FirstOrDefaultAsync(); + + if (v1 == null) return (PluginVersionPrincipal?)null; + + var v3 = v1.HydrateData(v2) + with + { + Plugin = null!, + }; + + var r = this._db.PluginVersions.Update(v3); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to update plugin '{Username}/{Name}:{Version}' with Record {@Record}", + username, name, version, v2.ToJson()); + return e; + } + } + + public async Task> UpdateVersion(string userId, Guid id, ulong version, + PluginVersionRecord v2) + { + try + { + this._logger.LogInformation( + "Updating plugin for User '{UserId}', Plugin '{Id}' with Record {@Record}", + userId, id, v2.ToJson()); + + + var v1 = await this._db.PluginVersions + .Include(x => x.Plugin) + .Where(x => x.Version == version && x.Plugin.Id == id && x.Plugin.UserId == userId) + .FirstOrDefaultAsync(); + + if (v1 == null) return (PluginVersionPrincipal?)null; + + var v3 = v1.HydrateData(v2) + with + { + Plugin = null!, + }; + + var r = this._db.PluginVersions.Update(v3); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to update plugin for User '{UserId}', Plugin '{Id}' with Record {@Record}", + userId, id, v2.ToJson()); + return e; + } + } +} diff --git a/App/Modules/Cyan/Data/Repositories/ProcessorRepository.cs b/App/Modules/Cyan/Data/Repositories/ProcessorRepository.cs new file mode 100644 index 0000000..bb17c98 --- /dev/null +++ b/App/Modules/Cyan/Data/Repositories/ProcessorRepository.cs @@ -0,0 +1,652 @@ +using App.Error.V1; +using App.Modules.Cyan.Data.Mappers; +using App.Modules.Cyan.Data.Models; +using App.StartUp.Database; +using App.Utility; +using CSharp_Result; +using Domain.Error; +using Domain.Model; +using Domain.Repository; +using EntityFramework.Exceptions.Common; +using LinqKit; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using NpgsqlTypes; + +namespace App.Modules.Cyan.Data.Repositories; + +public class ProcessorRepository : IProcessorRepository +{ + private readonly MainDbContext _db; + private readonly ILogger _logger; + + public ProcessorRepository(MainDbContext db, ILogger logger) + { + this._db = db; + this._logger = logger; + } + + public async Task>> Search(ProcessorSearch search) + { + try + { + this._logger.LogInformation("Searching for processors with Search Params '{@SearchParams}'", search.ToJson()); + var processors = this._db.Processors.AsQueryable(); + + if (search.Owner != null) + processors = processors + .Include(x => x.User) + .Where(x => x.User.Username == search.Owner); + + if (search.Search != null) + processors = processors + .Include(x => x.User) + .Where(x => + // Full text search + x.SearchVector + .Concat( + EF.Functions.ToTsVector("english", x.User.Username) + ) + .Concat( + EF.Functions.ArrayToTsVector(x.Tags) + ) + .Matches(EF.Functions.PlainToTsQuery("english", search.Search.Replace("/", " "))) || + EF.Functions.ILike(x.Name, $"%{search.Search}%") || + EF.Functions.ILike(x.User.Username, $"%{search.Search}%") + ) + // Rank with full text search + .OrderBy(x => + x.SearchVector + .Concat( + EF.Functions.ToTsVector(x.User.Username) + ) + .Concat( + EF.Functions.ArrayToTsVector(x.Tags) + ) + .Rank(EF.Functions.PlainToTsQuery("english", search.Search.Replace("/", " ")))); + + + processors = processors.Skip(search.Skip).Take(search.Limit); + var a = await processors + .ToArrayAsync(); + return a.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting processors with Search Params '{@SearchParams}'", search.ToJson()); + return e; + } + } + + public async Task> Get(Guid id) + { + try + { + this._logger.LogInformation("Getting processor with '{ID}'", id); + var processor = await this._db.Processors + .Where(x => x.Id == id) + .Include(x => x.Likes) + .Include(x => x.User) + .Include(x => x.Versions) + .ThenInclude(x => x.Templates) + .FirstOrDefaultAsync(); + + if (processor == null) return (Processor?)null; + + var info = new ProcessorInfo + { + Downloads = processor.Downloads, + Dependencies = (uint)processor.Versions.Sum(x => x.Templates.Count()), + Stars = (uint)processor.Likes.Count() + }; + return processor?.ToDomain(info); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting processor '{ProcessorId}'", id); + return e; + } + } + + public async Task> Get(string username, string name) + { + try + { + this._logger.LogInformation("Getting processor '{Username}/{Name}'", username, name); + var processor = await this._db.Processors + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .Include(x => x.Likes) + .Include(x => x.User) + .Include(x => x.Versions) + .ThenInclude(x => x.Templates) + .FirstOrDefaultAsync(); + + if (processor == null) return (Processor?)null; + + var info = new ProcessorInfo + { + Downloads = processor.Downloads, + Dependencies = (uint)processor.Versions.Sum(x => x.Templates.Count()), + Stars = (uint)processor.Likes.Count() + }; + return processor?.ToDomain(info); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting processor '{Username}/{Name}'", username, name); + return e; + } + } + + public async Task> Create(string userId, ProcessorRecord record, + ProcessorMetadata metadata) + { + try + { + var data = new ProcessorData(); + data = data + .HydrateData(record) + .HydrateData(metadata) with + { + Downloads = 0, + User = null!, + UserId = userId, + }; + this._logger.LogInformation("Creating processor {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + + var r = this._db.Processors.Add(data); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (UniqueConstraintException e) + { + this._logger.LogError(e, + "Failed to create processor due to conflict: {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + return new AlreadyExistException("Failed to create Processor due to conflicting with existing record", e, + typeof(ProcessorPrincipal)); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed updating processor {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + return e; + } + } + + public async Task> Update(string userId, Guid id, ProcessorMetadata v2) + { + try + { + var v1 = await this._db.Processors + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + if (v1 == null) return (ProcessorPrincipal?)null; + + var v3 = v1.HydrateData(v2) with { User = null!, }; + var updated = this._db.Processors.Update(v3); + await this._db.SaveChangesAsync(); + return updated.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger.LogError(e, + "Failed to update Processor with User ID '{UserID}' and Processor ID '{ProcessorID}': {@Record}", + userId, id, v2.ToJson()); + return e; + } + } + + public async Task> Update(string username, string name, ProcessorMetadata v2) + { + try + { + var v1 = await this._db.Processors + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + if (v1 == null) return (ProcessorPrincipal?)null; + + var v3 = v1.HydrateData(v2) with { User = null!, }; + var updated = this._db.Processors.Update(v3); + await this._db.SaveChangesAsync(); + return updated.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to update Processor '{Username}/{Name}': {@Record}", + username, name, v2.ToJson()); + return e; + } + } + + public async Task> Delete(string userId, Guid id) + { + try + { + var a = await this._db.Processors + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + if (a == null) return (Unit?)null; + + this._db.Processors.Remove(a); + await this._db.SaveChangesAsync(); + return new Unit(); + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to delete Processor '{ProcessorId}' for User '{UserId}", id, userId); + return e; + } + } + + public async Task> Like(string likerId, string username, string name, bool like) + { + try + { + // check if like already exist + var likeExist = await this._db.ProcessorLikes + .Include(x => x.Processor) + .ThenInclude(x => x.User) + .AnyAsync(x => x.UserId == likerId + && x.Processor.User.Username == username + && x.Processor.Name == name + ); + + if (like == likeExist) + return new LikeConflictError("Failed to like processors", $"{username}/{name}", "processor", + like ? "like" : "unlike").ToException(); + + var p = await this._db.Processors + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.User.Username == username && x.Name == name); + + if (p == null) return (Unit?)null; + + if (like) + { + // if like, check for conflict + var l = new ProcessorLikeData { UserId = likerId, User = null!, ProcessorId = p.Id, Processor = null! }; + var r = this._db.ProcessorLikes.Add(l); + await this._db.SaveChangesAsync(); + return new Unit(); + } + else + { + var l = await this._db.ProcessorLikes + .FirstOrDefaultAsync(x => x.UserId == likerId && x.ProcessorId == p.Id); + if (l == null) + { + this._logger.LogError( + "Race Condition, Failed to unlike Processor '{Username}/{Name}' for User '{UserId}'. User-Processor-Like entry does not exist even though it exist at the start of the query", + username, name, likerId); + + return new LikeRaceConditionError("Failed to like processors", $"{username}/{name}", "processor", + like ? "like" : "unlike").ToException(); + } + + this._db.Remove(l with { Processor = null!, User = null! }); + await this._db.SaveChangesAsync(); + return new Unit(); + } + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to {Like} Processor '{Username}/{Name}' for User '{UserId}", + like ? "like" : "unlike", username, name, likerId); + return e; + } + } + + public async Task> IncrementDownload(string username, string name) + { + try + { + var processor = await this._db.Processors + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + if (processor == null) return (uint?)null; + + processor = processor with { Downloads = processor.Downloads + 1, User = null!, }; + + var updated = this._db.Processors.Update(processor); + await this._db.SaveChangesAsync(); + return updated.Entity.Downloads; + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to increment download count for Processor '{Username}/{Name}'", + username, name); + return e; + } + } + + public async Task>> SearchVersion(string username, string name, + ProcessorVersionSearch version) + { + try + { + var query = this._db.ProcessorVersions + .Include(x => x.Processor) + .ThenInclude(x => x.User) + .Where(x => x.Processor.User.Username == username && x.Processor.Name == name) + .AsQueryable(); + + if (version.Search != null) + query = query.Where(x => + EF.Functions.ILike(x.Description, $"%{version.Search}%") || + version.Search.Contains(version.Search) + ); + + var processors = await query + .Skip(version.Skip) + .Take(version.Limit) + .ToArrayAsync(); + + return processors.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed searching processor version of Processor '{Username}/{Name}' with {@Params}", + username, name, version.ToJson()); + return e; + } + } + + public async Task>> SearchVersion(string userId, Guid id, + ProcessorVersionSearch version) + { + try + { + var query = this._db.ProcessorVersions + .Include(x => x.Processor) + .Where(x => x.Processor.UserId == userId && x.Processor.Id == id) + .AsQueryable(); + + if (version.Search != null) + query = query.Where(x => + EF.Functions.ILike(x.Description, $"%{version.Search}%") || + version.Search.Contains(version.Search) + ); + + var processors = await query + .Skip(version.Skip) + .Take(version.Limit) + .ToArrayAsync(); + + return processors.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed searching processor version of Processor '{ProcessorId}' of User '{UserId}' with {@Params}", + id, userId, version.ToJson()); + return e; + } + } + + public async Task>> GetAllVersion( + IEnumerable references) + { + var processorRefs = references as ProcessorVersionRef[] ?? references.ToArray(); + try + { + this._logger.LogInformation("Getting all plugin versions {@Processors}", processorRefs.ToJson()); + if (processorRefs.IsNullOrEmpty()) return Array.Empty(); + var query = this._db.ProcessorVersions + .Include(x => x.Processor) + .ThenInclude(x => x.User) + .AsQueryable(); + + var predicate = PredicateBuilder.New(true); + + predicate = processorRefs.Aggregate(predicate, (c, r) => + c.Or(x => x.Version == r.Version && x.Processor.Name == r.Name && x.Processor.User.Username == r.Username)); + + query = query.Where(predicate); + + var processorReferences = await query.ToArrayAsync(); + this._logger.LogInformation("Processor References: {@ProcessorReferences}", processorReferences.Select(x => x.Id)); + + if (processorReferences.Length != processorRefs.Length) + { + var found = processorReferences.Select(x => $"{x.Processor.User.Username}/{x.Processor.Name}:{x.Version}") + .ToArray(); + var search = processorRefs.Select(x => $"{x.Username}/{x.Name}:{x.Version}"); + var notFound = search.Except(found).ToArray(); + return new MultipleEntityNotFound("Processors not found", typeof(ProcessorPrincipal), notFound, found) + .ToException(); + } + + return processorReferences.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed searching processor versions '{@References}'", processorRefs.ToJson()); + return e; + } + } + + public async Task> GetVersion(string username, string name, ulong version) + { + try + { + this._logger.LogInformation("Getting processor version '{Username}/{Name}:{Version}'", username, name, version); + var processor = await this._db.ProcessorVersions + .Include(x => x.Processor) + .ThenInclude(x => x.User) + .Where(x => x.Processor.User.Username == username && x.Processor.Name == name && x.Version == version) + .FirstOrDefaultAsync(); + + return processor?.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed to get processor version '{Username}/{Name}:{Version}'", username, name, version); + return e; + } + } + + public async Task> GetVersion(string userId, Guid id, ulong version) + { + try + { + this._logger.LogInformation( + "Getting processor version for User '{UserId}', Processor: '{ProcessorId}', Version: {Version}'", userId, id, + version); + var processor = await this._db.ProcessorVersions + .Include(x => x.Processor) + .Where(x => x.Processor.UserId == userId && x.Processor.Id == id && x.Version == version) + .FirstOrDefaultAsync(); + + return processor?.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed to get processor version: User '{UserId}', Processor '{Name}', Version {Version}'", userId, + id, + version); + return e; + } + } + + public async Task> CreateVersion(string username, string name, + ProcessorVersionRecord record, + ProcessorVersionProperty property) + { + try + { + this._logger.LogInformation( + "Creating processor version for '{Username}/{Name}' with Record {@Record} and Property {@Property} ", + username, name, record.ToJson(), property.ToJson()); + + var processor = await this._db.Processors + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + + if (processor == null) return (ProcessorVersionPrincipal?)null; + + var latest = this._db.ProcessorVersions + .Where(x => x.ProcessorId == processor.Id) + .Max(x => x.Version as ulong?) ?? 0; + + var data = new ProcessorVersionData(); + data = data + .HydrateData(record) + .HydrateData(property) + with + { + ProcessorId = processor.Id, + Processor = null!, + Version = latest + 1, + CreatedAt = DateTime.UtcNow, + }; + + var r = this._db.ProcessorVersions.Add(data); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to create processor version for '{Username}/{Name}' with Record {@Record} and Property {@Property} ", + username, name, record.ToJson(), property.ToJson()); + return e; + } + } + + public async Task> CreateVersion(string userId, Guid id, + ProcessorVersionRecord record, + ProcessorVersionProperty property) + { + try + { + this._logger.LogInformation( + "Creating processor version for User '{UserId}', Processor '{Id}' with Record {@Record} and Property {@Property} ", + userId, id, record.ToJson(), property.ToJson()); + + var processor = await this._db.Processors + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + + if (processor == null) return (ProcessorVersionPrincipal?)null; + + var latest = this._db.ProcessorVersions + .Where(x => x.ProcessorId == processor.Id) + .Max(x => x.Version as ulong?) ?? 0; + + var data = new ProcessorVersionData(); + data = data + .HydrateData(record) + .HydrateData(property) + with + { + ProcessorId = processor.Id, + Processor = null!, + Version = latest + 1, + CreatedAt = DateTime.UtcNow, + }; + + var r = this._db.ProcessorVersions.Add(data); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to create processor version for User '{UserId}', Processor '{Id}' with Record {@Record} and Property {@Property}", + userId, id, record.ToJson(), property.ToJson()); + return e; + } + } + + public async Task> UpdateVersion(string username, string name, ulong version, + ProcessorVersionRecord v2) + { + try + { + this._logger.LogInformation( + "Updating processor '{Username}/{Name}:{Version}' with Record {@Record}", + username, name, version, v2.ToJson()); + + + var v1 = await this._db.ProcessorVersions + .Include(x => x.Processor) + .ThenInclude(x => x.User) + .Where(x => x.Version == version && x.Processor.Name == name && x.Processor.User.Username == username) + .FirstOrDefaultAsync(); + + if (v1 == null) return (ProcessorVersionPrincipal?)null; + + var v3 = v1.HydrateData(v2) + with + { + Processor = null!, + }; + + var r = this._db.ProcessorVersions.Update(v3); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to update processor '{Username}/{Name}:{Version}' with Record {@Record}", + username, name, version, v2.ToJson()); + return e; + } + } + + public async Task> UpdateVersion(string userId, Guid id, ulong version, + ProcessorVersionRecord v2) + { + try + { + this._logger.LogInformation( + "Updating processor for User '{UserId}', Processor '{Id}' with Record {@Record}", + userId, id, v2.ToJson()); + + + var v1 = await this._db.ProcessorVersions + .Include(x => x.Processor) + .Where(x => x.Version == version && x.Processor.Id == id && x.Processor.UserId == userId) + .FirstOrDefaultAsync(); + + if (v1 == null) return (ProcessorVersionPrincipal?)null; + + var v3 = v1.HydrateData(v2) + with + { + Processor = null!, + }; + + var r = this._db.ProcessorVersions.Update(v3); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to update processor for User '{UserId}', Processor '{Id}' with Record {@Record}", + userId, id, v2.ToJson()); + return e; + } + } +} diff --git a/App/Modules/Cyan/Data/Repositories/TemplateRepository.cs b/App/Modules/Cyan/Data/Repositories/TemplateRepository.cs new file mode 100644 index 0000000..a045e12 --- /dev/null +++ b/App/Modules/Cyan/Data/Repositories/TemplateRepository.cs @@ -0,0 +1,658 @@ +using System.Runtime.CompilerServices; +using App.Error.V1; +using App.Modules.Cyan.Data.Mappers; +using App.Modules.Cyan.Data.Models; +using App.StartUp.Database; +using App.Utility; +using CSharp_Result; +using Domain.Error; +using Domain.Model; +using Domain.Repository; +using EntityFramework.Exceptions.Common; +using Microsoft.EntityFrameworkCore; +using NpgsqlTypes; + +namespace App.Modules.Cyan.Data.Repositories; + +public class TemplateRepository : ITemplateRepository +{ + private readonly MainDbContext _db; + private readonly ILogger _logger; + + public TemplateRepository(MainDbContext db, ILogger logger) + { + this._db = db; + this._logger = logger; + } + + public async Task>> Search(TemplateSearch search) + { + try + { + this._logger.LogInformation("Searching for templates with Search Params '{@SearchParams}'", search.ToJson()); + var templates = this._db.Templates.AsQueryable(); + + if (search.Owner != null) + templates = templates + .Include(x => x.User) + .Where(x => x.User.Username == search.Owner); + + if (search.Search != null) + templates = templates + .Include(x => x.User) + .Where(x => + // Full text search + x.SearchVector + .Concat( + EF.Functions.ToTsVector("english", x.User.Username) + ) + .Concat( + EF.Functions.ArrayToTsVector(x.Tags) + ) + .Matches(EF.Functions.PlainToTsQuery("english", search.Search.Replace("/", " "))) || + EF.Functions.ILike(x.Name, $"%{search.Search}%") || + EF.Functions.ILike(x.User.Username, $"%{search.Search}%") + ) + // Rank with full text search + .OrderBy(x => + x.SearchVector + .Concat( + EF.Functions.ToTsVector(x.User.Username) + ) + .Concat( + EF.Functions.ArrayToTsVector(x.Tags) + ) + .Rank(EF.Functions.PlainToTsQuery("english", search.Search.Replace("/", " ")))); + + templates = templates.Skip(search.Skip).Take(search.Limit); + var a = await templates + .ToArrayAsync(); + return a.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting templates with Search Params '{@SearchParams}'", search.ToJson()); + return e; + } + } + + public async Task> Get(Guid id) + { + try + { + this._logger.LogInformation("Getting template with '{ID}'", id); + var template = await this._db.Templates + .Where(x => x.Id == id) + .Include(x => x.Likes) + .Include(x => x.User) + .Include(x => x.Versions) + .FirstOrDefaultAsync(); + + if (template == null) return (Template?)null; + + var info = new TemplateInfo { Downloads = template.Downloads, Stars = (uint)template.Likes.Count() }; + return template?.ToDomain(info); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting template '{TemplateId}'", id); + return e; + } + } + + public async Task> Get(string username, string name) + { + try + { + this._logger.LogInformation("Getting template '{Username}/{Name}'", username, name); + var template = await this._db.Templates + .Include(x => x.Likes) + .Include(x => x.Versions) + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + + if (template == null) return (Template?)null; + + var info = new TemplateInfo { Downloads = template.Downloads, Stars = (uint)template.Likes.Count() }; + return template?.ToDomain(info); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed getting template '{Username}/{Name}'", username, name); + return e; + } + } + + public async Task> Create(string userId, TemplateRecord record, TemplateMetadata metadata) + { + try + { + this._logger.LogInformation("Creating template {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + var data = new TemplateData(); + data = data + .HydrateData(record) + .HydrateData(metadata) with + { + Downloads = 0, + User = null!, + UserId = userId, + }; + this._logger.LogInformation("Creating template with Template Data: {@Template}", data.ToJson()); + var r = this._db.Templates.Add(data); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (UniqueConstraintException e) + { + this._logger.LogError(e, + "Failed to create template due to conflict: {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + return new AlreadyExistException("Failed to create Template due to conflicting with existing record", e, + typeof(TemplatePrincipal)); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed updating template {UserId} with Record {@Record} and Metadata {@Metadata}", userId, + record.ToJson(), metadata.ToJson()); + return e; + } + } + + public async Task> Update(string userId, Guid id, TemplateMetadata v2) + { + try + { + var v1 = await this._db.Templates + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + if (v1 == null) return (TemplatePrincipal?)null; + + var v3 = v1.HydrateData(v2) with { User = null!, }; + var updated = this._db.Templates.Update(v3); + await this._db.SaveChangesAsync(); + return updated.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger.LogError(e, + "Failed to update Template with User ID '{UserID}' and Template ID '{TemplateID}': {@Record}", + userId, id, v2.ToJson()); + return e; + } + } + + public async Task> Update(string username, string name, TemplateMetadata v2) + { + try + { + var v1 = await this._db.Templates + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + if (v1 == null) return (TemplatePrincipal?)null; + + var v3 = v1.HydrateData(v2) with { User = null!, }; + var updated = this._db.Templates.Update(v3); + await this._db.SaveChangesAsync(); + return updated.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to update Template '{Username}/{Name}': {@Record}", + username, name, v2.ToJson()); + return e; + } + } + + public async Task> Delete(string userId, Guid id) + { + try + { + var a = await this._db.Templates + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + if (a == null) return (Unit?)null; + + this._db.Templates.Remove(a); + await this._db.SaveChangesAsync(); + return new Unit(); + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to delete Template '{TemplateId}' for User '{UserId}", id, userId); + return e; + } + } + + public async Task> Like(string likerId, string username, string name, bool like) + { + try + { + // check if like already exist + var likeExist = await this._db.TemplateLikes + .Include(x => x.Template) + .ThenInclude(x => x.User) + .AnyAsync(x => x.UserId == likerId + && x.Template.User.Username == username + && x.Template.Name == name + ); + + if (like == likeExist) + return new LikeConflictError("Failed to like templates", $"{username}/{name}", "template", + like ? "like" : "unlike").ToException(); + + var p = await this._db.Templates + .Include(x => x.User) + .FirstOrDefaultAsync(x => x.User.Username == username && x.Name == name); + + if (p == null) return (Unit?)null; + + if (like) + { + // if like, check for conflict + var l = new TemplateLikeData { UserId = likerId, User = null!, TemplateId = p.Id, Template = null! }; + var r = this._db.TemplateLikes.Add(l); + await this._db.SaveChangesAsync(); + return new Unit(); + } + else + { + var l = await this._db.TemplateLikes + .FirstOrDefaultAsync(x => x.UserId == likerId && x.TemplateId == p.Id); + if (l == null) + { + this._logger.LogError( + "Race Condition, Failed to unlike Template '{Username}/{Name}' for User '{UserId}'. User-Template-Like entry does not exist even though it exist at the start of the query", + username, name, likerId); + + return new LikeRaceConditionError("Failed to like templates", $"{username}/{name}", "template", + like ? "like" : "unlike").ToException(); + } + + this._logger.LogInformation("Removing {@Like}", l); + this._db.TemplateLikes.Remove(l with { Template = null!, User = null!, }); + await this._db.SaveChangesAsync(); + return new Unit(); + } + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to {Like} Template '{Username}/{Name}' for User '{UserId}", + like ? "like" : "unlike", username, name, likerId); + return e; + } + } + + public async Task> IncrementDownload(string username, string name) + { + try + { + var template = await this._db.Templates + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + if (template == null) return (uint?)null; + + template = template with { Downloads = template.Downloads + 1, User = null!, }; + + var updated = this._db.Templates.Update(template); + await this._db.SaveChangesAsync(); + return updated.Entity.Downloads; + } + catch (Exception e) + { + this._logger.LogError(e, "Failed to increment download count for Template '{Username}/{Name}'", + username, name); + return e; + } + } + + public async Task>> SearchVersion(string username, string name, + TemplateVersionSearch version) + { + try + { + var query = this._db.TemplateVersions + .Include(x => x.Template) + .ThenInclude(x => x.User) + .Where(x => x.Template.User.Username == username && x.Template.Name == name) + .AsQueryable(); + + if (version.Search != null) + query = query.Where(x => + EF.Functions.ILike(x.Description, $"%{version.Search}%") || + version.Search.Contains(version.Search) + ); + + var templates = await query + .Skip(version.Skip) + .Take(version.Limit) + .ToArrayAsync(); + + return templates.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed searching template version of Template '{Username}/{Name}' with {@Params}", + username, name, version.ToJson()); + return e; + } + } + + public async Task>> SearchVersion(string userId, Guid id, + TemplateVersionSearch version) + { + try + { + var query = this._db.TemplateVersions + .Include(x => x.Template) + .Where(x => x.Template.UserId == userId && x.Template.Id == id) + .AsQueryable(); + + if (version.Search != null) + query = query.Where(x => + EF.Functions.ILike(x.Description, $"%{version.Search}%") || + version.Search.Contains(version.Search) + ); + + var templates = await query + .Skip(version.Skip) + .Take(version.Limit) + .ToArrayAsync(); + + return templates.Select(x => x.ToPrincipal()).ToResult(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed searching template version of Template '{TemplateId}' of User '{UserId}' with {@Params}", + id, userId, version.ToJson()); + return e; + } + } + + public async Task> GetVersion(string username, string name, ulong version) + { + try + { + this._logger.LogInformation("Getting template version '{Username}/{Name}:{Version}'", username, name, version); + var template = await this._db.TemplateVersions + .Include(x => x.Template) + .ThenInclude(x => x.User) + .Include(x => x.Plugins) + .ThenInclude(x => x.Plugin) + .Include(x => x.Processors) + .ThenInclude(x => x.Processor) + .Where(x => x.Template.User.Username == username && x.Template.Name == name && x.Version == version) + .FirstOrDefaultAsync(); + return template?.ToDomain(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed to get template version '{Username}/{Name}:{Version}'", username, name, version); + return e; + } + } + + public async Task> GetVersion(string userId, Guid id, ulong version) + { + try + { + this._logger.LogInformation( + "Getting template version for User '{UserId}', Template: '{TemplateId}', Version: {Version}'", userId, id, + version); + var template = await this._db.TemplateVersions + .Include(x => x.Template) + .Include(x => x.Plugins) + .Include(x => x.Processors) + .Where(x => x.Template.UserId == userId && x.Template.Id == id && x.Version == version) + .FirstOrDefaultAsync(); + + return template?.ToDomain(); + } + catch (Exception e) + { + this._logger + .LogError(e, "Failed to get template version: User '{UserId}', Template '{Name}', Version {Version}'", userId, + id, + version); + return e; + } + } + + public async Task> CreateVersion(string username, string name, + TemplateVersionRecord record, + TemplateVersionProperty property, IEnumerable processors, IEnumerable plugins) + { + await using var transaction = await this._db.Database.BeginTransactionAsync(); + try + { + this._logger.LogInformation( + "Creating template version for '{Username}/{Name}' with Record {@Record} and Property {@Property} ", + username, name, record.ToJson(), property.ToJson()); + + var template = await this._db.Templates + .Include(x => x.User) + .Where(x => x.User.Username == username && x.Name == name) + .FirstOrDefaultAsync(); + + if (template == null) + { + await transaction.CommitAsync(); + return (TemplateVersionPrincipal?)null; + } + + this._logger.LogInformation("Getting latest version for '{Username}/{Name}'", username, name); + var latest = this._db.TemplateVersions + .Where(x => x.TemplateId == template.Id) + .Max(x => x.Version as ulong?) ?? 0; + + this._logger.LogInformation("Obtained latest version for '{Username}/{Name}': {Version}", username, name, latest); + + var data = new TemplateVersionData(); + data = data + .HydrateData(record) + .HydrateData(property) + with + { + TemplateId = template.Id, + Template = null!, + Version = latest + 1, + CreatedAt = DateTime.UtcNow, + Plugins = null!, + Processors = null!, + }; + + var r = this._db.TemplateVersions.Add(data); + await this._db.SaveChangesAsync(); + var t = r.Entity.ToPrincipal(); + + // save plugin links + var pluginLinks = plugins.Select(x => new TemplatePluginVersionData + { + PluginId = x, + Plugin = null!, + TemplateId = t.Id, + Template = null!, + }); + this._logger.LogInformation("Saving plugins links for '{Username}/{Name}:{Version}', Plugins: {@Plugins}", + username, name, latest, pluginLinks.ToJson()); + this._db.TemplatePluginVersions.AddRange(pluginLinks); + var processorLinks = processors.Select(x => new TemplateProcessorVersionData + { + ProcessorId = x, + Processor = null!, + TemplateId = t.Id, + Template = null!, + }); + this._logger.LogInformation( + "Saving processors links for '{Username}/{Name}:{Version}', Processors: {@Processors}", username, name, latest, + processorLinks.ToJson()); + this._db.TemplateProcessorVersions.AddRange(processorLinks); + await this._db.SaveChangesAsync(); + await transaction.CommitAsync(); + return t; + } + catch (Exception e) + { + await transaction.RollbackAsync(); + this._logger + .LogError(e, + "Failed to create template version for '{Username}/{Name}' with Record {@Record} and Property {@Property} ", + username, name, record.ToJson(), property.ToJson()); + return e; + } + } + + public async Task> CreateVersion(string userId, Guid id, + TemplateVersionRecord record, + TemplateVersionProperty property, IEnumerable processors, IEnumerable plugins) + { + await using var transaction = await this._db.Database.BeginTransactionAsync(); + try + { + this._logger.LogInformation( + "Creating template version for User '{UserId}', Template '{Id}' with Record {@Record} and Property {@Property} ", + userId, id, record.ToJson(), property.ToJson()); + + var template = await this._db.Templates + .Where(x => x.UserId == userId && x.Id == id) + .FirstOrDefaultAsync(); + + if (template == null) return (TemplateVersionPrincipal?)null; + + var latest = this._db.TemplateVersions + .Where(x => x.TemplateId == template.Id) + .Max(x => x.Version as ulong?) ?? 0; + + var data = new TemplateVersionData(); + data = data + .HydrateData(record) + .HydrateData(property) + with + { + TemplateId = template.Id, + Template = null!, + Version = latest + 1, + CreatedAt = DateTime.UtcNow, + }; + + var r = this._db.TemplateVersions.Add(data); + await this._db.SaveChangesAsync(); + var t = r.Entity.ToPrincipal(); + + // save plugin links + var pluginLinks = plugins.Select(x => new TemplatePluginVersionData + { + PluginId = x, + Plugin = null!, + TemplateId = t.Id, + Template = null!, + }); + this._db.TemplatePluginVersions.AddRange(pluginLinks); + var processorLinks = processors.Select(x => new TemplateProcessorVersionData + { + ProcessorId = x, + Processor = null!, + TemplateId = t.Id, + Template = null!, + }); + this._db.TemplateProcessorVersions.AddRange(processorLinks); + await this._db.SaveChangesAsync(); + await transaction.CommitAsync(); + return t; + } + catch (Exception e) + { + await transaction.RollbackAsync(); + this._logger + .LogError(e, + "Failed to create template version for User '{UserId}', Template '{Id}' with Record {@Record} and Property {@Property}", + userId, id, record.ToJson(), property.ToJson()); + return e; + } + } + + public async Task> UpdateVersion(string username, string name, ulong version, + TemplateVersionRecord v2) + { + try + { + this._logger.LogInformation( + "Updating template '{Username}/{Name}:{Version}' with Record {@Record}", + username, name, version, v2.ToJson()); + + + var v1 = await this._db.TemplateVersions + .Include(x => x.Template) + .ThenInclude(x => x.User) + .Where(x => x.Version == version && x.Template.Name == name && x.Template.User.Username == username) + .FirstOrDefaultAsync(); + + if (v1 == null) return (TemplateVersionPrincipal?)null; + + var v3 = v1.HydrateData(v2) + with + { + Template = null!, + }; + + var r = this._db.TemplateVersions.Update(v3); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to update template '{Username}/{Name}:{Version}' with Record {@Record}", + username, name, version, v2.ToJson()); + return e; + } + } + + public async Task> UpdateVersion(string userId, Guid id, ulong version, + TemplateVersionRecord v2) + { + try + { + this._logger.LogInformation( + "Updating template for User '{UserId}', Template '{Id}' with Record {@Record}", + userId, id, v2.ToJson()); + + + var v1 = await this._db.TemplateVersions + .Include(x => x.Template) + .Where(x => x.Version == version && x.Template.Id == id && x.Template.UserId == userId) + .FirstOrDefaultAsync(); + + if (v1 == null) return (TemplateVersionPrincipal?)null; + + var v3 = v1.HydrateData(v2) + with + { + Template = null!, + }; + + var r = this._db.TemplateVersions.Update(v3); + await this._db.SaveChangesAsync(); + return r.Entity.ToPrincipal(); + } + catch (Exception e) + { + this._logger + .LogError(e, + "Failed to update template for User '{UserId}', Template '{Id}' with Record {@Record}", + userId, id, v2.ToJson()); + return e; + } + } +} diff --git a/App/Modules/DomainServices.cs b/App/Modules/DomainServices.cs index 977d018..1f29a92 100644 --- a/App/Modules/DomainServices.cs +++ b/App/Modules/DomainServices.cs @@ -1,4 +1,5 @@ +using App.Modules.Cyan.Data.Repositories; using App.Modules.Users.Data; using App.StartUp.Services; using App.Utility; @@ -26,8 +27,23 @@ public static IServiceCollection AddDomainServices(this IServiceCollection s) s.AddScoped() .AutoTrace(); + s.AddScoped() + .AutoTrace(); + s.AddScoped() + .AutoTrace(); + s.AddScoped() + .AutoTrace(); + + s.AddScoped() + .AutoTrace(); + + s.AddScoped() + .AutoTrace(); + + s.AddScoped() + .AutoTrace(); return s; } diff --git a/App/Modules/Users/API/V1/UserController.cs b/App/Modules/Users/API/V1/UserController.cs index 0150773..79d140c 100644 --- a/App/Modules/Users/API/V1/UserController.cs +++ b/App/Modules/Users/API/V1/UserController.cs @@ -41,8 +41,7 @@ public UserController(IUserService service, this._token = token; } - // Policy = AuthPolicies.OnlyAdmin - [Authorize, HttpGet] + [Authorize(Policy = AuthPolicies.OnlyAdmin), HttpGet] public async Task>> Search([FromQuery] SearchUserQuery query) { var x = await this._userSearchQueryValidator diff --git a/App/Modules/Users/Data/TokenData.cs b/App/Modules/Users/Data/TokenData.cs index 87dab9d..b1745c7 100644 --- a/App/Modules/Users/Data/TokenData.cs +++ b/App/Modules/Users/Data/TokenData.cs @@ -10,6 +10,7 @@ public record TokenData public bool Revoked { get; set; } = false; + // Foreign Key public string UserId { get; set; } = string.Empty; public UserData User { get; set; } = new(); } diff --git a/App/Modules/Users/Data/TokenRepository.cs b/App/Modules/Users/Data/TokenRepository.cs index 374ad94..7dcad80 100644 --- a/App/Modules/Users/Data/TokenRepository.cs +++ b/App/Modules/Users/Data/TokenRepository.cs @@ -1,11 +1,8 @@ -using App.Error.V1; using App.StartUp.Database; using App.Utility; using CSharp_Result; -using Domain.Error; using Domain.Model; using Domain.Repository; -using EntityFramework.Exceptions.Common; using Microsoft.EntityFrameworkCore; namespace App.Modules.Users.Data; diff --git a/App/Modules/Users/Data/UserData.cs b/App/Modules/Users/Data/UserData.cs index fae7782..076f509 100644 --- a/App/Modules/Users/Data/UserData.cs +++ b/App/Modules/Users/Data/UserData.cs @@ -1,3 +1,6 @@ +using App.Modules.Cyan.Data; +using App.Modules.Cyan.Data.Models; + namespace App.Modules.Users.Data; public record UserData @@ -6,5 +9,18 @@ public record UserData public string Username { get; set; } = string.Empty; - public IEnumerable Tokens { get; set; } = new List(); + // Foreign Keys + public IEnumerable Tokens { get; set; } = null!; + + public IEnumerable Templates { get; set; } = null!; + + public IEnumerable Plugins { get; set; } = null!; + + public IEnumerable Processors { get; set; } = null!; + + public IEnumerable TemplateLikes { get; set; } = null!; + + public IEnumerable PluginLikes { get; set; } = null!; + + public IEnumerable ProcessorLikes { get; set; } = null!; }; diff --git a/App/StartUp/Database/MainDbContext.cs b/App/StartUp/Database/MainDbContext.cs index 512561e..848a87c 100644 --- a/App/StartUp/Database/MainDbContext.cs +++ b/App/StartUp/Database/MainDbContext.cs @@ -1,9 +1,12 @@ +using App.Modules.Cyan.Data; +using App.Modules.Cyan.Data.Models; using App.Modules.Users.Data; using App.StartUp.Options; using App.StartUp.Services; using EntityFramework.Exceptions.PostgreSQL; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; +using NpgsqlTypes; namespace App.StartUp.Database; @@ -13,6 +16,22 @@ public class MainDbContext : DbContext public DbSet Users { get; set; } = null!; public DbSet Tokens { get; set; } = null!; + public DbSet Templates { get; set; } = null!; + public DbSet TemplateVersions { get; set; } = null!; + public DbSet TemplateProcessorVersions { get; set; } = null!; + public DbSet TemplatePluginVersions { get; set; } = null!; + + public DbSet Plugins { get; set; } = null!; + public DbSet PluginVersions { get; set; } = null!; + + public DbSet Processors { get; set; } = null!; + public DbSet ProcessorVersions { get; set; } = null!; + + // likes + public DbSet TemplateLikes { get; set; } = null!; + public DbSet PluginLikes { get; set; } = null!; + public DbSet ProcessorLikes { get; set; } = null!; + private readonly IOptionsMonitor> _options; public MainDbContext(IOptionsMonitor> options) @@ -37,7 +56,124 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .WithOne(x => x.User) .HasForeignKey(x => x.UserId); + user.HasMany(x => x.Templates) + .WithOne(x => x.User) + .HasForeignKey(x => x.UserId); + + user.HasMany(x => x.Plugins) + .WithOne(x => x.User) + .HasForeignKey(x => x.UserId); + + user.HasMany(x => x.Processors) + .WithOne(x => x.User) + .HasForeignKey(x => x.UserId); + + user.HasMany(x => x.TemplateLikes) + .WithOne(x => x.User) + .HasForeignKey(x => x.UserId); + + user.HasMany(x => x.PluginLikes) + .WithOne(x => x.User) + .HasForeignKey(x => x.UserId); + + user.HasMany(x => x.ProcessorLikes) + .WithOne(x => x.User) + .HasForeignKey(x => x.UserId); + + var templateLikes = modelBuilder.Entity(); + templateLikes + .HasIndex(x => new { x.UserId, x.TemplateId }) + .IsUnique(); + + var pluginLikes = modelBuilder.Entity(); + pluginLikes + .HasIndex(x => new { x.UserId, x.PluginId }) + .IsUnique(); + + var processorLikes = modelBuilder.Entity(); + processorLikes + .HasIndex(x => new { x.UserId, x.ProcessorId }) + .IsUnique(); + var token = modelBuilder.Entity(); token.HasIndex(x => x.ApiToken).IsUnique(); + + + var template = modelBuilder.Entity(); + template.HasIndex(x => new { x.UserId, x.Name }).IsUnique(); + + template + .HasGeneratedTsVectorColumn( + p => p.SearchVector, + "english", + p => new { p.Name, p.Description }) + .HasIndex(p => p.SearchVector) + .HasMethod("GIN"); + + template.HasMany(x => x.Versions) + .WithOne(x => x.Template) + .HasForeignKey(x => x.TemplateId); + + template.HasMany(x => x.Likes) + .WithOne(x => x.Template) + .HasForeignKey(x => x.TemplateId); + + var templateVersion = modelBuilder.Entity(); + + templateVersion.HasIndex(x => new { x.Id, x.Version }).IsUnique(); + + templateVersion.HasMany(x => x.Processors) + .WithOne(x => x.Template) + .HasForeignKey(x => x.TemplateId); + + templateVersion.HasMany(x => x.Plugins) + .WithOne(x => x.Template) + .HasForeignKey(x => x.TemplateId); + + + var plugin = modelBuilder.Entity(); + plugin.HasIndex(x => new { x.UserId, x.Name }).IsUnique(); + + plugin + .HasGeneratedTsVectorColumn( + p => p.SearchVector, + "english", + p => new { p.Name, p.Description }) + .HasIndex(p => p.SearchVector) + .HasMethod("GIN"); + + plugin.HasMany(x => x.Versions) + .WithOne(x => x.Plugin) + .HasForeignKey(x => x.PluginId); + + plugin.HasMany(x => x.Likes) + .WithOne(x => x.Plugin) + .HasForeignKey(x => x.PluginId); + + var pluginVersion = modelBuilder.Entity(); + + pluginVersion.HasIndex(x => new { x.Id, x.Version }).IsUnique(); + + var processor = modelBuilder.Entity(); + processor.HasIndex(x => new { x.UserId, x.Name }).IsUnique(); + + processor + .HasGeneratedTsVectorColumn( + p => p.SearchVector, + "english", + p => new { p.Name, p.Description }) + .HasIndex(p => p.SearchVector) + .HasMethod("GIN"); + + processor.HasMany(x => x.Versions) + .WithOne(x => x.Processor) + .HasForeignKey(x => x.ProcessorId); + + processor.HasMany(x => x.Likes) + .WithOne(x => x.Processor) + .HasForeignKey(x => x.ProcessorId); + + var processorVersion = modelBuilder.Entity(); + processorVersion.HasIndex(x => new { x.Id, x.Version }).IsUnique(); } } diff --git a/App/Utility/ValidationUtility.cs b/App/Utility/ValidationUtility.cs index 0065391..522fe21 100644 --- a/App/Utility/ValidationUtility.cs +++ b/App/Utility/ValidationUtility.cs @@ -64,10 +64,27 @@ public static IRuleBuilderOptions UsernameValid( return ruleBuilder .Length(1, 256) .WithMessage("Username has to be between 1 to 256 characters") - .Matches(@"[\w\d](\-?[\w\d]+)*") - .WithMessage("Username can only contain alphanumeric characters and dashes, and cannot end or start with dashes"); + .Matches(@"[\w](\-?[\w\d]+)*") + .WithMessage("Username can only contain alphanumeric characters and dashes, and cannot end or start with dashes or numbers"); } + public static IRuleBuilderOptions ShaValid( + this IRuleBuilder ruleBuilder) + { + return ruleBuilder + .Matches("^[0-9a-fA-F]{64}$") + .WithMessage("SHA can only have hexadecimal characters and exactly 64"); + } + + public static IRuleBuilderOptions DockerReferenceValid( + this IRuleBuilder ruleBuilder) + { + return ruleBuilder + .Matches(@"^((\w(-?\w+)*)(\.\w(-?\w+)*)*(:\d+)?/)?\w(-?\w+)*(/\w(-?\w+)*)*$") + .WithMessage("Invalid Docker reference"); + } + + public static IRuleBuilderOptions NameValid( this IRuleBuilder ruleBuilder) { diff --git a/App/packages.lock.json b/App/packages.lock.json index ac01c02..765b8ca 100644 --- a/App/packages.lock.json +++ b/App/packages.lock.json @@ -130,6 +130,16 @@ "resolved": "1.2.0", "contentHash": "5TidVQwiwZ5Ab6u443OUIPhEq+Qa/8Y4fbwDVxdMulkCAs1Bp8/HFL1ohn8MwUMt8Kw5k/FCPekL/r++CdM/CA==" }, + "LinqKit.Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[7.1.4, )", + "resolved": "7.1.4", + "contentHash": "Bsq1/HXOC29GjviRq971qYz3/+9GWKEKO4QdoaEjMt5HHHx8b4yHL8RGzohdiqjTjOOVM5oZF9eaOpLcFu8uUA==", + "dependencies": { + "LinqKit.Core": "1.2.4", + "Microsoft.EntityFrameworkCore": "7.0.0" + } + }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Direct", "requested": "[8.0.0-rc.2.23480.2, )", @@ -875,6 +885,11 @@ "Humanizer.Core": "[2.14.1]" } }, + "LinqKit.Core": { + "type": "Transitive", + "resolved": "1.2.4", + "contentHash": "cR88ymKSFZu1XZnmUzGl8eyeLRqnpiTMZBf+DBSr4kWRmfr40mwNdyLooCe3Or/WbkkmCo5erypLdmZTsNXi+w==" + }, "Microsoft.CSharp": { "type": "Transitive", "resolved": "4.7.0", diff --git a/Domain/Model/Plugin.cs b/Domain/Model/Plugin.cs new file mode 100644 index 0000000..0107ebb --- /dev/null +++ b/Domain/Model/Plugin.cs @@ -0,0 +1,60 @@ +namespace Domain.Model; + +public record PluginSearch +{ + public string? Search { get; init; } + + public string? Owner { get; init; } + + public int Limit { get; init; } + + public int Skip { get; init; } +} + + +public record Plugin +{ + public required PluginPrincipal Principal { get; init; } + + public required UserPrincipal User { get; init; } + + public required IEnumerable Versions { get; init; } + + // Telemetry, non-user controlled + public required PluginInfo Info { get; init; } +} + +public record PluginPrincipal +{ + public required Guid Id { get; init; } + + // User Controlled, updatable, metadata + public required PluginMetadata Metadata { get; init; } + + // User Controlled, not-updatable, on create + public required PluginRecord Record { get; init; } +} + +public record PluginRecord +{ + public required string Name { get; init; } +} + +public record PluginInfo +{ + public required uint Downloads { get; init; } + + public required uint Dependencies { get; init; } + + public required uint Stars { get; init; } +} + +public record PluginMetadata +{ + public required string Project { get; init; } + public required string Source { get; init; } + public required string Email { get; init; } + public required string[] Tags { get; init; } + public required string Description { get; init; } + public required string Readme { get; init; } +} diff --git a/Domain/Model/PluginVersion.cs b/Domain/Model/PluginVersion.cs new file mode 100644 index 0000000..a235c70 --- /dev/null +++ b/Domain/Model/PluginVersion.cs @@ -0,0 +1,47 @@ +namespace Domain.Model; + +public record PluginVersionSearch +{ + public string? Search { get; init; } + + public int Limit { get; init; } + + public int Skip { get; init; } +} + + +public record PluginVersion +{ + public required PluginVersionPrincipal Principal { get; init; } + + public required PluginPrincipal PluginPrincipal { get; init; } +} + +public record PluginVersionRef(string Username, string Name, ulong Version); + +public record PluginVersionPrincipal +{ + public required Guid Id { get; init; } + + public required ulong Version { get; init; } + + public required DateTime CreatedAt { get; init; } + + public required PluginVersionRecord Record { get; init; } + + public required PluginVersionProperty Property { get; init; } +} + +public record PluginVersionRecord +{ + public required string Description { get; init; } +} + +public record PluginVersionProperty +{ + + public required string DockerReference { get; init; } + + public required string DockerSha { get; init; } + +} diff --git a/Domain/Model/Processor.cs b/Domain/Model/Processor.cs new file mode 100644 index 0000000..587307a --- /dev/null +++ b/Domain/Model/Processor.cs @@ -0,0 +1,56 @@ +namespace Domain.Model; + +public record ProcessorSearch +{ + public string? Owner { get; init; } + public string? Search { get; init; } + + public int Limit { get; init; } + + public int Skip { get; init; } +} + +public record Processor +{ + public required ProcessorPrincipal Principal { get; init; } + public required UserPrincipal User { get; init; } + public required IEnumerable Versions { get; init; } + + // Telemetry, non-user controlled + public required ProcessorInfo Info { get; init; } +} + +public record ProcessorPrincipal +{ + public required Guid Id { get; init; } + + // User Controlled, updatable, metadata + public required ProcessorMetadata Metadata { get; init; } + + // User Controlled, not-updatable, on create + public required ProcessorRecord Record { get; init; } +} + +public record ProcessorInfo +{ + public required uint Downloads { get; init; } + + public required uint Dependencies { get; init; } + + public required uint Stars { get; init; } +} + +public record ProcessorRecord +{ + public required string Name { get; init; } +} + +public record ProcessorMetadata +{ + public required string Project { get; init; } + public required string Source { get; init; } + public required string Email { get; init; } + public required string[] Tags { get; init; } + public required string Description { get; init; } + public required string Readme { get; init; } +} diff --git a/Domain/Model/ProcessorVersion.cs b/Domain/Model/ProcessorVersion.cs new file mode 100644 index 0000000..023a9eb --- /dev/null +++ b/Domain/Model/ProcessorVersion.cs @@ -0,0 +1,44 @@ +namespace Domain.Model; + +public record ProcessorVersionSearch +{ + public string? Search { get; init; } + + public int Limit { get; init; } + + public int Skip { get; init; } +} + +public record ProcessorVersion +{ + public required ProcessorVersionPrincipal Principal { get; init; } + + public required ProcessorPrincipal ProcessorPrincipal { get; init; } +} + +public record ProcessorVersionRef(string Username, string Name, ulong Version); + +public record ProcessorVersionPrincipal +{ + public required Guid Id { get; init; } + + public required ulong Version { get; init; } + + public required DateTime CreatedAt { get; init; } + + public required ProcessorVersionRecord Record { get; init; } + + public required ProcessorVersionProperty Property { get; init; } +} + +public record ProcessorVersionRecord +{ + public required string Description { get; init; } +} + +public record ProcessorVersionProperty +{ + public required string DockerReference { get; init; } + + public required string DockerSha { get; init; } +} diff --git a/Domain/Model/Template.cs b/Domain/Model/Template.cs index 60ca983..750bd0c 100644 --- a/Domain/Model/Template.cs +++ b/Domain/Model/Template.cs @@ -2,34 +2,49 @@ namespace Domain.Model; public record TemplateSearch { + public string? Owner { get; init; } + public string? Search { get; init; } + + public int Limit { get; init; } + + public int Skip { get; init; } } public record Template { public required TemplatePrincipal Principal { get; init; } public required UserPrincipal User { get; init; } + public required IEnumerable Versions { get; init; } + // Telemetry, non-user controlled + public required TemplateInfo Info { get; init; } + } public record TemplatePrincipal { public required Guid Id { get; init; } - // Telemetry, non-user controlled - public required TemplateInfo Info { get; init; } // User Controlled, updatable, metadata public required TemplateMetadata Metadata { get; init; } + + // User Controlled, not-updatable, on create + public required TemplateRecord Record { get; init; } +} + +public record TemplateRecord +{ + public required string Name { get; init; } } public record TemplateInfo { - public required int Likes { get; init; } - public required int Downloads { get; init; } + public required uint Downloads { get; init; } + public required uint Stars { get; init; } } public record TemplateMetadata { - public required string Name { get; init; } public required string Project { get; init; } public required string Source { get; init; } public required string Email { get; init; } @@ -37,20 +52,3 @@ public record TemplateMetadata public required string Description { get; init; } public required string Readme { get; init; } } - -public record TemplateVersionPrincipal -{ - public required Guid Id { get; init; } - - public required uint Version { get; init; } - - public required DateTime CreatedAt { get; init; } - - public required string BlobDockerReference { get; init; } - - public required string BlobDockerSha { get; init; } - - public required string TemplateDockerReference { get; init; } - - public required string TemplateDockerSha { get; init; } -} diff --git a/Domain/Model/TemplateVersion.cs b/Domain/Model/TemplateVersion.cs new file mode 100644 index 0000000..708f527 --- /dev/null +++ b/Domain/Model/TemplateVersion.cs @@ -0,0 +1,50 @@ +namespace Domain.Model; + +public record TemplateVersionSearch +{ + public string? Search { get; init; } + + public int Limit { get; init; } + + public int Skip { get; init; } +} + +public record TemplateVersion +{ + public required TemplateVersionPrincipal Principal { get; init; } + + public required TemplatePrincipal TemplatePrincipal { get; init; } + + public required IEnumerable Plugins { get; init; } + + public required IEnumerable Processors { get; init; } +} + +public record TemplateVersionPrincipal +{ + public required Guid Id { get; init; } + + public required ulong Version { get; init; } + + public required DateTime CreatedAt { get; init; } + + public required TemplateVersionRecord Record { get; init; } + + public required TemplateVersionProperty Property { get; init; } +} + +public record TemplateVersionRecord +{ + public required string Description { get; init; } +} + +public record TemplateVersionProperty +{ + public required string BlobDockerReference { get; init; } + + public required string BlobDockerSha { get; init; } + + public required string TemplateDockerReference { get; init; } + + public required string TemplateDockerSha { get; init; } +} diff --git a/Domain/Repository/IPluginRepository.cs b/Domain/Repository/IPluginRepository.cs new file mode 100644 index 0000000..dd35bb0 --- /dev/null +++ b/Domain/Repository/IPluginRepository.cs @@ -0,0 +1,47 @@ +using System.Collections; +using CSharp_Result; +using Domain.Model; + +namespace Domain.Repository; + +public interface IPluginRepository +{ + Task>> Search(PluginSearch search); + + Task> Get(Guid id); + + Task> Get(string username, string name); + + Task> Create(string userId, PluginRecord record, PluginMetadata metadata); + + Task> Update(string userId, Guid id, PluginMetadata metadata); + + Task> Update(string username, string name, PluginMetadata metadata); + + Task> Delete(string userId, Guid id); + + Task> Like(string likerId, string username, string name, bool like); + + Task> IncrementDownload(string username, string name); + + Task>> SearchVersion(string username, string name, PluginVersionSearch version); + + Task>> SearchVersion(string userId, Guid id, PluginVersionSearch version); + + Task>> GetAllVersion(IEnumerable references); + + Task> GetVersion(string username, string name, ulong version); + + Task> GetVersion(string userId, Guid id, ulong version); + + Task> CreateVersion(string username, string name, PluginVersionRecord record, + PluginVersionProperty property); + + Task> CreateVersion(string userId, Guid id, PluginVersionRecord record, + PluginVersionProperty property); + + Task> UpdateVersion(string username, string name, ulong version, + PluginVersionRecord record); + Task> UpdateVersion(string userId, Guid id, ulong version, PluginVersionRecord record); + +} diff --git a/Domain/Repository/IProcessorRepository.cs b/Domain/Repository/IProcessorRepository.cs new file mode 100644 index 0000000..f3ef0f8 --- /dev/null +++ b/Domain/Repository/IProcessorRepository.cs @@ -0,0 +1,46 @@ +using CSharp_Result; +using Domain.Model; + +namespace Domain.Repository; + +public interface IProcessorRepository +{ + Task>> Search(ProcessorSearch search); + + Task> Get(Guid id); + + Task> Get(string username, string name); + + Task> Create(string userId, ProcessorRecord record, ProcessorMetadata metadata); + + Task> Update(string userId, Guid id, ProcessorMetadata metadata); + + Task> Update(string username, string name, ProcessorMetadata metadata); + + Task> Delete(string userId, Guid id); + + Task> Like(string likerId, string username, string name, bool like); + + Task> IncrementDownload(string username, string name); + + Task>> GetAllVersion(IEnumerable references); + + Task>> SearchVersion(string userId, string name, ProcessorVersionSearch version); + + Task>> SearchVersion(string userId, Guid id, ProcessorVersionSearch version); + + Task> GetVersion(string userId, string name, ulong version); + + Task> GetVersion(string userId, Guid id, ulong version); + + Task> CreateVersion(string userId, string name, ProcessorVersionRecord record, + ProcessorVersionProperty property); + + Task> CreateVersion(string userId, Guid id, ProcessorVersionRecord record, + ProcessorVersionProperty property); + + Task> UpdateVersion(string userId, Guid id, ulong version, ProcessorVersionRecord record); + + Task> UpdateVersion(string userId, string name, ulong version, + ProcessorVersionRecord record); +} diff --git a/Domain/Repository/ITemplateRepository.cs b/Domain/Repository/ITemplateRepository.cs index e0973e4..dd8b051 100644 --- a/Domain/Repository/ITemplateRepository.cs +++ b/Domain/Repository/ITemplateRepository.cs @@ -1,6 +1,47 @@ +using CSharp_Result; +using Domain.Model; + namespace Domain.Repository; public interface ITemplateRepository { + Task>> Search(TemplateSearch search); + + Task> Get(Guid id); + + Task> Get(string username, string name); + + Task> Create(string userId, TemplateRecord record, TemplateMetadata metadata); + + Task> Update(string userId, Guid id, TemplateMetadata metadata); + + Task> Update(string username, string name, TemplateMetadata metadata); + + Task> Delete(string userId, Guid id); + + Task> Like(string likerId, string username, string name, bool like); + + Task> IncrementDownload(string username, string name); + + Task>> SearchVersion(string userId, string name, TemplateVersionSearch version); + + Task>> SearchVersion(string userId, Guid id, TemplateVersionSearch version); + + Task> GetVersion(string userId, string name, ulong version); + + Task> GetVersion(string userId, Guid id, ulong version); + + Task> CreateVersion(string userId, string name, TemplateVersionRecord record, + TemplateVersionProperty property, IEnumerable processors, + IEnumerable plugins); + + Task> CreateVersion(string userId, Guid id, TemplateVersionRecord record, + TemplateVersionProperty property, IEnumerable processors, + IEnumerable plugins); + + Task> UpdateVersion(string userId, Guid id, ulong version, + TemplateVersionRecord record); + Task> UpdateVersion(string userId, string name, ulong version, + TemplateVersionRecord record); } diff --git a/Domain/Service/IPluginService.cs b/Domain/Service/IPluginService.cs new file mode 100644 index 0000000..a917298 --- /dev/null +++ b/Domain/Service/IPluginService.cs @@ -0,0 +1,40 @@ +using CSharp_Result; +using Domain.Model; + +namespace Domain.Service; + +public interface IPluginService +{ + Task>> Search(PluginSearch search); + + Task> Get(Guid id); + + Task> Get(string username, string name); + + Task> Create(string userId, PluginRecord record, PluginMetadata metadata); + + Task> Update(string userId, Guid id, PluginMetadata metadata); + + Task> Update(string username, string name, PluginMetadata metadata); + + Task> Like(string likerId, string username, string name, bool like); + Task> Delete(string userId, Guid id); + + Task>> SearchVersion(string username, string name, PluginVersionSearch version); + + Task>> SearchVersion(string userId, Guid id, PluginVersionSearch version); + + Task> GetVersion(string username, string name, ulong version, bool bumpDownload); + Task> GetVersion(string userId, Guid id, ulong version); + + Task> CreateVersion(string username, string name, PluginVersionRecord record, + PluginVersionProperty property); + + Task> CreateVersion(string userId, Guid id, PluginVersionRecord record, + PluginVersionProperty property); + + Task> UpdateVersion(string userId, Guid id, ulong version, PluginVersionRecord record); + + Task> UpdateVersion(string username, string name, ulong version, + PluginVersionRecord record); +} diff --git a/Domain/Service/IProcessorService.cs b/Domain/Service/IProcessorService.cs new file mode 100644 index 0000000..7f2e228 --- /dev/null +++ b/Domain/Service/IProcessorService.cs @@ -0,0 +1,42 @@ +using CSharp_Result; +using Domain.Model; + +namespace Domain.Service; + +public interface IProcessorService +{ + Task>> Search(ProcessorSearch search); + + Task> Get(Guid id); + + Task> Get(string username, string name); + + Task> Create(string userId, ProcessorRecord record, ProcessorMetadata metadata); + + Task> Update(string userId, Guid id, ProcessorMetadata metadata); + + Task> Update(string username, string name, ProcessorMetadata metadata); + + Task> Delete(string userId, Guid id); + + Task> Like(string likerId, string username, string name, bool like); + + Task>> SearchVersion(string username, string name, ProcessorVersionSearch version); + + Task>> SearchVersion(string userId, Guid id, ProcessorVersionSearch version); + + Task> GetVersion(string username, string name, ulong version, bool bumpDownload); + + Task> GetVersion(string userId, Guid id, ulong version); + + Task> CreateVersion(string username, string name, ProcessorVersionRecord record, + ProcessorVersionProperty property); + + Task> CreateVersion(string userId, Guid id, ProcessorVersionRecord record, + ProcessorVersionProperty property); + + Task> UpdateVersion(string userId, Guid id, ulong version, ProcessorVersionRecord record); + + Task> UpdateVersion(string username, string name, ulong version, + ProcessorVersionRecord record); +} diff --git a/Domain/Service/ITemplateService.cs b/Domain/Service/ITemplateService.cs new file mode 100644 index 0000000..2b408ac --- /dev/null +++ b/Domain/Service/ITemplateService.cs @@ -0,0 +1,48 @@ +using CSharp_Result; +using Domain.Model; + +namespace Domain.Service; + +public interface ITemplateService +{ + Task>> Search(TemplateSearch search); + + Task> Get(Guid id); + + Task> Get(string username, string name); + + Task> Create(string userId, TemplateRecord record, TemplateMetadata metadata); + + Task> Update(string userId, Guid id, TemplateMetadata metadata); + + Task> Update(string username, string name, TemplateMetadata metadata); + + Task> Delete(string userId, Guid id); + + Task> Like(string likerId, string username, string name, bool like); + + Task>> SearchVersion(string username, string name, + TemplateVersionSearch version); + + Task>> SearchVersion(string userId, Guid id, + TemplateVersionSearch version); + + + Task> GetVersion(string username, string name, ulong version, bool dumpDownload); + + Task> GetVersion(string userId, Guid id, ulong version); + + Task> CreateVersion(string username, string name, TemplateVersionRecord record, + TemplateVersionProperty property, IEnumerable processors, + IEnumerable plugins); + + Task> CreateVersion(string userId, Guid id, TemplateVersionRecord record, + TemplateVersionProperty property, IEnumerable processors, + IEnumerable plugins); + + Task> UpdateVersion(string userId, Guid id, ulong version, + TemplateVersionRecord record); + + Task> UpdateVersion(string username, string name, ulong version, + TemplateVersionRecord record); +} diff --git a/Domain/Service/PluginService.cs b/Domain/Service/PluginService.cs new file mode 100644 index 0000000..c947c91 --- /dev/null +++ b/Domain/Service/PluginService.cs @@ -0,0 +1,121 @@ +using CSharp_Result; +using Domain.Model; +using Domain.Repository; +using Microsoft.Extensions.Logging; + +namespace Domain.Service; + +public class PluginService : IPluginService +{ + private readonly IPluginRepository _repo; + private readonly ILogger _logger; + + public PluginService(IPluginRepository repo, ILogger logger) + { + this._logger = logger; + this._repo = repo; + } + + public Task>> Search(PluginSearch search) + { + return this._repo.Search(search); + } + + public Task> Get(Guid id) + { + return this._repo.Get(id); + } + + public Task> Get(string owner, string name) + { + return this._repo.Get(owner, name); + } + + public Task> Create(string userId, PluginRecord record, PluginMetadata metadata) + { + return this._repo.Create(userId, record, metadata); + } + + public Task> Update(string userId, Guid id, PluginMetadata metadata) + { + return this._repo.Update(userId, id, metadata); + } + + public Task> Update(string userId, string name, PluginMetadata metadata) + { + return this._repo.Update(userId, name, metadata); + } + + public Task> Like(string likerId, string userId, string name, bool like) + { + return this._repo.Like(likerId, userId, name, like); + } + + public Task> Delete(string userId, Guid id) + { + return this._repo.Delete(userId, id); + } + + public Task>> SearchVersion(string userId, string name, + PluginVersionSearch version) + { + return this._repo.SearchVersion(userId, name, version); + } + + public Task>> SearchVersion(string userId, Guid id, + PluginVersionSearch version) + { + return this._repo.SearchVersion(userId, id, version); + } + + public async Task> GetVersion(string username, string name, ulong version, + bool bumpDownload) + { + if (bumpDownload) + { + return await this._repo.GetVersion(username, name, version) + .DoAwait(DoType.Ignore, async _ => + { + var r = await this._repo.IncrementDownload(username, name); + if (r.IsFailure()) + { + var err = r.FailureOrDefault(); + this._logger.LogError(err, "Failed to increment download when obtaining version for Plugin '{Username}/{Name}:{Version}'", + username, name, version); + } + return r; + }); + } + return await this._repo + .GetVersion(username, name, version); + } + + public Task> GetVersion(string userId, Guid id, ulong version) + { + return this._repo.GetVersion(userId, id, version); + } + + public Task> CreateVersion(string userId, string name, PluginVersionRecord record, + PluginVersionProperty property) + { + return this._repo.CreateVersion(userId, name, record, property); + } + + public Task> CreateVersion(string userId, Guid id, PluginVersionRecord record, + PluginVersionProperty property) + { + return this._repo.CreateVersion(userId, id, record, property); + } + + public Task> UpdateVersion(string userId, Guid id, ulong version, + PluginVersionRecord record) + { + return this._repo.UpdateVersion(userId, id, version, record); + } + + public Task> UpdateVersion(string userId, string name, ulong version, + PluginVersionRecord record) + { + return this._repo.UpdateVersion(userId, name, version, record); + } +} diff --git a/Domain/Service/ProcessorService.cs b/Domain/Service/ProcessorService.cs new file mode 100644 index 0000000..692ff72 --- /dev/null +++ b/Domain/Service/ProcessorService.cs @@ -0,0 +1,114 @@ +using CSharp_Result; +using Domain.Model; +using Domain.Repository; +using Microsoft.Extensions.Logging; + +namespace Domain.Service; + +public class ProcessorService : IProcessorService +{ + private readonly IProcessorRepository _repo; + private readonly ILogger _logger; + + public ProcessorService(IProcessorRepository repo, ILogger logger) + { + this._repo = repo; + this._logger = logger; + } + + public Task>> Search(ProcessorSearch search) + { + return this._repo.Search(search); + } + + public Task> Get(Guid id) + { + return this._repo.Get(id); + } + + public Task> Get(string owner, string name) + { + return this._repo.Get(owner, name); + } + + public Task> Create(string userId, ProcessorRecord record, ProcessorMetadata metadata) + { + return this._repo.Create(userId, record, metadata); + } + + public Task> Update(string userId, Guid id, ProcessorMetadata metadata) + { + return this._repo.Update(userId, id, metadata); + } + + public Task> Update(string userId, string name, ProcessorMetadata metadata) + { + return this._repo.Update(userId, name, metadata); + } + + public Task> Delete(string userId, Guid id) + { + return this._repo.Delete(userId, id); + } + + public Task> Like(string likerId, string username, string name, bool like) + { + return this._repo.Like(likerId, username, name, like); + } + + public Task>> SearchVersion(string username, string name, ProcessorVersionSearch version) + { + return this._repo.SearchVersion(username, name, version); + } + + public Task>> SearchVersion(string userId, Guid id, ProcessorVersionSearch version) + { + return this._repo.SearchVersion(userId, id, version); + } + + public async Task> GetVersion(string username, string name, ulong version, bool bumpDownload) + { + if (bumpDownload) + { + return await this._repo.GetVersion(username, name, version) + .DoAwait(DoType.Ignore, async _ => + { + var r = await this._repo.IncrementDownload(username, name); + if (r.IsFailure()) + { + var err = r.FailureOrDefault(); + this._logger.LogError(err, "Failed to increment download when obtaining version for Processor '{Username}/{Name}:{Version}'", + username, name, version); + } + + return r; + }); + } + return await this._repo.GetVersion(username, name, version); + } + + public Task> GetVersion(string userId, Guid id, ulong version) + { + return this._repo.GetVersion(userId, id, version); + } + + public Task> CreateVersion(string username, string name, ProcessorVersionRecord record, ProcessorVersionProperty property) + { + return this._repo.CreateVersion(username, name, record, property); + } + + public Task> CreateVersion(string userId, Guid id, ProcessorVersionRecord record, ProcessorVersionProperty property) + { + return this._repo.CreateVersion(userId, id, record, property); + } + + public Task> UpdateVersion(string userId, Guid id, ulong version, ProcessorVersionRecord record) + { + return this._repo.UpdateVersion(userId, id, version, record); + } + + public Task> UpdateVersion(string username, string name, ulong version, ProcessorVersionRecord record) + { + return this._repo.UpdateVersion(username, name, version, record); + } +} diff --git a/Domain/Service/TemplateService.cs b/Domain/Service/TemplateService.cs new file mode 100644 index 0000000..9cd8110 --- /dev/null +++ b/Domain/Service/TemplateService.cs @@ -0,0 +1,157 @@ +using CSharp_Result; +using Domain.Model; +using Domain.Repository; +using Microsoft.Extensions.Logging; + +namespace Domain.Service; + +public class TemplateService : ITemplateService +{ + private readonly ITemplateRepository _repo; + private readonly IPluginRepository _plugin; + private readonly IProcessorRepository _processor; + private readonly ILogger _logger; + + + public TemplateService(ITemplateRepository repo, IPluginRepository plugin, IProcessorRepository processor, + ILogger logger) + { + this._repo = repo; + this._plugin = plugin; + this._processor = processor; + this._logger = logger; + } + + + public Task>> Search(TemplateSearch search) + { + return this._repo.Search(search); + } + + public Task> Get(Guid id) + { + return this._repo.Get(id); + } + + public Task> Get(string owner, string name) + { + return this._repo.Get(owner, name); + } + + public Task> Create(string userId, TemplateRecord record, TemplateMetadata metadata) + { + return this._repo.Create(userId, record, metadata); + } + + public Task> Update(string userId, Guid id, TemplateMetadata metadata) + { + return this._repo.Update(userId, id, metadata); + } + + public Task> Update(string userId, string name, TemplateMetadata metadata) + { + return this._repo.Update(userId, name, metadata); + } + + public Task> Delete(string userId, Guid id) + { + return this._repo.Delete(userId, id); + } + + public Task> Like(string likerId, string username, string name, bool like) + { + return this._repo.Like(likerId, username, name, like); + } + + public Task>> SearchVersion(string userId, string name, + TemplateVersionSearch version) + { + return this._repo.SearchVersion(userId, name, version); + } + + public Task>> SearchVersion(string userId, Guid id, + TemplateVersionSearch version) + { + return this._repo.SearchVersion(userId, id, version); + } + + public async Task> GetVersion(string username, string name, ulong version, + bool bumpDownload) + { + if (bumpDownload) + { + return await this._repo.GetVersion(username, name, version) + .DoAwait(DoType.Ignore, async _ => + { + var r = await this._repo.IncrementDownload(username, name); + if (r.IsFailure()) + { + var err = r.FailureOrDefault(); + this._logger.LogError(err, + "Failed to increment download when obtaining version for Template '{Username}/{Name}:{Version}'", + username, name, version); + } + + return r; + }); + } + + return await this._repo.GetVersion(username, name, version); + } + + public Task> GetVersion(string userId, Guid id, ulong version) + { + return this._repo.GetVersion(userId, id, version); + } + + public async Task> CreateVersion(string userId, string name, + TemplateVersionRecord record, TemplateVersionProperty property, + IEnumerable processors, IEnumerable plugins) + { + var pluginResults = await this._plugin.GetAllVersion(plugins); + var processorResults = await this._processor.GetAllVersion(processors); + + var a = from plugin in pluginResults + from processor in processorResults + select (plugin.Select(x => x.Id), processor.Select(x => x.Id)); + return await Task.FromResult(a) + .ThenAwait(refs => + { + var (pl, pr) = refs; + this._logger.LogInformation("Creating Template Version '{Name}' for '{UserId}', Processors: {@Processors}", name, userId, processors); + this._logger.LogInformation("Creating Template Version '{Name}' for '{UserId}', Plugins: {@Plugins}", name, userId, pl); + return this._repo.CreateVersion(userId, name, record, property, pr, pl); + }); + } + + public async Task> CreateVersion(string userId, Guid id, + TemplateVersionRecord record, + TemplateVersionProperty property, + IEnumerable processors, IEnumerable plugins) + { + var pluginResults = await this._plugin.GetAllVersion(plugins); + var processorResults = await this._processor.GetAllVersion(processors); + + var a = from plugin in pluginResults + from processor in processorResults + select (plugin.Select(x => x.Id), processor.Select(x => x.Id)); + return await Task.FromResult(a) + .ThenAwait(refs => + { + var (pr, pl) = refs; + return this._repo.CreateVersion(userId, id, record, property, pr, pl); + }); + } + + public Task> UpdateVersion(string userId, Guid id, ulong version, + TemplateVersionRecord record) + { + return this._repo.UpdateVersion(userId, id, version, record); + } + + public Task> UpdateVersion(string userId, string name, ulong version, + TemplateVersionRecord record) + { + return this._repo.UpdateVersion(userId, name, version, record); + } +} diff --git a/Taskfile.yml b/Taskfile.yml index 996f314..88d662e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -30,6 +30,10 @@ tasks: desc: Tears down the local development cluster cmds: - ./scripts/local/delete-k3d-cluster.sh + exec: + desc: Starts any application in the cluster + cmds: + - ./scripts/local/exec.sh ./config/dev.yaml {{.CLI_ARGS}} dev: desc: Starts developing application cmds: diff --git a/config/dev.yaml b/config/dev.yaml index b9ec185..c6146c3 100644 --- a/config/dev.yaml +++ b/config/dev.yaml @@ -12,7 +12,7 @@ type: local # fast mode # 1. does not start the cluster, automatically assumes that your cluster already created # 2. does not clean up resources. This makes your future starts faster -fast: false +fast: true # whether to start local cluster. If false, will connect to current context startCluster: true diff --git a/infra/api_chart/app/settings.pichu.yaml b/infra/api_chart/app/settings.pichu.yaml index 2907619..d40d4e8 100644 --- a/infra/api_chart/app/settings.pichu.yaml +++ b/infra/api_chart/app/settings.pichu.yaml @@ -24,7 +24,9 @@ Metrics: Enabled: true # Infra-based -Database: {} +Database: + MAIN: + AutoMigrate: false Cache: {} BlockStorage: {} # external diff --git a/infra/migration_chart/app/settings.pichu.yaml b/infra/migration_chart/app/settings.pichu.yaml index 2907619..d40d4e8 100644 --- a/infra/migration_chart/app/settings.pichu.yaml +++ b/infra/migration_chart/app/settings.pichu.yaml @@ -24,7 +24,9 @@ Metrics: Enabled: true # Infra-based -Database: {} +Database: + MAIN: + AutoMigrate: false Cache: {} BlockStorage: {} # external diff --git a/infra/root_chart/values.pichu.yaml b/infra/root_chart/values.pichu.yaml index 5bec24e..83c9e4e 100644 --- a/infra/root_chart/values.pichu.yaml +++ b/infra/root_chart/values.pichu.yaml @@ -6,6 +6,24 @@ bromine: target: &target "sulfone-zinc" +# -- YAML Anchor for PodSecurityContext +podSecurityContext: &podSecurityContext + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true + +# -- YAML Anchor for SecurityContext +securityContext: &securityContext + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + capabilities: + drop: + - ALL + api: enabled: true envFromSecret: *target @@ -14,7 +32,8 @@ api: image: repository: ghcr.io/atomicloud/sulfone.zinc/api-amd imagePullSecrets: [] - + securityContext: *securityContext + podSecurityContext: *podSecurityContext replicaCount: 1 configMountPath: /app/Config @@ -42,6 +61,8 @@ api: migration: enabled: true envFromSecret: *target + securityContext: *securityContext + podSecurityContext: *podSecurityContext image: repository: ghcr.io/atomicloud/sulfone.zinc/api-amd serviceTree: diff --git a/scripts/local/exec.sh b/scripts/local/exec.sh new file mode 100755 index 0000000..8f0248d --- /dev/null +++ b/scripts/local/exec.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +file="$1" + +# shellcheck disable=SC2124 +dev=${@:2} + +set -eou pipefail + +[ "$file" = '' ] && file="./config/dev.yaml" + +landscape="$(yq -r '.landscape' "$file")" +platform="$(yq -r '.platform' "$file")" +service="$(yq -r '.service' "$file")" + +name="$platform-$service-dev-proxy" +target="pod/$name" + +# shellcheck disable=SC2086 +mirrord exec --context "k3d-$landscape" --target "$target" --fs-mode local -e -n "$platform" -- $dev diff --git a/tasks/Taskfile.stop.yml b/tasks/Taskfile.stop.yml index da92a0c..f0755e3 100644 --- a/tasks/Taskfile.stop.yml +++ b/tasks/Taskfile.stop.yml @@ -6,11 +6,11 @@ tasks: cmds: - task: stop:dev - task: stop:test - stop:dev: + dev: desc: Stop the development workflow cmds: - tilt down - stop:test: + test: desc: Stop the test workflow cmds: - tilt down -- --config config/test.yaml --action test