From 3d3a5c2a5e15a3c8aa233cbbd6c3f17c3c0dd962 Mon Sep 17 00:00:00 2001 From: bob Date: Fri, 29 May 2026 12:21:42 -0500 Subject: [PATCH] init efcore --- .gitignore | 100 +++ .../BlueLaminate.EFCore.csproj | 19 + .../InventoryItemConfiguration.cs | 21 + .../PriceHistoryConfiguration.cs | 24 + .../SkinConditionConfiguration.cs | 18 + .../Configurations/SkinConfiguration.cs | 27 + .../SkinInstanceConfiguration.cs | 26 + .../Configurations/SteamUserConfiguration.cs | 13 + .../Configurations/TradeConfiguration.cs | 21 + .../Configurations/TradeItemConfiguration.cs | 19 + .../Data/SkinTrackerDbContext.cs | 42 ++ .../Data/SkinTrackerDbContextFactory.cs | 21 + .../ServiceCollectionExtensions.cs | 20 + .../Entities/InventoryItem.cs | 16 + .../Entities/PriceHistory.cs | 15 + .../BlueLaminate.EFCore/Entities/Skin.cs | 24 + .../Entities/SkinCondition.cs | 15 + .../Entities/SkinInstance.cs | 20 + .../BlueLaminate.EFCore/Entities/SteamUser.cs | 13 + .../BlueLaminate.EFCore/Entities/Trade.cs | 15 + .../BlueLaminate.EFCore/Entities/TradeItem.cs | 10 + .../BlueLaminate.EFCore/Entities/Weapon.cs | 11 + .../20260529170710_InitialCreate.Designer.cs | 573 ++++++++++++++++++ .../20260529170710_InitialCreate.cs | 354 +++++++++++ .../SkinTrackerDbContextModelSnapshot.cs | 570 +++++++++++++++++ BlueLaminate/BlueLaminate.EFCore/Program.cs | 7 + BlueLaminate/BlueLaminate.slnx | 3 + WeaponGrabber/WeaponScraper.py | 54 ++ 28 files changed, 2071 insertions(+) create mode 100644 .gitignore create mode 100644 BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/InventoryItemConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/PriceHistoryConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConditionConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/SkinInstanceConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/SteamUserConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/TradeConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Configurations/TradeItemConfiguration.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/InventoryItem.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/PriceHistory.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/Skin.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/SkinCondition.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/SkinInstance.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/SteamUser.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/Trade.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/TradeItem.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Entities/Weapon.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.Designer.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs create mode 100644 BlueLaminate/BlueLaminate.EFCore/Program.cs create mode 100644 BlueLaminate/BlueLaminate.slnx create mode 100644 WeaponGrabber/WeaponScraper.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c0a26a --- /dev/null +++ b/.gitignore @@ -0,0 +1,100 @@ +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio / Rider / VS Code +.vs/ +.vscode/ +.idea/ +*.user +*.suo +*.userprefs +*.rsuser +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# NuGet +*.nupkg +*.snupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ +*.nuget.props +*.nuget.targets +project.lock.json +project.fragment.lock.json +artifacts/ + +# MSBuild / test results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.VisualState.xml +TestResult.xml +[Bb]uild[Tt]ools/ +*.coverage +*.coveragexml +coverage*.json +coverage*.xml +coverage*.info + +# EF Core +*.mdf +*.ldf +*.ndf + +# Misc +*.dbmdl +*.dbproj.schemaview +*.jfm +.AppleDouble +.LSOverride +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ + +# Python (WeaponGrabber) +__pycache__/ +*.py[cod] +.venv/ +venv/ +env/ +*.egg-info/ +.pytest_cache/ diff --git a/BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj b/BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj new file mode 100644 index 0000000..264f4e1 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/BlueLaminate.EFCore.csproj @@ -0,0 +1,19 @@ + + + + Exe + net10.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/InventoryItemConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/InventoryItemConfiguration.cs new file mode 100644 index 0000000..ee27762 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/InventoryItemConfiguration.cs @@ -0,0 +1,21 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class InventoryItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.HasIndex(e => e.AssetId); + + entity.HasOne(e => e.User) + .WithMany(u => u.InventoryItems) + .HasForeignKey(e => e.UserId); + + entity.HasOne(e => e.SkinInstance) + .WithMany(i => i.InventoryItems) + .HasForeignKey(e => e.SkinInstanceId); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/PriceHistoryConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/PriceHistoryConfiguration.cs new file mode 100644 index 0000000..47d3b52 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/PriceHistoryConfiguration.cs @@ -0,0 +1,24 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class PriceHistoryConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.Property(e => e.Price).HasPrecision(18, 2); + + entity.HasIndex(e => new { e.SkinId, e.ConditionId, e.RecordedAt }); + + entity.HasOne(e => e.Skin) + .WithMany(s => s.PriceHistories) + .HasForeignKey(e => e.SkinId); + + entity.HasOne(e => e.Condition) + .WithMany(c => c.PriceHistories) + .HasForeignKey(e => e.ConditionId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConditionConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConditionConfiguration.cs new file mode 100644 index 0000000..b262006 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConditionConfiguration.cs @@ -0,0 +1,18 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class SkinConditionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.Property(e => e.MinFloat).HasColumnType("numeric(10,9)"); + entity.Property(e => e.MaxFloat).HasColumnType("numeric(10,9)"); + + entity.HasOne(e => e.Skin) + .WithMany(s => s.Conditions) + .HasForeignKey(e => e.SkinId); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConfiguration.cs new file mode 100644 index 0000000..18f87f4 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinConfiguration.cs @@ -0,0 +1,27 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class SkinConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.Property(e => e.FloatMin) + .HasColumnType("numeric(10,9)") + .HasDefaultValue(0.0m); + entity.Property(e => e.FloatMax) + .HasColumnType("numeric(10,9)") + .HasDefaultValue(1.0m); + + entity.Property(e => e.TrueFloat) + .HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", stored: true); + + entity.HasIndex(e => e.TrueFloat); + + entity.HasOne(e => e.Weapon) + .WithMany(w => w.Skins) + .HasForeignKey(e => e.WeaponId); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinInstanceConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinInstanceConfiguration.cs new file mode 100644 index 0000000..3308d6f --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/SkinInstanceConfiguration.cs @@ -0,0 +1,26 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class SkinInstanceConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.Property(e => e.FloatValue).HasColumnType("numeric(10,9)"); + + // Primary lookup key for trade fingerprinting. + entity.HasIndex(e => e.FloatValue); + entity.HasIndex(e => e.PaintSeed); + + entity.HasOne(e => e.Skin) + .WithMany(s => s.Instances) + .HasForeignKey(e => e.SkinId); + + entity.HasOne(e => e.Condition) + .WithMany(c => c.Instances) + .HasForeignKey(e => e.ConditionId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/SteamUserConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/SteamUserConfiguration.cs new file mode 100644 index 0000000..6240d8a --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/SteamUserConfiguration.cs @@ -0,0 +1,13 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class SteamUserConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.HasIndex(e => e.SteamId).IsUnique(); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/TradeConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/TradeConfiguration.cs new file mode 100644 index 0000000..92d4038 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/TradeConfiguration.cs @@ -0,0 +1,21 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class TradeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.HasOne(e => e.FromUser) + .WithMany(u => u.TradesSent) + .HasForeignKey(e => e.FromUserId) + .OnDelete(DeleteBehavior.Restrict); + + entity.HasOne(e => e.ToUser) + .WithMany(u => u.TradesReceived) + .HasForeignKey(e => e.ToUserId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Configurations/TradeItemConfiguration.cs b/BlueLaminate/BlueLaminate.EFCore/Configurations/TradeItemConfiguration.cs new file mode 100644 index 0000000..66f5d7b --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Configurations/TradeItemConfiguration.cs @@ -0,0 +1,19 @@ +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace BlueLaminate.EFCore.Configurations; + +public class TradeItemConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder entity) + { + entity.HasOne(e => e.Trade) + .WithMany(t => t.TradeItems) + .HasForeignKey(e => e.TradeId); + + entity.HasOne(e => e.InventoryItem) + .WithMany(i => i.TradeItems) + .HasForeignKey(e => e.InventoryItemId); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs new file mode 100644 index 0000000..f0bd2da --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContext.cs @@ -0,0 +1,42 @@ +using BlueLaminate.EFCore.Configurations; +using BlueLaminate.EFCore.Entities; +using Microsoft.EntityFrameworkCore; + +namespace BlueLaminate.EFCore.Data; + +public class SkinTrackerDbContext : DbContext +{ + static SkinTrackerDbContext() + { + // Store and read all timestamps as UTC (timestamptz). Required so that + // DateTimeOffset properties round-trip correctly with Npgsql. + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", false); + } + + public SkinTrackerDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Weapons => Set(); + public DbSet Skins => Set(); + public DbSet SkinConditions => Set(); + public DbSet SteamUsers => Set(); + public DbSet SkinInstances => Set(); + public DbSet InventoryItems => Set(); + public DbSet Trades => Set(); + public DbSet TradeItems => Set(); + public DbSet PriceHistories => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfiguration(new SkinConfiguration()); + modelBuilder.ApplyConfiguration(new SkinConditionConfiguration()); + modelBuilder.ApplyConfiguration(new SteamUserConfiguration()); + modelBuilder.ApplyConfiguration(new SkinInstanceConfiguration()); + modelBuilder.ApplyConfiguration(new InventoryItemConfiguration()); + modelBuilder.ApplyConfiguration(new TradeConfiguration()); + modelBuilder.ApplyConfiguration(new TradeItemConfiguration()); + modelBuilder.ApplyConfiguration(new PriceHistoryConfiguration()); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs new file mode 100644 index 0000000..5a3f7a0 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Data/SkinTrackerDbContextFactory.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace BlueLaminate.EFCore.Data; + +public class SkinTrackerDbContextFactory : IDesignTimeDbContextFactory +{ + public SkinTrackerDbContext CreateDbContext(string[] args) + { + var connectionString = + Environment.GetEnvironmentVariable("SKINTRACKER_CONNECTION") + ?? "Host=localhost;Port=5432;Database=skintracker;Username=postgres;Password=postgres"; + + var options = new DbContextOptionsBuilder() + .UseNpgsql(connectionString) + .UseSnakeCaseNamingConvention() + .Options; + + return new SkinTrackerDbContext(options); + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs b/BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..930331f --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/DependencyInjection/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using BlueLaminate.EFCore.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace BlueLaminate.EFCore.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddSkinTrackerData( + this IServiceCollection services, + string connectionString) + { + services.AddDbContext(options => + options + .UseNpgsql(connectionString) + .UseSnakeCaseNamingConvention()); + + return services; + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/InventoryItem.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/InventoryItem.cs new file mode 100644 index 0000000..de64429 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/InventoryItem.cs @@ -0,0 +1,16 @@ +namespace BlueLaminate.EFCore.Entities; + +public class InventoryItem +{ + public int Id { get; set; } + public int UserId { get; set; } + public SteamUser User { get; set; } = null!; + public int SkinInstanceId { get; set; } + public SkinInstance SkinInstance { get; set; } = null!; + + // Steam asset ID — changes on trade, not a stable identifier. + public string AssetId { get; set; } = null!; + public DateTimeOffset AcquiredAt { get; set; } + + public ICollection TradeItems { get; set; } = new List(); +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/PriceHistory.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/PriceHistory.cs new file mode 100644 index 0000000..8bc79c3 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/PriceHistory.cs @@ -0,0 +1,15 @@ +namespace BlueLaminate.EFCore.Entities; + +public class PriceHistory +{ + public int Id { get; set; } + public int SkinId { get; set; } + public Skin Skin { get; set; } = null!; + public int ConditionId { get; set; } + public SkinCondition Condition { get; set; } = null!; + + public decimal Price { get; set; } + public string Currency { get; set; } = null!; + public DateTimeOffset RecordedAt { get; set; } + public string Source { get; set; } = null!; +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/Skin.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/Skin.cs new file mode 100644 index 0000000..2272d80 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/Skin.cs @@ -0,0 +1,24 @@ +namespace BlueLaminate.EFCore.Entities; + +public class Skin +{ + public int Id { get; set; } + public int WeaponId { get; set; } + public Weapon Weapon { get; set; } = null!; + + public string Name { get; set; } = null!; + public string Rarity { get; set; } = null!; + public string? Description { get; set; } + public string? ImageUrl { get; set; } + + public decimal FloatMin { get; set; } + public decimal FloatMax { get; set; } + + // Computed in the database: float_min = 0.0 AND float_max = 1.0. + // A skin with a capped float range behaves differently in tradeup calculations. + public bool TrueFloat { get; private set; } + + public ICollection Conditions { get; set; } = new List(); + public ICollection Instances { get; set; } = new List(); + public ICollection PriceHistories { get; set; } = new List(); +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/SkinCondition.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/SkinCondition.cs new file mode 100644 index 0000000..e3525fb --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/SkinCondition.cs @@ -0,0 +1,15 @@ +namespace BlueLaminate.EFCore.Entities; + +public class SkinCondition +{ + public int Id { get; set; } + public int SkinId { get; set; } + public Skin Skin { get; set; } = null!; + + public string Condition { get; set; } = null!; + public decimal MinFloat { get; set; } + public decimal MaxFloat { get; set; } + + public ICollection Instances { get; set; } = new List(); + public ICollection PriceHistories { get; set; } = new List(); +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/SkinInstance.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/SkinInstance.cs new file mode 100644 index 0000000..0a4492c --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/SkinInstance.cs @@ -0,0 +1,20 @@ +namespace BlueLaminate.EFCore.Entities; + +public class SkinInstance +{ + public int Id { get; set; } + public int SkinId { get; set; } + public Skin Skin { get; set; } = null!; + public int ConditionId { get; set; } + public SkinCondition Condition { get; set; } = null!; + + // FloatValue + PaintSeed form a stable fingerprint across trades; the Steam + // asset_id changes on every trade but these do not. + public decimal FloatValue { get; set; } + public string PaintSeed { get; set; } = null!; + public bool StatTrak { get; set; } + public bool Souvenir { get; set; } + public DateTimeOffset FirstSeenAt { get; set; } + + public ICollection InventoryItems { get; set; } = new List(); +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/SteamUser.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/SteamUser.cs new file mode 100644 index 0000000..723e4b1 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/SteamUser.cs @@ -0,0 +1,13 @@ +namespace BlueLaminate.EFCore.Entities; + +public class SteamUser +{ + public int Id { get; set; } + public string SteamId { get; set; } = null!; + public string? DisplayName { get; set; } + public DateTimeOffset LastSyncedAt { get; set; } + + public ICollection InventoryItems { get; set; } = new List(); + public ICollection TradesSent { get; set; } = new List(); + public ICollection TradesReceived { get; set; } = new List(); +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/Trade.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/Trade.cs new file mode 100644 index 0000000..a7b8e6f --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/Trade.cs @@ -0,0 +1,15 @@ +namespace BlueLaminate.EFCore.Entities; + +public class Trade +{ + public int Id { get; set; } + public int FromUserId { get; set; } + public SteamUser FromUser { get; set; } = null!; + public int ToUserId { get; set; } + public SteamUser ToUser { get; set; } = null!; + + public DateTimeOffset TradedAt { get; set; } + public string? SteamTradeId { get; set; } + + public ICollection TradeItems { get; set; } = new List(); +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/TradeItem.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/TradeItem.cs new file mode 100644 index 0000000..aea0d07 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/TradeItem.cs @@ -0,0 +1,10 @@ +namespace BlueLaminate.EFCore.Entities; + +public class TradeItem +{ + public int Id { get; set; } + public int TradeId { get; set; } + public Trade Trade { get; set; } = null!; + public int InventoryItemId { get; set; } + public InventoryItem InventoryItem { get; set; } = null!; +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Entities/Weapon.cs b/BlueLaminate/BlueLaminate.EFCore/Entities/Weapon.cs new file mode 100644 index 0000000..17473ac --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Entities/Weapon.cs @@ -0,0 +1,11 @@ +namespace BlueLaminate.EFCore.Entities; + +public class Weapon +{ + public int Id { get; set; } + public string Name { get; set; } = null!; + public string Type { get; set; } = null!; + public string Team { get; set; } = null!; + + public ICollection Skins { get; set; } = new List(); +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.Designer.cs b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.Designer.cs new file mode 100644 index 0000000..c7edb91 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.Designer.cs @@ -0,0 +1,573 @@ +// +using System; +using BlueLaminate.EFCore.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BlueLaminate.EFCore.Migrations +{ + [DbContext(typeof(SkinTrackerDbContext))] + [Migration("20260529170710_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_at"); + + b.Property("AssetId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("asset_id"); + + b.Property("SkinInstanceId") + .HasColumnType("integer") + .HasColumnName("skin_instance_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_inventory_items"); + + b.HasIndex("AssetId") + .HasDatabaseName("ix_inventory_items_asset_id"); + + b.HasIndex("SkinInstanceId") + .HasDatabaseName("ix_inventory_items_skin_instance_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_inventory_items_user_id"); + + b.ToTable("inventory_items", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.PriceHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionId") + .HasColumnType("integer") + .HasColumnName("condition_id"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text") + .HasColumnName("currency"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasColumnName("price"); + + b.Property("RecordedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("recorded_at"); + + b.Property("SkinId") + .HasColumnType("integer") + .HasColumnName("skin_id"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("source"); + + b.HasKey("Id") + .HasName("pk_price_histories"); + + b.HasIndex("ConditionId") + .HasDatabaseName("ix_price_histories_condition_id"); + + b.HasIndex("SkinId", "ConditionId", "RecordedAt") + .HasDatabaseName("ix_price_histories_skin_id_condition_id_recorded_at"); + + b.ToTable("price_histories", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FloatMax") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(10,9)") + .HasDefaultValue(1.0m) + .HasColumnName("float_max"); + + b.Property("FloatMin") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(10,9)") + .HasDefaultValue(0.0m) + .HasColumnName("float_min"); + + b.Property("ImageUrl") + .HasColumnType("text") + .HasColumnName("image_url"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Rarity") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rarity"); + + b.Property("TrueFloat") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("boolean") + .HasColumnName("true_float") + .HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", true); + + b.Property("WeaponId") + .HasColumnType("integer") + .HasColumnName("weapon_id"); + + b.HasKey("Id") + .HasName("pk_skins"); + + b.HasIndex("TrueFloat") + .HasDatabaseName("ix_skins_true_float"); + + b.HasIndex("WeaponId") + .HasDatabaseName("ix_skins_weapon_id"); + + b.ToTable("skins", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Condition") + .IsRequired() + .HasColumnType("text") + .HasColumnName("condition"); + + b.Property("MaxFloat") + .HasColumnType("numeric(10,9)") + .HasColumnName("max_float"); + + b.Property("MinFloat") + .HasColumnType("numeric(10,9)") + .HasColumnName("min_float"); + + b.Property("SkinId") + .HasColumnType("integer") + .HasColumnName("skin_id"); + + b.HasKey("Id") + .HasName("pk_skin_conditions"); + + b.HasIndex("SkinId") + .HasDatabaseName("ix_skin_conditions_skin_id"); + + b.ToTable("skin_conditions", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionId") + .HasColumnType("integer") + .HasColumnName("condition_id"); + + b.Property("FirstSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_at"); + + b.Property("FloatValue") + .HasColumnType("numeric(10,9)") + .HasColumnName("float_value"); + + b.Property("PaintSeed") + .IsRequired() + .HasColumnType("text") + .HasColumnName("paint_seed"); + + b.Property("SkinId") + .HasColumnType("integer") + .HasColumnName("skin_id"); + + b.Property("Souvenir") + .HasColumnType("boolean") + .HasColumnName("souvenir"); + + b.Property("StatTrak") + .HasColumnType("boolean") + .HasColumnName("stat_trak"); + + b.HasKey("Id") + .HasName("pk_skin_instances"); + + b.HasIndex("ConditionId") + .HasDatabaseName("ix_skin_instances_condition_id"); + + b.HasIndex("FloatValue") + .HasDatabaseName("ix_skin_instances_float_value"); + + b.HasIndex("PaintSeed") + .HasDatabaseName("ix_skin_instances_paint_seed"); + + b.HasIndex("SkinId") + .HasDatabaseName("ix_skin_instances_skin_id"); + + b.ToTable("skin_instances", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SteamUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("LastSyncedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced_at"); + + b.Property("SteamId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("steam_id"); + + b.HasKey("Id") + .HasName("pk_steam_users"); + + b.HasIndex("SteamId") + .IsUnique() + .HasDatabaseName("ix_steam_users_steam_id"); + + b.ToTable("steam_users", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FromUserId") + .HasColumnType("integer") + .HasColumnName("from_user_id"); + + b.Property("SteamTradeId") + .HasColumnType("text") + .HasColumnName("steam_trade_id"); + + b.Property("ToUserId") + .HasColumnType("integer") + .HasColumnName("to_user_id"); + + b.Property("TradedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("traded_at"); + + b.HasKey("Id") + .HasName("pk_trades"); + + b.HasIndex("FromUserId") + .HasDatabaseName("ix_trades_from_user_id"); + + b.HasIndex("ToUserId") + .HasDatabaseName("ix_trades_to_user_id"); + + b.ToTable("trades", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.TradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InventoryItemId") + .HasColumnType("integer") + .HasColumnName("inventory_item_id"); + + b.Property("TradeId") + .HasColumnType("integer") + .HasColumnName("trade_id"); + + b.HasKey("Id") + .HasName("pk_trade_items"); + + b.HasIndex("InventoryItemId") + .HasDatabaseName("ix_trade_items_inventory_item_id"); + + b.HasIndex("TradeId") + .HasDatabaseName("ix_trade_items_trade_id"); + + b.ToTable("trade_items", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Weapon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Team") + .IsRequired() + .HasColumnType("text") + .HasColumnName("team"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_weapons"); + + b.ToTable("weapons", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SkinInstance", "SkinInstance") + .WithMany("InventoryItems") + .HasForeignKey("SkinInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inventory_items_skin_instances_skin_instance_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.SteamUser", "User") + .WithMany("InventoryItems") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inventory_items_steam_users_user_id"); + + b.Navigation("SkinInstance"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.PriceHistory", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "Condition") + .WithMany("PriceHistories") + .HasForeignKey("ConditionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_price_histories_skin_conditions_condition_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin") + .WithMany("PriceHistories") + .HasForeignKey("SkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_price_histories_skins_skin_id"); + + b.Navigation("Condition"); + + b.Navigation("Skin"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.Weapon", "Weapon") + .WithMany("Skins") + .HasForeignKey("WeaponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_skins_weapons_weapon_id"); + + b.Navigation("Weapon"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin") + .WithMany("Conditions") + .HasForeignKey("SkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_skin_conditions_skins_skin_id"); + + b.Navigation("Skin"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "Condition") + .WithMany("Instances") + .HasForeignKey("ConditionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_skin_instances_skin_conditions_condition_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin") + .WithMany("Instances") + .HasForeignKey("SkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_skin_instances_skins_skin_id"); + + b.Navigation("Condition"); + + b.Navigation("Skin"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SteamUser", "FromUser") + .WithMany("TradesSent") + .HasForeignKey("FromUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_trades_steam_users_from_user_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.SteamUser", "ToUser") + .WithMany("TradesReceived") + .HasForeignKey("ToUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_trades_steam_users_to_user_id"); + + b.Navigation("FromUser"); + + b.Navigation("ToUser"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.TradeItem", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.InventoryItem", "InventoryItem") + .WithMany("TradeItems") + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_trade_items_inventory_items_inventory_item_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.Trade", "Trade") + .WithMany("TradeItems") + .HasForeignKey("TradeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_trade_items_trades_trade_id"); + + b.Navigation("InventoryItem"); + + b.Navigation("Trade"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => + { + b.Navigation("TradeItems"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => + { + b.Navigation("Conditions"); + + b.Navigation("Instances"); + + b.Navigation("PriceHistories"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => + { + b.Navigation("Instances"); + + b.Navigation("PriceHistories"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SteamUser", b => + { + b.Navigation("InventoryItems"); + + b.Navigation("TradesReceived"); + + b.Navigation("TradesSent"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => + { + b.Navigation("TradeItems"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Weapon", b => + { + b.Navigation("Skins"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.cs b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.cs new file mode 100644 index 0000000..9b237fc --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Migrations/20260529170710_InitialCreate.cs @@ -0,0 +1,354 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BlueLaminate.EFCore.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "steam_users", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + steam_id = table.Column(type: "text", nullable: false), + display_name = table.Column(type: "text", nullable: true), + last_synced_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_steam_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "weapons", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + name = table.Column(type: "text", nullable: false), + type = table.Column(type: "text", nullable: false), + team = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_weapons", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "trades", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + from_user_id = table.Column(type: "integer", nullable: false), + to_user_id = table.Column(type: "integer", nullable: false), + traded_at = table.Column(type: "timestamp with time zone", nullable: false), + steam_trade_id = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("pk_trades", x => x.id); + table.ForeignKey( + name: "fk_trades_steam_users_from_user_id", + column: x => x.from_user_id, + principalTable: "steam_users", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_trades_steam_users_to_user_id", + column: x => x.to_user_id, + principalTable: "steam_users", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "skins", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + weapon_id = table.Column(type: "integer", nullable: false), + name = table.Column(type: "text", nullable: false), + rarity = table.Column(type: "text", nullable: false), + description = table.Column(type: "text", nullable: true), + image_url = table.Column(type: "text", nullable: true), + float_min = table.Column(type: "numeric(10,9)", nullable: false, defaultValue: 0.0m), + float_max = table.Column(type: "numeric(10,9)", nullable: false, defaultValue: 1.0m), + true_float = table.Column(type: "boolean", nullable: false, computedColumnSql: "float_min = 0.0 AND float_max = 1.0", stored: true) + }, + constraints: table => + { + table.PrimaryKey("pk_skins", x => x.id); + table.ForeignKey( + name: "fk_skins_weapons_weapon_id", + column: x => x.weapon_id, + principalTable: "weapons", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "skin_conditions", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + skin_id = table.Column(type: "integer", nullable: false), + condition = table.Column(type: "text", nullable: false), + min_float = table.Column(type: "numeric(10,9)", nullable: false), + max_float = table.Column(type: "numeric(10,9)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_skin_conditions", x => x.id); + table.ForeignKey( + name: "fk_skin_conditions_skins_skin_id", + column: x => x.skin_id, + principalTable: "skins", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "price_histories", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + skin_id = table.Column(type: "integer", nullable: false), + condition_id = table.Column(type: "integer", nullable: false), + price = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false), + currency = table.Column(type: "text", nullable: false), + recorded_at = table.Column(type: "timestamp with time zone", nullable: false), + source = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_price_histories", x => x.id); + table.ForeignKey( + name: "fk_price_histories_skin_conditions_condition_id", + column: x => x.condition_id, + principalTable: "skin_conditions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_price_histories_skins_skin_id", + column: x => x.skin_id, + principalTable: "skins", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "skin_instances", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + skin_id = table.Column(type: "integer", nullable: false), + condition_id = table.Column(type: "integer", nullable: false), + float_value = table.Column(type: "numeric(10,9)", nullable: false), + paint_seed = table.Column(type: "text", nullable: false), + stat_trak = table.Column(type: "boolean", nullable: false), + souvenir = table.Column(type: "boolean", nullable: false), + first_seen_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_skin_instances", x => x.id); + table.ForeignKey( + name: "fk_skin_instances_skin_conditions_condition_id", + column: x => x.condition_id, + principalTable: "skin_conditions", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "fk_skin_instances_skins_skin_id", + column: x => x.skin_id, + principalTable: "skins", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "inventory_items", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + user_id = table.Column(type: "integer", nullable: false), + skin_instance_id = table.Column(type: "integer", nullable: false), + asset_id = table.Column(type: "text", nullable: false), + acquired_at = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_inventory_items", x => x.id); + table.ForeignKey( + name: "fk_inventory_items_skin_instances_skin_instance_id", + column: x => x.skin_instance_id, + principalTable: "skin_instances", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_inventory_items_steam_users_user_id", + column: x => x.user_id, + principalTable: "steam_users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "trade_items", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + trade_id = table.Column(type: "integer", nullable: false), + inventory_item_id = table.Column(type: "integer", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_trade_items", x => x.id); + table.ForeignKey( + name: "fk_trade_items_inventory_items_inventory_item_id", + column: x => x.inventory_item_id, + principalTable: "inventory_items", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "fk_trade_items_trades_trade_id", + column: x => x.trade_id, + principalTable: "trades", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "ix_inventory_items_asset_id", + table: "inventory_items", + column: "asset_id"); + + migrationBuilder.CreateIndex( + name: "ix_inventory_items_skin_instance_id", + table: "inventory_items", + column: "skin_instance_id"); + + migrationBuilder.CreateIndex( + name: "ix_inventory_items_user_id", + table: "inventory_items", + column: "user_id"); + + migrationBuilder.CreateIndex( + name: "ix_price_histories_condition_id", + table: "price_histories", + column: "condition_id"); + + migrationBuilder.CreateIndex( + name: "ix_price_histories_skin_id_condition_id_recorded_at", + table: "price_histories", + columns: new[] { "skin_id", "condition_id", "recorded_at" }); + + migrationBuilder.CreateIndex( + name: "ix_skin_conditions_skin_id", + table: "skin_conditions", + column: "skin_id"); + + migrationBuilder.CreateIndex( + name: "ix_skin_instances_condition_id", + table: "skin_instances", + column: "condition_id"); + + migrationBuilder.CreateIndex( + name: "ix_skin_instances_float_value", + table: "skin_instances", + column: "float_value"); + + migrationBuilder.CreateIndex( + name: "ix_skin_instances_paint_seed", + table: "skin_instances", + column: "paint_seed"); + + migrationBuilder.CreateIndex( + name: "ix_skin_instances_skin_id", + table: "skin_instances", + column: "skin_id"); + + migrationBuilder.CreateIndex( + name: "ix_skins_true_float", + table: "skins", + column: "true_float"); + + migrationBuilder.CreateIndex( + name: "ix_skins_weapon_id", + table: "skins", + column: "weapon_id"); + + migrationBuilder.CreateIndex( + name: "ix_steam_users_steam_id", + table: "steam_users", + column: "steam_id", + unique: true); + + migrationBuilder.CreateIndex( + name: "ix_trade_items_inventory_item_id", + table: "trade_items", + column: "inventory_item_id"); + + migrationBuilder.CreateIndex( + name: "ix_trade_items_trade_id", + table: "trade_items", + column: "trade_id"); + + migrationBuilder.CreateIndex( + name: "ix_trades_from_user_id", + table: "trades", + column: "from_user_id"); + + migrationBuilder.CreateIndex( + name: "ix_trades_to_user_id", + table: "trades", + column: "to_user_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "price_histories"); + + migrationBuilder.DropTable( + name: "trade_items"); + + migrationBuilder.DropTable( + name: "inventory_items"); + + migrationBuilder.DropTable( + name: "trades"); + + migrationBuilder.DropTable( + name: "skin_instances"); + + migrationBuilder.DropTable( + name: "steam_users"); + + migrationBuilder.DropTable( + name: "skin_conditions"); + + migrationBuilder.DropTable( + name: "skins"); + + migrationBuilder.DropTable( + name: "weapons"); + } + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs b/BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs new file mode 100644 index 0000000..4c686a4 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Migrations/SkinTrackerDbContextModelSnapshot.cs @@ -0,0 +1,570 @@ +// +using System; +using BlueLaminate.EFCore.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace BlueLaminate.EFCore.Migrations +{ + [DbContext(typeof(SkinTrackerDbContext))] + partial class SkinTrackerDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AcquiredAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("acquired_at"); + + b.Property("AssetId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("asset_id"); + + b.Property("SkinInstanceId") + .HasColumnType("integer") + .HasColumnName("skin_instance_id"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_inventory_items"); + + b.HasIndex("AssetId") + .HasDatabaseName("ix_inventory_items_asset_id"); + + b.HasIndex("SkinInstanceId") + .HasDatabaseName("ix_inventory_items_skin_instance_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_inventory_items_user_id"); + + b.ToTable("inventory_items", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.PriceHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionId") + .HasColumnType("integer") + .HasColumnName("condition_id"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("text") + .HasColumnName("currency"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("numeric(18,2)") + .HasColumnName("price"); + + b.Property("RecordedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("recorded_at"); + + b.Property("SkinId") + .HasColumnType("integer") + .HasColumnName("skin_id"); + + b.Property("Source") + .IsRequired() + .HasColumnType("text") + .HasColumnName("source"); + + b.HasKey("Id") + .HasName("pk_price_histories"); + + b.HasIndex("ConditionId") + .HasDatabaseName("ix_price_histories_condition_id"); + + b.HasIndex("SkinId", "ConditionId", "RecordedAt") + .HasDatabaseName("ix_price_histories_skin_id_condition_id_recorded_at"); + + b.ToTable("price_histories", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("FloatMax") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(10,9)") + .HasDefaultValue(1.0m) + .HasColumnName("float_max"); + + b.Property("FloatMin") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(10,9)") + .HasDefaultValue(0.0m) + .HasColumnName("float_min"); + + b.Property("ImageUrl") + .HasColumnType("text") + .HasColumnName("image_url"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Rarity") + .IsRequired() + .HasColumnType("text") + .HasColumnName("rarity"); + + b.Property("TrueFloat") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("boolean") + .HasColumnName("true_float") + .HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", true); + + b.Property("WeaponId") + .HasColumnType("integer") + .HasColumnName("weapon_id"); + + b.HasKey("Id") + .HasName("pk_skins"); + + b.HasIndex("TrueFloat") + .HasDatabaseName("ix_skins_true_float"); + + b.HasIndex("WeaponId") + .HasDatabaseName("ix_skins_weapon_id"); + + b.ToTable("skins", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Condition") + .IsRequired() + .HasColumnType("text") + .HasColumnName("condition"); + + b.Property("MaxFloat") + .HasColumnType("numeric(10,9)") + .HasColumnName("max_float"); + + b.Property("MinFloat") + .HasColumnType("numeric(10,9)") + .HasColumnName("min_float"); + + b.Property("SkinId") + .HasColumnType("integer") + .HasColumnName("skin_id"); + + b.HasKey("Id") + .HasName("pk_skin_conditions"); + + b.HasIndex("SkinId") + .HasDatabaseName("ix_skin_conditions_skin_id"); + + b.ToTable("skin_conditions", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConditionId") + .HasColumnType("integer") + .HasColumnName("condition_id"); + + b.Property("FirstSeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_at"); + + b.Property("FloatValue") + .HasColumnType("numeric(10,9)") + .HasColumnName("float_value"); + + b.Property("PaintSeed") + .IsRequired() + .HasColumnType("text") + .HasColumnName("paint_seed"); + + b.Property("SkinId") + .HasColumnType("integer") + .HasColumnName("skin_id"); + + b.Property("Souvenir") + .HasColumnType("boolean") + .HasColumnName("souvenir"); + + b.Property("StatTrak") + .HasColumnType("boolean") + .HasColumnName("stat_trak"); + + b.HasKey("Id") + .HasName("pk_skin_instances"); + + b.HasIndex("ConditionId") + .HasDatabaseName("ix_skin_instances_condition_id"); + + b.HasIndex("FloatValue") + .HasDatabaseName("ix_skin_instances_float_value"); + + b.HasIndex("PaintSeed") + .HasDatabaseName("ix_skin_instances_paint_seed"); + + b.HasIndex("SkinId") + .HasDatabaseName("ix_skin_instances_skin_id"); + + b.ToTable("skin_instances", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SteamUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("LastSyncedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_synced_at"); + + b.Property("SteamId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("steam_id"); + + b.HasKey("Id") + .HasName("pk_steam_users"); + + b.HasIndex("SteamId") + .IsUnique() + .HasDatabaseName("ix_steam_users_steam_id"); + + b.ToTable("steam_users", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FromUserId") + .HasColumnType("integer") + .HasColumnName("from_user_id"); + + b.Property("SteamTradeId") + .HasColumnType("text") + .HasColumnName("steam_trade_id"); + + b.Property("ToUserId") + .HasColumnType("integer") + .HasColumnName("to_user_id"); + + b.Property("TradedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("traded_at"); + + b.HasKey("Id") + .HasName("pk_trades"); + + b.HasIndex("FromUserId") + .HasDatabaseName("ix_trades_from_user_id"); + + b.HasIndex("ToUserId") + .HasDatabaseName("ix_trades_to_user_id"); + + b.ToTable("trades", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.TradeItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("InventoryItemId") + .HasColumnType("integer") + .HasColumnName("inventory_item_id"); + + b.Property("TradeId") + .HasColumnType("integer") + .HasColumnName("trade_id"); + + b.HasKey("Id") + .HasName("pk_trade_items"); + + b.HasIndex("InventoryItemId") + .HasDatabaseName("ix_trade_items_inventory_item_id"); + + b.HasIndex("TradeId") + .HasDatabaseName("ix_trade_items_trade_id"); + + b.ToTable("trade_items", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Weapon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Team") + .IsRequired() + .HasColumnType("text") + .HasColumnName("team"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_weapons"); + + b.ToTable("weapons", (string)null); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SkinInstance", "SkinInstance") + .WithMany("InventoryItems") + .HasForeignKey("SkinInstanceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inventory_items_skin_instances_skin_instance_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.SteamUser", "User") + .WithMany("InventoryItems") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_inventory_items_steam_users_user_id"); + + b.Navigation("SkinInstance"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.PriceHistory", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "Condition") + .WithMany("PriceHistories") + .HasForeignKey("ConditionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_price_histories_skin_conditions_condition_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin") + .WithMany("PriceHistories") + .HasForeignKey("SkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_price_histories_skins_skin_id"); + + b.Navigation("Condition"); + + b.Navigation("Skin"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.Weapon", "Weapon") + .WithMany("Skins") + .HasForeignKey("WeaponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_skins_weapons_weapon_id"); + + b.Navigation("Weapon"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin") + .WithMany("Conditions") + .HasForeignKey("SkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_skin_conditions_skins_skin_id"); + + b.Navigation("Skin"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "Condition") + .WithMany("Instances") + .HasForeignKey("ConditionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_skin_instances_skin_conditions_condition_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin") + .WithMany("Instances") + .HasForeignKey("SkinId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_skin_instances_skins_skin_id"); + + b.Navigation("Condition"); + + b.Navigation("Skin"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.SteamUser", "FromUser") + .WithMany("TradesSent") + .HasForeignKey("FromUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_trades_steam_users_from_user_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.SteamUser", "ToUser") + .WithMany("TradesReceived") + .HasForeignKey("ToUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_trades_steam_users_to_user_id"); + + b.Navigation("FromUser"); + + b.Navigation("ToUser"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.TradeItem", b => + { + b.HasOne("BlueLaminate.EFCore.Entities.InventoryItem", "InventoryItem") + .WithMany("TradeItems") + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_trade_items_inventory_items_inventory_item_id"); + + b.HasOne("BlueLaminate.EFCore.Entities.Trade", "Trade") + .WithMany("TradeItems") + .HasForeignKey("TradeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_trade_items_trades_trade_id"); + + b.Navigation("InventoryItem"); + + b.Navigation("Trade"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b => + { + b.Navigation("TradeItems"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b => + { + b.Navigation("Conditions"); + + b.Navigation("Instances"); + + b.Navigation("PriceHistories"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b => + { + b.Navigation("Instances"); + + b.Navigation("PriceHistories"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b => + { + b.Navigation("InventoryItems"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.SteamUser", b => + { + b.Navigation("InventoryItems"); + + b.Navigation("TradesReceived"); + + b.Navigation("TradesSent"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b => + { + b.Navigation("TradeItems"); + }); + + modelBuilder.Entity("BlueLaminate.EFCore.Entities.Weapon", b => + { + b.Navigation("Skins"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BlueLaminate/BlueLaminate.EFCore/Program.cs b/BlueLaminate/BlueLaminate.EFCore/Program.cs new file mode 100644 index 0000000..54bfd60 --- /dev/null +++ b/BlueLaminate/BlueLaminate.EFCore/Program.cs @@ -0,0 +1,7 @@ +using BlueLaminate.EFCore.Data; + +// Build the context the same way the design-time factory does. Run +// `dotnet ef database update` to apply migrations to the configured database. +using var db = new SkinTrackerDbContextFactory().CreateDbContext(args); + +Console.WriteLine($"CS2 Skin Tracker — {db.Model.GetEntityTypes().Count()} entities mapped."); diff --git a/BlueLaminate/BlueLaminate.slnx b/BlueLaminate/BlueLaminate.slnx new file mode 100644 index 0000000..24dc98c --- /dev/null +++ b/BlueLaminate/BlueLaminate.slnx @@ -0,0 +1,3 @@ + + + diff --git a/WeaponGrabber/WeaponScraper.py b/WeaponGrabber/WeaponScraper.py new file mode 100644 index 0000000..cb3c9f2 --- /dev/null +++ b/WeaponGrabber/WeaponScraper.py @@ -0,0 +1,54 @@ +"""Print every CS2 weapon listed on the Counter-Strike wiki. + +Requires: pip install curl_cffi beautifulsoup4 + +Uses curl_cffi instead of requests because the wiki sits behind Cloudflare, +which blocks Python's default TLS fingerprint with a 403 even when the +User-Agent header looks like a browser. +""" +import re + +from bs4 import BeautifulSoup +from curl_cffi import requests + +URL = "https://counterstrike.fandom.com/wiki/Weapons" +TAB_HASH = "Global_Offensive_&_Counter-Strike_2" +ANNOTATION_RE = re.compile(r"\s*\((?:CT|T)\)\s*$") +STOCK_PREFIX_RE = re.compile(r"^Stock\s+") + + +def cs2_weapons(): + resp = requests.get(URL, impersonate="chrome", timeout=30) + resp.raise_for_status() + soup = BeautifulSoup(resp.text, "html.parser") + + weapons, seen = [], set() + for tabber in soup.select("div.tabber"): + tabs = tabber.select("li.wds-tabs__tab") + idx = next( + (i for i, t in enumerate(tabs) if t.get("data-hash") == TAB_HASH), + None, + ) + if idx is None: + continue + contents = tabber.find_all("div", class_="wds-tab__content") + if idx >= len(contents): + continue + for cap in contents[idx].select("div.lightbox-caption"): + name = cap.get_text(" ", strip=True) + name = ANNOTATION_RE.sub("", name) + name = STOCK_PREFIX_RE.sub("", name).strip() + if not name: + continue + if name not in seen: + seen.add(name) + weapons.append(name) + return weapons + + +if __name__ == "__main__": + weaps = cs2_weapons() + for w in weaps: + print(w) + + print(len(weaps)) \ No newline at end of file