Change to static skin catalog population
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.8" />
|
||||
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Logs;
|
||||
|
||||
namespace BlueLaminate.Cli.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal console sink for the OpenTelemetry log pipeline: one line per record
|
||||
/// as "{utc timestamp} {message}". Requires IncludeFormattedMessage so the
|
||||
/// message arrives with its template arguments already substituted.
|
||||
/// </summary>
|
||||
public sealed class CompactConsoleLogExporter : BaseExporter<LogRecord>
|
||||
{
|
||||
public override ExportResult Export(in Batch<LogRecord> batch)
|
||||
{
|
||||
foreach (var record in batch)
|
||||
{
|
||||
var message = record.FormattedMessage ?? record.Body ?? string.Empty;
|
||||
Console.WriteLine($"{record.Timestamp:yyyy-MM-dd HH:mm:ss.fff'Z'} {message}");
|
||||
}
|
||||
|
||||
return ExportResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,25 @@
|
||||
using System.CommandLine;
|
||||
using BlueLaminate.Cli;
|
||||
using BlueLaminate.Cli.Logging;
|
||||
using BlueLaminate.EFCore.Data;
|
||||
using BlueLaminate.Scraper.Weapons;
|
||||
using BlueLaminate.Scraper.Wiki;
|
||||
using BlueLaminate.Scraper.Skins;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using OpenTelemetry;
|
||||
using OpenTelemetry.Resources;
|
||||
using System.CommandLine;
|
||||
|
||||
// OpenTelemetry logging through a compact console sink that prints one
|
||||
// "{utc timestamp} {message}" line per record. Swapping in an OTLP exporter
|
||||
// later is a change here. Disposed at process exit so buffered records flush.
|
||||
using var loggerFactory = LoggerFactory.Create(logging =>
|
||||
{
|
||||
logging.AddOpenTelemetry(otel =>
|
||||
{
|
||||
otel.SetResourceBuilder(
|
||||
ResourceBuilder.CreateDefault().AddService("BlueLaminate.Cli"));
|
||||
otel.IncludeFormattedMessage = true;
|
||||
otel.AddProcessor(new SimpleLogRecordExportProcessor(new CompactConsoleLogExporter()));
|
||||
});
|
||||
});
|
||||
|
||||
// Entry point: System.CommandLine builds the command tree, parsing, and help.
|
||||
// New features are added as additional commands here as they're implemented.
|
||||
@@ -12,56 +29,73 @@ var forceOption = new Option<bool>("--force")
|
||||
};
|
||||
var dryRunOption = new Option<bool>("--dry-run")
|
||||
{
|
||||
Description = "Scrape and print the weapons without writing to the database."
|
||||
Description = "Load and print the skins without writing to the database."
|
||||
};
|
||||
|
||||
var syncWeapons = new Command(
|
||||
"sync-weapons",
|
||||
"Scrape the CS2 weapon catalogue from the wiki and upsert it (throttled to once a month).")
|
||||
var syncSkins = new Command(
|
||||
"sync-skins",
|
||||
"Load the CS2 skin catalogue from the CSGO-API dataset and upsert it (throttled to once a month).")
|
||||
{
|
||||
forceOption,
|
||||
dryRunOption,
|
||||
};
|
||||
syncWeapons.SetAction((parseResult, ct) =>
|
||||
SyncWeaponsAsync(parseResult.GetValue(forceOption), parseResult.GetValue(dryRunOption), ct));
|
||||
syncSkins.SetAction((parseResult, ct) =>
|
||||
SyncSkinsAsync(
|
||||
parseResult.GetValue(forceOption),
|
||||
parseResult.GetValue(dryRunOption),
|
||||
loggerFactory,
|
||||
ct));
|
||||
|
||||
var root = new RootCommand("BlueLaminate CLI — Counter-Strike skin tracker tools.")
|
||||
{
|
||||
syncWeapons,
|
||||
syncSkins,
|
||||
};
|
||||
|
||||
return await root.Parse(args).InvokeAsync();
|
||||
|
||||
// Fetch the CS2 weapon catalogue from the wiki and upsert it. Throttled to once
|
||||
// a month unless --force is passed; --dry-run scrapes and prints without a DB.
|
||||
static async Task<int> SyncWeaponsAsync(bool force, bool dryRun, CancellationToken ct)
|
||||
// Load the CS2 skin catalogue from the CSGO-API dataset and upsert it. Weapons
|
||||
// and collections are derived from the skins themselves. Throttled to once a
|
||||
// month unless --force; --dry-run loads and prints without a DB.
|
||||
static async Task<int> SyncSkinsAsync(
|
||||
bool force, bool dryRun, ILoggerFactory loggerFactory, CancellationToken ct)
|
||||
{
|
||||
var scraper = new WeaponWikiScraper(new WikiPageFetcher(CreateHttpClient()));
|
||||
var logger = loggerFactory.CreateLogger("BlueLaminate.Cli.SyncSkins");
|
||||
var client = new SkinCatalogClient(CreateHttpClient());
|
||||
|
||||
if (dryRun)
|
||||
{
|
||||
var weapons = await scraper.ScrapeAsync(ct);
|
||||
Console.WriteLine($"Scraped {weapons.Count} weapons (dry run, nothing written):");
|
||||
foreach (var w in weapons)
|
||||
Console.WriteLine($" {w.Name,-20} {w.Type,-16} {w.Team}");
|
||||
logger.LogInformation("Loading skin catalogue (dry run — nothing will be written).");
|
||||
var skins = await client.FetchAsync(ct);
|
||||
logger.LogInformation("Loaded {Count} skins.", skins.Count);
|
||||
Console.WriteLine($"Loaded {skins.Count} skins (dry run, nothing written):");
|
||||
foreach (var s in skins)
|
||||
{
|
||||
var tags = (s.StatTrakAvailable ? " ST" : "") + (s.SouvenirAvailable ? " SV" : "");
|
||||
var range = s.FloatMin is not null ? $"{s.FloatMin:0.00}-{s.FloatMax:0.00}" : "—";
|
||||
var sources = s.Sources.Count > 0 ? string.Join(", ", s.Sources.Select(x => x.Name)) : "—";
|
||||
Console.WriteLine(
|
||||
$" {s.WeaponName,-16} {s.Name,-24} {s.Rarity,-14} {range,-10} {sources}{tags}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
using var db = new SkinTrackerDbContextFactory().CreateDbContext([]);
|
||||
var result = await new WeaponSyncService(db, scraper).SyncAsync(force, ct);
|
||||
var service = new SkinSyncService(db, client, loggerFactory.CreateLogger<SkinSyncService>());
|
||||
var result = await service.SyncAsync(force, ct);
|
||||
|
||||
if (result.Skipped)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"Skipped: weapons were last synced {result.LastRanAt:u}. "
|
||||
$"Skipped: skins were last synced {result.LastRanAt:u}. "
|
||||
+ "Next run allowed one month later — pass --force to override.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"Synced {result.Scraped} weapons: {result.Inserted} inserted, "
|
||||
$"Synced {result.Loaded} skins: {result.Inserted} inserted, "
|
||||
+ $"{result.Updated} updated, "
|
||||
+ $"{result.Scraped - result.Inserted - result.Updated} unchanged.");
|
||||
+ $"{result.Loaded - result.Inserted - result.Updated} unchanged "
|
||||
+ $"({result.WeaponsCreated} weapons, {result.CollectionsCreated} collections created).");
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -70,10 +104,7 @@ static async Task<int> SyncWeaponsAsync(bool force, bool dryRun, CancellationTok
|
||||
static HttpClient CreateHttpClient()
|
||||
{
|
||||
var http = new HttpClient();
|
||||
// The wiki is fronted by Cloudflare; a browser-like User-Agent is accepted
|
||||
// on the MediaWiki API endpoint the scraper uses.
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd(
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
+ "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36");
|
||||
http.Timeout = TimeSpan.FromMinutes(2);
|
||||
http.DefaultRequestHeaders.UserAgent.ParseAdd("BlueLaminate.Cli");
|
||||
return http;
|
||||
}
|
||||
|
||||
201
BlueLaminate/BlueLaminate.Cli/SkinSyncService.cs
Normal file
201
BlueLaminate/BlueLaminate.Cli/SkinSyncService.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using BlueLaminate.EFCore.Data;
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using BlueLaminate.Scraper.Skins;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace BlueLaminate.Cli;
|
||||
|
||||
/// <param name="Skipped">True when the monthly throttle suppressed the run.</param>
|
||||
/// <param name="LastRanAt">When the previous successful run happened, if any.</param>
|
||||
public sealed record SkinSyncResult(
|
||||
bool Skipped,
|
||||
DateTimeOffset? LastRanAt,
|
||||
int Loaded,
|
||||
int Inserted,
|
||||
int Updated,
|
||||
int WeaponsCreated,
|
||||
int CollectionsCreated);
|
||||
|
||||
/// <summary>
|
||||
/// Loads the CS2 skin catalogue from the CSGO-API dataset and upserts it. The
|
||||
/// weapon list and the collections/containers are derived from the skins
|
||||
/// themselves, so any that are missing are created on the fly and no skin is
|
||||
/// dropped. Throttled to once a month unless forced, since the catalogue changes
|
||||
/// slowly.
|
||||
/// </summary>
|
||||
public sealed class SkinSyncService
|
||||
{
|
||||
public const string Source = "skins";
|
||||
|
||||
private readonly SkinTrackerDbContext _db;
|
||||
private readonly SkinCatalogClient _client;
|
||||
private readonly ILogger<SkinSyncService> _logger;
|
||||
|
||||
public SkinSyncService(
|
||||
SkinTrackerDbContext db,
|
||||
SkinCatalogClient client,
|
||||
ILogger<SkinSyncService> logger)
|
||||
{
|
||||
_db = db;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SkinSyncResult> SyncAsync(bool force = false, CancellationToken ct = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var lastRanAt = await _db.ScrapeRuns
|
||||
.Where(r => r.Source == Source)
|
||||
.OrderByDescending(r => r.RanAt)
|
||||
.Select(r => (DateTimeOffset?)r.RanAt)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (!force && lastRanAt is { } last && last.AddMonths(1) > now)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Skipping skin sync; last run was {LastRanAt:u} (throttled to monthly).", last);
|
||||
return new SkinSyncResult(true, last, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Starting skin sync (force: {Force}).", force);
|
||||
var catalog = await _client.FetchAsync(ct);
|
||||
_logger.LogInformation("Loaded {Count} skins from the catalogue.", catalog.Count);
|
||||
|
||||
var weapons = await _db.Weapons.ToDictionaryAsync(w => w.Name, ct);
|
||||
var collections = await _db.Collections.ToDictionaryAsync(c => c.Slug, ct);
|
||||
var existing = await _db.Skins
|
||||
.Include(s => s.Collections)
|
||||
.ToDictionaryAsync(s => s.Slug, ct);
|
||||
|
||||
var inserted = 0;
|
||||
var updated = 0;
|
||||
var weaponsCreated = 0;
|
||||
var collectionsCreated = 0;
|
||||
|
||||
foreach (var s in catalog)
|
||||
{
|
||||
var weapon = ResolveWeapon(weapons, s, ref weaponsCreated);
|
||||
var sources = ResolveCollections(collections, s, ref collectionsCreated);
|
||||
|
||||
if (existing.TryGetValue(s.Id, out var skin))
|
||||
{
|
||||
if (Apply(skin, s, weapon, sources))
|
||||
updated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
skin = new Skin { Slug = s.Id };
|
||||
Apply(skin, s, weapon, sources);
|
||||
_db.Skins.Add(skin);
|
||||
existing[s.Id] = skin;
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
_db.ScrapeRuns.Add(new ScrapeRun { Source = Source, RanAt = now, ItemCount = catalog.Count });
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Skin sync complete: {Loaded} loaded, {Inserted} inserted, {Updated} updated, "
|
||||
+ "{WeaponsCreated} weapons created, {CollectionsCreated} collections created.",
|
||||
catalog.Count, inserted, updated, weaponsCreated, collectionsCreated);
|
||||
|
||||
return new SkinSyncResult(
|
||||
false, lastRanAt, catalog.Count, inserted, updated, weaponsCreated, collectionsCreated);
|
||||
}
|
||||
|
||||
private Weapon ResolveWeapon(Dictionary<string, Weapon> weapons, CatalogSkin s, ref int created)
|
||||
{
|
||||
if (weapons.TryGetValue(s.WeaponName, out var weapon))
|
||||
{
|
||||
// Category/team can be refined as the catalogue grows; keep them current.
|
||||
weapon.Type = s.Category;
|
||||
weapon.Team = s.Team;
|
||||
return weapon;
|
||||
}
|
||||
|
||||
weapon = new Weapon { Name = s.WeaponName, Type = s.Category, Team = s.Team };
|
||||
_db.Weapons.Add(weapon);
|
||||
weapons[s.WeaponName] = weapon;
|
||||
created++;
|
||||
return weapon;
|
||||
}
|
||||
|
||||
private List<Collection> ResolveCollections(
|
||||
Dictionary<string, Collection> collections, CatalogSkin s, ref int created)
|
||||
{
|
||||
var resolved = new List<Collection>(s.Sources.Count);
|
||||
foreach (var source in s.Sources)
|
||||
{
|
||||
if (!collections.TryGetValue(source.Id, out var collection))
|
||||
{
|
||||
collection = new Collection { Slug = source.Id, Name = source.Name, Type = source.Type };
|
||||
_db.Collections.Add(collection);
|
||||
collections[source.Id] = collection;
|
||||
created++;
|
||||
}
|
||||
resolved.Add(collection);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
// Copies catalogue values onto the entity. Returns true if anything changed.
|
||||
// The weapon navigation is assigned directly (a newly created weapon has no
|
||||
// id yet to compare against, so reference-assigning is the only correct way
|
||||
// to wire the FK). The collection links are reconciled against the current set.
|
||||
private static bool Apply(Skin skin, CatalogSkin s, Weapon weapon, List<Collection> sources)
|
||||
{
|
||||
skin.Weapon = weapon;
|
||||
|
||||
var changed = false;
|
||||
|
||||
void Set<T>(Func<T> get, Action<T> set, T value)
|
||||
{
|
||||
if (!EqualityComparer<T>.Default.Equals(get(), value))
|
||||
{
|
||||
set(value);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
Set(() => skin.Name, v => skin.Name = v, s.Name);
|
||||
Set(() => skin.Rarity, v => skin.Rarity = v, s.Rarity);
|
||||
Set(() => skin.Description, v => skin.Description = v, s.Description);
|
||||
Set(() => skin.ImageUrl, v => skin.ImageUrl = v, s.ImageUrl);
|
||||
Set(() => skin.StatTrakAvailable, v => skin.StatTrakAvailable = v, s.StatTrakAvailable);
|
||||
Set(() => skin.SouvenirAvailable, v => skin.SouvenirAvailable = v, s.SouvenirAvailable);
|
||||
Set<decimal?>(() => skin.FloatMin, v => skin.FloatMin = v, s.FloatMin);
|
||||
Set<decimal?>(() => skin.FloatMax, v => skin.FloatMax = v, s.FloatMax);
|
||||
|
||||
if (ReconcileCollections(skin.Collections, sources))
|
||||
changed = true;
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Adds collections the skin newly belongs to and removes ones it no longer
|
||||
// does, comparing by slug. Returns true if the set changed.
|
||||
private static bool ReconcileCollections(ICollection<Collection> current, List<Collection> desired)
|
||||
{
|
||||
var changed = false;
|
||||
|
||||
foreach (var collection in desired)
|
||||
{
|
||||
if (!current.Any(c => c.Slug == collection.Slug))
|
||||
{
|
||||
current.Add(collection);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var collection in current.Where(c => desired.All(d => d.Slug != c.Slug)).ToList())
|
||||
{
|
||||
current.Remove(collection);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using BlueLaminate.EFCore.Data;
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using BlueLaminate.Scraper.Weapons;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BlueLaminate.Cli;
|
||||
|
||||
/// <param name="Skipped">True when the monthly throttle suppressed the run.</param>
|
||||
/// <param name="LastRanAt">When the previous successful run happened, if any.</param>
|
||||
public sealed record WeaponSyncResult(
|
||||
bool Skipped,
|
||||
DateTimeOffset? LastRanAt,
|
||||
int Scraped,
|
||||
int Inserted,
|
||||
int Updated);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the CS2 weapon catalogue and upserts it into the database. The
|
||||
/// catalogue changes rarely, so a run is throttled to at most once a month
|
||||
/// unless explicitly forced.
|
||||
/// </summary>
|
||||
public sealed class WeaponSyncService
|
||||
{
|
||||
public const string Source = "weapons";
|
||||
|
||||
private readonly SkinTrackerDbContext _db;
|
||||
private readonly WeaponWikiScraper _scraper;
|
||||
|
||||
public WeaponSyncService(SkinTrackerDbContext db, WeaponWikiScraper scraper)
|
||||
{
|
||||
_db = db;
|
||||
_scraper = scraper;
|
||||
}
|
||||
|
||||
public async Task<WeaponSyncResult> SyncAsync(bool force = false, CancellationToken ct = default)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
var lastRanAt = await _db.ScrapeRuns
|
||||
.Where(r => r.Source == Source)
|
||||
.OrderByDescending(r => r.RanAt)
|
||||
.Select(r => (DateTimeOffset?)r.RanAt)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
|
||||
if (!force && lastRanAt is { } last && last.AddMonths(1) > now)
|
||||
return new WeaponSyncResult(Skipped: true, last, Scraped: 0, Inserted: 0, Updated: 0);
|
||||
|
||||
var scraped = await _scraper.ScrapeAsync(ct);
|
||||
|
||||
var existing = await _db.Weapons.ToDictionaryAsync(w => w.Name, ct);
|
||||
var inserted = 0;
|
||||
var updated = 0;
|
||||
|
||||
foreach (var s in scraped)
|
||||
{
|
||||
if (existing.TryGetValue(s.Name, out var weapon))
|
||||
{
|
||||
if (weapon.Type != s.Type || weapon.Team != s.Team)
|
||||
{
|
||||
weapon.Type = s.Type;
|
||||
weapon.Team = s.Team;
|
||||
updated++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_db.Weapons.Add(new Weapon { Name = s.Name, Type = s.Type, Team = s.Team });
|
||||
inserted++;
|
||||
}
|
||||
}
|
||||
|
||||
_db.ScrapeRuns.Add(new ScrapeRun { Source = Source, RanAt = now, ItemCount = scraped.Count });
|
||||
await _db.SaveChangesAsync(ct);
|
||||
|
||||
return new WeaponSyncResult(Skipped: false, lastRanAt, scraped.Count, inserted, updated);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace BlueLaminate.EFCore.Configurations;
|
||||
|
||||
public class CollectionConfiguration : IEntityTypeConfiguration<Collection>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Collection> entity)
|
||||
{
|
||||
// Slug is the natural key the sync upserts against.
|
||||
entity.HasIndex(e => e.Slug).IsUnique();
|
||||
}
|
||||
}
|
||||
@@ -8,20 +8,27 @@ 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);
|
||||
// Nullable: null means the catalogue gives no wear range (e.g. vanilla
|
||||
// knives), distinct from a genuine 0.0–1.0 range.
|
||||
entity.Property(e => e.FloatMin).HasColumnType("numeric(10,9)");
|
||||
entity.Property(e => e.FloatMax).HasColumnType("numeric(10,9)");
|
||||
|
||||
entity.Property(e => e.TrueFloat)
|
||||
.HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", stored: true);
|
||||
|
||||
entity.HasIndex(e => e.TrueFloat);
|
||||
|
||||
// Slug is the natural key the sync upserts against.
|
||||
entity.HasIndex(e => e.Slug).IsUnique();
|
||||
|
||||
entity.HasOne(e => e.Weapon)
|
||||
.WithMany(w => w.Skins)
|
||||
.HasForeignKey(e => e.WeaponId);
|
||||
|
||||
// A skin can come from many collections and containers, and each of those
|
||||
// holds many skins.
|
||||
entity.HasMany(e => e.Collections)
|
||||
.WithMany(c => c.Skins)
|
||||
.UsingEntity(join => join.ToTable("skin_collections"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public class SkinTrackerDbContext : DbContext
|
||||
|
||||
public DbSet<Weapon> Weapons => Set<Weapon>();
|
||||
public DbSet<ScrapeRun> ScrapeRuns => Set<ScrapeRun>();
|
||||
public DbSet<Collection> Collections => Set<Collection>();
|
||||
public DbSet<Skin> Skins => Set<Skin>();
|
||||
public DbSet<SkinCondition> SkinConditions => Set<SkinCondition>();
|
||||
public DbSet<SteamUser> SteamUsers => Set<SteamUser>();
|
||||
@@ -38,6 +39,7 @@ public class SkinTrackerDbContext : DbContext
|
||||
|
||||
modelBuilder.ApplyConfiguration(new WeaponConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new ScrapeRunConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new CollectionConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinConditionConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SteamUserConfiguration());
|
||||
|
||||
21
BlueLaminate/BlueLaminate.EFCore/Entities/Collection.cs
Normal file
21
BlueLaminate/BlueLaminate.EFCore/Entities/Collection.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A source a skin originates from: either an in-game collection (e.g.
|
||||
/// "The Dead Hand Collection") or a container/case (e.g. "Glove Case").
|
||||
/// </summary>
|
||||
public class Collection
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>Stable id from the CSGO-API catalogue, e.g. "collection-set-community-37"
|
||||
/// or "crate-4288". The natural key.</summary>
|
||||
public string Slug { get; set; } = null!;
|
||||
|
||||
/// <summary>"Collection" or "Container".</summary>
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
public ICollection<Skin> Skins { get; set; } = new List<Skin>();
|
||||
}
|
||||
@@ -6,17 +6,29 @@ public class Skin
|
||||
public int WeaponId { get; set; }
|
||||
public Weapon Weapon { get; set; } = null!;
|
||||
|
||||
/// <summary>Stable id from the CSGO-API catalogue, e.g. "skin-e757fd7191f9". The natural key.</summary>
|
||||
public string Slug { 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; }
|
||||
public bool StatTrakAvailable { get; set; }
|
||||
public bool SouvenirAvailable { 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; }
|
||||
/// <summary>Every collection and container this skin originates from.</summary>
|
||||
public ICollection<Collection> Collections { get; set; } = new List<Collection>();
|
||||
|
||||
// Null when the catalogue gives no wear range (e.g. vanilla knives). Callers
|
||||
// must treat null as "unknown", not as a full 0.0–1.0 range.
|
||||
public decimal? FloatMin { get; set; }
|
||||
public decimal? FloatMax { get; set; }
|
||||
|
||||
// Computed in the database: float_min = 0.0 AND float_max = 1.0; null while the
|
||||
// bounds are unknown. 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>();
|
||||
|
||||
680
BlueLaminate/BlueLaminate.EFCore/Migrations/20260529192841_AddSkinCatalogFields.Designer.cs
generated
Normal file
680
BlueLaminate/BlueLaminate.EFCore/Migrations/20260529192841_AddSkinCatalogFields.Designer.cs
generated
Normal file
@@ -0,0 +1,680 @@
|
||||
// <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("20260529192841_AddSkinCatalogFields")]
|
||||
partial class AddSkinCatalogFields
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("skintracker")
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Collection", 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>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_collections");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_collections_slug");
|
||||
|
||||
b.ToTable("collections", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.ScrapeRun", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ItemCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("item_count");
|
||||
|
||||
b.Property<DateTimeOffset>("RanAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ran_at");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("source");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_scrape_runs");
|
||||
|
||||
b.HasIndex("Source", "RanAt")
|
||||
.HasDatabaseName("ix_scrape_runs_source_ran_at");
|
||||
|
||||
b.ToTable("scrape_runs", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("CollectionId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("collection_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<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<bool>("SouvenirAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("souvenir_available");
|
||||
|
||||
b.Property<bool>("StatTrakAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("stat_trak_available");
|
||||
|
||||
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("CollectionId")
|
||||
.HasDatabaseName("ix_skins_collection_id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skins_slug");
|
||||
|
||||
b.HasIndex("TrueFloat")
|
||||
.HasDatabaseName("ix_skins_true_float");
|
||||
|
||||
b.HasIndex("WeaponId")
|
||||
.HasDatabaseName("ix_skins_weapon_id");
|
||||
|
||||
b.ToTable("skins", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_weapons_name");
|
||||
|
||||
b.ToTable("weapons", "skintracker");
|
||||
});
|
||||
|
||||
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.Collection", "Collection")
|
||||
.WithMany("Skins")
|
||||
.HasForeignKey("CollectionId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_skins_collections_collection_id");
|
||||
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Weapon", "Weapon")
|
||||
.WithMany("Skins")
|
||||
.HasForeignKey("WeaponId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skins_weapons_weapon_id");
|
||||
|
||||
b.Navigation("Collection");
|
||||
|
||||
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.Collection", b =>
|
||||
{
|
||||
b.Navigation("Skins");
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BlueLaminate.EFCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSkinCatalogFields : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "slug",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "souvenir_available",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "stat_trak_available",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "collections",
|
||||
schema: "skintracker",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
name = table.Column<string>(type: "text", nullable: false),
|
||||
slug = table.Column<string>(type: "text", nullable: false),
|
||||
type = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_collections", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skins_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
column: "collection_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skins_slug",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
column: "slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_collections_slug",
|
||||
schema: "skintracker",
|
||||
table: "collections",
|
||||
column: "slug",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_skins_collections_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
column: "collection_id",
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "collections",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_skins_collections_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "collections",
|
||||
schema: "skintracker");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_skins_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_skins_slug",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "slug",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "souvenir_available",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "stat_trak_available",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
}
|
||||
}
|
||||
}
|
||||
676
BlueLaminate/BlueLaminate.EFCore/Migrations/20260529200100_MakeSkinFloatsNullable.Designer.cs
generated
Normal file
676
BlueLaminate/BlueLaminate.EFCore/Migrations/20260529200100_MakeSkinFloatsNullable.Designer.cs
generated
Normal file
@@ -0,0 +1,676 @@
|
||||
// <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("20260529200100_MakeSkinFloatsNullable")]
|
||||
partial class MakeSkinFloatsNullable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("skintracker")
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Collection", 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>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_collections");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_collections_slug");
|
||||
|
||||
b.ToTable("collections", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.ScrapeRun", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ItemCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("item_count");
|
||||
|
||||
b.Property<DateTimeOffset>("RanAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ran_at");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("source");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_scrape_runs");
|
||||
|
||||
b.HasIndex("Source", "RanAt")
|
||||
.HasDatabaseName("ix_scrape_runs_source_ran_at");
|
||||
|
||||
b.ToTable("scrape_runs", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Skin", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int?>("CollectionId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("collection_id");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<decimal?>("FloatMax")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.HasColumnName("float_max");
|
||||
|
||||
b.Property<decimal?>("FloatMin")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.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<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<bool>("SouvenirAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("souvenir_available");
|
||||
|
||||
b.Property<bool>("StatTrakAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("stat_trak_available");
|
||||
|
||||
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("CollectionId")
|
||||
.HasDatabaseName("ix_skins_collection_id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skins_slug");
|
||||
|
||||
b.HasIndex("TrueFloat")
|
||||
.HasDatabaseName("ix_skins_true_float");
|
||||
|
||||
b.HasIndex("WeaponId")
|
||||
.HasDatabaseName("ix_skins_weapon_id");
|
||||
|
||||
b.ToTable("skins", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_weapons_name");
|
||||
|
||||
b.ToTable("weapons", "skintracker");
|
||||
});
|
||||
|
||||
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.Collection", "Collection")
|
||||
.WithMany("Skins")
|
||||
.HasForeignKey("CollectionId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_skins_collections_collection_id");
|
||||
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Weapon", "Weapon")
|
||||
.WithMany("Skins")
|
||||
.HasForeignKey("WeaponId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skins_weapons_weapon_id");
|
||||
|
||||
b.Navigation("Collection");
|
||||
|
||||
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.Collection", b =>
|
||||
{
|
||||
b.Navigation("Skins");
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BlueLaminate.EFCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeSkinFloatsNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "float_min",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "numeric(10,9)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "numeric(10,9)",
|
||||
oldDefaultValue: 0.0m);
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "float_max",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "numeric(10,9)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "numeric(10,9)",
|
||||
oldDefaultValue: 1.0m);
|
||||
|
||||
migrationBuilder.AlterColumn<bool>(
|
||||
name: "true_float",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "boolean",
|
||||
nullable: true,
|
||||
computedColumnSql: "float_min = 0.0 AND float_max = 1.0",
|
||||
stored: true,
|
||||
oldClrType: typeof(bool),
|
||||
oldType: "boolean",
|
||||
oldComputedColumnSql: "float_min = 0.0 AND float_max = 1.0",
|
||||
oldStored: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "float_min",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "numeric(10,9)",
|
||||
nullable: false,
|
||||
defaultValue: 0.0m,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "numeric(10,9)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "float_max",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "numeric(10,9)",
|
||||
nullable: false,
|
||||
defaultValue: 1.0m,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "numeric(10,9)",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<bool>(
|
||||
name: "true_float",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
computedColumnSql: "float_min = 0.0 AND float_max = 1.0",
|
||||
stored: true,
|
||||
oldClrType: typeof(bool),
|
||||
oldType: "boolean",
|
||||
oldNullable: true,
|
||||
oldComputedColumnSql: "float_min = 0.0 AND float_max = 1.0",
|
||||
oldStored: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
692
BlueLaminate/BlueLaminate.EFCore/Migrations/20260529211544_UseStaticSkinCatalog.Designer.cs
generated
Normal file
692
BlueLaminate/BlueLaminate.EFCore/Migrations/20260529211544_UseStaticSkinCatalog.Designer.cs
generated
Normal file
@@ -0,0 +1,692 @@
|
||||
// <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("20260529211544_UseStaticSkinCatalog")]
|
||||
partial class UseStaticSkinCatalog
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("skintracker")
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Collection", 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>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_collections");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_collections_slug");
|
||||
|
||||
b.ToTable("collections", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.ScrapeRun", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("ItemCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("item_count");
|
||||
|
||||
b.Property<DateTimeOffset>("RanAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ran_at");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("source");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_scrape_runs");
|
||||
|
||||
b.HasIndex("Source", "RanAt")
|
||||
.HasDatabaseName("ix_scrape_runs_source_ran_at");
|
||||
|
||||
b.ToTable("scrape_runs", "skintracker");
|
||||
});
|
||||
|
||||
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")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.HasColumnName("float_max");
|
||||
|
||||
b.Property<decimal?>("FloatMin")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.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<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<bool>("SouvenirAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("souvenir_available");
|
||||
|
||||
b.Property<bool>("StatTrakAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("stat_trak_available");
|
||||
|
||||
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("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skins_slug");
|
||||
|
||||
b.HasIndex("TrueFloat")
|
||||
.HasDatabaseName("ix_skins_true_float");
|
||||
|
||||
b.HasIndex("WeaponId")
|
||||
.HasDatabaseName("ix_skins_weapon_id");
|
||||
|
||||
b.ToTable("skins", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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", "skintracker");
|
||||
});
|
||||
|
||||
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.HasIndex("Name")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_weapons_name");
|
||||
|
||||
b.ToTable("weapons", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionSkin", b =>
|
||||
{
|
||||
b.Property<int>("CollectionsId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("collections_id");
|
||||
|
||||
b.Property<int>("SkinsId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skins_id");
|
||||
|
||||
b.HasKey("CollectionsId", "SkinsId")
|
||||
.HasName("pk_skin_collections");
|
||||
|
||||
b.HasIndex("SkinsId")
|
||||
.HasDatabaseName("ix_skin_collections_skins_id");
|
||||
|
||||
b.ToTable("skin_collections", "skintracker");
|
||||
});
|
||||
|
||||
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("CollectionSkin", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Collection", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CollectionsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skin_collections_collections_collections_id");
|
||||
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Skin", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SkinsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skin_collections_skins_skins_id");
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BlueLaminate.EFCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UseStaticSkinCatalog : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_skins_collections_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_skins_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "skin_collections",
|
||||
schema: "skintracker",
|
||||
columns: table => new
|
||||
{
|
||||
collections_id = table.Column<int>(type: "integer", nullable: false),
|
||||
skins_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_skin_collections", x => new { x.collections_id, x.skins_id });
|
||||
table.ForeignKey(
|
||||
name: "fk_skin_collections_collections_collections_id",
|
||||
column: x => x.collections_id,
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "collections",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "fk_skin_collections_skins_skins_id",
|
||||
column: x => x.skins_id,
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "skins",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_collections_skins_id",
|
||||
schema: "skintracker",
|
||||
table: "skin_collections",
|
||||
column: "skins_id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "skin_collections",
|
||||
schema: "skintracker");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skins_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
column: "collection_id");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_skins_collections_collection_id",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
column: "collection_id",
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "collections",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,40 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Collection", 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>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("type");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_collections");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_collections_slug");
|
||||
|
||||
b.ToTable("collections", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -156,16 +190,12 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("description");
|
||||
|
||||
b.Property<decimal>("FloatMax")
|
||||
.ValueGeneratedOnAdd()
|
||||
b.Property<decimal?>("FloatMax")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.HasDefaultValue(1.0m)
|
||||
.HasColumnName("float_max");
|
||||
|
||||
b.Property<decimal>("FloatMin")
|
||||
.ValueGeneratedOnAdd()
|
||||
b.Property<decimal?>("FloatMin")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.HasDefaultValue(0.0m)
|
||||
.HasColumnName("float_min");
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
@@ -182,7 +212,20 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("rarity");
|
||||
|
||||
b.Property<bool>("TrueFloat")
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<bool>("SouvenirAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("souvenir_available");
|
||||
|
||||
b.Property<bool>("StatTrakAvailable")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("stat_trak_available");
|
||||
|
||||
b.Property<bool?>("TrueFloat")
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("true_float")
|
||||
@@ -195,6 +238,10 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_skins");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skins_slug");
|
||||
|
||||
b.HasIndex("TrueFloat")
|
||||
.HasDatabaseName("ix_skins_true_float");
|
||||
|
||||
@@ -427,6 +474,25 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.ToTable("weapons", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionSkin", b =>
|
||||
{
|
||||
b.Property<int>("CollectionsId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("collections_id");
|
||||
|
||||
b.Property<int>("SkinsId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skins_id");
|
||||
|
||||
b.HasKey("CollectionsId", "SkinsId")
|
||||
.HasName("pk_skin_collections");
|
||||
|
||||
b.HasIndex("SkinsId")
|
||||
.HasDatabaseName("ix_skin_collections_skins_id");
|
||||
|
||||
b.ToTable("skin_collections", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.SkinInstance", "SkinInstance")
|
||||
@@ -556,6 +622,23 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.Navigation("Trade");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CollectionSkin", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Collection", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CollectionsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skin_collections_collections_collections_id");
|
||||
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Skin", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("SkinsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skin_collections_skins_skins_id");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b =>
|
||||
{
|
||||
b.Navigation("TradeItems");
|
||||
|
||||
@@ -6,8 +6,4 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
36
BlueLaminate/BlueLaminate.Scraper/Skins/CatalogSkin.cs
Normal file
36
BlueLaminate/BlueLaminate.Scraper/Skins/CatalogSkin.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
namespace BlueLaminate.Scraper.Skins;
|
||||
|
||||
/// <summary>A single CS2 skin from the CSGO-API static catalogue (skins.json).</summary>
|
||||
/// <param name="Id">Stable catalogue id, e.g. "skin-e757fd7191f9". Globally unique natural key.</param>
|
||||
/// <param name="WeaponName">Owning weapon, e.g. "AK-47", "Hand Wraps", "Bayonet".</param>
|
||||
/// <param name="Category">Weapon category, e.g. "Rifles", "Knives", "Gloves". Becomes the weapon type.</param>
|
||||
/// <param name="Team">"CT", "T", or "Both".</param>
|
||||
/// <param name="Name">Skin/pattern name, e.g. "Dragon Lore"; "Vanilla" for knives with no finish.</param>
|
||||
/// <param name="Rarity">Rarity tier, e.g. "Covert", "Classified", "Extraordinary".</param>
|
||||
/// <param name="Description">Flavour/description text, or null.</param>
|
||||
/// <param name="ImageUrl">Catalogue image URL, or null.</param>
|
||||
/// <param name="StatTrakAvailable">True if a StatTrak variant exists.</param>
|
||||
/// <param name="SouvenirAvailable">True if a Souvenir variant exists.</param>
|
||||
/// <param name="FloatMin">Minimum wear value, or null when the catalogue gives none (e.g. vanilla knives).</param>
|
||||
/// <param name="FloatMax">Maximum wear value, or null.</param>
|
||||
/// <param name="Sources">Collections and containers this skin belongs to.</param>
|
||||
public sealed record CatalogSkin(
|
||||
string Id,
|
||||
string WeaponName,
|
||||
string Category,
|
||||
string Team,
|
||||
string Name,
|
||||
string Rarity,
|
||||
string? Description,
|
||||
string? ImageUrl,
|
||||
bool StatTrakAvailable,
|
||||
bool SouvenirAvailable,
|
||||
decimal? FloatMin,
|
||||
decimal? FloatMax,
|
||||
IReadOnlyList<CatalogSource> Sources);
|
||||
|
||||
/// <summary>A collection or container a skin originates from.</summary>
|
||||
/// <param name="Id">Stable catalogue id, e.g. "collection-set-community-37" or "crate-4288". Natural key.</param>
|
||||
/// <param name="Name">Display name, e.g. "The Dead Hand Collection", "Glove Case".</param>
|
||||
/// <param name="Type">"Collection" or "Container".</param>
|
||||
public sealed record CatalogSource(string Id, string Name, string Type);
|
||||
105
BlueLaminate/BlueLaminate.Scraper/Skins/SkinCatalogClient.cs
Normal file
105
BlueLaminate/BlueLaminate.Scraper/Skins/SkinCatalogClient.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace BlueLaminate.Scraper.Skins;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the CS2 skin catalogue from the ByMykel/CSGO-API static dataset
|
||||
/// (skins.json) and maps it to <see cref="CatalogSkin"/> records. This replaces
|
||||
/// the old HTML scraper: one JSON file carries every skin with its weapon,
|
||||
/// category, rarity, wear range, and the collections/containers it comes from.
|
||||
/// </summary>
|
||||
public sealed class SkinCatalogClient
|
||||
{
|
||||
public const string DefaultUrl =
|
||||
"https://raw.githubusercontent.com/ByMykel/CSGO-API/refs/heads/main/public/api/en/skins.json";
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly string _url;
|
||||
|
||||
public SkinCatalogClient(HttpClient http, string? url = null)
|
||||
{
|
||||
_http = http;
|
||||
_url = url ?? DefaultUrl;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CatalogSkin>> FetchAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var stream = await _http.GetStreamAsync(_url, ct);
|
||||
var dtos = await JsonSerializer.DeserializeAsync<List<SkinDto>>(stream, Options, ct)
|
||||
?? throw new InvalidOperationException("skins.json deserialized to null.");
|
||||
|
||||
return dtos.Select(Map).ToList();
|
||||
}
|
||||
|
||||
private static CatalogSkin Map(SkinDto dto)
|
||||
{
|
||||
var sources = new List<CatalogSource>();
|
||||
AddSources(sources, dto.Collections, "Collection");
|
||||
AddSources(sources, dto.Crates, "Container");
|
||||
|
||||
return new CatalogSkin(
|
||||
Id: dto.Id,
|
||||
WeaponName: dto.Weapon?.Name ?? "Unknown",
|
||||
Category: dto.Category?.Name ?? "Unknown",
|
||||
Team: MapTeam(dto.Team?.Id),
|
||||
// Knives with no finish carry a null pattern; "Vanilla" is the community term.
|
||||
Name: dto.Pattern?.Name ?? "Vanilla",
|
||||
Rarity: dto.Rarity?.Name ?? "Unknown",
|
||||
Description: dto.Description,
|
||||
ImageUrl: dto.Image,
|
||||
StatTrakAvailable: dto.Stattrak,
|
||||
SouvenirAvailable: dto.Souvenir,
|
||||
FloatMin: dto.MinFloat,
|
||||
FloatMax: dto.MaxFloat,
|
||||
Sources: sources);
|
||||
}
|
||||
|
||||
private static void AddSources(List<CatalogSource> into, List<NamedDto>? items, string type)
|
||||
{
|
||||
if (items is null)
|
||||
return;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (string.IsNullOrEmpty(item.Id) || string.IsNullOrEmpty(item.Name))
|
||||
continue;
|
||||
if (into.Any(s => s.Id == item.Id))
|
||||
continue;
|
||||
into.Add(new CatalogSource(item.Id, item.Name, type));
|
||||
}
|
||||
}
|
||||
|
||||
private static string MapTeam(string? teamId) => teamId switch
|
||||
{
|
||||
"terrorists" => "T",
|
||||
"counter-terrorists" => "CT",
|
||||
_ => "Both",
|
||||
};
|
||||
|
||||
private sealed record SkinDto(
|
||||
string Id,
|
||||
string? Name,
|
||||
string? Description,
|
||||
NamedDto? Weapon,
|
||||
NamedDto? Category,
|
||||
NamedDto? Pattern,
|
||||
decimal? MinFloat,
|
||||
decimal? MaxFloat,
|
||||
NamedDto? Rarity,
|
||||
bool Stattrak,
|
||||
bool Souvenir,
|
||||
string? Image,
|
||||
NamedDto? Team,
|
||||
List<NamedDto>? Collections,
|
||||
List<NamedDto>? Crates);
|
||||
|
||||
private sealed record NamedDto(string? Id, string? Name);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace BlueLaminate.Scraper.Weapons;
|
||||
|
||||
/// <summary>A single CS2 weapon parsed from the Counter-Strike wiki.</summary>
|
||||
/// <param name="Name">Display name, e.g. "AK-47".</param>
|
||||
/// <param name="Type">Category from the wiki heading, e.g. "Pistols", "Assault Rifles".</param>
|
||||
/// <param name="Team">"CT", "T", or "Both".</param>
|
||||
public sealed record ScrapedWeapon(string Name, string Type, string Team);
|
||||
@@ -1,172 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using BlueLaminate.Scraper.Wiki;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace BlueLaminate.Scraper.Weapons;
|
||||
|
||||
/// <summary>
|
||||
/// Scrapes the CS2 weapon catalogue from the wiki's "Weapons" page.
|
||||
///
|
||||
/// Layout: the page has one "tabber" per weapon category, each immediately
|
||||
/// preceded by a section heading (the category / Type). Inside each tabber the
|
||||
/// "Global Offensive & Counter-Strike 2" tab holds a gallery of captions —
|
||||
/// one per weapon, optionally suffixed with "(CT)" or "(T)" for team-locked
|
||||
/// weapons.
|
||||
/// </summary>
|
||||
public sealed class WeaponWikiScraper
|
||||
{
|
||||
private const string Page = "Weapons";
|
||||
private const string Cs2TabHash = "Global_Offensive_&_Counter-Strike_2";
|
||||
|
||||
// Matches a trailing "(CT)" / "(T)" team annotation, capturing the team.
|
||||
private static readonly Regex TeamAnnotation =
|
||||
new(@"\s*\((CT|T)\)\s*$", RegexOptions.Compiled);
|
||||
|
||||
// The wiki labels the default knife "Stock Knife"; drop the prefix.
|
||||
private static readonly Regex StockPrefix =
|
||||
new(@"^Stock\s+", RegexOptions.Compiled);
|
||||
|
||||
private readonly WikiPageFetcher _fetcher;
|
||||
|
||||
public WeaponWikiScraper(WikiPageFetcher fetcher) => _fetcher = fetcher;
|
||||
|
||||
public async Task<IReadOnlyList<ScrapedWeapon>> ScrapeAsync(CancellationToken ct = default)
|
||||
{
|
||||
var doc = await _fetcher.LoadAsync(Page, ct);
|
||||
|
||||
// Headings and tabbers in document order so each tabber inherits the
|
||||
// most recent heading as its category.
|
||||
var nodes = doc.DocumentNode.SelectNodes(
|
||||
"//h2 | //h3 | //h4 | "
|
||||
+ "//div[contains(concat(' ', normalize-space(@class), ' '), ' tabber ')]");
|
||||
|
||||
var aggregator = new WeaponAggregator();
|
||||
string? currentType = null;
|
||||
|
||||
if (nodes is not null)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (node.Name is "h2" or "h3" or "h4")
|
||||
{
|
||||
currentType = HeadingText(node);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentType is null)
|
||||
continue;
|
||||
|
||||
foreach (var caption in Cs2Captions(node))
|
||||
aggregator.Add(caption, currentType);
|
||||
}
|
||||
}
|
||||
|
||||
return aggregator.Build();
|
||||
}
|
||||
|
||||
/// <summary>Caption texts from the CS2 tab of a single tabber, if present.</summary>
|
||||
private static IEnumerable<string> Cs2Captions(HtmlNode tabber)
|
||||
{
|
||||
var tabs = tabber.SelectNodes(
|
||||
".//li[contains(concat(' ', normalize-space(@class), ' '), ' wds-tabs__tab ')]");
|
||||
if (tabs is null)
|
||||
yield break;
|
||||
|
||||
var index = -1;
|
||||
for (var i = 0; i < tabs.Count; i++)
|
||||
{
|
||||
// HtmlAgilityPack returns attribute values un-decoded, and the wiki
|
||||
// entity-encodes the "&" in this hash (&).
|
||||
var hash = HtmlEntity.DeEntitize(tabs[i].GetAttributeValue("data-hash", string.Empty));
|
||||
if (hash == Cs2TabHash)
|
||||
{
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (index < 0)
|
||||
yield break;
|
||||
|
||||
var contents = tabber.SelectNodes(
|
||||
".//div[contains(concat(' ', normalize-space(@class), ' '), ' wds-tab__content ')]");
|
||||
if (contents is null || index >= contents.Count)
|
||||
yield break;
|
||||
|
||||
var captions = contents[index].SelectNodes(
|
||||
".//div[contains(concat(' ', normalize-space(@class), ' '), ' lightbox-caption ')]");
|
||||
if (captions is null)
|
||||
yield break;
|
||||
|
||||
foreach (var caption in captions)
|
||||
yield return WikiText.Normalize(caption.InnerText);
|
||||
}
|
||||
|
||||
private static string HeadingText(HtmlNode heading)
|
||||
{
|
||||
var headline = heading.SelectSingleNode(
|
||||
".//span[contains(concat(' ', normalize-space(@class), ' '), ' mw-headline ')]");
|
||||
return WikiText.Normalize((headline ?? heading).InnerText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collapses the per-caption rows into one weapon per name, tracking which
|
||||
/// teams it appeared for so a weapon shown as both "(CT)" and "(T)" (or with
|
||||
/// no annotation) resolves to "Both".
|
||||
/// </summary>
|
||||
private sealed class WeaponAggregator
|
||||
{
|
||||
private sealed class Entry
|
||||
{
|
||||
public required string Type { get; init; }
|
||||
public bool SawCt;
|
||||
public bool SawT;
|
||||
public bool SawUnannotated;
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, Entry> _byName = new();
|
||||
private readonly List<string> _order = new();
|
||||
|
||||
public void Add(string caption, string type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(caption))
|
||||
return;
|
||||
|
||||
var match = TeamAnnotation.Match(caption);
|
||||
var name = TeamAnnotation.Replace(caption, string.Empty);
|
||||
name = StockPrefix.Replace(name, string.Empty).Trim();
|
||||
if (name.Length == 0)
|
||||
return;
|
||||
|
||||
if (!_byName.TryGetValue(name, out var entry))
|
||||
{
|
||||
entry = new Entry { Type = type };
|
||||
_byName[name] = entry;
|
||||
_order.Add(name);
|
||||
}
|
||||
|
||||
if (!match.Success)
|
||||
entry.SawUnannotated = true;
|
||||
else if (match.Groups[1].Value == "CT")
|
||||
entry.SawCt = true;
|
||||
else
|
||||
entry.SawT = true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ScrapedWeapon> Build()
|
||||
{
|
||||
var result = new List<ScrapedWeapon>(_order.Count);
|
||||
foreach (var name in _order)
|
||||
{
|
||||
var e = _byName[name];
|
||||
var team =
|
||||
e.SawUnannotated || (e.SawCt && e.SawT) ? "Both"
|
||||
: e.SawCt ? "CT"
|
||||
: e.SawT ? "T"
|
||||
: "Both";
|
||||
result.Add(new ScrapedWeapon(name, e.Type, team));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace BlueLaminate.Scraper.Wiki;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches a rendered page from the Counter-Strike Fandom wiki, shared by all
|
||||
/// wiki scrapers.
|
||||
///
|
||||
/// The rendered HTML pages sit behind Cloudflare, which 403s .NET's TLS
|
||||
/// fingerprint regardless of headers. The MediaWiki <c>action=parse</c> API is
|
||||
/// not challenged, so we fetch the same content as JSON from there and return
|
||||
/// the embedded HTML as a parsed document.
|
||||
/// </summary>
|
||||
public sealed class WikiPageFetcher
|
||||
{
|
||||
private const string ApiBase = "https://counterstrike.fandom.com/api.php";
|
||||
|
||||
private readonly HttpClient _http;
|
||||
|
||||
public WikiPageFetcher(HttpClient http) => _http = http;
|
||||
|
||||
/// <summary>Loads a wiki page (e.g. "Weapons") as a parsed HTML document.</summary>
|
||||
public async Task<HtmlDocument> LoadAsync(string page, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"{ApiBase}?action=parse&page={Uri.EscapeDataString(page)}&prop=text&format=json";
|
||||
|
||||
using var resp = await _http.GetAsync(url, ct);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
await using var stream = await resp.Content.ReadAsStreamAsync(ct);
|
||||
using var json = await JsonDocument.ParseAsync(stream, cancellationToken: ct);
|
||||
|
||||
if (json.RootElement.TryGetProperty("error", out var error))
|
||||
{
|
||||
var info = error.TryGetProperty("info", out var i) ? i.GetString() : "unknown error";
|
||||
throw new InvalidOperationException($"Wiki API returned an error for page '{page}': {info}");
|
||||
}
|
||||
|
||||
var html = json.RootElement
|
||||
.GetProperty("parse")
|
||||
.GetProperty("text")
|
||||
.GetProperty("*")
|
||||
.GetString()
|
||||
?? throw new InvalidOperationException($"Wiki API response for page '{page}' had no parsed text.");
|
||||
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
return doc;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using HtmlAgilityPack;
|
||||
|
||||
namespace BlueLaminate.Scraper.Wiki;
|
||||
|
||||
/// <summary>Text helpers shared by wiki scrapers.</summary>
|
||||
public static class WikiText
|
||||
{
|
||||
private static readonly Regex Whitespace = new(@"\s+", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>Decodes HTML entities and collapses whitespace runs to single spaces.</summary>
|
||||
public static string Normalize(string raw) =>
|
||||
Whitespace.Replace(HtmlEntity.DeEntitize(raw) ?? string.Empty, " ").Trim();
|
||||
}
|
||||
37
db/02_readonly_role.sql
Normal file
37
db/02_readonly_role.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- ============================================================
|
||||
-- CS2 Skin Tracker — read-only reporting/BI role
|
||||
-- Run as a superuser (e.g. postgres) OR as skintracker_app,
|
||||
-- connected to the skintracker database. Safe to re-run.
|
||||
--
|
||||
-- Replace the password placeholder before running.
|
||||
-- This role can SELECT every table in the skintracker schema
|
||||
-- (existing and future) and nothing else: no INSERT/UPDATE/
|
||||
-- DELETE, no DDL, no access to other schemas.
|
||||
-- ============================================================
|
||||
|
||||
-- 1. Login role, guarded so the script is idempotent.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'skintracker_readonly') THEN
|
||||
CREATE ROLE skintracker_readonly WITH LOGIN PASSWORD 'change-me-readonly-password';
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- 2. Allow the role to connect to the database and resolve names
|
||||
-- in the skintracker schema by default.
|
||||
GRANT CONNECT ON DATABASE skintracker TO skintracker_readonly;
|
||||
GRANT USAGE ON SCHEMA skintracker TO skintracker_readonly;
|
||||
ALTER ROLE skintracker_readonly SET search_path = skintracker;
|
||||
|
||||
-- 3. Read access to every existing table/view in the schema.
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA skintracker TO skintracker_readonly;
|
||||
|
||||
-- 4. Read access to tables the app role creates LATER (e.g. new
|
||||
-- migrations). Without this, future tables would not be visible.
|
||||
-- Must name the role that OWNS new objects — that is skintracker_app.
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE skintracker_app IN SCHEMA skintracker
|
||||
GRANT SELECT ON TABLES TO skintracker_readonly;
|
||||
|
||||
-- 5. Belt-and-braces: ensure the role can never write to public either.
|
||||
REVOKE CREATE ON SCHEMA public FROM skintracker_readonly;
|
||||
82
db/03_catalog_audit.sql
Normal file
82
db/03_catalog_audit.sql
Normal file
@@ -0,0 +1,82 @@
|
||||
-- ============================================================
|
||||
-- CS2 Skin Tracker — catalog audit (read-only)
|
||||
-- Run against the skintracker database as any role with SELECT
|
||||
-- (e.g. skintracker_readonly). Pure SELECTs, safe to re-run.
|
||||
--
|
||||
-- Purpose: sanity-check the weapon/glove skin catalog for the
|
||||
-- gaps that break tradeup math and pricing joins.
|
||||
-- ============================================================
|
||||
|
||||
SET search_path = skintracker;
|
||||
|
||||
-- 1. Totals — quick scale check.
|
||||
SELECT 'skins' AS table, count(*) FROM skins
|
||||
UNION ALL SELECT 'weapons', count(*) FROM weapons
|
||||
UNION ALL SELECT 'collections', count(*) FROM collections
|
||||
UNION ALL SELECT 'skin_collections', count(*) FROM skin_collections
|
||||
UNION ALL SELECT 'skin_conditions', count(*) FROM skin_conditions;
|
||||
|
||||
-- 2. Orphan skins: belong to NO collection/container.
|
||||
-- Expected: only "Howl" (Contraband, removed from its collection).
|
||||
SELECT s.id, s.slug, s.name, s.rarity
|
||||
FROM skins s
|
||||
LEFT JOIN skin_collections sc ON sc.skins_id = s.id
|
||||
WHERE sc.skins_id IS NULL
|
||||
ORDER BY s.name;
|
||||
|
||||
-- 3. Missing float bounds.
|
||||
-- Expected: only Vanilla (default) knives, which have no wear range.
|
||||
SELECT s.id, s.slug, s.name, s.float_min, s.float_max
|
||||
FROM skins s
|
||||
WHERE s.float_min IS NULL OR s.float_max IS NULL
|
||||
ORDER BY s.name;
|
||||
|
||||
-- 4. Rarity hygiene: any nulls/blanks, and the distribution.
|
||||
SELECT coalesce(nullif(rarity, ''), '<empty/null>') AS rarity, count(*)
|
||||
FROM skins
|
||||
GROUP BY 1
|
||||
ORDER BY 2 DESC;
|
||||
|
||||
-- 5. Inverted or out-of-range float bounds (should return 0 rows).
|
||||
SELECT id, slug, name, float_min, float_max
|
||||
FROM skins
|
||||
WHERE float_min > float_max OR float_min < 0 OR float_max > 1;
|
||||
|
||||
-- 6. Duplicate slugs (unique index should keep this empty).
|
||||
SELECT slug, count(*)
|
||||
FROM skins
|
||||
GROUP BY slug
|
||||
HAVING count(*) > 1;
|
||||
|
||||
-- 7. Collection type split (expect only Collection / Container).
|
||||
SELECT coalesce(type, '<null>') AS type, count(*)
|
||||
FROM collections
|
||||
GROUP BY 1
|
||||
ORDER BY 2 DESC;
|
||||
|
||||
-- 8. Skins with no weapon link (should return 0 rows).
|
||||
SELECT s.id, s.slug
|
||||
FROM skins s
|
||||
LEFT JOIN weapons w ON w.id = s.weapon_id
|
||||
WHERE w.id IS NULL;
|
||||
|
||||
-- 9. Skin count per weapon type — confirms only weapon/glove items.
|
||||
-- Note: Zeus x27 (type "Equipment") skins ARE valid catalog/tradeup items.
|
||||
SELECT w.type, count(s.id) AS skins
|
||||
FROM weapons w
|
||||
LEFT JOIN skins s ON s.weapon_id = w.id
|
||||
GROUP BY w.type
|
||||
ORDER BY skins DESC;
|
||||
|
||||
-- 10. StatTrak / Souvenir availability.
|
||||
SELECT
|
||||
sum(CASE WHEN stat_trak_available THEN 1 ELSE 0 END) AS stattrak,
|
||||
sum(CASE WHEN souvenir_available THEN 1 ELSE 0 END) AS souvenir,
|
||||
count(*) AS total
|
||||
FROM skins;
|
||||
|
||||
-- 11. true_float split (capped range vs full 0.0–1.0; null = unknown bounds).
|
||||
SELECT coalesce(true_float::text, '<null>') AS true_float, count(*)
|
||||
FROM skins
|
||||
GROUP BY 1
|
||||
ORDER BY 2 DESC;
|
||||
Reference in New Issue
Block a user