diff --git a/src/isnd/Controllers/Packages/PackagesController.Put.cs b/src/isnd/Controllers/Packages/PackagesController.Put.cs index 4c5b1b3..12335ce 100644 --- a/src/isnd/Controllers/Packages/PackagesController.Put.cs +++ b/src/isnd/Controllers/Packages/PackagesController.Put.cs @@ -145,7 +145,7 @@ namespace isnd.Controllers dbContext.PackageVersions.Remove(v); } - // FIXME default type or null + // Default package type must be non-null for composite key consistency. if (types==null || types.Count==0) dbContext.PackageVersions.Add (new PackageVersion{ @@ -157,7 +157,7 @@ namespace isnd.Controllers Revision = version.Revision, IsPrerelease = version.IsPrerelease, FullString = version.ToFullString(), - Type = null, + Type = string.Empty, LatestCommit = commit }); else @@ -171,7 +171,7 @@ namespace isnd.Controllers Patch = version.Patch, IsPrerelease = version.IsPrerelease, FullString = version.ToFullString(), - Type = type.Name, + Type = type.Name ?? string.Empty, LatestCommit = commit }; dbContext.PackageVersions.Add(pkgver); diff --git a/src/isnd/Controllers/Packages/PackagesController.WebViews.cs b/src/isnd/Controllers/Packages/PackagesController.WebViews.cs index eb6c477..6432261 100644 --- a/src/isnd/Controllers/Packages/PackagesController.WebViews.cs +++ b/src/isnd/Controllers/Packages/PackagesController.WebViews.cs @@ -60,6 +60,7 @@ namespace isnd.Controllers { return NotFound(); } + pkgtype ??= string.Empty; // var report = await packageManager.DeletePackageAsync(id, lower, type); var packageVersion = await dbContext.PackageVersions.Include(p => p.Package) @@ -77,6 +78,7 @@ namespace isnd.Controllers public async Task DeleteConfirmed(string PackageId, string FullString, string Type) { + Type ??= string.Empty; PackageVersion packageVersion = await dbContext.PackageVersions .Include(pv => pv.Package) .FirstOrDefaultAsync(m => m.PackageId == PackageId diff --git a/src/isnd/Data/ApplicationDbContext.cs b/src/isnd/Data/ApplicationDbContext.cs index d01c0b6..c965f22 100644 --- a/src/isnd/Data/ApplicationDbContext.cs +++ b/src/isnd/Data/ApplicationDbContext.cs @@ -22,6 +22,19 @@ namespace isnd.Data public ApplicationDbContext(DbContextOptions options) : base(options) { } + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity(entity => + { + entity.HasKey(v => new { v.PackageId, v.FullString, v.Type }); + entity.Property(v => v.Type) + .IsRequired() + .HasDefaultValue(string.Empty); + }); + } + /// /// User API keys /// diff --git a/src/isnd/Data/Packages/PackageVersion.cs b/src/isnd/Data/Packages/PackageVersion.cs index 95c7eb1..f055b12 100644 --- a/src/isnd/Data/Packages/PackageVersion.cs +++ b/src/isnd/Data/Packages/PackageVersion.cs @@ -38,7 +38,6 @@ namespace isnd.Data /// [StringLength(256)] [Required] - [Key] public string FullString { get; set; } public bool IsPrerelease { get; set; } diff --git a/src/isnd/Migrations/20260705211205_pkgver_composite_identity_fix.Designer.cs b/src/isnd/Migrations/20260705211205_pkgver_composite_identity_fix.Designer.cs new file mode 100644 index 0000000..77ef305 --- /dev/null +++ b/src/isnd/Migrations/20260705211205_pkgver_composite_identity_fix.Designer.cs @@ -0,0 +1,457 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using isnd.Data; + +#nullable disable + +namespace isndhost.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260705211205_pkgver_composite_identity_fix")] + partial class pkgver_composite_identity_fix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.9") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("isnd.Data.ApiKeys.ApiKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ValidityPeriodInDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("ApiKeys"); + }); + + modelBuilder.Entity("isnd.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("FullName") + .HasColumnType("text"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("isnd.Data.PackageVersion", b => + { + b.Property("PackageId") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("FullString") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasDefaultValue(""); + + b.Property("CommitNId") + .HasColumnType("bigint"); + + b.Property("IsPrerelease") + .HasColumnType("boolean"); + + b.Property("Major") + .HasColumnType("integer"); + + b.Property("Minor") + .HasColumnType("integer"); + + b.Property("Patch") + .HasColumnType("integer"); + + b.Property("Revision") + .HasColumnType("integer"); + + b.HasKey("PackageId", "FullString", "Type"); + + b.HasIndex("CommitNId"); + + b.ToTable("PackageVersions"); + }); + + modelBuilder.Entity("isnd.Data.Packages.Commit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer"); + + b.Property("TimeStamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Commits"); + }); + + modelBuilder.Entity("isnd.Data.Packages.Package", b => + { + b.Property("Id") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("CommitNId") + .HasColumnType("bigint"); + + b.Property("CommitTimeStamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Public") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("CommitNId"); + + b.HasIndex("OwnerId"); + + b.ToTable("Packages"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("isnd.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("isnd.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("isnd.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("isnd.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("isnd.Data.ApiKeys.ApiKey", b => + { + b.HasOne("isnd.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("isnd.Data.PackageVersion", b => + { + b.HasOne("isnd.Data.Packages.Commit", "LatestCommit") + .WithMany("Versions") + .HasForeignKey("CommitNId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("isnd.Data.Packages.Package", "Package") + .WithMany("Versions") + .HasForeignKey("PackageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LatestCommit"); + + b.Navigation("Package"); + }); + + modelBuilder.Entity("isnd.Data.Packages.Package", b => + { + b.HasOne("isnd.Data.Packages.Commit", "LatestVersion") + .WithMany() + .HasForeignKey("CommitNId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("isnd.Data.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LatestVersion"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("isnd.Data.Packages.Commit", b => + { + b.Navigation("Versions"); + }); + + modelBuilder.Entity("isnd.Data.Packages.Package", b => + { + b.Navigation("Versions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/isnd/Migrations/20260705211205_pkgver_composite_identity_fix.cs b/src/isnd/Migrations/20260705211205_pkgver_composite_identity_fix.cs new file mode 100644 index 0000000..f61688a --- /dev/null +++ b/src/isnd/Migrations/20260705211205_pkgver_composite_identity_fix.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace isndhost.Migrations +{ + /// + public partial class pkgver_composite_identity_fix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_PackageVersions", + table: "PackageVersions"); + + migrationBuilder.DropIndex( + name: "IX_PackageVersions_PackageId", + table: "PackageVersions"); + + migrationBuilder.Sql("UPDATE \"PackageVersions\" SET \"Type\" = '' WHERE \"Type\" IS NULL;"); + + migrationBuilder.AlterColumn( + name: "Type", + table: "PackageVersions", + type: "character varying(256)", + maxLength: 256, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldNullable: true); + + migrationBuilder.AddPrimaryKey( + name: "PK_PackageVersions", + table: "PackageVersions", + columns: new[] { "PackageId", "FullString", "Type" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropPrimaryKey( + name: "PK_PackageVersions", + table: "PackageVersions"); + + migrationBuilder.AlterColumn( + name: "Type", + table: "PackageVersions", + type: "character varying(256)", + maxLength: 256, + nullable: true, + oldClrType: typeof(string), + oldType: "character varying(256)", + oldMaxLength: 256, + oldDefaultValue: ""); + + migrationBuilder.Sql("UPDATE \"PackageVersions\" SET \"Type\" = NULL WHERE \"Type\" = '';"); + + migrationBuilder.AddPrimaryKey( + name: "PK_PackageVersions", + table: "PackageVersions", + column: "FullString"); + + migrationBuilder.CreateIndex( + name: "IX_PackageVersions_PackageId", + table: "PackageVersions", + column: "PackageId"); + } + } +} diff --git a/src/isnd/Migrations/ApplicationDbContextModelSnapshot.cs b/src/isnd/Migrations/ApplicationDbContextModelSnapshot.cs index 1dba50c..917c397 100644 --- a/src/isnd/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/isnd/Migrations/ApplicationDbContextModelSnapshot.cs @@ -249,10 +249,20 @@ namespace isndhost.Migrations modelBuilder.Entity("isnd.Data.PackageVersion", b => { + b.Property("PackageId") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + b.Property("FullString") .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("Type") + .ValueGeneratedOnAdd() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasDefaultValue(""); + b.Property("CommitNId") .HasColumnType("bigint"); @@ -265,27 +275,16 @@ namespace isndhost.Migrations b.Property("Minor") .HasColumnType("integer"); - b.Property("PackageId") - .IsRequired() - .HasMaxLength(1024) - .HasColumnType("character varying(1024)"); - b.Property("Patch") .HasColumnType("integer"); b.Property("Revision") .HasColumnType("integer"); - b.Property("Type") - .HasMaxLength(256) - .HasColumnType("character varying(256)"); - - b.HasKey("FullString"); + b.HasKey("PackageId", "FullString", "Type"); b.HasIndex("CommitNId"); - b.HasIndex("PackageId"); - b.ToTable("PackageVersions"); }); diff --git a/src/isnd/Services/PackageManager.cs b/src/isnd/Services/PackageManager.cs index be2f280..84ac2c0 100644 --- a/src/isnd/Services/PackageManager.cs +++ b/src/isnd/Services/PackageManager.cs @@ -18,6 +18,7 @@ namespace isnd.Services public class PackageManager : IPackageManager { public const string BASE_API_LEVEL = "3.0.0"; + private static string NormalizePackageType(string packageType) => packageType ?? string.Empty; ApplicationDbContext dbContext; @@ -190,6 +191,7 @@ namespace isnd.Services public async Task DeletePackageAsync(string pkgid, string version, string type) { + type = NormalizePackageType(type); // TODO deletion on disk var commit = new Commit { @@ -214,6 +216,7 @@ namespace isnd.Services public async Task GetPackageAsync(string pkgid, string version, string type) { + type = NormalizePackageType(type); return await dbContext.PackageVersions.SingleOrDefaultAsync( v => v.PackageId == pkgid && v.FullString == version && @@ -234,6 +237,7 @@ namespace isnd.Services public async Task UserAskForPackageDeletionAsync(string uid, string id, string lower, string type) { + type = NormalizePackageType(type); PackageVersion packageVersion = await dbContext.PackageVersions .Include(pv => pv.Package) .FirstOrDefaultAsync(m => m.PackageId == id