Storage - Replace LiteDB with SQLite + EF Core (#843)

Storage - Added LiteDB to SQLite migration
UI - Try to die a bit more gracefully
Core - Delay start watching plugins for hot reload after initializing
UI - Simplify category management logic
UI - Avoid crash during profile icon load
Storage - Fix entry metadata retrieval
This commit is contained in:
RobertBeekman authored and GitHub committed 2024-03-13 20:19:21 +01:00
1 parent 9132301dbf
commit 551921db9f
130 files changed
+2965 -1135

No files matched your search

@@ -1,47 +1,50 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories;
internal class DeviceRepository : IDeviceRepository
internal class DeviceRepository(Func<ArtemisDbContext> getContext) : IDeviceRepository
{
private readonly LiteRepository _repository;
public DeviceRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<DeviceEntity>().EnsureIndex(s => s.Id);
}
public void Add(DeviceEntity deviceEntity)
{
_repository.Insert(deviceEntity);
using ArtemisDbContext dbContext = getContext();
dbContext.Devices.Add(deviceEntity);
dbContext.SaveChanges();
}
public void Remove(DeviceEntity deviceEntity)
{
_repository.Delete<DeviceEntity>(deviceEntity.Id);
using ArtemisDbContext dbContext = getContext();
dbContext.Devices.Remove(deviceEntity);
dbContext.SaveChanges();
}
public DeviceEntity? Get(string id)
{
return _repository.FirstOrDefault<DeviceEntity>(s => s.Id == id);
using ArtemisDbContext dbContext = getContext();
return dbContext.Devices.FirstOrDefault(d => d.Id == id);
}
public List<DeviceEntity> GetAll()
{
return _repository.Query<DeviceEntity>().Include(s => s.InputIdentifiers).ToList();
using ArtemisDbContext dbContext = getContext();
return dbContext.Devices.ToList();
}
public void Save(DeviceEntity deviceEntity)
{
_repository.Upsert(deviceEntity);
using ArtemisDbContext dbContext = getContext();
dbContext.Update(deviceEntity);
dbContext.SaveChanges();
}
public void Save(IEnumerable<DeviceEntity> deviceEntities)
public void SaveRange(IEnumerable<DeviceEntity> deviceEntities)
{
_repository.Upsert(deviceEntities);
using ArtemisDbContext dbContext = getContext();
dbContext.UpdateRange(deviceEntities);
dbContext.SaveChanges();
}
}
@@ -1,54 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Artemis.Storage.Entities.Workshop;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories;
internal class EntryRepository : IEntryRepository
internal class EntryRepository(Func<ArtemisDbContext> getContext) : IEntryRepository
{
private readonly LiteRepository _repository;
public EntryRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<EntryEntity>().EnsureIndex(s => s.Id);
_repository.Database.GetCollection<EntryEntity>().EnsureIndex(s => s.EntryId);
}
public void Add(EntryEntity entryEntity)
{
_repository.Insert(entryEntity);
using ArtemisDbContext dbContext = getContext();
dbContext.Entries.Add(entryEntity);
dbContext.SaveChanges();
}
public void Remove(EntryEntity entryEntity)
{
_repository.Delete<EntryEntity>(entryEntity.Id);
using ArtemisDbContext dbContext = getContext();
dbContext.Entries.Remove(entryEntity);
dbContext.SaveChanges();
}
public EntryEntity? Get(Guid id)
{
return _repository.FirstOrDefault<EntryEntity>(s => s.Id == id);
using ArtemisDbContext dbContext = getContext();
return dbContext.Entries.FirstOrDefault(s => s.Id == id);
}
public EntryEntity? GetByEntryId(long entryId)
{
return _repository.FirstOrDefault<EntryEntity>(s => s.EntryId == entryId);
using ArtemisDbContext dbContext = getContext();
return dbContext.Entries.FirstOrDefault(s => s.EntryId == entryId);
}
public List<EntryEntity> GetAll()
{
return _repository.Query<EntryEntity>().ToList();
using ArtemisDbContext dbContext = getContext();
return dbContext.Entries.ToList();
}
public void Save(EntryEntity entryEntity)
{
_repository.Upsert(entryEntity);
}
public void Save(IEnumerable<EntryEntity> entryEntities)
{
_repository.Upsert(entryEntities);
using ArtemisDbContext dbContext = getContext();
dbContext.Update(entryEntity);
dbContext.SaveChanges();
}
}
@@ -10,5 +10,5 @@ public interface IDeviceRepository : IRepository
DeviceEntity? Get(string id);
List<DeviceEntity> GetAll();
void Save(DeviceEntity deviceEntity);
void Save(IEnumerable<DeviceEntity> deviceEntities);
void SaveRange(IEnumerable<DeviceEntity> deviceEntities);
}
@@ -12,5 +12,4 @@ public interface IEntryRepository : IRepository
EntryEntity? GetByEntryId(long entryId);
List<EntryEntity> GetAll();
void Save(EntryEntity entryEntity);
void Save(IEnumerable<EntryEntity> entryEntities);
}
@@ -5,13 +5,9 @@ namespace Artemis.Storage.Repositories.Interfaces;
public interface IPluginRepository : IRepository
{
void AddPlugin(PluginEntity pluginEntity);
PluginEntity? GetPluginByGuid(Guid pluginGuid);
void SavePlugin(PluginEntity pluginEntity);
void AddSetting(PluginSettingEntity pluginSettingEntity);
PluginSettingEntity? GetSettingByGuid(Guid pluginGuid);
PluginSettingEntity? GetSettingByNameAndGuid(string name, Guid pluginGuid);
PluginEntity? GetPluginByPluginGuid(Guid pluginGuid);
void SaveSetting(PluginSettingEntity pluginSettingEntity);
void SavePlugin(PluginEntity pluginEntity);
PluginSettingEntity? GetSettingByNameAndGuid(string name, Guid pluginGuid);
void RemoveSettings(Guid pluginGuid);
}
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Storage.Repositories.Interfaces;
@@ -11,8 +10,7 @@ public interface IProfileCategoryRepository : IRepository
void Remove(ProfileCategoryEntity profileCategoryEntity);
List<ProfileCategoryEntity> GetAll();
ProfileCategoryEntity? Get(Guid id);
Stream? GetProfileIconStream(Guid id);
void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream);
ProfileCategoryEntity IsUnique(string name, Guid? id);
bool IsUnique(string name, Guid? id);
void Save(ProfileCategoryEntity profileCategoryEntity);
void SaveRange(List<ProfileCategoryEntity> profileCategoryEntities);
}
@@ -1,14 +1,15 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Text.Json.Nodes;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Storage.Repositories.Interfaces;
public interface IProfileRepository : IRepository
{
void Add(ProfileEntity profileEntity);
void Remove(ProfileEntity profileEntity);
List<ProfileEntity> GetAll();
ProfileEntity? Get(Guid id);
void Save(ProfileEntity profileEntity);
void Add(ProfileContainerEntity profileContainerEntity);
void Remove(ProfileContainerEntity profileContainerEntity);
void Save(ProfileContainerEntity profileContainerEntity);
void SaveRange(List<ProfileContainerEntity> profileContainerEntities);
void MigrateProfiles();
void MigrateProfile(JsonObject? configurationJson, JsonObject? profileJson);
}
@@ -1,14 +0,0 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.General;
namespace Artemis.Storage.Repositories.Interfaces;
public interface IQueuedActionRepository : IRepository
{
void Add(QueuedActionEntity queuedActionEntity);
void Remove(QueuedActionEntity queuedActionEntity);
List<QueuedActionEntity> GetAll();
List<QueuedActionEntity> GetByType(string type);
bool IsTypeQueued(string type);
void ClearByType(string type);
}
@@ -0,0 +1,9 @@
using Artemis.Storage.Entities.General;
namespace Artemis.Storage.Repositories.Interfaces;
public interface IReleaseRepository : IRepository
{
bool SaveVersionInstallDate(string version);
ReleaseEntity? GetPreviousInstalledVersion();
}
@@ -1,59 +1,44 @@
using System;
using System.Linq;
using Artemis.Storage.Entities.Plugins;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
using Microsoft.EntityFrameworkCore;
namespace Artemis.Storage.Repositories;
internal class PluginRepository : IPluginRepository
internal class PluginRepository(Func<ArtemisDbContext> getContext) : IPluginRepository
{
private readonly LiteRepository _repository;
public PluginRepository(LiteRepository repository)
public PluginEntity? GetPluginByPluginGuid(Guid pluginGuid)
{
_repository = repository;
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => new {s.Name, s.PluginGuid}, true);
}
public void AddPlugin(PluginEntity pluginEntity)
{
_repository.Insert(pluginEntity);
}
public PluginEntity? GetPluginByGuid(Guid pluginGuid)
{
return _repository.FirstOrDefault<PluginEntity>(p => p.Id == pluginGuid);
}
public void SavePlugin(PluginEntity pluginEntity)
{
_repository.Upsert(pluginEntity);
}
public void AddSetting(PluginSettingEntity pluginSettingEntity)
{
_repository.Insert(pluginSettingEntity);
}
public PluginSettingEntity? GetSettingByGuid(Guid pluginGuid)
{
return _repository.FirstOrDefault<PluginSettingEntity>(p => p.PluginGuid == pluginGuid);
using ArtemisDbContext dbContext = getContext();
return dbContext.Plugins.Include(p => p.Features).FirstOrDefault(p => p.PluginGuid == pluginGuid);
}
public PluginSettingEntity? GetSettingByNameAndGuid(string name, Guid pluginGuid)
{
return _repository.FirstOrDefault<PluginSettingEntity>(p => p.Name == name && p.PluginGuid == pluginGuid);
using ArtemisDbContext dbContext = getContext();
return dbContext.PluginSettings.FirstOrDefault(p => p.Name == name && p.PluginGuid == pluginGuid);
}
public void SaveSetting(PluginSettingEntity pluginSettingEntity)
{
_repository.Upsert(pluginSettingEntity);
}
/// <inheritdoc />
public void RemoveSettings(Guid pluginGuid)
{
_repository.DeleteMany<PluginSettingEntity>(s => s.PluginGuid == pluginGuid);
using ArtemisDbContext dbContext = getContext();
dbContext.PluginSettings.RemoveRange(dbContext.PluginSettings.Where(s => s.PluginGuid == pluginGuid));
dbContext.SaveChanges();
}
public void SaveSetting(PluginSettingEntity pluginSettingEntity)
{
using ArtemisDbContext dbContext = getContext();
dbContext.PluginSettings.Update(pluginSettingEntity);
dbContext.SaveChanges();
}
public void SavePlugin(PluginEntity pluginEntity)
{
using ArtemisDbContext dbContext = getContext();
dbContext.Update(pluginEntity);
dbContext.SaveChanges();
}
}
@@ -1,93 +1,75 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
using Microsoft.EntityFrameworkCore;
namespace Artemis.Storage.Repositories;
internal class ProfileCategoryRepository : IProfileCategoryRepository
internal class ProfileCategoryRepository(Func<ArtemisDbContext> getContext, IProfileRepository profileRepository) : IProfileCategoryRepository
{
private readonly ILiteStorage<Guid> _profileIcons;
private readonly LiteRepository _repository;
public ProfileCategoryRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<ProfileCategoryEntity>().EnsureIndex(s => s.Name, true);
_profileIcons = _repository.Database.GetStorage<Guid>("profileIcons");
}
private bool _migratedProfiles;
public void Add(ProfileCategoryEntity profileCategoryEntity)
{
_repository.Insert(profileCategoryEntity);
using ArtemisDbContext dbContext = getContext();
dbContext.ProfileCategories.Add(profileCategoryEntity);
dbContext.SaveChanges();
}
public void Remove(ProfileCategoryEntity profileCategoryEntity)
{
_repository.Delete<ProfileCategoryEntity>(profileCategoryEntity.Id);
using ArtemisDbContext dbContext = getContext();
dbContext.ProfileCategories.Remove(profileCategoryEntity);
dbContext.SaveChanges();
}
public List<ProfileCategoryEntity> GetAll()
{
List<ProfileCategoryEntity> categories = _repository.Query<ProfileCategoryEntity>().ToList();
// Update all profile versions to the current version, profile migrations don't apply to LiteDB so anything loadable is assumed to be up to date
foreach (ProfileCategoryEntity profileCategoryEntity in categories)
UpdateProfileVersions(profileCategoryEntity);
if (!_migratedProfiles)
{
profileRepository.MigrateProfiles();
_migratedProfiles = true;
}
return categories;
using ArtemisDbContext dbContext = getContext();
return dbContext.ProfileCategories.Include(c => c.ProfileConfigurations).ToList();
}
public ProfileCategoryEntity? Get(Guid id)
{
ProfileCategoryEntity? result = _repository.FirstOrDefault<ProfileCategoryEntity>(p => p.Id == id);
if (result == null)
return null;
if (!_migratedProfiles)
{
profileRepository.MigrateProfiles();
_migratedProfiles = true;
}
// Update all profile versions to the current version, profile migrations don't apply to LiteDB so anything loadable is assumed to be up to date
UpdateProfileVersions(result);
return result;
}
public ProfileCategoryEntity IsUnique(string name, Guid? id)
{
name = name.Trim();
if (id == null)
return _repository.FirstOrDefault<ProfileCategoryEntity>(p => p.Name == name);
return _repository.FirstOrDefault<ProfileCategoryEntity>(p => p.Name == name && p.Id != id.Value);
using ArtemisDbContext dbContext = getContext();
return dbContext.ProfileCategories.Include(c => c.ProfileConfigurations).FirstOrDefault(c => c.Id == id);
}
public void Save(ProfileCategoryEntity profileCategoryEntity)
{
_repository.Upsert(profileCategoryEntity);
using ArtemisDbContext dbContext = getContext();
dbContext.Update(profileCategoryEntity);
dbContext.SaveChanges();
}
public Stream? GetProfileIconStream(Guid id)
public void SaveRange(List<ProfileCategoryEntity> profileCategoryEntities)
{
if (!_profileIcons.Exists(id))
return null;
MemoryStream stream = new();
_profileIcons.Download(id, stream);
return stream;
using ArtemisDbContext dbContext = getContext();
dbContext.UpdateRange(profileCategoryEntities);
dbContext.SaveChanges();
}
public void SaveProfileIconStream(ProfileConfigurationEntity profileConfigurationEntity, Stream stream)
public bool IsUnique(string name, Guid? id)
{
if (profileConfigurationEntity.FileIconId == Guid.Empty)
profileConfigurationEntity.FileIconId = Guid.NewGuid();
using ArtemisDbContext dbContext = getContext();
if (stream == null && _profileIcons.Exists(profileConfigurationEntity.FileIconId))
_profileIcons.Delete(profileConfigurationEntity.FileIconId);
_profileIcons.Upload(profileConfigurationEntity.FileIconId, profileConfigurationEntity.FileIconId + ".png", stream);
}
private static void UpdateProfileVersions(ProfileCategoryEntity profileCategoryEntity)
{
foreach (ProfileConfigurationEntity profileConfigurationEntity in profileCategoryEntity.ProfileConfigurations)
profileConfigurationEntity.Version = StorageMigrationService.PROFILE_VERSION;
name = name.Trim();
return id == null
? dbContext.ProfileCategories.Any(p => p.Name == name)
: dbContext.ProfileCategories.Any(p => p.Name == name && p.Id != id.Value);
}
}
@@ -1,43 +1,104 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Nodes;
using Artemis.Storage.Entities;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Exceptions;
using Artemis.Storage.Migrations;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
using Microsoft.EntityFrameworkCore;
using Serilog;
namespace Artemis.Storage.Repositories;
internal class ProfileRepository : IProfileRepository
public class ProfileRepository(ILogger logger, Func<ArtemisDbContext> getContext, List<IProfileMigration> profileMigrators) : IProfileRepository
{
private readonly LiteRepository _repository;
public ProfileRepository(LiteRepository repository)
public void Add(ProfileContainerEntity profileContainerEntity)
{
_repository = repository;
_repository.Database.GetCollection<ProfileEntity>().EnsureIndex(s => s.Name);
using ArtemisDbContext dbContext = getContext();
dbContext.ProfileContainers.Add(profileContainerEntity);
dbContext.SaveChanges();
}
public void Add(ProfileEntity profileEntity)
public void Remove(ProfileContainerEntity profileContainerEntity)
{
_repository.Insert(profileEntity);
using ArtemisDbContext dbContext = getContext();
dbContext.ProfileContainers.Remove(profileContainerEntity);
dbContext.SaveChanges();
}
public void Remove(ProfileEntity profileEntity)
public void Save(ProfileContainerEntity profileContainerEntity)
{
_repository.Delete<ProfileEntity>(profileEntity.Id);
using ArtemisDbContext dbContext = getContext();
dbContext.Update(profileContainerEntity);
dbContext.SaveChanges();
}
public List<ProfileEntity> GetAll()
public void SaveRange(List<ProfileContainerEntity> profileContainerEntities)
{
return _repository.Query<ProfileEntity>().ToList();
using ArtemisDbContext dbContext = getContext();
dbContext.UpdateRange(profileContainerEntities);
dbContext.SaveChanges();
}
public ProfileEntity? Get(Guid id)
public void MigrateProfiles()
{
return _repository.FirstOrDefault<ProfileEntity>(p => p.Id == id);
using ArtemisDbContext dbContext = getContext();
int max = profileMigrators.Max(m => m.Version);
// Query the ProfileContainerEntity table directly, grabbing the ID, profile, and configuration
List<RawProfileContainer> containers = dbContext.Database
.SqlQueryRaw<RawProfileContainer>("SELECT Id, Profile, ProfileConfiguration FROM ProfileContainers WHERE json_extract(ProfileConfiguration, '$.Version') < {0}", max)
.ToList();
foreach (RawProfileContainer rawProfileContainer in containers)
{
try
{
JsonObject? profileConfiguration = JsonNode.Parse(rawProfileContainer.ProfileConfiguration)?.AsObject();
JsonObject? profile = JsonNode.Parse(rawProfileContainer.Profile)?.AsObject();
if (profileConfiguration == null || profile == null)
{
logger.Error("Failed to parse profile or profile configuration of profile container {Id}", rawProfileContainer.Id);
continue;
}
MigrateProfile(profileConfiguration, profile);
rawProfileContainer.Profile = profile.ToString();
rawProfileContainer.ProfileConfiguration = profileConfiguration.ToString();
// Write the updated containers back to the database
dbContext.Database.ExecuteSqlRaw(
"UPDATE ProfileContainers SET Profile = {0}, ProfileConfiguration = {1} WHERE Id = {2}",
rawProfileContainer.Profile,
rawProfileContainer.ProfileConfiguration,
rawProfileContainer.Id);
}
catch (Exception e)
{
logger.Error(e, "Failed to migrate profile container {Id}", rawProfileContainer.Id);
}
}
}
public void Save(ProfileEntity profileEntity)
public void MigrateProfile(JsonObject? configurationJson, JsonObject? profileJson)
{
_repository.Upsert(profileEntity);
if (configurationJson == null || profileJson == null)
return;
configurationJson["Version"] ??= 0;
foreach (IProfileMigration profileMigrator in profileMigrators.OrderBy(m => m.Version))
{
if (profileMigrator.Version <= configurationJson["Version"]!.GetValue<int>())
continue;
logger.Information("Migrating profile '{Name}' from version {OldVersion} to {NewVersion}", configurationJson["Name"], configurationJson["Version"], profileMigrator.Version);
profileMigrator.Migrate(configurationJson, profileJson);
configurationJson["Version"] = profileMigrator.Version;
}
}
}
@@ -1,57 +0,0 @@
using System.Collections.Generic;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories;
public class QueuedActionRepository : IQueuedActionRepository
{
private readonly LiteRepository _repository;
public QueuedActionRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<QueuedActionEntity>().EnsureIndex(s => s.Type);
}
#region Implementation of IQueuedActionRepository
/// <inheritdoc />
public void Add(QueuedActionEntity queuedActionEntity)
{
_repository.Insert(queuedActionEntity);
}
/// <inheritdoc />
public void Remove(QueuedActionEntity queuedActionEntity)
{
_repository.Delete<QueuedActionEntity>(queuedActionEntity.Id);
}
/// <inheritdoc />
public List<QueuedActionEntity> GetAll()
{
return _repository.Query<QueuedActionEntity>().ToList();
}
/// <inheritdoc />
public List<QueuedActionEntity> GetByType(string type)
{
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
}
/// <inheritdoc />
public bool IsTypeQueued(string type)
{
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).Count() > 0;
}
/// <inheritdoc />
public void ClearByType(string type)
{
_repository.DeleteMany<QueuedActionEntity>(q => q.Type == type);
}
#endregion
}
@@ -1,38 +1,28 @@
using System;
using System.Linq;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories;
public class ReleaseRepository : IReleaseRepository
public class ReleaseRepository(Func<ArtemisDbContext> getContext) : IReleaseRepository
{
private readonly LiteRepository _repository;
public ReleaseRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<ReleaseEntity>().EnsureIndex(s => s.Version, true);
}
public bool SaveVersionInstallDate(string version)
{
ReleaseEntity release = _repository.Query<ReleaseEntity>().Where(r => r.Version == version).FirstOrDefault();
using ArtemisDbContext dbContext = getContext();
ReleaseEntity? release = dbContext.Releases.FirstOrDefault(r => r.Version == version);
if (release != null)
return false;
_repository.Insert(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow});
dbContext.Releases.Add(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow});
dbContext.SaveChanges();
return true;
}
public ReleaseEntity GetPreviousInstalledVersion()
public ReleaseEntity? GetPreviousInstalledVersion()
{
return _repository.Query<ReleaseEntity>().OrderByDescending(r => r.InstalledAt).Skip(1).FirstOrDefault();
using ArtemisDbContext dbContext = getContext();
return dbContext.Releases.AsEnumerable().OrderByDescending(r => r.InstalledAt).Skip(1).FirstOrDefault();
}
}
public interface IReleaseRepository : IRepository
{
bool SaveVersionInstallDate(string version);
ReleaseEntity GetPreviousInstalledVersion();
}