Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4/4] Query for Updates from Nexus, and Fix Miscellaneous Bugs in DataStore #2113

Merged
merged 47 commits into from
Oct 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
78ace0f
WIP: Documentation for Update Detection
Sewer56 Sep 19, 2024
796c123
Added: Additional Edge Cases to Update Logic Docs
Sewer56 Sep 23, 2024
0cfb392
Added: Extra case of `Archived in the Middle`
Sewer56 Sep 23, 2024
6498415
Fixed: Indentation for `file_updates` field.
Sewer56 Sep 23, 2024
600ec0b
Finalized 'Updating Mods' doc with simplified Implementation requested.
Sewer56 Sep 24, 2024
2155750
Fixed: Minor Notes from older Research Doc
Sewer56 Sep 24, 2024
d151e86
Merge branch 'main' into detect-updates-docs-only
Sewer56 Sep 24, 2024
b54efac
Fixed: Added Missing 'Updating Mods' mkdocs sidebar item.
Sewer56 Sep 24, 2024
71a131b
[Working WIP] Added: Initial Implementation of Generic Page Caching S…
Sewer56 Sep 25, 2024
6fe3105
Merge remote-tracking branch 'origin/main' into updates-cache-system
Sewer56 Sep 26, 2024
7afd32d
Tech Debt Reduction: Add additional V2 GraphQL Types and Correct Size…
Sewer56 Sep 26, 2024
1173046
Added: Missing 'UInt32' types in attribute definitions
Sewer56 Sep 26, 2024
3eb2986
Improved: Accuracy of documentation for FileId struct.
Sewer56 Sep 26, 2024
2de875a
Added: Method for constructing UidForMod and UidForFile from GraphQL …
Sewer56 Sep 26, 2024
a0d80d4
Rename: ICanGetUid to ICanGetUidForMod
Sewer56 Sep 26, 2024
e8e09e3
Added: Tests for UidForModTests and UidForFileTests
Sewer56 Sep 27, 2024
e4349be
Removed: Unused Tests.cs file
Sewer56 Sep 27, 2024
2c96668
Merge branch 'updates-cache-system' into start-adding-v2-types
Sewer56 Sep 27, 2024
dd05a2b
Added: Mixin for V1 API Results to ModUpdates Library
Sewer56 Sep 30, 2024
4a4d8a8
Merge remote-tracking branch 'origin/main' into query-updates-for-items
Sewer56 Sep 30, 2024
fd18359
Added: Mod Page Metadata now is ready for handling update info with V…
Sewer56 Sep 30, 2024
94ae079
Added: Mixin for page metadata.
Sewer56 Sep 30, 2024
dbd6d20
Merge branch 'main' into start-adding-v2-types
Sewer56 Sep 30, 2024
99211ed
Merge branch 'start-adding-v2-types' into query-updates-for-items
Sewer56 Sep 30, 2024
13220bd
NexusModsModPageMetadata: Correctly Use uid as 'primary key'
Sewer56 Sep 30, 2024
85bccf8
Update: Use GameId from uid field of NexusModsModPageMetadata
Sewer56 Sep 30, 2024
3811439
Added: Fetch Mod Page Metadata from the DB
Sewer56 Oct 1, 2024
ada1f8d
Added: Note about field in PageMetadataMixin
Sewer56 Oct 1, 2024
1ebdb01
Use Uid in NexusModsFileMetadata, and Add Relevant Constructors for U…
Sewer56 Oct 1, 2024
8f2e271
Added: Code for running actual update check, and relevant constructs.
Sewer56 Oct 1, 2024
1d4b692
V1: Fix field names on ModUpdate structure to align with Nexus V1 API
Sewer56 Oct 1, 2024
e36c543
Added: Remaining Fixups to make the Update Check 'work'
Sewer56 Oct 1, 2024
2c28878
Added: A note regarding adding more tests.
Sewer56 Oct 1, 2024
4bbc899
Merge remote-tracking branch 'origin/main' into query-updates-for-items
Sewer56 Oct 1, 2024
731a620
Merge branch 'main' into start-adding-v2-types
Sewer56 Oct 1, 2024
edf3775
Merge remote-tracking branch 'origin/main' into start-adding-v2-types
Sewer56 Oct 2, 2024
7a6863b
Merge branch 'start-adding-v2-types' into query-updates-for-items
Sewer56 Oct 2, 2024
8722385
Added: Small note to ModUpdateMixin about choice of field.
Sewer56 Oct 2, 2024
039e8da
Removed: FilesUpdatedAt field, as it is now currently unused.
Sewer56 Oct 2, 2024
3438784
Improve: Clarify last updated date is in UTC
Sewer56 Oct 2, 2024
ca7eb28
Updated Note: It's no longer messy, but we still need to upgrade to V2.
Sewer56 Oct 2, 2024
a68e6dd
Merge branch 'main' into start-adding-v2-types
Sewer56 Oct 2, 2024
7745cdf
Merge branch 'start-adding-v2-types' into query-updates-for-items
Sewer56 Oct 2, 2024
406e290
Improved: Now also updates mod pages.
Sewer56 Oct 3, 2024
66c21c2
Merge interfaces into IModFeedItem
Sewer56 Oct 3, 2024
7c70cbd
Merge branch 'main' into query-updates-for-items
halgari Oct 3, 2024
50042f6
Fix some merge conflicts
halgari Oct 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using NexusMods.Abstractions.Games.DTO;
using NexusMods.Abstractions.NexusWebApi.Types.V2;

