init efcore

This commit is contained in:
bob
2026-05-29 12:21:42 -05:00
commit 3d3a5c2a5e
28 changed files with 2071 additions and 0 deletions

100
.gitignore vendored Normal file
View File

@@ -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/

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class InventoryItemConfiguration : IEntityTypeConfiguration<InventoryItem>
{
public void Configure(EntityTypeBuilder<InventoryItem> 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);
}
}

View File

@@ -0,0 +1,24 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class PriceHistoryConfiguration : IEntityTypeConfiguration<PriceHistory>
{
public void Configure(EntityTypeBuilder<PriceHistory> 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);
}
}

View File

@@ -0,0 +1,18 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class SkinConditionConfiguration : IEntityTypeConfiguration<SkinCondition>
{
public void Configure(EntityTypeBuilder<SkinCondition> 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);
}
}

View File

@@ -0,0 +1,27 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class SkinConfiguration : IEntityTypeConfiguration<Skin>
{
public void Configure(EntityTypeBuilder<Skin> 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);
}
}

View File

@@ -0,0 +1,26 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class SkinInstanceConfiguration : IEntityTypeConfiguration<SkinInstance>
{
public void Configure(EntityTypeBuilder<SkinInstance> 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);
}
}

View File

@@ -0,0 +1,13 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class SteamUserConfiguration : IEntityTypeConfiguration<SteamUser>
{
public void Configure(EntityTypeBuilder<SteamUser> entity)
{
entity.HasIndex(e => e.SteamId).IsUnique();
}
}

View File

@@ -0,0 +1,21 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class TradeConfiguration : IEntityTypeConfiguration<Trade>
{
public void Configure(EntityTypeBuilder<Trade> 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);
}
}

View File

@@ -0,0 +1,19 @@
using BlueLaminate.EFCore.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace BlueLaminate.EFCore.Configurations;
public class TradeItemConfiguration : IEntityTypeConfiguration<TradeItem>
{
public void Configure(EntityTypeBuilder<TradeItem> 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);
}
}

View File

@@ -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<SkinTrackerDbContext> options)
: base(options)
{
}
public DbSet<Weapon> Weapons => Set<Weapon>();
public DbSet<Skin> Skins => Set<Skin>();
public DbSet<SkinCondition> SkinConditions => Set<SkinCondition>();
public DbSet<SteamUser> SteamUsers => Set<SteamUser>();
public DbSet<SkinInstance> SkinInstances => Set<SkinInstance>();
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
public DbSet<Trade> Trades => Set<Trade>();
public DbSet<TradeItem> TradeItems => Set<TradeItem>();
public DbSet<PriceHistory> PriceHistories => Set<PriceHistory>();
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());
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace BlueLaminate.EFCore.Data;
public class SkinTrackerDbContextFactory : IDesignTimeDbContextFactory<SkinTrackerDbContext>
{
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<SkinTrackerDbContext>()
.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention()
.Options;
return new SkinTrackerDbContext(options);
}
}

View File

@@ -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<SkinTrackerDbContext>(options =>
options
.UseNpgsql(connectionString)
.UseSnakeCaseNamingConvention());
return services;
}
}

View File

@@ -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<TradeItem> TradeItems { get; set; } = new List<TradeItem>();
}

View File

@@ -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!;
}

View File

@@ -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<SkinCondition> Conditions { get; set; } = new List<SkinCondition>();
public ICollection<SkinInstance> Instances { get; set; } = new List<SkinInstance>();
public ICollection<PriceHistory> PriceHistories { get; set; } = new List<PriceHistory>();
}

View File

@@ -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<SkinInstance> Instances { get; set; } = new List<SkinInstance>();
public ICollection<PriceHistory> PriceHistories { get; set; } = new List<PriceHistory>();
}

View File

@@ -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<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
}

View File

@@ -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<InventoryItem> InventoryItems { get; set; } = new List<InventoryItem>();
public ICollection<Trade> TradesSent { get; set; } = new List<Trade>();
public ICollection<Trade> TradesReceived { get; set; } = new List<Trade>();
}

View File

@@ -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<TradeItem> TradeItems { get; set; } = new List<TradeItem>();
}

View File

@@ -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!;
}

View File

@@ -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<Skin> Skins { get; set; } = new List<Skin>();
}

View File

