diff --git a/.gitignore b/.gitignore index 77ee8a3..373ed0a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,12 +4,15 @@ obj/ *.user ## Local runtime data (db, uploads, data-protection keys) -data/ -**/Data/*.db -**/Data/*.db-shm -**/Data/*.db-wal +## NOTE: src/WishNinja/Data/ holds SOURCE (DbContext, entities, migrations) and must be +## committed — only ignore the runtime artifacts that land inside it during local dev. +/data/ +*.db +*.db-shm +*.db-wal src/WishNinja/smoke-data/ -src/WishNinja/Data/ +src/WishNinja/Data/uploads/ +src/WishNinja/Data/keys/ ## IDE .vs/ diff --git a/src/WishNinja/Data/ApplicationDbContext.cs b/src/WishNinja/Data/ApplicationDbContext.cs new file mode 100644 index 0000000..1fac667 --- /dev/null +++ b/src/WishNinja/Data/ApplicationDbContext.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using WishNinja.Data.Entities; + +namespace WishNinja.Data; + +public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) +{ + public DbSet Wishlists => Set(); + public DbSet WishlistShares => Set(); + public DbSet WishlistItems => Set(); + public DbSet Claims => Set(); + public DbSet Invites => Set(); + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + builder.Entity(e => + { + e.HasOne(w => w.Owner) + .WithMany() + .HasForeignKey(w => w.OwnerId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasMany(w => w.Items) + .WithOne(i => i.Wishlist!) + .HasForeignKey(i => i.WishlistId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasMany(w => w.Shares) + .WithOne(s => s.Wishlist!) + .HasForeignKey(s => s.WishlistId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(w => w.OwnerId); + }); + + builder.Entity(e => + { + e.HasOne(s => s.User) + .WithMany() + .HasForeignKey(s => s.UserId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(s => new { s.WishlistId, s.UserId }).IsUnique(); + }); + + builder.Entity(e => + { + e.HasMany(i => i.Claims) + .WithOne(c => c.WishlistItem!) + .HasForeignKey(c => c.WishlistItemId) + .OnDelete(DeleteBehavior.Cascade); + + e.Property(i => i.Price).HasColumnType("decimal(18,2)"); + e.HasIndex(i => i.WishlistId); + }); + + builder.Entity(e => + { + e.HasOne(c => c.ClaimedByUser) + .WithMany() + .HasForeignKey(c => c.ClaimedByUserId) + .OnDelete(DeleteBehavior.Cascade); + + e.HasIndex(c => c.WishlistItemId); + }); + + builder.Entity(e => + { + e.HasOne(i => i.InvitedByUser) + .WithMany() + .HasForeignKey(i => i.InvitedByUserId) + .OnDelete(DeleteBehavior.Restrict); + + e.HasIndex(i => i.TokenHash); + e.HasIndex(i => i.Email); + }); + } +} diff --git a/src/WishNinja/Data/ApplicationUser.cs b/src/WishNinja/Data/ApplicationUser.cs new file mode 100644 index 0000000..365e11f --- /dev/null +++ b/src/WishNinja/Data/ApplicationUser.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Identity; + +namespace WishNinja.Data; + +// Add profile data for application users by adding properties to the ApplicationUser class +public class ApplicationUser : IdentityUser +{ + /// Friendly name shown throughout the app (e.g. on shared wishlists). + public string DisplayName { get; set; } = string.Empty; + + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + + /// When set, the account is disabled and cannot sign in. + public bool IsDisabled { get; set; } +} diff --git a/src/WishNinja/Data/Entities/Claim.cs b/src/WishNinja/Data/Entities/Claim.cs new file mode 100644 index 0000000..5ffb776 --- /dev/null +++ b/src/WishNinja/Data/Entities/Claim.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; + +namespace WishNinja.Data.Entities; + +/// +/// A reservation of (part of) a wishlist item by a non-owner. Crucially, claim data is never +/// surfaced to the list owner so the surprise is preserved. +/// +public class Claim +{ + public int Id { get; set; } + + public int WishlistItemId { get; set; } + public WishlistItem? WishlistItem { get; set; } + + public string ClaimedByUserId { get; set; } = string.Empty; + public ApplicationUser? ClaimedByUser { get; set; } + + /// How many units this claim covers (>= 1). + public int Quantity { get; set; } = 1; + + [MaxLength(500)] + public string? Note { get; set; } + + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; +} diff --git a/src/WishNinja/Data/Entities/Enums.cs b/src/WishNinja/Data/Entities/Enums.cs new file mode 100644 index 0000000..eceee41 --- /dev/null +++ b/src/WishNinja/Data/Entities/Enums.cs @@ -0,0 +1,21 @@ +namespace WishNinja.Data.Entities; + +/// Who is allowed to view a wishlist. +public enum WishlistVisibility +{ + /// Visible to every signed-in member of the instance. + AllMembers = 0, + + /// Visible only to users named in rows. + SpecificUsers = 1, +} + +/// How an item's image is stored. Images are always stored locally — URLs entered in the +/// UI are downloaded and saved as uploaded files, so there is no external-reference kind. +public enum ImageKind +{ + None = 0, + + /// File stored locally under the uploads directory; see . + Uploaded = 1, +} diff --git a/src/WishNinja/Data/Entities/Invite.cs b/src/WishNinja/Data/Entities/Invite.cs new file mode 100644 index 0000000..a92435e --- /dev/null +++ b/src/WishNinja/Data/Entities/Invite.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; + +namespace WishNinja.Data.Entities; + +/// +/// An admin-issued invitation. The raw token is emailed to the recipient; only its hash is stored. +/// Single-use: is set once redeemed. +/// +public class Invite +{ + public int Id { get; set; } + + [Required, MaxLength(256)] + public string Email { get; set; } = string.Empty; + + /// SHA-256 hash (Base64) of the raw token. The raw token is never persisted. + [Required, MaxLength(64)] + public string TokenHash { get; set; } = string.Empty; + + /// Identity role granted on acceptance (e.g. "User" or "Admin"). + [Required, MaxLength(64)] + public string Role { get; set; } = "User"; + + public string InvitedByUserId { get; set; } = string.Empty; + public ApplicationUser? InvitedByUser { get; set; } + + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + public DateTimeOffset ExpiresAt { get; set; } + public DateTimeOffset? AcceptedAt { get; set; } + + public bool IsAccepted => AcceptedAt is not null; +} diff --git a/src/WishNinja/Data/Entities/Wishlist.cs b/src/WishNinja/Data/Entities/Wishlist.cs new file mode 100644 index 0000000..7b41976 --- /dev/null +++ b/src/WishNinja/Data/Entities/Wishlist.cs @@ -0,0 +1,29 @@ +using System.ComponentModel.DataAnnotations; + +namespace WishNinja.Data.Entities; + +public class Wishlist +{ + public int Id { get; set; } + + /// Owner of the list. The owner never sees claim information for their own items. + public string OwnerId { get; set; } = string.Empty; + public ApplicationUser? Owner { get; set; } + + [Required, MaxLength(200)] + public string Title { get; set; } = string.Empty; + + [MaxLength(2000)] + public string? Description { get; set; } + + public WishlistVisibility Visibility { get; set; } = WishlistVisibility.AllMembers; + + public bool IsArchived { get; set; } + + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + + public List Items { get; set; } = new(); + + /// Explicit shares; only meaningful when is SpecificUsers. + public List Shares { get; set; } = new(); +} diff --git a/src/WishNinja/Data/Entities/WishlistItem.cs b/src/WishNinja/Data/Entities/WishlistItem.cs new file mode 100644 index 0000000..3b84597 --- /dev/null +++ b/src/WishNinja/Data/Entities/WishlistItem.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; + +namespace WishNinja.Data.Entities; + +public class WishlistItem +{ + public int Id { get; set; } + + public int WishlistId { get; set; } + public Wishlist? Wishlist { get; set; } + + [Required, MaxLength(300)] + public string Name { get; set; } = string.Empty; + + [MaxLength(2000)] + public string? Description { get; set; } + + /// Link to the product/store page. + [MaxLength(2000)] + public string? ProductUrl { get; set; } + + public ImageKind ImageKind { get; set; } = ImageKind.None; + + /// Relative file name within the uploads directory when is Uploaded. + [MaxLength(260)] + public string? ImagePath { get; set; } + + public decimal? Price { get; set; } + + /// 1 = highest priority. Lower sorts first within a list. + public int Priority { get; set; } = 3; + + /// How many of this item are wanted. Claims are capped at this quantity. + public int Quantity { get; set; } = 1; + + public int SortOrder { get; set; } + + public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; + + public List Claims { get; set; } = new(); +} diff --git a/src/WishNinja/Data/Entities/WishlistShare.cs b/src/WishNinja/Data/Entities/WishlistShare.cs new file mode 100644 index 0000000..e7c074f --- /dev/null +++ b/src/WishNinja/Data/Entities/WishlistShare.cs @@ -0,0 +1,13 @@ +namespace WishNinja.Data.Entities; + +/// Grants a specific user view access to a wishlist whose visibility is SpecificUsers. +public class WishlistShare +{ + public int Id { get; set; } + + public int WishlistId { get; set; } + public Wishlist? Wishlist { get; set; } + + public string UserId { get; set; } = string.Empty; + public ApplicationUser? User { get; set; } +} diff --git a/src/WishNinja/Data/Migrations/20260610205250_InitialCreate.Designer.cs b/src/WishNinja/Data/Migrations/20260610205250_InitialCreate.Designer.cs new file mode 100644 index 0000000..028ab51 --- /dev/null +++ b/src/WishNinja/Data/Migrations/20260610205250_InitialCreate.Designer.cs @@ -0,0 +1,620 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WishNinja.Data; + +#nullable disable + +namespace WishNinja.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260610205250_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + 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"); + + 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"); + + 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") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .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.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("BLOB"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserPasskeys", (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") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("WishNinja.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("IsDisabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Claim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimedByUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("WishlistItemId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClaimedByUserId"); + + b.HasIndex("WishlistItemId"); + + b.ToTable("Claims"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AcceptedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("InvitedByUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InvitedByUserId"); + + b.HasIndex("TokenHash"); + + b.ToTable("Invites"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Wishlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsArchived") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Wishlists"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ImageKind") + .HasColumnType("INTEGER"); + + b.Property("ImagePath") + .HasMaxLength(260) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ProductUrl") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("WishlistId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("WishlistId"); + + b.ToTable("WishlistItems"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WishlistId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("WishlistId", "UserId") + .IsUnique(); + + b.ToTable("WishlistShares"); + }); + + 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("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Microsoft.AspNetCore.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject") + .IsRequired(); + + b1.Property("ClientDataJson") + .IsRequired(); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey") + .IsRequired(); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AspNetUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("TEXT"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data") + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Claim", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "ClaimedByUser") + .WithMany() + .HasForeignKey("ClaimedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WishNinja.Data.Entities.WishlistItem", "WishlistItem") + .WithMany("Claims") + .HasForeignKey("WishlistItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClaimedByUser"); + + b.Navigation("WishlistItem"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Invite", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "InvitedByUser") + .WithMany() + .HasForeignKey("InvitedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvitedByUser"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Wishlist", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistItem", b => + { + b.HasOne("WishNinja.Data.Entities.Wishlist", "Wishlist") + .WithMany("Items") + .HasForeignKey("WishlistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Wishlist"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistShare", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WishNinja.Data.Entities.Wishlist", "Wishlist") + .WithMany("Shares") + .HasForeignKey("WishlistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("Wishlist"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Wishlist", b => + { + b.Navigation("Items"); + + b.Navigation("Shares"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistItem", b => + { + b.Navigation("Claims"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/WishNinja/Data/Migrations/20260610205250_InitialCreate.cs b/src/WishNinja/Data/Migrations/20260610205250_InitialCreate.cs new file mode 100644 index 0000000..3950f14 --- /dev/null +++ b/src/WishNinja/Data/Migrations/20260610205250_InitialCreate.cs @@ -0,0 +1,446 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace WishNinja.Data.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + DisplayName = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + IsDisabled = table.Column(type: "INTEGER", nullable: false), + UserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "TEXT", maxLength: 256, nullable: true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "TEXT", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "INTEGER", nullable: false), + PasswordHash = table.Column(type: "TEXT", nullable: true), + SecurityStamp = table.Column(type: "TEXT", nullable: true), + ConcurrencyStamp = table.Column(type: "TEXT", nullable: true), + PhoneNumber = table.Column(type: "TEXT", maxLength: 256, nullable: true), + PhoneNumberConfirmed = table.Column(type: "INTEGER", nullable: false), + TwoFactorEnabled = table.Column(type: "INTEGER", nullable: false), + LockoutEnd = table.Column(type: "TEXT", nullable: true), + LockoutEnabled = table.Column(type: "INTEGER", nullable: false), + AccessFailedCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + RoleId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "TEXT", nullable: false), + ClaimType = table.Column(type: "TEXT", nullable: true), + ClaimValue = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ProviderKey = table.Column(type: "TEXT", maxLength: 128, nullable: false), + ProviderDisplayName = table.Column(type: "TEXT", nullable: true), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserPasskeys", + columns: table => new + { + CredentialId = table.Column(type: "BLOB", maxLength: 1024, nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + Data = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserPasskeys", x => x.CredentialId); + table.ForeignKey( + name: "FK_AspNetUserPasskeys_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + LoginProvider = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Name = table.Column(type: "TEXT", maxLength: 128, nullable: false), + Value = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Invites", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Email = table.Column(type: "TEXT", maxLength: 256, nullable: false), + TokenHash = table.Column(type: "TEXT", maxLength: 64, nullable: false), + Role = table.Column(type: "TEXT", maxLength: 64, nullable: false), + InvitedByUserId = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false), + ExpiresAt = table.Column(type: "TEXT", nullable: false), + AcceptedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Invites", x => x.Id); + table.ForeignKey( + name: "FK_Invites_AspNetUsers_InvitedByUserId", + column: x => x.InvitedByUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "Wishlists", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + OwnerId = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + Visibility = table.Column(type: "INTEGER", nullable: false), + IsArchived = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Wishlists", x => x.Id); + table.ForeignKey( + name: "FK_Wishlists_AspNetUsers_OwnerId", + column: x => x.OwnerId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "WishlistItems", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + WishlistId = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 300, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + ProductUrl = table.Column(type: "TEXT", maxLength: 2000, nullable: true), + ImageKind = table.Column(type: "INTEGER", nullable: false), + ImagePath = table.Column(type: "TEXT", maxLength: 260, nullable: true), + Price = table.Column(type: "decimal(18,2)", nullable: true), + Priority = table.Column(type: "INTEGER", nullable: false), + Quantity = table.Column(type: "INTEGER", nullable: false), + SortOrder = table.Column(type: "INTEGER", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WishlistItems", x => x.Id); + table.ForeignKey( + name: "FK_WishlistItems_Wishlists_WishlistId", + column: x => x.WishlistId, + principalTable: "Wishlists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "WishlistShares", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + WishlistId = table.Column(type: "INTEGER", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_WishlistShares", x => x.Id); + table.ForeignKey( + name: "FK_WishlistShares_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_WishlistShares_Wishlists_WishlistId", + column: x => x.WishlistId, + principalTable: "Wishlists", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Claims", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + WishlistItemId = table.Column(type: "INTEGER", nullable: false), + ClaimedByUserId = table.Column(type: "TEXT", nullable: false), + Quantity = table.Column(type: "INTEGER", nullable: false), + Note = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Claims", x => x.Id); + table.ForeignKey( + name: "FK_Claims_AspNetUsers_ClaimedByUserId", + column: x => x.ClaimedByUserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Claims_WishlistItems_WishlistItemId", + column: x => x.WishlistItemId, + principalTable: "WishlistItems", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserPasskeys_UserId", + table: "AspNetUserPasskeys", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Claims_ClaimedByUserId", + table: "Claims", + column: "ClaimedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Claims_WishlistItemId", + table: "Claims", + column: "WishlistItemId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_Email", + table: "Invites", + column: "Email"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_InvitedByUserId", + table: "Invites", + column: "InvitedByUserId"); + + migrationBuilder.CreateIndex( + name: "IX_Invites_TokenHash", + table: "Invites", + column: "TokenHash"); + + migrationBuilder.CreateIndex( + name: "IX_WishlistItems_WishlistId", + table: "WishlistItems", + column: "WishlistId"); + + migrationBuilder.CreateIndex( + name: "IX_Wishlists_OwnerId", + table: "Wishlists", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_WishlistShares_UserId", + table: "WishlistShares", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_WishlistShares_WishlistId_UserId", + table: "WishlistShares", + columns: new[] { "WishlistId", "UserId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserPasskeys"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Claims"); + + migrationBuilder.DropTable( + name: "Invites"); + + migrationBuilder.DropTable( + name: "WishlistShares"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "WishlistItems"); + + migrationBuilder.DropTable( + name: "Wishlists"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/WishNinja/Data/Migrations/ApplicationDbContextModelSnapshot.cs b/src/WishNinja/Data/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..bbc42df --- /dev/null +++ b/src/WishNinja/Data/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,617 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using WishNinja.Data; + +#nullable disable + +namespace WishNinja.Data.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "10.0.2"); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + 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"); + + 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"); + + 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") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .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.IdentityUserPasskey", b => + { + b.Property("CredentialId") + .HasMaxLength(1024) + .HasColumnType("BLOB"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("CredentialId"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserPasskeys", (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") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("WishNinja.Data.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("IsDisabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Claim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimedByUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("WishlistItemId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClaimedByUserId"); + + b.HasIndex("WishlistItemId"); + + b.ToTable("Claims"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Invite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AcceptedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExpiresAt") + .HasColumnType("TEXT"); + + b.Property("InvitedByUserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Role") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("InvitedByUserId"); + + b.HasIndex("TokenHash"); + + b.ToTable("Invites"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Wishlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("IsArchived") + .HasColumnType("INTEGER"); + + b.Property("OwnerId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("Wishlists"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("ImageKind") + .HasColumnType("INTEGER"); + + b.Property("ImagePath") + .HasMaxLength(260) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ProductUrl") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Quantity") + .HasColumnType("INTEGER"); + + b.Property("SortOrder") + .HasColumnType("INTEGER"); + + b.Property("WishlistId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("WishlistId"); + + b.ToTable("WishlistItems"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistShare", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("WishlistId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("WishlistId", "UserId") + .IsUnique(); + + b.ToTable("WishlistShares"); + }); + + 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("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserPasskey", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Microsoft.AspNetCore.Identity.IdentityPasskeyData", "Data", b1 => + { + b1.Property("IdentityUserPasskeyCredentialId"); + + b1.Property("AttestationObject") + .IsRequired(); + + b1.Property("ClientDataJson") + .IsRequired(); + + b1.Property("CreatedAt"); + + b1.Property("IsBackedUp"); + + b1.Property("IsBackupEligible"); + + b1.Property("IsUserVerified"); + + b1.Property("Name"); + + b1.Property("PublicKey") + .IsRequired(); + + b1.Property("SignCount"); + + b1.PrimitiveCollection("Transports"); + + b1.HasKey("IdentityUserPasskeyCredentialId"); + + b1.ToTable("AspNetUserPasskeys"); + + b1 + .ToJson("Data") + .HasColumnType("TEXT"); + + b1.WithOwner() + .HasForeignKey("IdentityUserPasskeyCredentialId"); + }); + + b.Navigation("Data") + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Claim", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "ClaimedByUser") + .WithMany() + .HasForeignKey("ClaimedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WishNinja.Data.Entities.WishlistItem", "WishlistItem") + .WithMany("Claims") + .HasForeignKey("WishlistItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClaimedByUser"); + + b.Navigation("WishlistItem"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Invite", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "InvitedByUser") + .WithMany() + .HasForeignKey("InvitedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("InvitedByUser"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Wishlist", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "Owner") + .WithMany() + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistItem", b => + { + b.HasOne("WishNinja.Data.Entities.Wishlist", "Wishlist") + .WithMany("Items") + .HasForeignKey("WishlistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Wishlist"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistShare", b => + { + b.HasOne("WishNinja.Data.ApplicationUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("WishNinja.Data.Entities.Wishlist", "Wishlist") + .WithMany("Shares") + .HasForeignKey("WishlistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("Wishlist"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.Wishlist", b => + { + b.Navigation("Items"); + + b.Navigation("Shares"); + }); + + modelBuilder.Entity("WishNinja.Data.Entities.WishlistItem", b => + { + b.Navigation("Claims"); + }); +#pragma warning restore 612, 618 + } + } +}