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:
tsommer committed 2023-01-22 19:35:57 +01:00
1 parent 7a742a18e7
commit 0587af18f2
20 files changed
+411 -62

No files matched your search

+1 -1
View File
@@ -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(),
};
}
+65 -22
View File
@@ -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}");
}
}
}
}