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)