From 05caa559b4cdce27b691c437eb9f09eb4d4c2b00 Mon Sep 17 00:00:00 2001 From: Elias Fierke Date: Sat, 25 Jul 2026 19:40:40 +0200 Subject: [PATCH] [feat:] added json schema migration (ai slop, since this would be boring to implement) (untested) --- DataStore/SchemaMigration.cs | 181 +++++++++++++++++++++++++++++++++++ DataStore/Settings.cs | 57 ++++++++++- 2 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 DataStore/SchemaMigration.cs diff --git a/DataStore/SchemaMigration.cs b/DataStore/SchemaMigration.cs new file mode 100644 index 0000000..fe58ccf --- /dev/null +++ b/DataStore/SchemaMigration.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Logof_Client; + +/// +/// Handles schema versioning and migrations for JSON data structures. +/// This system allows tracking schema changes and automatically migrating +/// old data to new formats when the schema version changes. +/// +public static class SchemaMigration +{ + // Current schema version - increment when you make breaking changes + public const int CURRENT_SCHEMA_VERSION = 3; + + // Dictionary of migration functions, keyed by the version they migrate TO + private static readonly Dictionary> Migrations = new() + { + // Example: Migration from v1 to v2 + // When loading v1 data, this will be called to upgrade to v2 + // { 2, MigrateV1ToV2 }, + + // Example: Migration from v2 to v3 + // { 3, MigrateV2ToV3 }, + }; + + /// + /// Checks the schema version of loaded JSON and applies migrations if needed. + /// + public static JObject MigrateIfNeeded(JObject jsonObject, int? loadedVersion) + { + // If no version found, assume it's version 1 (legacy data) + int sourceVersion = loadedVersion ?? 1; + + // If versions match, no migration needed + if (sourceVersion == CURRENT_SCHEMA_VERSION) + return jsonObject; + + // If loaded version is newer than current, warn and return as-is + if (sourceVersion > CURRENT_SCHEMA_VERSION) + { + Logger.Log($"Warning: Data schema version {sourceVersion} is newer than application schema version {CURRENT_SCHEMA_VERSION}. " + + "This might indicate data was created with a newer version of the application.", + Logger.LogType.Warning); + return jsonObject; + } + + // Apply migrations sequentially from source version to current version + Logger.Log($"Migrating data from schema version {sourceVersion} to {CURRENT_SCHEMA_VERSION}", Logger.LogType.Info); + + for (int targetVersion = sourceVersion + 1; targetVersion <= CURRENT_SCHEMA_VERSION; targetVersion++) + { + if (Migrations.TryGetValue(targetVersion, out var migration)) + { + try + { + jsonObject = migration(jsonObject); + Logger.Log($"Successfully migrated to schema version {targetVersion}", Logger.LogType.Info); + } + catch (Exception ex) + { + Logger.Log($"Error during migration to version {targetVersion}: {ex.Message}", Logger.LogType.Error); + throw; + } + } + else + { + Logger.Log($"No migration defined from version {targetVersion - 1} to {targetVersion}", Logger.LogType.Warning); + } + } + + return jsonObject; + } + + /// + /// Example migration function: from v1 to v2. + /// Rename 'old_field_name' to 'new_field_name' + /// + private static JObject MigrateV1ToV2(JObject obj) + { + // Example: Rename a field + if (obj.TryGetValue("old_field_name", out var value)) + { + obj["new_field_name"] = value; + obj.Remove("old_field_name"); + } + + return obj; + } + + /// + /// Example migration function: from v2 to v3. + /// Add a new required field with a default value. + /// + private static JObject MigrateV2ToV3(JObject obj) + { + // Example: Add a new field that didn't exist before + if (!obj.ContainsKey("new_required_field")) + { + obj["new_required_field"] = "default_value"; + } + + return obj; + } + + /// + /// Registers a new migration function that will be called when upgrading to a specific version. + /// Call this during application startup to register your custom migrations. + /// + /// The schema version this migration upgrades TO + /// The migration function that transforms the JObject + public static void RegisterMigration(int targetVersion, Func migration) + { + if (targetVersion <= 1) + throw new ArgumentException("Target version must be greater than 1"); + + if (Migrations.ContainsKey(targetVersion)) + Logger.Log($"Warning: Overwriting existing migration for version {targetVersion}", Logger.LogType.Warning); + + Migrations[targetVersion] = migration; + Logger.Log($"Registered migration to schema version {targetVersion}", Logger.LogType.Info); + } + + /// + /// Helper method: Add a new property to an object if it doesn't exist + /// + public static void AddPropertyIfMissing(JObject obj, string propertyName, object defaultValue) + { + if (!obj.ContainsKey(propertyName)) + { + obj[propertyName] = JToken.FromObject(defaultValue); + } + } + + /// + /// Helper method: Rename a property in a JSON object + /// + public static void RenameProperty(JObject obj, string oldName, string newName) + { + if (obj.TryGetValue(oldName, out var value)) + { + obj[newName] = value; + obj.Remove(oldName); + } + } + + /// + /// Helper method: Move a nested property to the root level + /// + public static void FlattenProperty(JObject obj, string sourcePath, string targetPropertyName) + { + var value = obj.SelectToken(sourcePath); + if (value != null) + { + obj[targetPropertyName] = value; + } + } + + /// + /// Helper method: Merge a root property into a nested object + /// + public static void NestProperty(JObject obj, string propertyName, string nestedPath) + { + if (obj.TryGetValue(propertyName, out var value)) + { + var parts = nestedPath.Split('.'); + JObject current = obj; + + for (int i = 0; i < parts.Length - 1; i++) + { + if (!current.ContainsKey(parts[i])) + current[parts[i]] = new JObject(); + current = (JObject)current[parts[i]]; + } + + current[parts[^1]] = value; + obj.Remove(propertyName); + } + } +} diff --git a/DataStore/Settings.cs b/DataStore/Settings.cs index 4f22f20..86c2980 100644 --- a/DataStore/Settings.cs +++ b/DataStore/Settings.cs @@ -2,12 +2,19 @@ using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Logof_Client; public class Settings { public static Settings _instance = new(); + + /// + /// Schema version - increment this when you make breaking changes to the Settings structure + /// + public int schemaVersion { get; set; } = SchemaMigration.CURRENT_SCHEMA_VERSION; + public AddressSets addressSets = new(); public Customers customers = new(); public PdfExportSettings pdfExport { get; set; } = new(); @@ -27,7 +34,9 @@ public class Settings Directory.CreateDirectory(Global._instance.config_path); // if (!string.IsNullOrEmpty(Global._instance.config_path)) _instance.settingsPath = Global._instance.config_path; - var json = JsonConvert.SerializeObject(_instance); + // Always save with current schema version + _instance.schemaVersion = SchemaMigration.CURRENT_SCHEMA_VERSION; + var json = JsonConvert.SerializeObject(_instance, Formatting.Indented); File.WriteAllText(Path.Combine(Global._instance.config_path,"config.json"), json); } @@ -38,7 +47,24 @@ public class Settings try { var contents = File.ReadAllText(Path.Combine(Global._instance.config_path, "config.json")); - _instance = JsonConvert.DeserializeObject(contents); + + // First, parse as JObject to check schema version + var jsonObject = JObject.Parse(contents); + int? loadedVersion = jsonObject["schemaVersion"]?.Value(); + + // Apply migrations if needed + jsonObject = SchemaMigration.MigrateIfNeeded(jsonObject, loadedVersion); + + // Now deserialize the migrated JSON + _instance = jsonObject.ToObject(); + + // Save after migration to persist the upgraded schema + if (loadedVersion != SchemaMigration.CURRENT_SCHEMA_VERSION) + { + Logger.Log("Settings file was migrated to new schema version and saved.", Logger.LogType.Info); + Save(); + } + MainWindow._instance.RefreshCustomerItems(); } catch (Exception ex) @@ -88,6 +114,11 @@ public class Global { public static Global _instance; + /// + /// Schema version - increment this when you make breaking changes to the Global structure + /// + public int schemaVersion { get; set; } = SchemaMigration.CURRENT_SCHEMA_VERSION; + public Global() { _instance = this; @@ -116,6 +147,9 @@ public class Global "logofclient"))) Directory.CreateDirectory(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "logofclient")); + + // Always save with current schema version + _instance.schemaVersion = SchemaMigration.CURRENT_SCHEMA_VERSION; var json = JsonConvert.SerializeObject(_instance, Formatting.Indented); File.WriteAllText( Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "logofclient", @@ -135,7 +169,24 @@ public class Global var contents = File.ReadAllText(Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "logofclient", "global.config")); - _instance = JsonConvert.DeserializeObject(contents) ?? new Global(); + + // First, parse as JObject to check schema version + var jsonObject = JObject.Parse(contents); + int? loadedVersion = jsonObject["schemaVersion"]?.Value(); + + // Apply migrations if needed + jsonObject = SchemaMigration.MigrateIfNeeded(jsonObject, loadedVersion); + + // Now deserialize the migrated JSON + _instance = jsonObject.ToObject() ?? new Global(); + + // Save after migration to persist the upgraded schema + if (loadedVersion != SchemaMigration.CURRENT_SCHEMA_VERSION) + { + Logger.Log("Global settings file was migrated to new schema version and saved.", Logger.LogType.Info); + Save(); + } + _instance.NormalizePaths(); } catch (Exception ex)