Added Git Export Feature
- Added import algorithm - Added FromJsonX() methods to data models - Added possibility to work with temp. database file - Added export processor to handle export process & triggers - Added something changed event e.g. as export trigger - Added possibility to import a JSON export - Updated Git icon
This commit is contained in:
20 files changed
+411
-62
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -167,11 +167,145 @@ public sealed class DataContext : DbContext, IDataContext
|
||||
}, jsonSettings);
|
||||
}
|
||||
|
||||
public static async Task ImportAndLoadAsync(string path)
|
||||
/// <summary>
|
||||
/// Stores data needed to resolve a parent-child relationship.
|
||||
/// </summary>
|
||||
/// <param name="ParentId">The parent id we want to resolve.</param>
|
||||
/// <param name="Entity">The entity for which we want to resolve the parent.</param>
|
||||
/// <typeparam name="T">The type of the entity.</typeparam>
|
||||
private readonly record struct TreeResolver<T>(Guid ParentId, T Entity);
|
||||
|
||||
/// <summary>
|
||||
/// Imports data from a JSON file into an empty database.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the JSON export.</param>
|
||||
/// <exception cref="InvalidOperationException">When the database is not empty.</exception>
|
||||
public async Task ImportAsync(string path)
|
||||
{
|
||||
// We import that JSON data file into an new, empty database file
|
||||
// at a temporary location. Next, we enable the auto export feature
|
||||
// to keep the source file up-to-date.
|
||||
if(await this.Settings.AnyAsync() ||
|
||||
await this.Sections.AnyAsync() ||
|
||||
await this.TextElements.AnyAsync() ||
|
||||
await this.Translations.AnyAsync())
|
||||
throw new InvalidOperationException("The database is not empty. In order to import data, the database must be empty.");
|
||||
|
||||
// Start a transaction:
|
||||
await using var transaction = await this.Database.BeginTransactionAsync();
|
||||
|
||||
// Configure the JSON serializer:
|
||||
var jsonSettings = new JsonSerializerOptions
|
||||
{
|
||||
Converters = { new JsonUniqueIdConverter() },
|
||||
};
|
||||
|
||||
await using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var jsonData = await JsonSerializer.DeserializeAsync<JsonData>(fileStream, jsonSettings);
|
||||
|
||||
// --------------------
|
||||
// Import the settings:
|
||||
// --------------------
|
||||
foreach (var setting in jsonData.Settings)
|
||||
this.Settings.Add(Setting.FromJsonSetting(setting));
|
||||
|
||||
// --------------------
|
||||
// Import the sections:
|
||||
// --------------------
|
||||
|
||||
// We must store the intermediate data in a list, because we need to resolve
|
||||
// the parent-child relationships in a second step.
|
||||
var allSections = new Dictionary<Guid, TreeResolver<Section>>();
|
||||
var sectionToTextElements = new Dictionary<Guid, List<Guid>>();
|
||||
|
||||
// Read the data from the JSON file:
|
||||
foreach (var section in jsonData.Sections)
|
||||
{
|
||||
// Convert the next element:
|
||||
var nextSection = Section.FromJsonSection(section);
|
||||
|
||||
// Store the element:
|
||||
allSections.Add(nextSection.UniqueId, new (section.ParentUniqueId.UniqueId, nextSection));
|
||||
sectionToTextElements.Add(nextSection.UniqueId, section.TextElements.Select(n => n.UniqueId).ToList());
|
||||
}
|
||||
|
||||
// Now, resolve the parent-child relationships for the sections:
|
||||
foreach (var (uniqueId, (parentId, section)) in allSections)
|
||||
section.Parent = parentId == Guid.Empty ? null : allSections[parentId].Entity;
|
||||
|
||||
// -------------------------
|
||||
// Import the text elements:
|
||||
// -------------------------
|
||||
|
||||
// We must store the intermediate data in a list, because we need to resolve
|
||||
// the parent-child relationships in a second step.
|
||||
var allTextElements = new Dictionary<Guid, TextElement>();
|
||||
var textElementToTranslations = new Dictionary<Guid, List<Guid>>();
|
||||
|
||||
// Read the data from the JSON file:
|
||||
foreach (var textElement in jsonData.TextElements)
|
||||
{
|
||||
// Convert the next element:
|
||||
var nextTextElement = TextElement.FromJsonTextElement(textElement);
|
||||
|
||||
// We know that the section is already imported, because we imported the sections first:
|
||||
nextTextElement.Section = allSections[textElement.SectionUniqueId.UniqueId].Entity;
|
||||
|
||||
// Store the element in the list:
|
||||
allTextElements.Add(nextTextElement.UniqueId, nextTextElement);
|
||||
textElementToTranslations.Add(nextTextElement.UniqueId, textElement.Translations.Select(n => n.UniqueId).ToList());
|
||||
}
|
||||
|
||||
// Now, resolve the parent-child relationships for the text elements to the sections:
|
||||
foreach (var (sectionUniqueId, textElementsIds) in sectionToTextElements)
|
||||
{
|
||||
var section = allSections[sectionUniqueId].Entity;
|
||||
section.TextElements.AddRange(textElementsIds.Select(n => allTextElements[n]));
|
||||
}
|
||||
|
||||
// Free the memory:
|
||||
sectionToTextElements.Clear();
|
||||
|
||||
// ------------------------
|
||||
// Import the translations:
|
||||
// ------------------------
|
||||
|
||||
// We must store the intermediate data in a list, because we need to resolve
|
||||
// the parent-child relationships in a second step.
|
||||
var allTranslations = new Dictionary<Guid, Translation>();
|
||||
|
||||
// Read the data from the JSON file:
|
||||
foreach (var translation in jsonData.Translations)
|
||||
{
|
||||
// Convert the next element:
|
||||
var nextTranslation = Translation.FromJsonTranslation(translation);
|
||||
|
||||
// We know that the text element is already imported, because we imported the text elements first:
|
||||
nextTranslation.TextElement = allTextElements[translation.TextElementUniqueId.UniqueId];
|
||||
|
||||
// Store the element in the list:
|
||||
allTranslations.Add(nextTranslation.UniqueId, nextTranslation);
|
||||
}
|
||||
|
||||
// Now, resolve the parent-child relationships for the translations to the text elements:
|
||||
foreach (var (textElementUniqueId, translationsIds) in textElementToTranslations)
|
||||
{
|
||||
var textElement = allTextElements[textElementUniqueId];
|
||||
textElement.Translations.AddRange(translationsIds.Select(n => allTranslations[n]));
|
||||
}
|
||||
|
||||
// Free the memory:
|
||||
textElementToTranslations.Clear();
|
||||
|
||||
// ---------------------------------
|
||||
// Add all the data to the database:
|
||||
// ---------------------------------
|
||||
this.Sections.AddRange(allSections.Values.Select(n => n.Entity));
|
||||
this.TextElements.AddRange(allTextElements.Values);
|
||||
this.Translations.AddRange(allTranslations.Values);
|
||||
|
||||
// Save the changes:
|
||||
await this.SaveChangesAsync();
|
||||
|
||||
// Commit the transaction:
|
||||
await transaction.CommitAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -31,4 +31,14 @@ public sealed class Section
|
||||
ParentUniqueId = this.Parent?.JsonUniqueId ?? new DataContext.JsonUniqueId("null", Guid.Empty, "Sec"),
|
||||
TextElements = this.TextElements.Select(n => n.JsonUniqueId).ToList()
|
||||
};
|
||||
|
||||
internal static Section FromJsonSection(DataContext.JsonSection jsonSection) => new()
|
||||
{
|
||||
UniqueId = jsonSection.UniqueId.UniqueId,
|
||||
Name = jsonSection.Name,
|
||||
DataKey = jsonSection.DataKey,
|
||||
Depth = jsonSection.Depth,
|
||||
Parent = null,
|
||||
TextElements = new(),
|
||||
};
|
||||
}
|
||||
@@ -31,4 +31,14 @@ public sealed class Setting
|
||||
IntegerValue = this.IntegerValue,
|
||||
TextValue = this.TextValue,
|
||||
};
|
||||
|
||||
internal static Setting FromJsonSetting(DataContext.JsonSetting jsonSetting) => new()
|
||||
{
|
||||
UniqueId = jsonSetting.UniqueId.UniqueId,
|
||||
Code = jsonSetting.Code,
|
||||
BoolValue = jsonSetting.BoolValue,
|
||||
GuidValue = jsonSetting.GuidValue,
|
||||
IntegerValue = jsonSetting.IntegerValue,
|
||||
TextValue = jsonSetting.TextValue,
|
||||
};
|
||||
}
|
||||
@@ -14,4 +14,8 @@ public static class SettingNames
|
||||
public static readonly string GENERATOR_DOTNET_DEFAULT_CULTURE = "Generator .NET Default Culture";
|
||||
public static readonly string GENERATOR_GODOT_ENABLED = "Generator Godot Enabled";
|
||||
public static readonly string GENERATOR_GODOT_DESTINATION_PATH = "Generator Godot Destination Path";
|
||||
public static readonly string AUTO_EXPORT_ENABLED = "Auto-Export Enabled";
|
||||
public static readonly string AUTO_EXPORT_DESTINATION_PATH = "Auto-Export Destination Path";
|
||||
public static readonly string AUTO_EXPORT_FILENAME = "Auto-Export Filename";
|
||||
public static readonly string AUTO_EXPORT_SENSITIVE_DATA = "Auto-Export Sensitive Data";
|
||||
}
|
||||
@@ -31,4 +31,14 @@ public sealed class TextElement
|
||||
SectionUniqueId = this.Section.JsonUniqueId,
|
||||
Translations = this.Translations.Select(n => n.JsonUniqueId).ToList(),
|
||||
};
|
||||
|
||||
internal static TextElement FromJsonTextElement(DataContext.JsonTextElement jsonTextElement) => new()
|
||||
{
|
||||
UniqueId = jsonTextElement.UniqueId.UniqueId,
|
||||
Code = jsonTextElement.Code,
|
||||
Name = jsonTextElement.Name,
|
||||
IsMultiLine = jsonTextElement.IsMultiLine,
|
||||
Section = new(),
|
||||
Translations = new(),
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public sealed class Translation
|
||||
|
||||
public Guid UniqueId { get; set; }
|
||||
|
||||
public TextElement TextElement { get; set; }
|
||||
public TextElement TextElement { get; set; } = new();
|
||||
|
||||
public string Culture { get; set; } = "en-US";
|
||||
|
||||
@@ -26,4 +26,13 @@ public sealed class Translation
|
||||
TextElementUniqueId = this.TextElement.JsonUniqueId,
|
||||
Text = this.Text,
|
||||
};
|
||||
|
||||
internal static Translation FromJsonTranslation(DataContext.JsonTranslation jsonTranslation) => new()
|
||||
{
|
||||
UniqueId = jsonTranslation.UniqueId.UniqueId,
|
||||
Culture = jsonTranslation.Culture,
|
||||
TranslateManual = jsonTranslation.TranslateManual,
|
||||
Text = jsonTranslation.Text,
|
||||
TextElement = new(),
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using DataModel.Database.Common;
|
||||
using DataModel.Database;
|
||||
using DataModel.Database.Common;
|
||||
using DataModel.MigrationScripts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -12,9 +13,12 @@ public static class Setup
|
||||
private const string DB_READ_WRITE_MODE = "ReadWrite";
|
||||
private const string DB_READ_WRITE_CREATE_MODE = "ReadWriteCreate";
|
||||
|
||||
private static string usedDataFile = string.Empty;
|
||||
|
||||
public static string DataFile => Setup.usedDataFile;
|
||||
private static string USED_DATA_FILE = string.Empty;
|
||||
private static SetupMaintenance SETUP_MAINTENANCE = new();
|
||||
|
||||
public static string DataFile => Setup.USED_DATA_FILE;
|
||||
|
||||
public static SetupMaintenance Maintenance => Setup.SETUP_MAINTENANCE;
|
||||
|
||||
/// <summary>
|
||||
/// Tries to migrate the database.
|
||||
@@ -37,31 +41,52 @@ public static class Setup
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add the database to the DI system
|
||||
/// Imports a JSON file into a new database.
|
||||
/// </summary>
|
||||
public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true)
|
||||
public static async Task ImportDataAndAddDatabase(this IServiceCollection serviceCollection, string path2JSONFile)
|
||||
{
|
||||
Setup.usedDataFile = path2DataFile;
|
||||
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient);
|
||||
var tempPath = Path.GetTempFileName();
|
||||
|
||||
Setup.USED_DATA_FILE = tempPath;
|
||||
Setup.SETUP_MAINTENANCE = new(path2JSONFile, true);
|
||||
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={tempPath};Mode={DB_READ_WRITE_CREATE_MODE};"), ServiceLifetime.Transient);
|
||||
|
||||
// Get the database service:
|
||||
await using var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||
var dbContext = serviceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
// Next, we import the data from the provided JSON file:
|
||||
await dbContext.ImportAsync(path2JSONFile);
|
||||
|
||||
//
|
||||
// Next, we enable the auto-export feature to keep the source file up to date.
|
||||
// The auto-export feature might exist, but we enforce it, when we work with a
|
||||
// temporary database source by a JSON file.
|
||||
//
|
||||
|
||||
// Enable the auto-export feature:
|
||||
var autoExportEnabled = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_ENABLED);
|
||||
autoExportEnabled.BoolValue = true;
|
||||
|
||||
// Set the auto-export path and file:
|
||||
var autoExportPath = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_DESTINATION_PATH);
|
||||
autoExportPath.TextValue = Path.GetDirectoryName(path2JSONFile) ?? string.Empty;
|
||||
|
||||
var autoExportFile = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_FILENAME);
|
||||
autoExportFile.TextValue = Path.GetFileName(path2JSONFile);
|
||||
|
||||
// Save the changes:
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create the database instance from the given path.
|
||||
/// Creates and adds the database instance to the DI system (extension method).
|
||||
/// </summary>
|
||||
public static DataContext CreateDatabaseInstance(string path2DataFile, bool createWhenNecessary = true)
|
||||
public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true)
|
||||
{
|
||||
// Store the path to the database:
|
||||
Setup.usedDataFile = path2DataFile;
|
||||
|
||||
// Create a database builder:
|
||||
var builder = new DbContextOptionsBuilder<DataContext>();
|
||||
|
||||
// Add the database configuration to the builder:
|
||||
builder.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};");
|
||||
|
||||
// Next, construct the database context:
|
||||
var dbContext = new DataContext(builder.Options);
|
||||
return dbContext;
|
||||
Setup.USED_DATA_FILE = path2DataFile;
|
||||
Setup.SETUP_MAINTENANCE = new(path2DataFile, false);
|
||||
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -84,4 +109,22 @@ public static class Setup
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public readonly record struct SetupMaintenance(string PathToDataFile = "", bool RemoveTempDatabaseAfterwards = false) : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
if (!this.RemoveTempDatabaseAfterwards)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(this.PathToDataFile);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to remove the temporary database file: {e.Message} // {e.InnerException?.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user