@@ -0,0 +1,573 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("AcquiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("acquired_at");
b.Property<string>("AssetId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("asset_id");
b.Property<int>("SkinInstanceId")
.HasColumnType("integer")
.HasColumnName("skin_instance_id");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("ConditionId")
.HasColumnType("integer")
.HasColumnName("condition_id");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("text")
.HasColumnName("currency");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)")
.HasColumnName("price");
b.Property<DateTimeOffset>("RecordedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("recorded_at");
b.Property<int>("SkinId")
.HasColumnType("integer")
.HasColumnName("skin_id");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<decimal>("FloatMax")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(10,9)")
.HasDefaultValue(1.0m)
.HasColumnName("float_max");
b.Property<decimal>("FloatMin")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(10,9)")
.HasDefaultValue(0.0m)
.HasColumnName("float_min");
b.Property<string>("ImageUrl")
.HasColumnType("text")
.HasColumnName("image_url");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<string>("Rarity")
.IsRequired()
.HasColumnType("text")
.HasColumnName("rarity");
b.Property<bool>("TrueFloat")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("boolean")
.HasColumnName("true_float")
.HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", true);
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Condition")
.IsRequired()
.HasColumnType("text")
.HasColumnName("condition");
b.Property<decimal>("MaxFloat")
.HasColumnType("numeric(10,9)")
.HasColumnName("max_float");
b.Property<decimal>("MinFloat")
.HasColumnType("numeric(10,9)")
.HasColumnName("min_float");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("ConditionId")
.HasColumnType("integer")
.HasColumnName("condition_id");
b.Property<DateTimeOffset>("FirstSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("first_seen_at");
b.Property<decimal>("FloatValue")
.HasColumnType("numeric(10,9)")
.HasColumnName("float_value");
b.Property<string>("PaintSeed")
.IsRequired()
.HasColumnType("text")
.HasColumnName("paint_seed");
b.Property<int>("SkinId")
.HasColumnType("integer")
.HasColumnName("skin_id");
b.Property<bool>("Souvenir")
.HasColumnType("boolean")
.HasColumnName("souvenir");
b.Property<bool>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DisplayName")
.HasColumnType("text")
.HasColumnName("display_name");
b.Property<DateTimeOffset>("LastSyncedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_synced_at");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("FromUserId")
.HasColumnType("integer")
.HasColumnName("from_user_id");
b.Property<string>("SteamTradeId")
.HasColumnType("text")
.HasColumnName("steam_trade_id");
b.Property<int>("ToUserId")
.HasColumnType("integer")
.HasColumnName("to_user_id");
b.Property<DateTimeOffset>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("InventoryItemId")
.HasColumnType("integer")
.HasColumnName("inventory_item_id");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<string>("Team")
.IsRequired()
.HasColumnType("text")
.HasColumnName("team");
b.Property<string>("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
}
}
}

View File

@@ -0,0 +1,354 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace BlueLaminate.EFCore.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "steam_users",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
steam_id = table.Column<string>(type: "text", nullable: false),
display_name = table.Column<string>(type: "text", nullable: true),
last_synced_at = table.Column<DateTimeOffset>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
name = table.Column<string>(type: "text", nullable: false),
type = table.Column<string>(type: "text", nullable: false),
team = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("pk_weapons", x => x.id);
});
migrationBuilder.CreateTable(
name: "trades",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
from_user_id = table.Column<int>(type: "integer", nullable: false),
to_user_id = table.Column<int>(type: "integer", nullable: false),
traded_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
steam_trade_id = table.Column<string>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
weapon_id = table.Column<int>(type: "integer", nullable: false),
name = table.Column<string>(type: "text", nullable: false),
rarity = table.Column<string>(type: "text", nullable: false),
description = table.Column<string>(type: "text", nullable: true),
image_url = table.Column<string>(type: "text", nullable: true),
float_min = table.Column<decimal>(type: "numeric(10,9)", nullable: false, defaultValue: 0.0m),
float_max = table.Column<decimal>(type: "numeric(10,9)", nullable: false, defaultValue: 1.0m),
true_float = table.Column<bool>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
skin_id = table.Column<int>(type: "integer", nullable: false),
condition = table.Column<string>(type: "text", nullable: false),
min_float = table.Column<decimal>(type: "numeric(10,9)", nullable: false),
max_float = table.Column<decimal>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
skin_id = table.Column<int>(type: "integer", nullable: false),
condition_id = table.Column<int>(type: "integer", nullable: false),
price = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
currency = table.Column<string>(type: "text", nullable: false),
recorded_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
source = table.Column<string>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
skin_id = table.Column<int>(type: "integer", nullable: false),
condition_id = table.Column<int>(type: "integer", nullable: false),
float_value = table.Column<decimal>(type: "numeric(10,9)", nullable: false),
paint_seed = table.Column<string>(type: "text", nullable: false),
stat_trak = table.Column<bool>(type: "boolean", nullable: false),
souvenir = table.Column<bool>(type: "boolean", nullable: false),
first_seen_at = table.Column<DateTimeOffset>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
user_id = table.Column<int>(type: "integer", nullable: false),
skin_instance_id = table.Column<int>(type: "integer", nullable: false),
asset_id = table.Column<string>(type: "text", nullable: false),
acquired_at = table.Column<DateTimeOffset>(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<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
trade_id = table.Column<int>(type: "integer", nullable: false),
inventory_item_id = table.Column<int>(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");
}
/// <inheritdoc />
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");
}
}
}

