Change to static skin catalog population
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.CommandLine" Version="2.0.8" />
|
<PackageReference Include="System.CommandLine" Version="2.0.8" />
|
||||||
|
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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;
|
||||||
|
using BlueLaminate.Cli.Logging;
|
||||||
using BlueLaminate.EFCore.Data;
|
using BlueLaminate.EFCore.Data;
|
||||||
using BlueLaminate.Scraper.Weapons;
|
using BlueLaminate.Scraper.Skins;
|
||||||
using BlueLaminate.Scraper.Wiki;
|
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.
|
// Entry point: System.CommandLine builds the command tree, parsing, and help.
|
||||||
// New features are added as additional commands here as they're implemented.
|
// 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")
|
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(
|
var syncSkins = new Command(
|
||||||
"sync-weapons",
|
"sync-skins",
|
||||||
"Scrape the CS2 weapon catalogue from the wiki and upsert it (throttled to once a month).")
|
"Load the CS2 skin catalogue from the CSGO-API dataset and upsert it (throttled to once a month).")
|
||||||
{
|
{
|
||||||
forceOption,
|
forceOption,
|
||||||
dryRunOption,
|
dryRunOption,
|
||||||
};
|
};
|
||||||
syncWeapons.SetAction((parseResult, ct) =>
|
syncSkins.SetAction((parseResult, ct) =>
|
||||||
SyncWeaponsAsync(parseResult.GetValue(forceOption), parseResult.GetValue(dryRunOption), ct));
|
SyncSkinsAsync(
|
||||||
|
parseResult.GetValue(forceOption),
|
||||||
|
parseResult.GetValue(dryRunOption),
|
||||||
|
loggerFactory,
|
||||||
|
ct));
|
||||||
|
|
||||||
var root = new RootCommand("BlueLaminate CLI — Counter-Strike skin tracker tools.")
|
var root = new RootCommand("BlueLaminate CLI — Counter-Strike skin tracker tools.")
|
||||||
{
|
{
|
||||||
syncWeapons,
|
syncSkins,
|
||||||
};
|
};
|
||||||
|
|
||||||
return await root.Parse(args).InvokeAsync();
|
return await root.Parse(args).InvokeAsync();
|
||||||
|
|
||||||
// Fetch the CS2 weapon catalogue from the wiki and upsert it. Throttled to once
|
// Load the CS2 skin catalogue from the CSGO-API dataset and upsert it. Weapons
|
||||||
// a month unless --force is passed; --dry-run scrapes and prints without a DB.
|
// and collections are derived from the skins themselves. Throttled to once a
|
||||||
static async Task<int> SyncWeaponsAsync(bool force, bool dryRun, CancellationToken ct)
|
// 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)
|
if (dryRun)
|
||||||
{
|
{
|
||||||
var weapons = await scraper.ScrapeAsync(ct);
|
logger.LogInformation("Loading skin catalogue (dry run — nothing will be written).");
|
||||||
Console.WriteLine($"Scraped {weapons.Count} weapons (dry run, nothing written):");
|
var skins = await client.FetchAsync(ct);
|
||||||
foreach (var w in weapons)
|
logger.LogInformation("Loaded {Count} skins.", skins.Count);
|
||||||
Console.WriteLine($" {w.Name,-20} {w.Type,-16} {w.Team}");
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var db = new SkinTrackerDbContextFactory().CreateDbContext([]);
|
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)
|
if (result.Skipped)
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
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.");
|
+ "Next run allowed one month later — pass --force to override.");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"Synced {result.Scraped} weapons: {result.Inserted} inserted, "
|
$"Synced {result.Loaded} skins: {result.Inserted} inserted, "
|
||||||
+ $"{result.Updated} updated, "
|
+ $"{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;
|
return 0;
|
||||||
@@ -70,10 +104,7 @@ static async Task<int> SyncWeaponsAsync(bool force, bool dryRun, CancellationTok
|
|||||||
static HttpClient CreateHttpClient()
|
static HttpClient CreateHttpClient()
|
||||||
{
|
{
|
||||||
var http = new HttpClient();
|
var http = new HttpClient();
|
||||||
// The wiki is fronted by Cloudflare; a browser-like User-Agent is accepted
|
http.Timeout = TimeSpan.FromMinutes(2);
|
||||||
// on the MediaWiki API endpoint the scraper uses.
|
http.DefaultRequestHeaders.UserAgent.ParseAdd("BlueLaminate.Cli");
|
||||||
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");
|
|
||||||
return http;
|
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)
|
public void Configure(EntityTypeBuilder<Skin> entity)
|
||||||
{
|
{
|
||||||
entity.Property(e => e.FloatMin)
|
// Nullable: null means the catalogue gives no wear range (e.g. vanilla
|
||||||
.HasColumnType("numeric(10,9)")
|
// knives), distinct from a genuine 0.0–1.0 range.
|
||||||
.HasDefaultValue(0.0m);
|
entity.Property(e => e.FloatMin).HasColumnType("numeric(10,9)");
|
||||||
entity.Property(e => e.FloatMax)
|
entity.Property(e => e.FloatMax).HasColumnType("numeric(10,9)");
|
||||||
.HasColumnType("numeric(10,9)")
|
|
||||||
.HasDefaultValue(1.0m);
|
|
||||||
|
|
||||||
entity.Property(e => e.TrueFloat)
|
entity.Property(e => e.TrueFloat)
|
||||||
.HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", stored: true);
|
.HasComputedColumnSql("float_min = 0.0 AND float_max = 1.0", stored: true);
|
||||||
|
|
||||||
entity.HasIndex(e => e.TrueFloat);
|
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)
|
entity.HasOne(e => e.Weapon)
|
||||||
.WithMany(w => w.Skins)
|
.WithMany(w => w.Skins)
|
||||||
.HasForeignKey(e => e.WeaponId);
|
.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<Weapon> Weapons => Set<Weapon>();
|
||||||
public DbSet<ScrapeRun> ScrapeRuns => Set<ScrapeRun>();
|
public DbSet<ScrapeRun> ScrapeRuns => Set<ScrapeRun>();
|
||||||
|
public DbSet<Collection> Collections => Set<Collection>();
|
||||||
public DbSet<Skin> Skins => Set<Skin>();
|
public DbSet<Skin> Skins => Set<Skin>();
|
||||||
public DbSet<SkinCondition> SkinConditions => Set<SkinCondition>();
|
public DbSet<SkinCondition> SkinConditions => Set<SkinCondition>();
|
||||||
public DbSet<SteamUser> SteamUsers => Set<SteamUser>();
|
public DbSet<SteamUser> SteamUsers => Set<SteamUser>();
|
||||||
@@ -38,6 +39,7 @@ public class SkinTrackerDbContext : DbContext
|
|||||||
|
|
||||||
modelBuilder.ApplyConfiguration(new WeaponConfiguration());
|
modelBuilder.ApplyConfiguration(new WeaponConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new ScrapeRunConfiguration());
|
modelBuilder.ApplyConfiguration(new ScrapeRunConfiguration());
|
||||||
|
modelBuilder.ApplyConfiguration(new CollectionConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new SkinConfiguration());
|
modelBuilder.ApplyConfiguration(new SkinConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new SkinConditionConfiguration());
|
modelBuilder.ApplyConfiguration(new SkinConditionConfiguration());
|
||||||
modelBuilder.ApplyConfiguration(new SteamUserConfiguration());
|
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 int WeaponId { get; set; }
|
||||||
public Weapon Weapon { get; set; } = null!;
|
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 Name { get; set; } = null!;
|
||||||
public string Rarity { get; set; } = null!;
|
public string Rarity { get; set; } = null!;
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public string? ImageUrl { get; set; }
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
public decimal FloatMin { get; set; }
|
public bool StatTrakAvailable { get; set; }
|
||||||
public decimal FloatMax { get; set; }
|
public bool SouvenirAvailable { get; set; }
|
||||||
|
|
||||||
// Computed in the database: float_min = 0.0 AND float_max = 1.0.
|
/// <summary>Every collection and container this skin originates from.</summary>
|
||||||
// A skin with a capped float range behaves differently in tradeup calculations.
|
public ICollection<Collection> Collections { get; set; } = new List<Collection>();
|
||||||
public bool TrueFloat { get; private set; }
|
|
||||||
|
// 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<SkinCondition> Conditions { get; set; } = new List<SkinCondition>();
|
||||||
public ICollection<SkinInstance> Instances { get; set; } = new List<SkinInstance>();
|
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);
|
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 =>
|
modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b =>
|
||||||
{
|
{
|
||||||
b.Property<int>("Id")
|
b.Property<int>("Id")
|
||||||
@@ -156,16 +190,12 @@ namespace BlueLaminate.EFCore.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("description");
|
.HasColumnName("description");
|
||||||
|
|
||||||
b.Property<decimal>("FloatMax")
|
b.Property<decimal?>("FloatMax")
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("numeric(10,9)")
|
.HasColumnType("numeric(10,9)")
|
||||||
.HasDefaultValue(1.0m)
|
|
||||||
.HasColumnName("float_max");
|
.HasColumnName("float_max");
|
||||||
|
|
||||||
b.Property<decimal>("FloatMin")
|
b.Property<decimal?>("FloatMin")
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("numeric(10,9)")
|
.HasColumnType("numeric(10,9)")
|
||||||
.HasDefaultValue(0.0m)
|
|
||||||
.HasColumnName("float_min");
|
.HasColumnName("float_min");
|
||||||
|
|
||||||
b.Property<string>("ImageUrl")
|
b.Property<string>("ImageUrl")
|
||||||
@@ -182,7 +212,20 @@ namespace BlueLaminate.EFCore.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("rarity");
|
.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()
|
.ValueGeneratedOnAddOrUpdate()
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasColumnName("true_float")
|
.HasColumnName("true_float")
|
||||||
@@ -195,6 +238,10 @@ namespace BlueLaminate.EFCore.Migrations
|
|||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
.HasName("pk_skins");
|
.HasName("pk_skins");
|
||||||
|
|
||||||
|
b.HasIndex("Slug")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_skins_slug");
|
||||||
|
|
||||||
b.HasIndex("TrueFloat")
|
b.HasIndex("TrueFloat")
|
||||||
.HasDatabaseName("ix_skins_true_float");
|
.HasDatabaseName("ix_skins_true_float");
|
||||||
|
|
||||||
@@ -427,6 +474,25 @@ namespace BlueLaminate.EFCore.Migrations
|
|||||||
b.ToTable("weapons", "skintracker");
|
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 =>
|
modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("BlueLaminate.EFCore.Entities.SkinInstance", "SkinInstance")
|
b.HasOne("BlueLaminate.EFCore.Entities.SkinInstance", "SkinInstance")
|
||||||
@@ -556,6 +622,23 @@ namespace BlueLaminate.EFCore.Migrations
|
|||||||
b.Navigation("Trade");
|
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 =>
|
modelBuilder.Entity("BlueLaminate.EFCore.Entities.InventoryItem", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("TradeItems");
|
b.Navigation("TradeItems");
|
||||||
|
|||||||
@@ -6,8 +6,4 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="HtmlAgilityPack" Version="1.12.4" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</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