mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-09 18:36:27 +00:00
Core - Fix storing, cloning and migrating profiles
- New profiles are stored at the current migration version, so they're no longer migrated on every start and can be duplicated right away - A category created at the top is listed first right away, so its profiles render on top - CloneProfileConfiguration makes a copy of the stored data, saving a clone stores its profile through the original - Importing a profile raises ProfileAdded - A profile configuration that isn't valid JSON is logged and skipped instead of stopping all migrations - A stored profile without a version is migrated from version 0, like an imported one - ProfileCategoryRepository.IsUnique returns true when the name is free - M0001 and M0004 leave a missing node type or storage out instead of writing null - M0003 also unwraps a list that is an item of another list
This commit is contained in:
1 parent
4296b3ffe7
commit
32b0f7169b
14 files changed
+118
-90
No files matched your search
@@ -65,16 +65,11 @@ public class ProfileCategoryTests : ProfileServiceTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CreateProfileCategory_AtTheTop_IsListedLastUntilTheCategoriesAreSortedAgain()
|
public void CreateProfileCategory_AtTheTop_IsListedFirst()
|
||||||
{
|
{
|
||||||
|
// Profiles are updated and rendered in list order, the first category ends up on top
|
||||||
ProfileCategory category = ProfileService.CreateProfileCategory("Tests", true);
|
ProfileCategory category = ProfileService.CreateProfileCategory("Tests", true);
|
||||||
|
|
||||||
// Pinned as-is: the category is created with order 1 but appended to the end of ProfileCategories. Profiles are
|
|
||||||
// updated and rendered in list order (the first category ends up on top), so until something saves a category,
|
|
||||||
// which sorts the list, this category's profiles are drawn underneath the others rather than on top
|
|
||||||
ProfileService.ProfileCategories.Last().ShouldBeSameAs(category);
|
|
||||||
|
|
||||||
ProfileService.SaveProfileCategory(General);
|
|
||||||
ProfileService.ProfileCategories.First().ShouldBeSameAs(category);
|
ProfileService.ProfileCategories.First().ShouldBeSameAs(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +249,7 @@ public class ProfileCategoryTests : ProfileServiceTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void CloneProfileConfiguration_SharesTheStoredDataWithTheOriginal()
|
public void CloneProfileConfiguration_HasItsOwnCopyOfTheStoredData()
|
||||||
{
|
{
|
||||||
ProfileConfiguration original = ProfileService.CreateProfileConfiguration(General, "Original", "TestTube");
|
ProfileConfiguration original = ProfileService.CreateProfileConfiguration(General, "Original", "TestTube");
|
||||||
|
|
||||||
@@ -267,12 +262,11 @@ public class ProfileCategoryTests : ProfileServiceTestBase
|
|||||||
// The clone is not added to the category
|
// The clone is not added to the category
|
||||||
General.ProfileConfigurations.ShouldBe([original]);
|
General.ProfileConfigurations.ShouldBe([original]);
|
||||||
|
|
||||||
// Pinned as-is: documented as creating a copy, but the clone uses the same entity as the original, so saving
|
clone.Entity.ShouldNotBeSameAs(original.Entity);
|
||||||
// changes made to the clone changes the original's stored data too
|
clone.Entity.Profile.ShouldNotBeSameAs(original.Entity.Profile);
|
||||||
clone.Entity.ShouldBeSameAs(original.Entity);
|
|
||||||
clone.Name = "Changed through the clone";
|
clone.Name = "Changed through the clone";
|
||||||
clone.Save();
|
clone.Save();
|
||||||
original.Entity.ProfileConfiguration.Name.ShouldBe("Changed through the clone");
|
original.Entity.ProfileConfiguration.Name.ShouldBe("Original");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ public class ProfileImportExportTests : ProfileServiceTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ImportProfile_DoesNotRaiseProfileAdded()
|
public async Task ImportProfile_RaisesProfileAdded()
|
||||||
{
|
{
|
||||||
ProfileConfiguration original = CreateSavedProfile("Test profile");
|
ProfileConfiguration original = CreateSavedProfile("Test profile");
|
||||||
int profileAdded = 0;
|
int profileAdded = 0;
|
||||||
@@ -138,9 +138,7 @@ public class ProfileImportExportTests : ProfileServiceTestBase
|
|||||||
|
|
||||||
await ImportExport(original, General, true);
|
await ImportExport(original, General, true);
|
||||||
|
|
||||||
// Pinned as-is: creating a profile raises ProfileAdded but importing one doesn't, only the category's own
|
profileAdded.ShouldBe(1);
|
||||||
// ProfileConfigurationAdded event tells listeners about an imported profile
|
|
||||||
profileAdded.ShouldBe(0);
|
|
||||||
addedToCategory.ShouldBe(1);
|
addedToCategory.ShouldBe(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,42 +289,41 @@ public class ProfileImportExportTests : ProfileServiceTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProfilesThatWereNeverSavedInTheEditor_AreStoredAsVersionZero()
|
public void NewProfiles_AreStoredAtTheCurrentVersion()
|
||||||
{
|
{
|
||||||
ProfileConfiguration configuration = ProfileService.CreateProfileConfiguration(General, "Test profile", "TestTube");
|
ProfileConfiguration configuration = ProfileService.CreateProfileConfiguration(General, "Test profile", "TestTube");
|
||||||
GetStoredProfiles().Single().ProfileConfiguration.Version.ShouldBe(0);
|
int version = GetStoredProfiles().Single().ProfileConfiguration.Version;
|
||||||
|
version.ShouldBeGreaterThan(0);
|
||||||
|
|
||||||
ProfileService.SaveProfile(ProfileService.ActivateProfile(configuration), true);
|
ProfileService.SaveProfile(ProfileService.ActivateProfile(configuration), true);
|
||||||
|
|
||||||
GetStoredProfiles().Single().ProfileConfiguration.Version.ShouldBeGreaterThan(0);
|
GetStoredProfiles().Single().ProfileConfiguration.Version.ShouldBe(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ProfilesThatWereNeverSavedInTheEditor_FailToMigrateOnStartupButStillLoad()
|
public void NewProfiles_AreLeftAloneByTheMigrationsOnStartup()
|
||||||
{
|
{
|
||||||
ProfileService.CreateProfileConfiguration(General, "Test profile", "TestTube");
|
ProfileService.CreateProfileConfiguration(General, "Test profile", "TestTube");
|
||||||
|
int version = GetStoredProfiles().Single().ProfileConfiguration.Version;
|
||||||
|
|
||||||
// Artemis migrates stored profiles once on startup, a failure is logged and the profile is left as it is
|
// Artemis migrates stored profiles once on startup
|
||||||
Resolve<IProfileRepository>().MigrateProfiles();
|
Resolve<IProfileRepository>().MigrateProfiles();
|
||||||
|
|
||||||
GetStoredProfiles().Single().ProfileConfiguration.Version.ShouldBe(0);
|
GetStoredProfiles().Single().ProfileConfiguration.Version.ShouldBe(version);
|
||||||
CreateNewProfileService().ProfileCategories.Single(c => c.Name == "General").ProfileConfigurations.Single().Name.ShouldBe("Test profile");
|
CreateNewProfileService().ProfileCategories.Single(c => c.Name == "General").ProfileConfigurations.Single().Name.ShouldBe("Test profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task ImportProfile_OfAProfileThatWasNeverSavedInTheEditor_Fails()
|
public async Task ImportProfile_OfAProfileThatWasNeverSavedInTheEditor_Works()
|
||||||
{
|
{
|
||||||
|
// Duplicating a freshly created profile from the sidebar or the editor is an export followed by an import
|
||||||
ProfileConfiguration configuration = ProfileService.CreateProfileConfiguration(General, "Test profile", "TestTube");
|
ProfileConfiguration configuration = ProfileService.CreateProfileConfiguration(General, "Test profile", "TestTube");
|
||||||
await using Stream export = await ProfileService.ExportProfile(configuration);
|
await using Stream export = await ProfileService.ExportProfile(configuration);
|
||||||
|
|
||||||
// Pinned as-is: saving the category (which is all creating a profile does) stores the profile without setting its
|
ProfileConfiguration imported = await ProfileService.ImportProfile(export, General, true, false, "copy");
|
||||||
// migration version, only SaveProfile sets it. The export says version 0, so importing runs every migration on
|
|
||||||
// JSON that is already up to date and the first one fails on it. Duplicating a freshly created profile from the
|
imported.Name.ShouldBe("Test profile - copy");
|
||||||
// sidebar or the editor (an export followed by an import) runs into this. Restarting Artemis runs into it too, but
|
General.ProfileConfigurations.Count.ShouldBe(2);
|
||||||
// there the failed migration is only logged and the profile loads normally, see the test above
|
|
||||||
InvalidOperationException exception = await Should.ThrowAsync<InvalidOperationException>(() => ProfileService.ImportProfile(export, General, true, false, "copy"));
|
|
||||||
exception.StackTrace!.ShouldContain("M0001NodeProviders");
|
|
||||||
General.ProfileConfigurations.ShouldBe([configuration]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ public class M0001NodeProvidersTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Migrate_NodeWithoutType_GetsANullType()
|
public void Migrate_NodeWithoutType_LeavesTheTypeOut()
|
||||||
{
|
{
|
||||||
JsonObject profile = OldProfileJson.Parse(OldProfileJson.Version0Profile);
|
JsonObject profile = OldProfileJson.Parse(OldProfileJson.Version0Profile);
|
||||||
JsonObject node = profile.FindNode(OldProfileJson.LayerTransformNodeId);
|
JsonObject node = profile.FindNode(OldProfileJson.LayerTransformNodeId);
|
||||||
@@ -120,10 +120,7 @@ public class M0001NodeProvidersTests
|
|||||||
|
|
||||||
_migration.Migrate(new JsonObject(), profile);
|
_migration.Migrate(new JsonObject(), profile);
|
||||||
|
|
||||||
// Pinned as-is: a missing type is written back as "Type": null rather than left out, which gives the NodeEntity
|
node.ContainsKey("Type").ShouldBeFalse();
|
||||||
// a null type even though the property isn't nullable
|
|
||||||
node.ContainsKey("Type").ShouldBeTrue();
|
|
||||||
node["Type"].ShouldBeNull();
|
|
||||||
node["ProviderId"]!.GetValue<string>().ShouldBe(OldProfileJson.GeneralNodesProviderId);
|
node["ProviderId"]!.GetValue<string>().ShouldBe(OldProfileJson.GeneralNodesProviderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public class M0003SystemTextJsonTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Migrate_ListDirectlyInsideAList_KeepsItsValuesWrapper()
|
public void Migrate_ListDirectlyInsideAList_LosesItsValuesWrapperToo()
|
||||||
{
|
{
|
||||||
JsonObject profile = OldProfileJson.Parse("""
|
JsonObject profile = OldProfileJson.Parse("""
|
||||||
{"Rows": {"$type": "List", "$values": [{"$type": "List", "$values": [1, 2]}]}}
|
{"Rows": {"$type": "List", "$values": [{"$type": "List", "$values": [1, 2]}]}}
|
||||||
@@ -123,9 +123,7 @@ public class M0003SystemTextJsonTests
|
|||||||
|
|
||||||
_migration.Migrate(new JsonObject(), profile);
|
_migration.Migrate(new JsonObject(), profile);
|
||||||
|
|
||||||
// Pinned as-is: items of a list are only searched for lists in their properties, a list that is itself an item
|
profile.ShouldBeJson("""{"Rows": [[1, 2]]}""");
|
||||||
// keeps its "$values" wrapper. No profile entity has a list of lists, so old profiles aren't affected.
|
|
||||||
profile.ShouldBeJson("""{"Rows": [{"$values": [1, 2]}]}""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ public class M0004NodeStorageTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Migrate_NodeWithoutStorage_GetsANullStorage()
|
public void Migrate_NodeWithoutStorage_LeavesTheStorageOut()
|
||||||
{
|
{
|
||||||
(JsonObject configuration, JsonObject profile) = Version3Profile();
|
(JsonObject configuration, JsonObject profile) = Version3Profile();
|
||||||
JsonObject node = profile.FindNode(OldProfileJson.LayerTransformNodeId);
|
JsonObject node = profile.FindNode(OldProfileJson.LayerTransformNodeId);
|
||||||
@@ -130,10 +130,7 @@ public class M0004NodeStorageTests
|
|||||||
|
|
||||||
_migration.Migrate(configuration, profile);
|
_migration.Migrate(configuration, profile);
|
||||||
|
|
||||||
// Pinned as-is: a missing storage is written back as "Storage": null rather than left out, which gives the
|
node.ContainsKey("Storage").ShouldBeFalse();
|
||||||
// NodeEntity a null storage even though the property isn't nullable
|
|
||||||
node.ContainsKey("Storage").ShouldBeTrue();
|
|
||||||
node["Storage"].ShouldBeNull();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -147,8 +147,8 @@ public class ProfileMigrationTests : IDisposable
|
|||||||
configuration.Remove("Version");
|
configuration.Remove("Version");
|
||||||
JsonObject profile = OldProfileJson.Parse(OldProfileJson.MigratedProfile);
|
JsonObject profile = OldProfileJson.Parse(OldProfileJson.MigratedProfile);
|
||||||
|
|
||||||
// Pinned as-is: without a version the profile is taken to be from before version 1 and M0001 fails on the
|
// Without a version the profile is taken to be from before version 1, the version was added to tell those apart.
|
||||||
// current format, so a profile in the current format without a version can't be imported
|
// M0001 fails on the current format, so a profile in the current format without a version can't be imported
|
||||||
Should.Throw<InvalidOperationException>(() => _services.Resolve<IProfileRepository>().MigrateProfile(configuration, profile));
|
Should.Throw<InvalidOperationException>(() => _services.Resolve<IProfileRepository>().MigrateProfile(configuration, profile));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,15 +117,13 @@ public class ProfileCategoryRepositoryTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void IsUnique_ReturnsWhetherTheNameIsTaken()
|
public void IsUnique_ReturnsWhetherTheNameIsFree()
|
||||||
{
|
{
|
||||||
ProfileCategoryEntity games = new() {Name = "Games"};
|
ProfileCategoryEntity games = new() {Name = "Games"};
|
||||||
_repository.Add(games);
|
_repository.Add(games);
|
||||||
|
|
||||||
// Pinned as-is: despite its name this returns true when another category already has the name, so true means
|
_repository.IsUnique("Games", null).ShouldBeFalse();
|
||||||
// the name is not unique. Nothing in Artemis calls it at the moment.
|
_repository.IsUnique("Other", null).ShouldBeTrue();
|
||||||
_repository.IsUnique("Games", null).ShouldBeTrue();
|
|
||||||
_repository.IsUnique("Other", null).ShouldBeFalse();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -134,8 +132,8 @@ public class ProfileCategoryRepositoryTests : IDisposable
|
|||||||
ProfileCategoryEntity games = new() {Name = "Games"};
|
ProfileCategoryEntity games = new() {Name = "Games"};
|
||||||
_repository.Add(games);
|
_repository.Add(games);
|
||||||
|
|
||||||
_repository.IsUnique("Games", games.Id).ShouldBeFalse();
|
_repository.IsUnique("Games", games.Id).ShouldBeTrue();
|
||||||
_repository.IsUnique("Games", Guid.NewGuid()).ShouldBeTrue();
|
_repository.IsUnique("Games", Guid.NewGuid()).ShouldBeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -143,8 +141,8 @@ public class ProfileCategoryRepositoryTests : IDisposable
|
|||||||
{
|
{
|
||||||
_repository.Add(new ProfileCategoryEntity {Name = "Games"});
|
_repository.Add(new ProfileCategoryEntity {Name = "Games"});
|
||||||
|
|
||||||
_repository.IsUnique(" Games ", null).ShouldBeTrue();
|
_repository.IsUnique(" Games ", null).ShouldBeFalse();
|
||||||
_repository.IsUnique("games", null).ShouldBeFalse();
|
_repository.IsUnique("games", null).ShouldBeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Artemis.Core.Tests.Infrastructure.Storage;
|
|||||||
using Artemis.Storage.Entities.Profile;
|
using Artemis.Storage.Entities.Profile;
|
||||||
using Artemis.Storage.Repositories;
|
using Artemis.Storage.Repositories;
|
||||||
using Artemis.Storage.Repositories.Interfaces;
|
using Artemis.Storage.Repositories.Interfaces;
|
||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace Artemis.Core.Tests.Storage.Repositories;
|
namespace Artemis.Core.Tests.Storage.Repositories;
|
||||||
@@ -138,17 +137,17 @@ public class ProfileRepositoryTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MigrateProfiles_ProfileWithoutVersion_IsNotMigrated()
|
public void MigrateProfiles_ProfileWithoutVersion_IsMigratedFromVersion0()
|
||||||
{
|
{
|
||||||
ProfileCategoryEntity category = StoredProfiles.AddCategory(_services.CreateDbContext);
|
ProfileCategoryEntity category = StoredProfiles.AddCategory(_services.CreateDbContext);
|
||||||
Guid id = StoredProfiles.InsertRaw(_services.CreateDbContext, category.Id, OldProfileJson.Version0Configuration, OldProfileJson.Version0Profile);
|
Guid id = StoredProfiles.InsertRaw(_services.CreateDbContext, category.Id, OldProfileJson.Version0Configuration, OldProfileJson.Version0Profile);
|
||||||
|
|
||||||
_repository.MigrateProfiles();
|
_repository.MigrateProfiles();
|
||||||
|
|
||||||
// Pinned as-is: the database query only picks profiles whose version is below the latest, a missing version
|
// Like MigrateProfile does, a missing version counts as version 0
|
||||||
// doesn't count as lower. MigrateProfile itself treats a missing version as version 0. Profiles stored by
|
(string configuration, string profile) = StoredProfiles.ReadRaw(_services.CreateDbContext, id);
|
||||||
// Entity Framework always have a version, so this only matters for rows written some other way.
|
JsonNode.Parse(configuration)!["Version"]!.GetValue<int>().ShouldBeGreaterThan(0);
|
||||||
StoredProfiles.ReadRaw(_services.CreateDbContext, id).ShouldBe((OldProfileJson.Version0Configuration, OldProfileJson.Version0Profile));
|
profile.ShouldNotBe(OldProfileJson.Version0Profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@@ -169,38 +168,35 @@ public class ProfileRepositoryTests : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MigrateProfiles_ConfigurationThatIsNotJson_StopsAllMigrations()
|
public void MigrateProfiles_ConfigurationThatIsNotJson_IsLoggedAndSkipped()
|
||||||
{
|
{
|
||||||
ProfileCategoryEntity category = StoredProfiles.AddCategory(_services.CreateDbContext);
|
ProfileCategoryEntity category = StoredProfiles.AddCategory(_services.CreateDbContext);
|
||||||
StoredProfiles.InsertRaw(_services.CreateDbContext, category.Id, "{broken", OldProfileJson.Version0Profile);
|
StoredProfiles.InsertRaw(_services.CreateDbContext, category.Id, "{broken", OldProfileJson.Version0Profile);
|
||||||
Guid working = StoredProfiles.InsertRaw(_services.CreateDbContext, category.Id, StoredProfiles.Version0ConfigurationWithVersion(), OldProfileJson.Version0Profile);
|
Guid working = StoredProfiles.InsertRaw(_services.CreateDbContext, category.Id, StoredProfiles.Version0ConfigurationWithVersion(), OldProfileJson.Version0Profile);
|
||||||
|
|
||||||
// Pinned as-is: the version is read from the configuration by the database query, which fails on the broken
|
_repository.MigrateProfiles();
|
||||||
// JSON before any profile is migrated. Unlike a broken profile, this isn't caught and logged.
|
|
||||||
Should.Throw<SqliteException>(() => _repository.MigrateProfiles()).Message.ShouldContain("malformed JSON");
|
_log.Errors.ShouldHaveSingleItem().MessageTemplate.Text.ShouldBe("Failed to migrate profile container {Id}");
|
||||||
StoredProfiles.ReadRaw(_services.CreateDbContext, working).Profile.ShouldBe(OldProfileJson.Version0Profile);
|
StoredProfiles.ReadRaw(_services.CreateDbContext, working).Profile.ShouldNotBe(OldProfileJson.Version0Profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void MigrateProfiles_ProfileCreatedInArtemis_IsStoredAtVersion0AndFailsToMigrate()
|
public void MigrateProfiles_ProfileCreatedInArtemis_IsStoredAtTheCurrentVersion()
|
||||||
{
|
{
|
||||||
IProfileService profileService = _services.Resolve<IProfileService>();
|
IProfileService profileService = _services.Resolve<IProfileService>();
|
||||||
ProfileCategory category = profileService.CreateProfileCategory("Created");
|
ProfileCategory category = profileService.CreateProfileCategory("Created");
|
||||||
profileService.CreateProfileConfiguration(category, "New profile", "Keyboard");
|
profileService.CreateProfileConfiguration(category, "New profile", "Keyboard");
|
||||||
|
|
||||||
// Pinned as-is: a new profile is stored by saving its category, which doesn't set the version like
|
int version;
|
||||||
// ProfileRepository.Save does. The profile is in the current format, but at version 0 M0001 runs on it and
|
|
||||||
// fails. The error is logged each time the profiles are migrated (when the profile service starts) until the
|
|
||||||
// profile is saved by itself.
|
|
||||||
using (var context = _services.CreateDbContext())
|
using (var context = _services.CreateDbContext())
|
||||||
context.ProfileContainers.Single().ProfileConfiguration.Version.ShouldBe(0);
|
version = context.ProfileContainers.Single().ProfileConfiguration.Version;
|
||||||
|
version.ShouldBeGreaterThan(0);
|
||||||
|
|
||||||
_repository.MigrateProfiles();
|
_repository.MigrateProfiles();
|
||||||
|
|
||||||
_log.Errors.ShouldHaveSingleItem().MessageTemplate.Text.ShouldBe("Failed to migrate profile container {Id}");
|
_log.Errors.ShouldBeEmpty();
|
||||||
_log.Errors.Single().Exception.ShouldBeOfType<InvalidOperationException>();
|
|
||||||
using (var context = _services.CreateDbContext())
|
using (var context = _services.CreateDbContext())
|
||||||
context.ProfileContainers.Single().ProfileConfiguration.Version.ShouldBe(0);
|
context.ProfileContainers.Single().ProfileConfiguration.Version.ShouldBe(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ProfileContainerEntity CreateContainer(ProfileCategoryEntity category, string name = "Profile")
|
private static ProfileContainerEntity CreateContainer(ProfileCategoryEntity category, string name = "Profile")
|
||||||
|
|||||||
@@ -161,7 +161,16 @@ internal class ProfileService : IProfileService
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ProfileConfiguration CloneProfileConfiguration(ProfileConfiguration profileConfiguration)
|
public ProfileConfiguration CloneProfileConfiguration(ProfileConfiguration profileConfiguration)
|
||||||
{
|
{
|
||||||
return new ProfileConfiguration(profileConfiguration.Category, profileConfiguration.Entity);
|
ProfileContainerEntity original = profileConfiguration.Entity;
|
||||||
|
ProfileContainerEntity copy = new()
|
||||||
|
{
|
||||||
|
Id = original.Id,
|
||||||
|
Icon = original.Icon.ToArray(),
|
||||||
|
ProfileCategory = original.ProfileCategory,
|
||||||
|
ProfileConfiguration = CopyEntity(original.ProfileConfiguration),
|
||||||
|
Profile = CopyEntity(original.Profile)
|
||||||
|
};
|
||||||
|
return new ProfileConfiguration(profileConfiguration.Category, copy);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -234,7 +243,7 @@ internal class ProfileService : IProfileService
|
|||||||
}
|
}
|
||||||
|
|
||||||
_profileCategoryRepository.Add(profileCategory.Entity);
|
_profileCategoryRepository.Add(profileCategory.Entity);
|
||||||
ProfileCategories = new ReadOnlyCollection<ProfileCategory>([..ProfileCategories, profileCategory]);
|
ProfileCategories = new ReadOnlyCollection<ProfileCategory>(ProfileCategories.Append(profileCategory).OrderBy(c => c.Order).ToList());
|
||||||
|
|
||||||
OnProfileCategoryAdded(new ProfileCategoryEventArgs(profileCategory));
|
OnProfileCategoryAdded(new ProfileCategoryEventArgs(profileCategory));
|
||||||
return profileCategory;
|
return profileCategory;
|
||||||
@@ -259,6 +268,8 @@ internal class ProfileService : IProfileService
|
|||||||
|
|
||||||
category.AddProfileConfiguration(configuration, category.ProfileConfigurations.FirstOrDefault());
|
category.AddProfileConfiguration(configuration, category.ProfileConfigurations.FirstOrDefault());
|
||||||
SaveProfileCategory(category);
|
SaveProfileCategory(category);
|
||||||
|
// The profile is in the current format, saving it stores the current migration version with it
|
||||||
|
_profileRepository.Save(configuration.Entity);
|
||||||
|
|
||||||
OnProfileAdded(new ProfileConfigurationEventArgs(configuration));
|
OnProfileAdded(new ProfileConfigurationEventArgs(configuration));
|
||||||
return configuration;
|
return configuration;
|
||||||
@@ -303,7 +314,18 @@ internal class ProfileService : IProfileService
|
|||||||
profile.IsFreshImport = false;
|
profile.IsFreshImport = false;
|
||||||
profile.ProfileEntity.IsFreshImport = false;
|
profile.ProfileEntity.IsFreshImport = false;
|
||||||
|
|
||||||
_profileRepository.Save(profile.Configuration.Entity);
|
// A copy made by CloneProfileConfiguration has its own stored data, save it into that of the original
|
||||||
|
ProfileContainerEntity entity = profile.Configuration.Entity;
|
||||||
|
ProfileConfiguration? original = ProfileCategories
|
||||||
|
.SelectMany(c => c.ProfileConfigurations)
|
||||||
|
.FirstOrDefault(c => c.Entity != entity && c.Entity.Id == entity.Id);
|
||||||
|
if (original != null)
|
||||||
|
{
|
||||||
|
original.Entity.Profile = CopyEntity(entity.Profile);
|
||||||
|
entity = original.Entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
_profileRepository.Save(entity);
|
||||||
|
|
||||||
// If the provided profile is external (cloned or from the workshop?) but it is loaded locally too, reload the local instance
|
// If the provided profile is external (cloned or from the workshop?) but it is loaded locally too, reload the local instance
|
||||||
// A bit dodge but it ensures local instances always represent the latest stored version
|
// A bit dodge but it ensures local instances always represent the latest stored version
|
||||||
@@ -429,6 +451,7 @@ internal class ProfileService : IProfileService
|
|||||||
profileConfiguration.LoadModules(modules);
|
profileConfiguration.LoadModules(modules);
|
||||||
SaveProfileCategory(category);
|
SaveProfileCategory(category);
|
||||||
|
|
||||||
|
OnProfileAdded(new ProfileConfigurationEventArgs(profileConfiguration));
|
||||||
return profileConfiguration;
|
return profileConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,6 +471,11 @@ internal class ProfileService : IProfileService
|
|||||||
SaveProfileCategory(profile.Configuration.Category);
|
SaveProfileCategory(profile.Configuration.Category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static T CopyEntity<T>(T entity)
|
||||||
|
{
|
||||||
|
return CoreJson.Deserialize<T>(CoreJson.Serialize(entity))!;
|
||||||
|
}
|
||||||
|
|
||||||
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
|
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
|
||||||
{
|
{
|
||||||
_pendingKeyboardEvents.Add(e);
|
_pendingKeyboardEvents.Add(e);
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ namespace Artemis.Storage.Migrations.Profile
|
|||||||
if (jsonNode == null)
|
if (jsonNode == null)
|
||||||
continue;
|
continue;
|
||||||
JsonObject nodeObject = jsonNode.AsObject();
|
JsonObject nodeObject = jsonNode.AsObject();
|
||||||
nodeObject["Type"] = nodeObject["Type"]?.GetValue<string>().Replace("Artemis.VisualScripting.Nodes", "Artemis.Plugins.Nodes.General.Nodes");
|
if (nodeObject["Type"] != null)
|
||||||
|
nodeObject["Type"] = nodeObject["Type"]!.GetValue<string>().Replace("Artemis.VisualScripting.Nodes", "Artemis.Plugins.Nodes.General.Nodes");
|
||||||
nodeObject["ProviderId"] = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78";
|
nodeObject["ProviderId"] = "Artemis.Plugins.Nodes.General.GeneralNodesProvider-d9e1ee78";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,7 @@ internal class M0003SystemTextJson : IProfileMigration
|
|||||||
{
|
{
|
||||||
obj.Remove("$values");
|
obj.Remove("$values");
|
||||||
jsonObject[key] = values;
|
jsonObject[key] = values;
|
||||||
foreach (JsonNode? jsonNode in values.ToList())
|
ConvertListItems(values);
|
||||||
{
|
|
||||||
if (jsonNode is JsonObject childObject)
|
|
||||||
ConvertToSystemTextJson(childObject);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.Remove("$type");
|
obj.Remove("$type");
|
||||||
@@ -52,6 +48,27 @@ internal class M0003SystemTextJson : IProfileMigration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConvertListItems(JsonArray values)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < values.Count; i++)
|
||||||
|
{
|
||||||
|
if (values[i] is not JsonObject childObject)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// A list that is itself an item of a list is wrapped too
|
||||||
|
if (childObject["$type"] != null && childObject["$values"] is JsonArray childValues)
|
||||||
|
{
|
||||||
|
childObject.Remove("$values");
|
||||||
|
values[i] = childValues;
|
||||||
|
ConvertListItems(childValues);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConvertToSystemTextJson(childObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static void FilterType(JsonObject jsonObject)
|
internal static void FilterType(JsonObject jsonObject)
|
||||||
{
|
{
|
||||||
// Replace or remove $type depending on whether there's a matching JsonDerivedType
|
// Replace or remove $type depending on whether there's a matching JsonDerivedType
|
||||||
|
|||||||
@@ -97,7 +97,8 @@ namespace Artemis.Storage.Migrations.Profile
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
JsonObject nodeObject = jsonNode.AsObject();
|
JsonObject nodeObject = jsonNode.AsObject();
|
||||||
nodeObject["Storage"] = MigrateNodeStorageJson(nodeObject["Storage"]?.GetValue<string>(), _logger);
|
if (nodeObject["Storage"] != null)
|
||||||
|
nodeObject["Storage"] = MigrateNodeStorageJson(nodeObject["Storage"]!.GetValue<string>(), _logger);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ internal class ProfileCategoryRepository(Func<ArtemisDbContext> getContext, IPro
|
|||||||
|
|
||||||
name = name.Trim();
|
name = name.Trim();
|
||||||
return id == null
|
return id == null
|
||||||
? dbContext.ProfileCategories.Any(p => p.Name == name)
|
? !dbContext.ProfileCategories.Any(p => p.Name == name)
|
||||||
: dbContext.ProfileCategories.Any(p => p.Name == name && p.Id != id.Value);
|
: !dbContext.ProfileCategories.Any(p => p.Name == name && p.Id != id.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,9 +51,13 @@ public class ProfileRepository(ILogger logger, Func<ArtemisDbContext> getContext
|
|||||||
using ArtemisDbContext dbContext = getContext();
|
using ArtemisDbContext dbContext = getContext();
|
||||||
int max = profileMigrators.Max(m => m.Version);
|
int max = profileMigrators.Max(m => m.Version);
|
||||||
|
|
||||||
// Query the ProfileContainerEntity table directly, grabbing the ID, profile, and configuration
|
// Query the ProfileContainerEntity table directly, grabbing the ID, profile, and configuration.
|
||||||
|
// A missing version counts as version 0, and configurations that aren't valid JSON are included so that they fail
|
||||||
|
// and get logged below rather than failing the query
|
||||||
List<RawProfileContainer> containers = dbContext.Database
|
List<RawProfileContainer> containers = dbContext.Database
|
||||||
.SqlQueryRaw<RawProfileContainer>("SELECT Id, Profile, ProfileConfiguration FROM ProfileContainers WHERE json_extract(ProfileConfiguration, '$.Version') < {0}", max)
|
.SqlQueryRaw<RawProfileContainer>(
|
||||||
|
"SELECT Id, Profile, ProfileConfiguration FROM ProfileContainers WHERE CASE WHEN json_valid(ProfileConfiguration) THEN COALESCE(json_extract(ProfileConfiguration, '$.Version'), 0) < {0} ELSE 1 END",
|
||||||
|
max)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (RawProfileContainer rawProfileContainer in containers)
|
foreach (RawProfileContainer rawProfileContainer in containers)
|
||||||
|
|||||||
Reference in new issue
Block a user