almost ready
This commit is contained in:
@@ -8,7 +8,8 @@ public class InventoryItemConfiguration : IEntityTypeConfiguration<InventoryItem
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<InventoryItem> entity)
|
||||
{
|
||||
entity.HasIndex(e => e.AssetId);
|
||||
// A Steam asset id identifies one physical copy; never store it twice.
|
||||
entity.HasIndex(e => e.AssetId).IsUnique();
|
||||
|
||||
entity.HasOne(e => e.User)
|
||||
.WithMany(u => u.InventoryItems)
|
||||
|
||||
@@ -31,6 +31,14 @@ public class ListingConfiguration : IEntityTypeConfiguration<Listing>
|
||||
.HasForeignKey(e => e.SkinId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Wear band the sweep targeted (set directly from the sweep unit, not
|
||||
// best-effort). Set null on delete so a condition row can change without
|
||||
// blocking its listings — matching the cs.money/skin.land tables.
|
||||
entity.HasOne(e => e.Condition)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.ConditionId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
// Listings roll up to the physical item they represent.
|
||||
entity.HasOne(e => e.SkinInstance)
|
||||
.WithMany(i => i.Listings)
|
||||
|
||||
@@ -8,12 +8,11 @@ public class SkinConditionConfiguration : IEntityTypeConfiguration<SkinCondition
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SkinCondition> entity)
|
||||
{
|
||||
entity.Property(e => e.MinFloat).HasColumnType("numeric(10,9)");
|
||||
entity.Property(e => e.MaxFloat).HasColumnType("numeric(10,9)");
|
||||
entity.Property(e => e.FloatMin).HasColumnType("numeric(10,9)");
|
||||
entity.Property(e => e.FloatMax).HasColumnType("numeric(10,9)");
|
||||
|
||||
// The catalogue sweep orders bands by this (never-swept first, then stalest),
|
||||
// so index it like the equivalent column on skins.
|
||||
entity.HasIndex(e => e.ListingsSweptAt);
|
||||
// Per-site "last swept" checkpoints live in skin_condition_sweeps (one row per
|
||||
// site); see SkinConditionSweepConfiguration for the indexes that order them.
|
||||
|
||||
entity.HasOne(e => e.Skin)
|
||||
.WithMany(s => s.Conditions)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace BlueLaminate.EFCore.Configurations;
|
||||
|
||||
public class SkinConditionSweepConfiguration : IEntityTypeConfiguration<SkinConditionSweep>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SkinConditionSweep> entity)
|
||||
{
|
||||
// One checkpoint per band per site: the natural key, and what the upsert
|
||||
// ("stamp") in SweepCheckpoints relies on.
|
||||
entity.HasIndex(e => new { e.SkinConditionId, e.Source }).IsUnique();
|
||||
|
||||
// Each site's sweep orders its bands never-swept-first then stalest; index the
|
||||
// ordering it scans (filter by source, sort by swept_at).
|
||||
entity.HasIndex(e => new { e.Source, e.SweptAt });
|
||||
|
||||
entity.HasOne(e => e.SkinCondition)
|
||||
.WithMany(c => c.Sweeps)
|
||||
.HasForeignKey(e => e.SkinConditionId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -29,9 +29,8 @@ public class SkinConfiguration : IEntityTypeConfiguration<Skin>
|
||||
.IsUnique()
|
||||
.HasFilter("def_index IS NOT NULL AND paint_index IS NOT NULL");
|
||||
|
||||
// The catalogue sweep orders skins by when they were last swept (nulls
|
||||
// first) to resume across capped runs; index that ordering.
|
||||
entity.HasIndex(e => e.ListingsSweptAt);
|
||||
// Per-site "last swept" checkpoints live in skin_sweeps (one row per site);
|
||||
// see SkinSweepConfiguration for the indexes that order them.
|
||||
|
||||
entity.HasOne(e => e.Weapon)
|
||||
.WithMany(w => w.Skins)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace BlueLaminate.EFCore.Configurations;
|
||||
|
||||
public class SkinLandListingConfiguration : IEntityTypeConfiguration<SkinLandListing>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SkinLandListing> entity)
|
||||
{
|
||||
// skin.land's offer id is the natural key; ingest upserts against it and must
|
||||
// never create duplicates.
|
||||
entity.HasIndex(e => e.ListingId).IsUnique();
|
||||
|
||||
entity.Property(e => e.Price).HasPrecision(18, 2);
|
||||
// Full precision (matches SkinInstance/cs.money) even though skin.land offers
|
||||
// aren't fingerprinted — keep the float lossless for later analysis.
|
||||
entity.Property(e => e.FloatValue).HasColumnType("numeric(20,18)");
|
||||
|
||||
// Enum as text so the DB is self-describing (matches the project's leaning).
|
||||
entity.Property(e => e.Status).HasConversion<string>();
|
||||
|
||||
// Targeted scrape: results are filtered/sorted by skin+wear and by activity.
|
||||
entity.HasIndex(e => new { e.SkinId, e.ConditionId });
|
||||
entity.HasIndex(e => e.Status);
|
||||
|
||||
// Each job targets a known skin, so this link is required (Restrict: a skin with
|
||||
// live listings shouldn't be deleted out from under them).
|
||||
entity.HasOne(e => e.Skin)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.SkinId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasOne(e => e.Condition)
|
||||
.WithMany()
|
||||
.HasForeignKey(e => e.ConditionId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace BlueLaminate.EFCore.Configurations;
|
||||
|
||||
public class SkinSweepConfiguration : IEntityTypeConfiguration<SkinSweep>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SkinSweep> entity)
|
||||
{
|
||||
// One checkpoint per skin per site: the natural key the upsert relies on.
|
||||
entity.HasIndex(e => new { e.SkinId, e.Source }).IsUnique();
|
||||
|
||||
// Mirror SkinConditionSweep: index the (source, swept_at) ordering each sweep scans.
|
||||
entity.HasIndex(e => new { e.Source, e.SweptAt });
|
||||
|
||||
entity.HasOne(e => e.Skin)
|
||||
.WithMany(s => s.Sweeps)
|
||||
.HasForeignKey(e => e.SkinId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@ public class TradeConfiguration : IEntityTypeConfiguration<Trade>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Trade> entity)
|
||||
{
|
||||
// Steam's trade id is the natural key for an observed trade. Nullable (some
|
||||
// trades are reconstructed without one); Postgres keeps multiple NULLs distinct.
|
||||
entity.HasIndex(e => e.SteamTradeId).IsUnique();
|
||||
|
||||
entity.HasOne(e => e.FromUser)
|
||||
.WithMany(u => u.TradesSent)
|
||||
.HasForeignKey(e => e.FromUserId)
|
||||
|
||||
@@ -23,6 +23,8 @@ public class SkinTrackerDbContext : DbContext
|
||||
public DbSet<Collection> Collections => Set<Collection>();
|
||||
public DbSet<Skin> Skins => Set<Skin>();
|
||||
public DbSet<SkinCondition> SkinConditions => Set<SkinCondition>();
|
||||
public DbSet<SkinSweep> SkinSweeps => Set<SkinSweep>();
|
||||
public DbSet<SkinConditionSweep> SkinConditionSweeps => Set<SkinConditionSweep>();
|
||||
public DbSet<SteamUser> SteamUsers => Set<SteamUser>();
|
||||
public DbSet<SkinInstance> SkinInstances => Set<SkinInstance>();
|
||||
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
|
||||
@@ -31,6 +33,7 @@ public class SkinTrackerDbContext : DbContext
|
||||
public DbSet<PriceHistory> PriceHistories => Set<PriceHistory>();
|
||||
public DbSet<Listing> Listings => Set<Listing>();
|
||||
public DbSet<CsMoneyListing> CsMoneyListings => Set<CsMoneyListing>();
|
||||
public DbSet<SkinLandListing> SkinLandListings => Set<SkinLandListing>();
|
||||
|
||||
/// <summary>Read-only cross-market view UNIONing the per-market listing tables.</summary>
|
||||
public DbSet<MarketListing> MarketListings => Set<MarketListing>();
|
||||
@@ -47,6 +50,8 @@ public class SkinTrackerDbContext : DbContext
|
||||
modelBuilder.ApplyConfiguration(new CollectionConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinConditionConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinSweepConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinConditionSweepConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SteamUserConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinInstanceConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new InventoryItemConfiguration());
|
||||
@@ -55,6 +60,7 @@ public class SkinTrackerDbContext : DbContext
|
||||
modelBuilder.ApplyConfiguration(new PriceHistoryConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new ListingConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new CsMoneyListingConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SkinLandListingConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new MarketListingConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
64
BlueLaminate/BlueLaminate.EFCore/Data/SweepCheckpoints.cs
Normal file
64
BlueLaminate/BlueLaminate.EFCore/Data/SweepCheckpoints.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using BlueLaminate.EFCore.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BlueLaminate.EFCore.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Write helpers for the per-site sweep checkpoints (<see cref="SkinSweep"/> /
|
||||
/// <see cref="SkinConditionSweep"/>). Each marketplace sweeper stamps its own row
|
||||
/// keyed by <c>(entity, source)</c>, so a band swept on one site is still "never
|
||||
/// swept" on another. Adding a new site means a new <see cref="SweepSource"/>
|
||||
/// constant — no schema changes.
|
||||
/// <para>
|
||||
/// Reads stay inline in the sweep queries (a correlated subquery over the navigation
|
||||
/// for the relevant <c>Source</c>) so EF can translate and order by them server-side.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SweepCheckpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Record that <paramref name="source"/> just swept this wear band. Upserts the
|
||||
/// single (condition, source) row via the change tracker; the caller persists with
|
||||
/// <see cref="DbContext.SaveChangesAsync"/>.
|
||||
/// </summary>
|
||||
public static async Task StampConditionAsync(
|
||||
SkinTrackerDbContext db, int conditionId, string source, DateTimeOffset sweptAt, CancellationToken ct)
|
||||
{
|
||||
var existing = await db.SkinConditionSweeps
|
||||
.FirstOrDefaultAsync(s => s.SkinConditionId == conditionId && s.Source == source, ct);
|
||||
if (existing is null)
|
||||
{
|
||||
db.SkinConditionSweeps.Add(new SkinConditionSweep
|
||||
{
|
||||
SkinConditionId = conditionId,
|
||||
Source = source,
|
||||
SweptAt = sweptAt,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.SweptAt = sweptAt;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>As <see cref="StampConditionAsync"/>, for a whole-skin unit (no wear bands).</summary>
|
||||
public static async Task StampSkinAsync(
|
||||
SkinTrackerDbContext db, int skinId, string source, DateTimeOffset sweptAt, CancellationToken ct)
|
||||
{
|
||||
var existing = await db.SkinSweeps
|
||||
.FirstOrDefaultAsync(s => s.SkinId == skinId && s.Source == source, ct);
|
||||
if (existing is null)
|
||||
{
|
||||
db.SkinSweeps.Add(new SkinSweep
|
||||
{
|
||||
SkinId = skinId,
|
||||
Source = source,
|
||||
SweptAt = sweptAt,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.SweptAt = sweptAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,9 +36,12 @@ public class Listing
|
||||
/// <summary>"buy_now" or "auction".</summary>
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
/// <summary>Asking price in USD.</summary>
|
||||
/// <summary>Asking price.</summary>
|
||||
public decimal Price { get; set; }
|
||||
|
||||
/// <summary>Currency of <see cref="Price"/>. CSFloat lists in USD.</summary>
|
||||
public string Currency { get; set; } = "USD";
|
||||
|
||||
/// <summary>When CSFloat says the listing was created.</summary>
|
||||
public DateTimeOffset ListedAt { get; set; }
|
||||
|
||||
@@ -48,7 +51,13 @@ public class Listing
|
||||
public int PaintIndex { get; set; }
|
||||
public string MarketHashName { get; set; } = null!;
|
||||
public string? WearName { get; set; }
|
||||
public decimal FloatValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Exact float, or null for items with no float at all (e.g. Vanilla knives).
|
||||
/// Null is deliberately distinct from a genuine 0.0 float; a floatless item
|
||||
/// also can't be fingerprinted, so its <see cref="SkinInstanceId"/> stays null.
|
||||
/// </summary>
|
||||
public decimal? FloatValue { get; set; }
|
||||
public int PaintSeed { get; set; }
|
||||
public bool IsStatTrak { get; set; }
|
||||
public bool IsSouvenir { get; set; }
|
||||
@@ -68,6 +77,15 @@ public class Listing
|
||||
public int? SkinId { get; set; }
|
||||
public Skin? Skin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The wear band this listing belongs to. Unlike <see cref="SkinId"/> this is NOT
|
||||
/// best-effort: the catalogue sweep pages one skin+wear band at a time, so the band
|
||||
/// is set directly from the sweep unit. Null for whole-skin sweeps (e.g. vanilla
|
||||
/// knives with no wear bands).
|
||||
/// </summary>
|
||||
public int? ConditionId { get; set; }
|
||||
public SkinCondition? Condition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The physical item (by fingerprint) this listing is for. Many listings over
|
||||
/// time roll up to one instance, forming its market-movement history. Nullable
|
||||
|
||||
@@ -16,12 +16,6 @@ public class Skin
|
||||
public int? DefIndex { get; set; }
|
||||
public int? PaintIndex { get; set; }
|
||||
|
||||
// When the catalogue-driven listing sweep last fully covered this skin. The
|
||||
// sweep processes least-recently-swept skins first (nulls = never swept), so
|
||||
// capped runs chain across the whole catalogue and the stalest data refreshes
|
||||
// first. Null until the first sweep reaches this skin.
|
||||
public DateTimeOffset? ListingsSweptAt { get; set; }
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
public string Rarity { get; set; } = null!;
|
||||
public string? Description { get; set; }
|
||||
@@ -44,6 +38,12 @@ public class Skin
|
||||
public bool? TrueFloat { get; private set; }
|
||||
|
||||
public ICollection<SkinCondition> Conditions { get; set; } = new List<SkinCondition>();
|
||||
|
||||
// Per-site "last swept" checkpoints for the whole-skin sweep unit — only used for
|
||||
// skins with no wear bands (the per-band checkpoint lives on SkinCondition.Sweeps).
|
||||
// The sweep processes never-swept (no row) / stalest skins first. See SkinSweep.
|
||||
public ICollection<SkinSweep> Sweeps { get; set; } = new List<SkinSweep>();
|
||||
|
||||
public ICollection<SkinInstance> Instances { get; set; } = new List<SkinInstance>();
|
||||
public ICollection<PriceHistory> PriceHistories { get; set; } = new List<PriceHistory>();
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ public class SkinCondition
|
||||
public Skin Skin { get; set; } = null!;
|
||||
|
||||
public string Condition { get; set; } = null!;
|
||||
public decimal MinFloat { get; set; }
|
||||
public decimal MaxFloat { get; set; }
|
||||
public decimal FloatMin { get; set; }
|
||||
public decimal FloatMax { get; set; }
|
||||
|
||||
// When the catalogue-driven listing sweep last fully covered this skin's wear
|
||||
// band. The sweep splits each skin by wear and pages one band at a time, so this
|
||||
// is the per-band checkpoint: an interrupted run resumes from never-swept/stalest
|
||||
// bands rather than redoing a whole skin. Null until the first sweep reaches it.
|
||||
public DateTimeOffset? ListingsSweptAt { get; set; }
|
||||
// Per-site "last swept" checkpoints for this wear band — one row per marketplace
|
||||
// (Source). The sweep splits each skin by wear and pages one band at a time, so
|
||||
// this is the per-band checkpoint: an interrupted run resumes from never-swept
|
||||
// (no row) / stalest bands rather than redoing a whole skin. Tracked per site so a
|
||||
// band swept on CSFloat is still never-swept on cs.money. See SkinConditionSweep.
|
||||
public ICollection<SkinConditionSweep> Sweeps { get; set; } = new List<SkinConditionSweep>();
|
||||
|
||||
public ICollection<SkinInstance> Instances { get; set; } = new List<SkinInstance>();
|
||||
public ICollection<PriceHistory> PriceHistories { get; set; } = new List<PriceHistory>();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One site's "last swept" checkpoint for a single wear band. The catalogue sweep
|
||||
/// processes least-recently-swept bands first (no row = never swept), so capped/looping
|
||||
/// runs chain across the catalogue and refresh the stalest data first. Keyed by
|
||||
/// <c>(SkinConditionId, Source)</c> so each marketplace tracks its own progress
|
||||
/// independently — a band swept on one site stays never-swept on another.
|
||||
/// </summary>
|
||||
public class SkinConditionSweep
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int SkinConditionId { get; set; }
|
||||
public SkinCondition SkinCondition { get; set; } = null!;
|
||||
|
||||
/// <summary>Which site swept it — a <see cref="SweepSource"/> value.</summary>
|
||||
public string Source { get; set; } = null!;
|
||||
|
||||
public DateTimeOffset SweptAt { get; set; }
|
||||
}
|
||||
@@ -26,9 +26,11 @@ public class SkinInstance
|
||||
public SkinCondition? Condition { get; set; }
|
||||
|
||||
// The fingerprint. FloatValue is stored at full precision (see config) so
|
||||
// that exact-match dupe detection isn't fooled by rounding.
|
||||
// that exact-match dupe detection isn't fooled by rounding. An instance is
|
||||
// only created for items that have a float + paint seed (skins), so both are
|
||||
// non-null here even though some listings (e.g. vanilla knives) lack them.
|
||||
public decimal FloatValue { get; set; }
|
||||
public string PaintSeed { get; set; } = null!;
|
||||
public int PaintSeed { get; set; }
|
||||
public bool StatTrak { get; set; }
|
||||
public bool Souvenir { get; set; }
|
||||
public DateTimeOffset FirstSeenAt { get; set; }
|
||||
|
||||
54
BlueLaminate/BlueLaminate.EFCore/Entities/SkinLandListing.cs
Normal file
54
BlueLaminate/BlueLaminate.EFCore/Entities/SkinLandListing.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One offer observed on skin.land via its internal
|
||||
/// <c>GET /api/v2/obtained-skins?skin_id={id}&page={n}</c> endpoint (scraped through
|
||||
/// the Python worker, since skin.land has no public API and sits behind Cloudflare).
|
||||
/// <para>
|
||||
/// Kept in its own table like <see cref="CsMoneyListing"/>, but deliberately thinner:
|
||||
/// skin.land exposes a full-precision float and price but <b>no paint seed / def index</b>,
|
||||
/// so an offer can't be fingerprinted to a market-agnostic <see cref="SkinInstance"/> and
|
||||
/// there is no cross-market roll-up or dupe detection here (revisit if pattern is ever
|
||||
/// exposed). StatTrak and Souvenir live on <em>separate</em> skin.land pages (their own
|
||||
/// <c>stattrak-</c>/<c>souvenir-</c> slugs); v1 sweeps the base page per skin+wear, so
|
||||
/// <see cref="IsStatTrak"/>/<see cref="IsSouvenir"/> are normally false.
|
||||
/// </para>
|
||||
/// Soft-tracked across sweeps exactly like <see cref="CsMoneyListing"/>:
|
||||
/// <see cref="FirstSeenAt"/>/<see cref="LastSeenAt"/> bound the observation window and
|
||||
/// <see cref="Status"/> flips to <see cref="ListingStatus.Removed"/> when a once-seen
|
||||
/// offer stops appearing (sold/delisted).
|
||||
/// </summary>
|
||||
public class SkinLandListing
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>skin.land's offer id (obtained-skin <c>id</c>). Natural key for dedup.</summary>
|
||||
public long ListingId { get; set; }
|
||||
|
||||
// Catalogue links. Like cs.money (and unlike the CSFloat global sweep) these are NOT
|
||||
// best-effort: each scrape job targets one skin+wear, so we set them directly.
|
||||
public int SkinId { get; set; }
|
||||
public Skin Skin { get; set; } = null!;
|
||||
public int? ConditionId { get; set; }
|
||||
public SkinCondition? Condition { get; set; }
|
||||
|
||||
// Item identity, from the offer's skin block.
|
||||
public string MarketHashName { get; set; } = null!;
|
||||
public decimal? FloatValue { get; set; } // item_float (string, full precision)
|
||||
public bool IsStatTrak { get; set; }
|
||||
public bool IsSouvenir { get; set; }
|
||||
public string? NameTag { get; set; } // offer.name_tag (rare; affects value)
|
||||
public int StickerCount { get; set; }
|
||||
|
||||
// Pricing. skin.land returns a single price (the amount to buy/withdraw the item).
|
||||
public decimal Price { get; set; } // final_withdrawal_price
|
||||
public string Currency { get; set; } = "USD"; // prices are read in USD
|
||||
|
||||
public string? InspectLink { get; set; } // item_link (steam:// inspect)
|
||||
|
||||
// Soft-tracking across sweeps.
|
||||
public DateTimeOffset FirstSeenAt { get; set; }
|
||||
public DateTimeOffset LastSeenAt { get; set; }
|
||||
public ListingStatus Status { get; set; }
|
||||
public DateTimeOffset? RemovedAt { get; set; }
|
||||
}
|
||||
20
BlueLaminate/BlueLaminate.EFCore/Entities/SkinSweep.cs
Normal file
20
BlueLaminate/BlueLaminate.EFCore/Entities/SkinSweep.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// One site's "last swept" checkpoint for a whole skin — used only for skins with no
|
||||
/// wear bands (e.g. vanilla knives), which are swept as a single unit. The per-band
|
||||
/// equivalent is <see cref="SkinConditionSweep"/>. Keyed by <c>(SkinId, Source)</c> so
|
||||
/// each marketplace tracks its own progress independently.
|
||||
/// </summary>
|
||||
public class SkinSweep
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public int SkinId { get; set; }
|
||||
public Skin Skin { get; set; } = null!;
|
||||
|
||||
/// <summary>Which site swept it — a <see cref="SweepSource"/> value.</summary>
|
||||
public string Source { get; set; } = null!;
|
||||
|
||||
public DateTimeOffset SweptAt { get; set; }
|
||||
}
|
||||
23
BlueLaminate/BlueLaminate.EFCore/Entities/SweepSource.cs
Normal file
23
BlueLaminate/BlueLaminate.EFCore/Entities/SweepSource.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace BlueLaminate.EFCore.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Canonical site identifiers for per-site sweep checkpoints — the <c>Source</c>
|
||||
/// discriminator on <see cref="SkinSweep"/> and <see cref="SkinConditionSweep"/>.
|
||||
/// Each marketplace sweeper stamps its own checkpoint under one of these, so a band
|
||||
/// swept on one site is still "never swept" on another.
|
||||
/// <para>
|
||||
/// To add sweeping for a new marketplace, add one constant here and have that
|
||||
/// sweeper read/stamp checkpoints with it — no schema or query changes needed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SweepSource
|
||||
{
|
||||
/// <summary>CSFloat catalogue-driven sweep (<c>ListingSweepService.SweepCatalogAsync</c>).</summary>
|
||||
public const string CsFloatCatalog = "listings-catalog";
|
||||
|
||||
/// <summary>cs.money worker sweep (<c>CsMoneyIngestService</c>).</summary>
|
||||
public const string CsMoney = "csmoney";
|
||||
|
||||
/// <summary>skin.land worker sweep (<c>SkinLandIngestService</c>).</summary>
|
||||
public const string SkinLand = "skinland";
|
||||
}
|
||||
1207
BlueLaminate/BlueLaminate.EFCore/Migrations/20260531203937_AddPerSiteSweepCheckpoints.Designer.cs
generated
Normal file
1207
BlueLaminate/BlueLaminate.EFCore/Migrations/20260531203937_AddPerSiteSweepCheckpoints.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BlueLaminate.EFCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPerSiteSweepCheckpoints : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_skins_listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_skin_conditions_listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "skin_condition_sweeps",
|
||||
schema: "skintracker",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
skin_condition_id = table.Column<int>(type: "integer", nullable: false),
|
||||
source = table.Column<string>(type: "text", nullable: false),
|
||||
swept_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_skin_condition_sweeps", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_skin_condition_sweeps_skin_conditions_skin_condition_id",
|
||||
column: x => x.skin_condition_id,
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "skin_conditions",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "skin_sweeps",
|
||||
schema: "skintracker",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
skin_id = table.Column<int>(type: "integer", nullable: false),
|
||||
source = table.Column<string>(type: "text", nullable: false),
|
||||
swept_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_skin_sweeps", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_skin_sweeps_skins_skin_id",
|
||||
column: x => x.skin_id,
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "skins",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_condition_sweeps_skin_condition_id_source",
|
||||
schema: "skintracker",
|
||||
table: "skin_condition_sweeps",
|
||||
columns: new[] { "skin_condition_id", "source" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_condition_sweeps_source_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skin_condition_sweeps",
|
||||
columns: new[] { "source", "swept_at" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_sweeps_skin_id_source",
|
||||
schema: "skintracker",
|
||||
table: "skin_sweeps",
|
||||
columns: new[] { "skin_id", "source" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_sweeps_source_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skin_sweeps",
|
||||
columns: new[] { "source", "swept_at" });
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "skin_condition_sweeps",
|
||||
schema: "skintracker");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "skin_sweeps",
|
||||
schema: "skintracker");
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<DateTimeOffset>(
|
||||
name: "listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skins_listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skins",
|
||||
column: "listings_swept_at");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_conditions_listings_swept_at",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions",
|
||||
column: "listings_swept_at");
|
||||
}
|
||||
}
|
||||
}
|
||||
1323
BlueLaminate/BlueLaminate.EFCore/Migrations/20260531212842_AddSkinLandListings.Designer.cs
generated
Normal file
1323
BlueLaminate/BlueLaminate.EFCore/Migrations/20260531212842_AddSkinLandListings.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BlueLaminate.EFCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSkinLandListings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "skin_land_listings",
|
||||
schema: "skintracker",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
listing_id = table.Column<long>(type: "bigint", nullable: false),
|
||||
skin_id = table.Column<int>(type: "integer", nullable: false),
|
||||
condition_id = table.Column<int>(type: "integer", nullable: true),
|
||||
market_hash_name = table.Column<string>(type: "text", nullable: false),
|
||||
float_value = table.Column<decimal>(type: "numeric(20,18)", nullable: true),
|
||||
is_stat_trak = table.Column<bool>(type: "boolean", nullable: false),
|
||||
is_souvenir = table.Column<bool>(type: "boolean", nullable: false),
|
||||
name_tag = table.Column<string>(type: "text", nullable: true),
|
||||
sticker_count = table.Column<int>(type: "integer", nullable: false),
|
||||
price = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
currency = table.Column<string>(type: "text", nullable: false),
|
||||
inspect_link = table.Column<string>(type: "text", nullable: true),
|
||||
first_seen_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
last_seen_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
status = table.Column<string>(type: "text", nullable: false),
|
||||
removed_at = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("pk_skin_land_listings", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "fk_skin_land_listings_skin_conditions_condition_id",
|
||||
column: x => x.condition_id,
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "skin_conditions",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "fk_skin_land_listings_skins_skin_id",
|
||||
column: x => x.skin_id,
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "skins",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_land_listings_condition_id",
|
||||
schema: "skintracker",
|
||||
table: "skin_land_listings",
|
||||
column: "condition_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_land_listings_listing_id",
|
||||
schema: "skintracker",
|
||||
table: "skin_land_listings",
|
||||
column: "listing_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_land_listings_skin_id_condition_id",
|
||||
schema: "skintracker",
|
||||
table: "skin_land_listings",
|
||||
columns: new[] { "skin_id", "condition_id" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_skin_land_listings_status",
|
||||
schema: "skintracker",
|
||||
table: "skin_land_listings",
|
||||
column: "status");
|
||||
|
||||
// Extend the cross-market read view with a skin.land arm. skin.land exposes no
|
||||
// paint seed / asset id / instance fingerprint, so those columns are NULL; the
|
||||
// wear comes from the joined condition row (the offer table doesn't store it).
|
||||
migrationBuilder.Sql("""
|
||||
CREATE OR REPLACE VIEW skintracker.market_listings AS
|
||||
SELECT
|
||||
'csfloat'::text AS marketplace,
|
||||
l.cs_float_listing_id AS external_id,
|
||||
l.skin_id AS skin_id,
|
||||
NULL::integer AS condition_id,
|
||||
l.skin_instance_id AS skin_instance_id,
|
||||
l.market_hash_name AS market_hash_name,
|
||||
l.wear_name AS wear,
|
||||
l.float_value AS float_value,
|
||||
l.paint_seed AS paint_seed,
|
||||
l.is_stat_trak AS is_stat_trak,
|
||||
l.is_souvenir AS is_souvenir,
|
||||
l.sticker_count AS sticker_count,
|
||||
l.price AS price,
|
||||
'USD'::text AS currency,
|
||||
l.inspect_link AS inspect_link,
|
||||
l.asset_id AS asset_id,
|
||||
l.status AS status,
|
||||
l.first_seen_at AS first_seen_at,
|
||||
l.last_seen_at AS last_seen_at,
|
||||
l.removed_at AS removed_at
|
||||
FROM skintracker.listings l
|
||||
UNION ALL
|
||||
SELECT
|
||||
'csmoney'::text,
|
||||
c.sell_order_id::text,
|
||||
c.skin_id,
|
||||
c.condition_id,
|
||||
c.skin_instance_id,
|
||||
c.market_hash_name,
|
||||
-- Normalise cs.money's wear short code to the full wear name the
|
||||
-- other arms emit (csfloat wear_name / skinland condition), so the
|
||||
-- view's `wear` column is consistent across marketplaces.
|
||||
CASE lower(c.quality)
|
||||
WHEN 'fn' THEN 'Factory New'
|
||||
WHEN 'mw' THEN 'Minimal Wear'
|
||||
WHEN 'ft' THEN 'Field-Tested'
|
||||
WHEN 'ww' THEN 'Well-Worn'
|
||||
WHEN 'bs' THEN 'Battle-Scarred'
|
||||
ELSE c.quality
|
||||
END,
|
||||
c.float_value,
|
||||
c.paint_seed,
|
||||
c.is_stat_trak,
|
||||
c.is_souvenir,
|
||||
c.sticker_count,
|
||||
c.price,
|
||||
c.currency,
|
||||
c.inspect_link,
|
||||
c.asset_id,
|
||||
c.status,
|
||||
c.first_seen_at,
|
||||
c.last_seen_at,
|
||||
c.removed_at
|
||||
FROM skintracker.cs_money_listings c
|
||||
UNION ALL
|
||||
SELECT
|
||||
'skinland'::text,
|
||||
s.listing_id::text,
|
||||
s.skin_id,
|
||||
s.condition_id,
|
||||
NULL::integer,
|
||||
s.market_hash_name,
|
||||
sc.condition,
|
||||
s.float_value,
|
||||
NULL::integer,
|
||||
s.is_stat_trak,
|
||||
s.is_souvenir,
|
||||
s.sticker_count,
|
||||
s.price,
|
||||
s.currency,
|
||||
s.inspect_link,
|
||||
NULL::text,
|
||||
s.status,
|
||||
s.first_seen_at,
|
||||
s.last_seen_at,
|
||||
s.removed_at
|
||||
FROM skintracker.skin_land_listings s
|
||||
LEFT JOIN skintracker.skin_conditions sc ON sc.id = s.condition_id;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Restore the pre-skin.land view (csfloat + csmoney) before dropping the table
|
||||
// it references, so the view never points at a missing relation.
|
||||
migrationBuilder.Sql("""
|
||||
CREATE OR REPLACE VIEW skintracker.market_listings AS
|
||||
SELECT
|
||||
'csfloat'::text AS marketplace,
|
||||
l.cs_float_listing_id AS external_id,
|
||||
l.skin_id AS skin_id,
|
||||
NULL::integer AS condition_id,
|
||||
l.skin_instance_id AS skin_instance_id,
|
||||
l.market_hash_name AS market_hash_name,
|
||||
l.wear_name AS wear,
|
||||
l.float_value AS float_value,
|
||||
l.paint_seed AS paint_seed,
|
||||
l.is_stat_trak AS is_stat_trak,
|
||||
l.is_souvenir AS is_souvenir,
|
||||
l.sticker_count AS sticker_count,
|
||||
l.price AS price,
|
||||
'USD'::text AS currency,
|
||||
l.inspect_link AS inspect_link,
|
||||
l.asset_id AS asset_id,
|
||||
l.status AS status,
|
||||
l.first_seen_at AS first_seen_at,
|
||||
l.last_seen_at AS last_seen_at,
|
||||
l.removed_at AS removed_at
|
||||
FROM skintracker.listings l
|
||||
UNION ALL
|
||||
SELECT
|
||||
'csmoney'::text,
|
||||
c.sell_order_id::text,
|
||||
c.skin_id,
|
||||
c.condition_id,
|
||||
c.skin_instance_id,
|
||||
c.market_hash_name,
|
||||
-- Normalise cs.money's wear short code to the full wear name the
|
||||
-- other arms emit (csfloat wear_name / skinland condition), so the
|
||||
-- view's `wear` column is consistent across marketplaces.
|
||||
CASE lower(c.quality)
|
||||
WHEN 'fn' THEN 'Factory New'
|
||||
WHEN 'mw' THEN 'Minimal Wear'
|
||||
WHEN 'ft' THEN 'Field-Tested'
|
||||
WHEN 'ww' THEN 'Well-Worn'
|
||||
WHEN 'bs' THEN 'Battle-Scarred'
|
||||
ELSE c.quality
|
||||
END,
|
||||
c.float_value,
|
||||
c.paint_seed,
|
||||
c.is_stat_trak,
|
||||
c.is_souvenir,
|
||||
c.sticker_count,
|
||||
c.price,
|
||||
c.currency,
|
||||
c.inspect_link,
|
||||
c.asset_id,
|
||||
c.status,
|
||||
c.first_seen_at,
|
||||
c.last_seen_at,
|
||||
c.removed_at
|
||||
FROM skintracker.cs_money_listings c;
|
||||
""");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "skin_land_listings",
|
||||
schema: "skintracker");
|
||||
}
|
||||
}
|
||||
}
|
||||
1323
BlueLaminate/BlueLaminate.EFCore/Migrations/20260601024227_MakeListingFloatNullable.Designer.cs
generated
Normal file
1323
BlueLaminate/BlueLaminate.EFCore/Migrations/20260601024227_MakeListingFloatNullable.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BlueLaminate.EFCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakeListingFloatNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "float_value",
|
||||
schema: "skintracker",
|
||||
table: "listings",
|
||||
type: "numeric(20,18)",
|
||||
nullable: true,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "numeric(20,18)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<decimal>(
|
||||
name: "float_value",
|
||||
schema: "skintracker",
|
||||
table: "listings",
|
||||
type: "numeric(20,18)",
|
||||
nullable: false,
|
||||
defaultValue: 0m,
|
||||
oldClrType: typeof(decimal),
|
||||
oldType: "numeric(20,18)",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,308 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace BlueLaminate.EFCore.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ConsistencyPass_FloatBoundsCurrencyConditionPaintSeed : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_inventory_items_asset_id",
|
||||
schema: "skintracker",
|
||||
table: "inventory_items");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "min_float",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions",
|
||||
newName: "float_min");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "max_float",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions",
|
||||
newName: "float_max");
|
||||
|
||||
// text -> integer needs an explicit USING cast; EF's AlterColumn omits it and
|
||||
// Postgres won't cast automatically. Every stored seed is a stringified
|
||||
// integer, so the cast is total.
|
||||
migrationBuilder.Sql(
|
||||
"ALTER TABLE skintracker.skin_instances " +
|
||||
"ALTER COLUMN paint_seed TYPE integer USING paint_seed::integer;");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "condition_id",
|
||||
schema: "skintracker",
|
||||
table: "listings",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "currency",
|
||||
schema: "skintracker",
|
||||
table: "listings",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "USD");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_trades_steam_trade_id",
|
||||
schema: "skintracker",
|
||||
table: "trades",
|
||||
column: "steam_trade_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_listings_condition_id",
|
||||
schema: "skintracker",
|
||||
table: "listings",
|
||||
column: "condition_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_inventory_items_asset_id",
|
||||
schema: "skintracker",
|
||||
table: "inventory_items",
|
||||
column: "asset_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "fk_listings_skin_conditions_condition_id",
|
||||
schema: "skintracker",
|
||||
table: "listings",
|
||||
column: "condition_id",
|
||||
principalSchema: "skintracker",
|
||||
principalTable: "skin_conditions",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
|
||||
// Now that listings carries its own condition_id and currency, the csfloat
|
||||
// arm of the cross-market view uses them instead of NULL / a hardcoded 'USD'.
|
||||
migrationBuilder.Sql("""
|
||||
CREATE OR REPLACE VIEW skintracker.market_listings AS
|
||||
SELECT
|
||||
'csfloat'::text AS marketplace,
|
||||
l.cs_float_listing_id AS external_id,
|
||||
l.skin_id AS skin_id,
|
||||
l.condition_id AS condition_id,
|
||||
l.skin_instance_id AS skin_instance_id,
|
||||
l.market_hash_name AS market_hash_name,
|
||||
l.wear_name AS wear,
|
||||
l.float_value AS float_value,
|
||||
l.paint_seed AS paint_seed,
|
||||
l.is_stat_trak AS is_stat_trak,
|
||||
l.is_souvenir AS is_souvenir,
|
||||
l.sticker_count AS sticker_count,
|
||||
l.price AS price,
|
||||
l.currency AS currency,
|
||||
l.inspect_link AS inspect_link,
|
||||
l.asset_id AS asset_id,
|
||||
l.status AS status,
|
||||
l.first_seen_at AS first_seen_at,
|
||||
l.last_seen_at AS last_seen_at,
|
||||
l.removed_at AS removed_at
|
||||
FROM skintracker.listings l
|
||||
UNION ALL
|
||||
SELECT
|
||||
'csmoney'::text,
|
||||
c.sell_order_id::text,
|
||||
c.skin_id,
|
||||
c.condition_id,
|
||||
c.skin_instance_id,
|
||||
c.market_hash_name,
|
||||
CASE lower(c.quality)
|
||||
WHEN 'fn' THEN 'Factory New'
|
||||
WHEN 'mw' THEN 'Minimal Wear'
|
||||
WHEN 'ft' THEN 'Field-Tested'
|
||||
WHEN 'ww' THEN 'Well-Worn'
|
||||
WHEN 'bs' THEN 'Battle-Scarred'
|
||||
ELSE c.quality
|
||||
END,
|
||||
c.float_value,
|
||||
c.paint_seed,
|
||||
c.is_stat_trak,
|
||||
c.is_souvenir,
|
||||
c.sticker_count,
|
||||
c.price,
|
||||
c.currency,
|
||||
c.inspect_link,
|
||||
c.asset_id,
|
||||
c.status,
|
||||
c.first_seen_at,
|
||||
c.last_seen_at,
|
||||
c.removed_at
|
||||
FROM skintracker.cs_money_listings c
|
||||
UNION ALL
|
||||
SELECT
|
||||
'skinland'::text,
|
||||
s.listing_id::text,
|
||||
s.skin_id,
|
||||
s.condition_id,
|
||||
NULL::integer,
|
||||
s.market_hash_name,
|
||||
sc.condition,
|
||||
s.float_value,
|
||||
NULL::integer,
|
||||
s.is_stat_trak,
|
||||
s.is_souvenir,
|
||||
s.sticker_count,
|
||||
s.price,
|
||||
s.currency,
|
||||
s.inspect_link,
|
||||
NULL::text,
|
||||
s.status,
|
||||
s.first_seen_at,
|
||||
s.last_seen_at,
|
||||
s.removed_at
|
||||
FROM skintracker.skin_land_listings s
|
||||
LEFT JOIN skintracker.skin_conditions sc ON sc.id = s.condition_id;
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Restore the view to its pre-migration form (csfloat condition_id/currency
|
||||
// hardcoded) FIRST, so the listings columns it now references can be dropped.
|
||||
migrationBuilder.Sql("""
|
||||
CREATE OR REPLACE VIEW skintracker.market_listings AS
|
||||
SELECT
|
||||
'csfloat'::text AS marketplace,
|
||||
l.cs_float_listing_id AS external_id,
|
||||
l.skin_id AS skin_id,
|
||||
NULL::integer AS condition_id,
|
||||
l.skin_instance_id AS skin_instance_id,
|
||||
l.market_hash_name AS market_hash_name,
|
||||
l.wear_name AS wear,
|
||||
l.float_value AS float_value,
|
||||
l.paint_seed AS paint_seed,
|
||||
l.is_stat_trak AS is_stat_trak,
|
||||
l.is_souvenir AS is_souvenir,
|
||||
l.sticker_count AS sticker_count,
|
||||
l.price AS price,
|
||||
'USD'::text AS currency,
|
||||
l.inspect_link AS inspect_link,
|
||||
l.asset_id AS asset_id,
|
||||
l.status AS status,
|
||||
l.first_seen_at AS first_seen_at,
|
||||
l.last_seen_at AS last_seen_at,
|
||||
l.removed_at AS removed_at
|
||||
FROM skintracker.listings l
|
||||
UNION ALL
|
||||
SELECT
|
||||
'csmoney'::text,
|
||||
c.sell_order_id::text,
|
||||
c.skin_id,
|
||||
c.condition_id,
|
||||
c.skin_instance_id,
|
||||
c.market_hash_name,
|
||||
CASE lower(c.quality)
|
||||
WHEN 'fn' THEN 'Factory New'
|
||||
WHEN 'mw' THEN 'Minimal Wear'
|
||||
WHEN 'ft' THEN 'Field-Tested'
|
||||
WHEN 'ww' THEN 'Well-Worn'
|
||||
WHEN 'bs' THEN 'Battle-Scarred'
|
||||
ELSE c.quality
|
||||
END,
|
||||
c.float_value,
|
||||
c.paint_seed,
|
||||
c.is_stat_trak,
|
||||
c.is_souvenir,
|
||||
c.sticker_count,
|
||||
c.price,
|
||||
c.currency,
|
||||
c.inspect_link,
|
||||
c.asset_id,
|
||||
c.status,
|
||||
c.first_seen_at,
|
||||
c.last_seen_at,
|
||||
c.removed_at
|
||||
FROM skintracker.cs_money_listings c
|
||||
UNION ALL
|
||||
SELECT
|
||||
'skinland'::text,
|
||||
s.listing_id::text,
|
||||
s.skin_id,
|
||||
s.condition_id,
|
||||
NULL::integer,
|
||||
s.market_hash_name,
|
||||
sc.condition,
|
||||
s.float_value,
|
||||
NULL::integer,
|
||||
s.is_stat_trak,
|
||||
s.is_souvenir,
|
||||
s.sticker_count,
|
||||
s.price,
|
||||
s.currency,
|
||||
s.inspect_link,
|
||||
NULL::text,
|
||||
s.status,
|
||||
s.first_seen_at,
|
||||
s.last_seen_at,
|
||||
s.removed_at
|
||||
FROM skintracker.skin_land_listings s
|
||||
LEFT JOIN skintracker.skin_conditions sc ON sc.id = s.condition_id;
|
||||
""");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "fk_listings_skin_conditions_condition_id",
|
||||
schema: "skintracker",
|
||||
table: "listings");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_trades_steam_trade_id",
|
||||
schema: "skintracker",
|
||||
table: "trades");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_listings_condition_id",
|
||||
schema: "skintracker",
|
||||
table: "listings");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "ix_inventory_items_asset_id",
|
||||
schema: "skintracker",
|
||||
table: "inventory_items");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "condition_id",
|
||||
schema: "skintracker",
|
||||
table: "listings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "currency",
|
||||
schema: "skintracker",
|
||||
table: "listings");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "float_min",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions",
|
||||
newName: "min_float");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "float_max",
|
||||
schema: "skintracker",
|
||||
table: "skin_conditions",
|
||||
newName: "max_float");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "paint_seed",
|
||||
schema: "skintracker",
|
||||
table: "skin_instances",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_inventory_items_asset_id",
|
||||
schema: "skintracker",
|
||||
table: "inventory_items",
|
||||
column: "asset_id");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +215,7 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasName("pk_inventory_items");
|
||||
|
||||
b.HasIndex("AssetId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_inventory_items_asset_id");
|
||||
|
||||
b.HasIndex("SkinInstanceId")
|
||||
@@ -239,11 +240,20 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("asset_id");
|
||||
|
||||
b.Property<int?>("ConditionId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("condition_id");
|
||||
|
||||
b.Property<string>("CsFloatListingId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("cs_float_listing_id");
|
||||
|
||||
b.Property<string>("Currency")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("currency");
|
||||
|
||||
b.Property<int>("DefIndex")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("def_index");
|
||||
@@ -252,7 +262,7 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("first_seen_at");
|
||||
|
||||
b.Property<decimal>("FloatValue")
|
||||
b.Property<decimal?>("FloatValue")
|
||||
.HasColumnType("numeric(20,18)")
|
||||
.HasColumnName("float_value");
|
||||
|
||||
@@ -334,6 +344,9 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.HasIndex("AssetId")
|
||||
.HasDatabaseName("ix_listings_asset_id");
|
||||
|
||||
b.HasIndex("ConditionId")
|
||||
.HasDatabaseName("ix_listings_condition_id");
|
||||
|
||||
b.HasIndex("CsFloatListingId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_listings_cs_float_listing_id");
|
||||
@@ -553,10 +566,6 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("image_url");
|
||||
|
||||
b.Property<DateTimeOffset?>("ListingsSweptAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("listings_swept_at");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
@@ -597,9 +606,6 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_skins");
|
||||
|
||||
b.HasIndex("ListingsSweptAt")
|
||||
.HasDatabaseName("ix_skins_listings_swept_at");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skins_slug");
|
||||
@@ -632,17 +638,13 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("condition");
|
||||
|
||||
b.Property<DateTimeOffset?>("ListingsSweptAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("listings_swept_at");
|
||||
|
||||
b.Property<decimal>("MaxFloat")
|
||||
b.Property<decimal>("FloatMax")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.HasColumnName("max_float");
|
||||
.HasColumnName("float_max");
|
||||
|
||||
b.Property<decimal>("MinFloat")
|
||||
b.Property<decimal>("FloatMin")
|
||||
.HasColumnType("numeric(10,9)")
|
||||
.HasColumnName("min_float");
|
||||
.HasColumnName("float_min");
|
||||
|
||||
b.Property<int>("SkinId")
|
||||
.HasColumnType("integer")
|
||||
@@ -651,15 +653,47 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_skin_conditions");
|
||||
|
||||
b.HasIndex("ListingsSweptAt")
|
||||
.HasDatabaseName("ix_skin_conditions_listings_swept_at");
|
||||
|
||||
b.HasIndex("SkinId")
|
||||
.HasDatabaseName("ix_skin_conditions_skin_id");
|
||||
|
||||
b.ToTable("skin_conditions", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinConditionSweep", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("SkinConditionId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skin_condition_id");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("source");
|
||||
|
||||
b.Property<DateTimeOffset>("SweptAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("swept_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_skin_condition_sweeps");
|
||||
|
||||
b.HasIndex("SkinConditionId", "Source")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skin_condition_sweeps_skin_condition_id_source");
|
||||
|
||||
b.HasIndex("Source", "SweptAt")
|
||||
.HasDatabaseName("ix_skin_condition_sweeps_source_swept_at");
|
||||
|
||||
b.ToTable("skin_condition_sweeps", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -689,9 +723,8 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_seen_at");
|
||||
|
||||
b.Property<string>("PaintSeed")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
b.Property<int>("PaintSeed")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("paint_seed");
|
||||
|
||||
b.Property<int>("SkinId")
|
||||
@@ -725,6 +758,137 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.ToTable("skin_instances", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinLandListing", 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<DateTimeOffset>("FirstSeenAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("first_seen_at");
|
||||
|
||||
b.Property<decimal?>("FloatValue")
|
||||
.HasColumnType("numeric(20,18)")
|
||||
.HasColumnName("float_value");
|
||||
|
||||
b.Property<string>("InspectLink")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("inspect_link");
|
||||
|
||||
b.Property<bool>("IsSouvenir")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_souvenir");
|
||||
|
||||
b.Property<bool>("IsStatTrak")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("is_stat_trak");
|
||||
|
||||
b.Property<DateTimeOffset>("LastSeenAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_seen_at");
|
||||
|
||||
b.Property<long>("ListingId")
|
||||
.HasColumnType("bigint")
|
||||
.HasColumnName("listing_id");
|
||||
|
||||
b.Property<string>("MarketHashName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("market_hash_name");
|
||||
|
||||
b.Property<string>("NameTag")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("name_tag");
|
||||
|
||||
b.Property<decimal>("Price")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)")
|
||||
.HasColumnName("price");
|
||||
|
||||
b.Property<DateTimeOffset?>("RemovedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("removed_at");
|
||||
|
||||
b.Property<int>("SkinId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skin_id");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<int>("StickerCount")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("sticker_count");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_skin_land_listings");
|
||||
|
||||
b.HasIndex("ConditionId")
|
||||
.HasDatabaseName("ix_skin_land_listings_condition_id");
|
||||
|
||||
b.HasIndex("ListingId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skin_land_listings_listing_id");
|
||||
|
||||
b.HasIndex("Status")
|
||||
.HasDatabaseName("ix_skin_land_listings_status");
|
||||
|
||||
b.HasIndex("SkinId", "ConditionId")
|
||||
.HasDatabaseName("ix_skin_land_listings_skin_id_condition_id");
|
||||
|
||||
b.ToTable("skin_land_listings", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinSweep", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("SkinId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("skin_id");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("source");
|
||||
|
||||
b.Property<DateTimeOffset>("SweptAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("swept_at");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("pk_skin_sweeps");
|
||||
|
||||
b.HasIndex("SkinId", "Source")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_skin_sweeps_skin_id_source");
|
||||
|
||||
b.HasIndex("Source", "SweptAt")
|
||||
.HasDatabaseName("ix_skin_sweeps_source_swept_at");
|
||||
|
||||
b.ToTable("skin_sweeps", "skintracker");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SteamUser", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -788,6 +952,10 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.HasIndex("FromUserId")
|
||||
.HasDatabaseName("ix_trades_from_user_id");
|
||||
|
||||
b.HasIndex("SteamTradeId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_trades_steam_trade_id");
|
||||
|
||||
b.HasIndex("ToUserId")
|
||||
.HasDatabaseName("ix_trades_to_user_id");
|
||||
|
||||
@@ -927,6 +1095,12 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Listing", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "Condition")
|
||||
.WithMany()
|
||||
.HasForeignKey("ConditionId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_listings_skin_conditions_condition_id");
|
||||
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin")
|
||||
.WithMany()
|
||||
.HasForeignKey("SkinId")
|
||||
@@ -939,6 +1113,8 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_listings_skin_instances_skin_instance_id");
|
||||
|
||||
b.Navigation("Condition");
|
||||
|
||||
b.Navigation("Skin");
|
||||
|
||||
b.Navigation("SkinInstance");
|
||||
@@ -989,6 +1165,18 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.Navigation("Skin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinConditionSweep", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "SkinCondition")
|
||||
.WithMany("Sweeps")
|
||||
.HasForeignKey("SkinConditionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skin_condition_sweeps_skin_conditions_skin_condition_id");
|
||||
|
||||
b.Navigation("SkinCondition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "Condition")
|
||||
@@ -1009,6 +1197,38 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.Navigation("Skin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinLandListing", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.SkinCondition", "Condition")
|
||||
.WithMany()
|
||||
.HasForeignKey("ConditionId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("fk_skin_land_listings_skin_conditions_condition_id");
|
||||
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin")
|
||||
.WithMany()
|
||||
.HasForeignKey("SkinId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skin_land_listings_skins_skin_id");
|
||||
|
||||
b.Navigation("Condition");
|
||||
|
||||
b.Navigation("Skin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinSweep", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.Skin", "Skin")
|
||||
.WithMany("Sweeps")
|
||||
.HasForeignKey("SkinId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("fk_skin_sweeps_skins_skin_id");
|
||||
|
||||
b.Navigation("Skin");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.Trade", b =>
|
||||
{
|
||||
b.HasOne("BlueLaminate.EFCore.Entities.SteamUser", "FromUser")
|
||||
@@ -1080,6 +1300,8 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.Navigation("Instances");
|
||||
|
||||
b.Navigation("PriceHistories");
|
||||
|
||||
b.Navigation("Sweeps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinCondition", b =>
|
||||
@@ -1087,6 +1309,8 @@ namespace BlueLaminate.EFCore.Migrations
|
||||
b.Navigation("Instances");
|
||||
|
||||
b.Navigation("PriceHistories");
|
||||
|
||||
b.Navigation("Sweeps");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("BlueLaminate.EFCore.Entities.SkinInstance", b =>
|
||||
|
||||
Reference in New Issue
Block a user