View File

@@ -0,0 +1,570 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTimeOffset>("AcquiredAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("acquired_at");
b.Property<string>("AssetId")
.IsRequired()
.HasColumnType("text")
.HasColumnName("asset_id");
b.Property<int>("SkinInstanceId")
.HasColumnType("integer")
.HasColumnName("skin_instance_id");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("ConditionId")
.HasColumnType("integer")
.HasColumnName("condition_id");
b.Property<string>("Currency")
.IsRequired()
.HasColumnType("text")
.HasColumnName("currency");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)")
.HasColumnName("price");
b.Property<DateTimeOffset>("RecordedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("recorded_at");
b.Property<int>("SkinId")
.HasColumnType("integer")
.HasColumnName("skin_id");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Description")
.HasColumnType("text")
.HasColumnName("description");
b.Property<decimal>("FloatMax")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(10,9)")
.HasDefaultValue(1.0m)
.HasColumnName("float_max");
b.Property<decimal>("FloatMin")
.ValueGeneratedOnAdd()
.HasColumnType("numeric(10,9)")
.HasDefaultValue(0.0m)
.HasColumnName("float_min");
b.Property<string>("ImageUrl")
.HasColumnType("text")
.HasColumnName("image_url");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<string>("Rarity")
.IsRequired()
.HasColumnType("text")
.HasColumnName("rarity");
b.Property<bool>("TrueFloat")
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("boolean")
.HasColumnName("true_float")
.HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", true);
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Condition")
.IsRequired()
.HasColumnType("text")
.HasColumnName("condition");
b.Property<decimal>("MaxFloat")
.HasColumnType("numeric(10,9)")
.HasColumnName("max_float");
b.Property<decimal>("MinFloat")
.HasColumnType("numeric(10,9)")
.HasColumnName("min_float");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("ConditionId")
.HasColumnType("integer")
.HasColumnName("condition_id");
b.Property<DateTimeOffset>("FirstSeenAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("first_seen_at");
b.Property<decimal>("FloatValue")
.HasColumnType("numeric(10,9)")
.HasColumnName("float_value");
b.Property<string>("PaintSeed")
.IsRequired()
.HasColumnType("text")
.HasColumnName("paint_seed");
b.Property<int>("SkinId")
.HasColumnType("integer")
.HasColumnName("skin_id");
b.Property<bool>("Souvenir")
.HasColumnType("boolean")
.HasColumnName("souvenir");
b.Property<bool>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("DisplayName")
.HasColumnType("text")
.HasColumnName("display_name");
b.Property<DateTimeOffset>("LastSyncedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("last_synced_at");
b.Property<string>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("FromUserId")
.HasColumnType("integer")
.HasColumnName("from_user_id");
b.Property<string>("SteamTradeId")
.HasColumnType("text")
.HasColumnName("steam_trade_id");
b.Property<int>("ToUserId")
.HasColumnType("integer")
.HasColumnName("to_user_id");
b.Property<DateTimeOffset>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("InventoryItemId")
.HasColumnType("integer")
.HasColumnName("inventory_item_id");
b.Property<int>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text")
.HasColumnName("name");
b.Property<string>("Team")
.IsRequired()
.HasColumnType("text")
.HasColumnName("team");
b.Property<string>("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
}
}
}

View File

@@ -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.");

View File

@@ -0,0 +1,3 @@
<Solution>
<Project Path="BlueLaminate.EFCore/BlueLaminate.EFCore.csproj" />
</Solution>

View File

@@ -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))