namespace NexusMods.Abstractions.Collections.Json;

Expand All @@ -17,6 +18,9 @@ public class Mod
[JsonPropertyName("optional")]
public bool Optional { get; init; }

/// <summary>
/// TODO: Deprecate this with <see cref="GameId"/>
/// </summary>
[JsonPropertyName("domainName")]
public required GameDomain DomainName { get; init; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using NexusMods.Abstractions.MnemonicDB.Attributes;
using NexusMods.Abstractions.NexusWebApi.Types;
using NexusMods.Abstractions.NexusWebApi.Types.V2;
using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid;
using NexusMods.MnemonicDB.Abstractions.Attributes;
using NexusMods.MnemonicDB.Abstractions.Models;

Expand All @@ -16,9 +17,9 @@ public partial class NexusModsFileMetadata : IModelDefinition
private const string Namespace = "NexusMods.Library.NexusModsFileMetadata";

/// <summary>
/// The ID of the file.
/// Unique identifier for the file on Nexus Mods.
/// </summary>
public static readonly FileIdAttribute FileId = new(Namespace, nameof(FileId)) { IsIndexed = true };
public static readonly UidForFileAttribute Uid = new(Namespace, nameof(Uid)) { IsIndexed = true };

/// <summary>
/// The name of the file.
Expand All @@ -31,7 +32,12 @@ public partial class NexusModsFileMetadata : IModelDefinition
public static readonly StringAttribute Version = new(Namespace, nameof(Version));

/// <summary>
/// The size of the file in bytes, this is optional in the NexusMods API for whatever reason.
/// The date the file was uploaded at.
/// </summary>
public static readonly DateTimeAttribute UploadedAt = new(Namespace, nameof(UploadedAt));

/// <summary>
/// The size in bytes of the file.
/// </summary>
public static readonly SizeAttribute Size = new(Namespace, nameof(Size)) { IsOptional = true };

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using JetBrains.Annotations;
using NexusMods.Abstractions.MnemonicDB.Attributes;
using NexusMods.Abstractions.NexusWebApi.Types;
using NexusMods.Abstractions.Resources.DB;
using NexusMods.Abstractions.NexusWebApi.Types.V2;
using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid;
using NexusMods.Abstractions.Telemetry;
using NexusMods.MnemonicDB.Abstractions.Attributes;
using NexusMods.MnemonicDB.Abstractions.Models;
Expand All @@ -20,7 +20,7 @@ public partial class NexusModsModPageMetadata : IModelDefinition
/// <summary>
/// The ID of the mod page.
/// </summary>
public static readonly ModIdAttribute ModId = new(Namespace, nameof(ModId)) { IsIndexed = true };
public static readonly UidForModAttribute Uid = new(Namespace, nameof(Uid)) { IsIndexed = true };

/// <summary>
/// The name of the mod page.
Expand All @@ -30,8 +30,17 @@ public partial class NexusModsModPageMetadata : IModelDefinition
/// <summary>
/// The game of the mod page.
/// </summary>
/// <remarks>
/// This will be deprecated in the future, since V2 API only needs <see cref="Uid"/>
/// which contains the <see cref="GameId"/> The <see cref="GameDomain"/> is a legacy field of the V1 API.
/// </remarks>
public static readonly GameDomainAttribute GameDomain = new(Namespace, nameof(GameDomain)) { IsIndexed = true };

/// <summary>
/// The last time the mod page was updated (UTC). This is useful for cache invalidation.
/// </summary>
public static readonly DateTimeAttribute UpdatedAt = new(Namespace, nameof(UpdatedAt));

/// <summary>
/// Uri for the full sized picture of the mod.
/// </summary>
Expand All @@ -51,6 +60,6 @@ public partial class NexusModsModPageMetadata : IModelDefinition

public partial struct ReadOnly
{
public Uri GetUri() => NexusModsUrlBuilder.CreateGenericUri($"https://nexusmods.com/{GameDomain}/mods/{ModId}");
public Uri GetUri() => NexusModsUrlBuilder.CreateGenericUri($"https://nexusmods.com/{GameDomain}/mods/{Uid.ModId}");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class ModUpdate : IJsonArraySerializable<ModUpdate>
/// <remarks>
/// Expressed as a Unix timestamp.
/// </remarks>
[JsonPropertyName("LatestFileUpdated")]
[JsonPropertyName("latest_file_update")]
public long LatestFileUpdated { get; set; }

/// <summary>
Expand All @@ -47,7 +47,7 @@ public class ModUpdate : IJsonArraySerializable<ModUpdate>
/// <remarks>
/// Expressed as a Unix timestamp.
/// </remarks>
[JsonPropertyName("LatestModActivity")]
[JsonPropertyName("latest_mod_activity")]
public long LatestModActivity { get; set; }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using NexusMods.Abstractions.Games.DTO;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.Attributes;
using NexusMods.MnemonicDB.Abstractions.ElementComparers;
using TransparentValueObjects;
namespace NexusMods.Abstractions.NexusWebApi.Types.V2;

Expand All @@ -9,4 +13,50 @@ namespace NexusMods.Abstractions.NexusWebApi.Types.V2;
{
/// <inheritdoc/>
public static GameId DefaultValue => From(default(uint));

/// <summary>
/// Maps a given <see cref="GameDomain"/> to a <see cref="GameId"/> using known mappings.
/// This is a TEMPORARY API, until full migration to V2 is complete.
/// After that it should be REMOVED.
/// </summary>
public static GameId FromGameDomain(GameDomain domain)
{
return domain.Value switch
{
"stardewvalley" => (GameId)1704,
"cyberpunk2077" => (GameId)3333,
"baldursgate3" => (GameId)3474,
_ => throw new ArgumentOutOfRangeException(nameof(domain), domain, null),
};
}

/// <summary>
/// Maps a given <see cref="GameId"/> to a <see cref="GameDomain"/> using known mappings.
/// This is a TEMPORARY API, until full migration to V2 is complete.
/// After that it should be REMOVED.
/// </summary>
public GameDomain ToGameDomain()
{
var value = Value;
return value switch
{
1704 => GameDomain.From("stardewvalley"),
3333 => GameDomain.From("cyberpunk2077"),
3474 => GameDomain.From("baldursgate3"),
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null),
};
}
}

/// <summary>
/// Game ID attribute, for game identifiers from the GraphQL (V2) API.
/// </summary>
public class GameIdAttribute(string ns, string name)
: ScalarAttribute<GameId, uint>(ValueTags.UInt32, ns, name)
{
/// <inheritdoc />
protected override uint ToLowLevel(GameId value) => value.Value;

/// <inheritdoc />
protected override GameId FromLowLevel(uint value, ValueTags tags, AttributeResolver resolver) => GameId.From(value);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.Attributes;
using NexusMods.MnemonicDB.Abstractions.ElementComparers;
namespace NexusMods.Abstractions.NexusWebApi.Types.V2.Uid;

/// <summary>
Expand All @@ -25,6 +28,13 @@ public struct UidForFile
/// </summary>
public GameId GameId;

/// <summary/>
public UidForFile(FileId fileId, GameId gameId)
{
FileId = fileId;
GameId = gameId;
}

/// <summary>
/// Decodes a Nexus Mods API result which contains an 'uid' field into a <see cref="UidForFile"/>.
/// </summary>
Expand All @@ -44,3 +54,17 @@ public struct UidForFile
/// </summary>
public static UidForFile FromUlong(ulong value) => Unsafe.As<ulong, UidForFile>(ref value);
}

/// <summary>
/// Attribute that uniquely identifies a file on Nexus Mods.
/// See <see cref="UidForFile"/> for more details.
/// </summary>
public class UidForFileAttribute(string ns, string name)
: ScalarAttribute<UidForFile, ulong>(ValueTags.UInt64, ns, name)
{
/// <inheritdoc />
protected override ulong ToLowLevel(UidForFile value) => value.AsUlong;

/// <inheritdoc />
protected override UidForFile FromLowLevel(ulong value, ValueTags tags, AttributeResolver resolver) => UidForFile.FromUlong(value);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using NexusMods.MnemonicDB.Abstractions;
using NexusMods.MnemonicDB.Abstractions.Attributes;
using NexusMods.MnemonicDB.Abstractions.ElementComparers;
namespace NexusMods.Abstractions.NexusWebApi.Types.V2.Uid;

/// <summary>
Expand Down Expand Up @@ -36,6 +39,11 @@ public struct UidForMod
/// This throws if <param name="uid"/> is not a valid number.
/// </remarks>
public static UidForMod FromV2Api(string uid) => FromUlong(ulong.Parse(uid));

/// <summary>
/// Converts the UID to a string accepted by the V2 API.
/// </summary>
public string ToV2Api() => AsUlong.ToString();

/// <summary>
/// Reinterprets the current <see cref="UidForMod"/> as a single <see cref="ulong"/>.
Expand All @@ -47,3 +55,17 @@ public struct UidForMod
/// </summary>
public static UidForMod FromUlong(ulong value) => Unsafe.As<ulong, UidForMod>(ref value);
}

/// <summary>
/// Attribute that uniquely identifies a mod on Nexus Mods.
/// See <see cref="UidForMod"/> for more details.
/// </summary>
public class UidForModAttribute(string ns, string name)
: ScalarAttribute<UidForMod, ulong>(ValueTags.UInt64, ns, name)
{
/// <inheritdoc />
protected override ulong ToLowLevel(UidForMod value) => value.AsUlong;

/// <inheritdoc />
protected override UidForMod FromLowLevel(ulong value, ValueTags tags, AttributeResolver resolver) => UidForMod.FromUlong(value);
}
22 changes: 22 additions & 0 deletions src/Networking/NexusMods.Networking.ModUpdates/IModFeedItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid;
namespace NexusMods.Networking.ModUpdates;

/// <summary>
/// Represents an individual item from a 'mod feed'; with the 'mod feed' being
/// the result of an API call that returns one or more mods from the Nexus API.
/// (Either V1 or V2 API)
/// </summary>
public interface IModFeedItem
{
/// <summary>
/// Returns a unique identifier for the given item, based on the ID format
/// used in the NexusMods V2 API.
/// </summary>
public UidForMod GetModPageId();

/// <summary>
/// Retrieves the time the item was last updated.
/// This date is in UTC.
/// </summary>
public DateTime GetLastUpdatedDateUtc();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using NexusMods.Abstractions.NexusWebApi.DTOs;
using NexusMods.Abstractions.NexusWebApi.Types.V2;
using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid;
namespace NexusMods.Networking.ModUpdates.Mixins;

/// <summary>
/// Implements the (V1) mod update API mixin.
/// </summary>
public readonly struct ModFeedItemUpdateMixin : IModFeedItem
{
private readonly DateTime _lastUpdatedDate;
private readonly GameId _gameId;
private readonly ModId _modId;

/// <summary/>
private ModFeedItemUpdateMixin(ModUpdate update, GameId gameId)
{
// Note(sewer): V2 doesn't have 'last file updated' field, so we have to use 'last mod page update' time.
// Well, this whole struct is, will be making that ticket to backend, and replace
// this when V2 gets relevant API.
_lastUpdatedDate = update.LatestModActivityUtc;
_gameId = gameId;
_modId = update.ModId;
}

/// <summary>
/// Transforms the result of a V1 API call for mod updates into the Mixin.
/// </summary>
public static IEnumerable<ModFeedItemUpdateMixin> FromUpdateResults(IEnumerable<ModUpdate> updates, GameId gameId) => updates.Select(update => new ModFeedItemUpdateMixin(update, gameId));

/// <inheritdoc />
public DateTime GetLastUpdatedDateUtc() => _lastUpdatedDate;

/// <inheritdoc />
public UidForMod GetModPageId() => new()
{
GameId = _gameId,
ModId = _modId,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using NexusMods.Abstractions.NexusModsLibrary;
using NexusMods.Abstractions.NexusWebApi.Types.V2.Uid;
using NexusMods.MnemonicDB.Abstractions;
namespace NexusMods.Networking.ModUpdates.Mixins;

/// <summary>
/// Implements the MnemonicDB mod page mixin based on V2 API Results.
/// </summary>
public struct PageMetadataMixin : IModFeedItem
{
private readonly NexusModsModPageMetadata.ReadOnly _metadata;

private PageMetadataMixin(NexusModsModPageMetadata.ReadOnly metadata) => _metadata = metadata;

/// <inheritodc/>
public UidForMod GetModPageId() => new()
{
GameId = _metadata.Uid.GameId,
ModId = _metadata.Uid.ModId,
};

/// <summary/>
public EntityId GetModPageEntityId() => _metadata.Id;

/// <inheritodc/>
public DateTime GetLastUpdatedDateUtc() => _metadata.UpdatedAt; // <= TODO: Change this with 'last file updated at' when V2 supports this field.

/// <summary>
/// Returns the database entries containing page metadata(s) as a mixin.
/// </summary>
public static IEnumerable<PageMetadataMixin> EnumerateDatabaseEntries(IDb db) => NexusModsModPageMetadata.All(db).Select(only => new PageMetadataMixin(only));

/// <summary/>
public static implicit operator NexusModsModPageMetadata.ReadOnly(PageMetadataMixin mixin) => mixin._metadata;

/// <summary/>
public static implicit operator PageMetadataMixin(NexusModsModPageMetadata.ReadOnly metadata) => new(metadata);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using NexusMods.Abstractions.NexusWebApi.Types.V2;
using NexusMods.Networking.ModUpdates.Traits;
namespace NexusMods.Networking.ModUpdates;

/// <summary>
Expand All @@ -12,7 +11,7 @@ namespace NexusMods.Networking.ModUpdates;
/// you to use mods and API responses which are sourced from multiple feeds (games),
/// as opposed to a single feed.
/// </summary>
public class MultiFeedCacheUpdater<TUpdateableItem> where TUpdateableItem : ICanGetLastUpdatedTimestamp, ICanGetUidForMod
public class MultiFeedCacheUpdater<TUpdateableItem> where TUpdateableItem : IModFeedItem
{
private readonly Dictionary<GameId, PerFeedCacheUpdater<TUpdateableItem>> _updaters;

Expand Down Expand Up @@ -41,7 +40,7 @@ public MultiFeedCacheUpdater(TUpdateableItem[] items, TimeSpan expiry)
var groupedList = new List<(GameId, List<TUpdateableItem>)>();
foreach (var item in items)
{
var gameId = item.GetUniqueId().GameId;
var gameId = item.GetModPageId().GameId;

// Get or Update List for this GameId.
var found = false;
Expand Down Expand Up @@ -73,15 +72,15 @@ public MultiFeedCacheUpdater(TUpdateableItem[] items, TimeSpan expiry)
/// include items corresponding to multiple feeds (games); the feed source
/// is automatically detected.
///
/// Wrap elements in a struct that implements <see cref="ICanGetLastUpdatedTimestamp"/>
/// and <see cref="ICanGetUidForMod"/> if necessary.
/// Wrap elements in a struct that implements <see cref="IModFeedItem"/>
/// and <see cref="IModFeedItem"/> if necessary.
/// </param>
public void Update<T>(IEnumerable<T> items) where T : ICanGetLastUpdatedTimestamp, ICanGetUidForMod
public void Update<T>(IEnumerable<T> items) where T : IModFeedItem
{
foreach (var item in items)
{
// Determine feed
var feed = item.GetUniqueId().GameId;
var feed = item.GetModPageId().GameId;

// The result may contain items from feeds which we are not tracking.
// For instance, results for other games. This is not an error, we
Expand Down
Loading
Loading