using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.Commons.Entities.InboundApi; using ScadaLink.Commons.Entities.Notifications; using ScadaLink.Commons.Entities.Scripts; using ScadaLink.Commons.Entities.Templates; namespace ScadaLink.Transport.Serialization; /// /// Converts between the persistence-shaped and /// the wire-shaped . The conversion strips EF /// identity (FK ints) and references inter-entity links by name so bundles /// remain portable across environments. Secrets are carved into a per-entity /// so the bundle's secret-free public surface can /// be inspected (or, in future, exported without secrets) without touching the /// rest of the payload. /// public sealed class EntitySerializer { public BundleContentDto ToBundleContent(EntityAggregate aggregate) { ArgumentNullException.ThrowIfNull(aggregate); var folderNameById = aggregate.TemplateFolders.ToDictionary(f => f.Id, f => f.Name); var templateNameById = aggregate.Templates.ToDictionary(t => t.Id, t => t.Name); return new BundleContentDto( TemplateFolders: aggregate.TemplateFolders.Select(f => new TemplateFolderDto( Name: f.Name, ParentName: f.ParentFolderId is { } pid && folderNameById.TryGetValue(pid, out var pname) ? pname : null, SortOrder: f.SortOrder)).ToList(), Templates: aggregate.Templates.Select(t => new TemplateDto( Name: t.Name, FolderName: t.FolderId is { } fid && folderNameById.TryGetValue(fid, out var fname) ? fname : null, BaseTemplateName: t.ParentTemplateId is { } btid && templateNameById.TryGetValue(btid, out var bname) ? bname : null, Description: t.Description, Attributes: t.Attributes.Select(a => new TemplateAttributeDto( Name: a.Name, Value: a.Value, DataType: a.DataType, IsLocked: a.IsLocked, Description: a.Description, DataSourceReference: a.DataSourceReference)).ToList(), Alarms: t.Alarms.Select(a => new TemplateAlarmDto( Name: a.Name, Description: a.Description, PriorityLevel: a.PriorityLevel, TriggerType: a.TriggerType, TriggerConfiguration: a.TriggerConfiguration, IsLocked: a.IsLocked)).ToList(), Scripts: t.Scripts.Select(s => new TemplateScriptDto( Name: s.Name, Code: s.Code, TriggerType: s.TriggerType, TriggerConfiguration: s.TriggerConfiguration, ParameterDefinitions: s.ParameterDefinitions, ReturnDefinition: s.ReturnDefinition, IsLocked: s.IsLocked)).ToList(), Compositions: t.Compositions.Select(c => new TemplateCompositionDto( InstanceName: c.InstanceName, ComposedTemplateName: templateNameById.TryGetValue(c.ComposedTemplateId, out var cn) ? cn : string.Empty)).ToList())).ToList(), SharedScripts: aggregate.SharedScripts.Select(s => new SharedScriptDto( Name: s.Name, Code: s.Code, ParameterDefinitions: s.ParameterDefinitions, ReturnDefinition: s.ReturnDefinition)).ToList(), ExternalSystems: aggregate.ExternalSystems.Select(sys => { var methods = aggregate.ExternalSystemMethods .Where(m => m.ExternalSystemDefinitionId == sys.Id) .Select(m => new ExternalSystemMethodDto( Name: m.Name, HttpMethod: m.HttpMethod, Path: m.Path, ParameterDefinitions: m.ParameterDefinitions, ReturnDefinition: m.ReturnDefinition)) .ToList(); SecretsBlock? secrets = null; if (!string.IsNullOrEmpty(sys.AuthConfiguration)) { secrets = new SecretsBlock(new Dictionary(StringComparer.Ordinal) { ["AuthConfiguration"] = sys.AuthConfiguration, }); } return new ExternalSystemDto( Name: sys.Name, BaseUrl: sys.EndpointUrl, AuthType: sys.AuthType, Methods: methods, Secrets: secrets); }).ToList(), DatabaseConnections: aggregate.DatabaseConnections.Select(dc => new DatabaseConnectionDto( Name: dc.Name, MaxRetries: dc.MaxRetries, RetryDelay: dc.RetryDelay, // Connection strings typically embed credentials inline; treat the entire // string as secret rather than try to parse out the password fragment. Secrets: new SecretsBlock(new Dictionary(StringComparer.Ordinal) { ["ConnectionString"] = dc.ConnectionString, }))).ToList(), NotificationLists: aggregate.NotificationLists.Select(nl => new NotificationListDto( Name: nl.Name, Type: nl.Type, Recipients: nl.Recipients.Select(r => new NotificationRecipientDto( Name: r.Name, EmailAddress: r.EmailAddress)).ToList())).ToList(), SmtpConfigs: aggregate.SmtpConfigurations.Select(smtp => { SecretsBlock? secrets = null; if (!string.IsNullOrEmpty(smtp.Credentials)) { secrets = new SecretsBlock(new Dictionary(StringComparer.Ordinal) { ["Credentials"] = smtp.Credentials, }); } return new SmtpConfigDto( Host: smtp.Host, Port: smtp.Port, AuthType: smtp.AuthType, FromAddress: smtp.FromAddress, TlsMode: smtp.TlsMode, ConnectionTimeoutSeconds: smtp.ConnectionTimeoutSeconds, MaxConcurrentConnections: smtp.MaxConcurrentConnections, MaxRetries: smtp.MaxRetries, RetryDelay: smtp.RetryDelay, Secrets: secrets); }).ToList(), // ApiKey stores only KeyHash already; no plaintext to carve. SecretsBlock // stays null per design — KeyHash is on the public DTO. ApiKeys: aggregate.ApiKeys.Select(k => new ApiKeyDto( Name: k.Name, KeyHash: k.KeyHash, IsEnabled: k.IsEnabled, Secrets: null)).ToList(), ApiMethods: aggregate.ApiMethods.Select(m => new ApiMethodDto( Name: m.Name, Script: m.Script, ApprovedApiKeyIds: m.ApprovedApiKeyIds, ParameterDefinitions: m.ParameterDefinitions, ReturnDefinition: m.ReturnDefinition, TimeoutSeconds: m.TimeoutSeconds)).ToList()); } public EntityAggregate FromBundleContent(BundleContentDto content) { ArgumentNullException.ThrowIfNull(content); // Folders: assign synthetic ids by ordinal, resolve parent pointers by name. var folders = content.TemplateFolders .Select((dto, ix) => new TemplateFolder(dto.Name) { Id = ix + 1, SortOrder = dto.SortOrder }) .ToList(); var folderIdByName = folders.ToDictionary(f => f.Name, f => f.Id, StringComparer.Ordinal); for (var i = 0; i < folders.Count; i++) { var parentName = content.TemplateFolders[i].ParentName; if (parentName is not null && folderIdByName.TryGetValue(parentName, out var parentId)) { folders[i].ParentFolderId = parentId; } } // Templates: same pattern — assign ids, second pass resolves base + folder. var templates = content.Templates .Select((dto, ix) => { var t = new Template(dto.Name) { Id = ix + 1, Description = dto.Description }; foreach (var a in dto.Attributes) { t.Attributes.Add(new TemplateAttribute(a.Name) { TemplateId = t.Id, Value = a.Value, DataType = a.DataType, IsLocked = a.IsLocked, Description = a.Description, DataSourceReference = a.DataSourceReference, }); } foreach (var al in dto.Alarms) { t.Alarms.Add(new TemplateAlarm(al.Name) { TemplateId = t.Id, Description = al.Description, PriorityLevel = al.PriorityLevel, TriggerType = al.TriggerType, TriggerConfiguration = al.TriggerConfiguration, IsLocked = al.IsLocked, }); } foreach (var s in dto.Scripts) { t.Scripts.Add(new TemplateScript(s.Name, s.Code) { TemplateId = t.Id, TriggerType = s.TriggerType, TriggerConfiguration = s.TriggerConfiguration, ParameterDefinitions = s.ParameterDefinitions, ReturnDefinition = s.ReturnDefinition, IsLocked = s.IsLocked, }); } return t; }) .ToList(); var templateIdByName = templates.ToDictionary(t => t.Name, t => t.Id, StringComparer.Ordinal); for (var i = 0; i < templates.Count; i++) { var dto = content.Templates[i]; if (dto.FolderName is not null && folderIdByName.TryGetValue(dto.FolderName, out var fid)) { templates[i].FolderId = fid; } if (dto.BaseTemplateName is not null && templateIdByName.TryGetValue(dto.BaseTemplateName, out var btid)) { templates[i].ParentTemplateId = btid; } foreach (var compDto in dto.Compositions) { var comp = new TemplateComposition(compDto.InstanceName) { TemplateId = templates[i].Id, }; if (templateIdByName.TryGetValue(compDto.ComposedTemplateName, out var ctid)) { comp.ComposedTemplateId = ctid; } templates[i].Compositions.Add(comp); } } var sharedScripts = content.SharedScripts .Select((dto, ix) => new SharedScript(dto.Name, dto.Code) { Id = ix + 1, ParameterDefinitions = dto.ParameterDefinitions, ReturnDefinition = dto.ReturnDefinition, }) .ToList(); var externalSystems = new List(); var externalSystemMethods = new List(); for (var ix = 0; ix < content.ExternalSystems.Count; ix++) { var dto = content.ExternalSystems[ix]; var sys = new ExternalSystemDefinition(dto.Name, dto.BaseUrl, dto.AuthType) { Id = ix + 1, AuthConfiguration = dto.Secrets?.Values.TryGetValue("AuthConfiguration", out var auth) == true ? auth : null, }; externalSystems.Add(sys); foreach (var m in dto.Methods) { externalSystemMethods.Add(new ExternalSystemMethod(m.Name, m.HttpMethod, m.Path) { ExternalSystemDefinitionId = sys.Id, ParameterDefinitions = m.ParameterDefinitions, ReturnDefinition = m.ReturnDefinition, }); } } var databaseConnections = content.DatabaseConnections .Select((dto, ix) => { var connStr = dto.Secrets?.Values.TryGetValue("ConnectionString", out var cs) == true ? cs : string.Empty; return new DatabaseConnectionDefinition(dto.Name, connStr) { Id = ix + 1, MaxRetries = dto.MaxRetries, RetryDelay = dto.RetryDelay, }; }) .ToList(); var notificationLists = content.NotificationLists .Select((dto, ix) => { var list = new NotificationList(dto.Name) { Id = ix + 1, Type = dto.Type }; foreach (var r in dto.Recipients) { list.Recipients.Add(new NotificationRecipient(r.Name, r.EmailAddress) { NotificationListId = list.Id, }); } return list; }) .ToList(); var smtpConfigurations = content.SmtpConfigs .Select((dto, ix) => new SmtpConfiguration(dto.Host, dto.AuthType, dto.FromAddress) { Id = ix + 1, Port = dto.Port, Credentials = dto.Secrets?.Values.TryGetValue("Credentials", out var cred) == true ? cred : null, TlsMode = dto.TlsMode, ConnectionTimeoutSeconds = dto.ConnectionTimeoutSeconds, MaxConcurrentConnections = dto.MaxConcurrentConnections, MaxRetries = dto.MaxRetries, RetryDelay = dto.RetryDelay, }) .ToList(); var apiKeys = content.ApiKeys .Select((dto, ix) => { var key = ApiKey.FromHash(dto.Name, dto.KeyHash); key.Id = ix + 1; key.IsEnabled = dto.IsEnabled; return key; }) .ToList(); var apiMethods = content.ApiMethods .Select((dto, ix) => new ApiMethod(dto.Name, dto.Script) { Id = ix + 1, ApprovedApiKeyIds = dto.ApprovedApiKeyIds, ParameterDefinitions = dto.ParameterDefinitions, ReturnDefinition = dto.ReturnDefinition, TimeoutSeconds = dto.TimeoutSeconds, }) .ToList(); return new EntityAggregate( TemplateFolders: folders, Templates: templates, SharedScripts: sharedScripts, ExternalSystems: externalSystems, ExternalSystemMethods: externalSystemMethods, DatabaseConnections: databaseConnections, NotificationLists: notificationLists, SmtpConfigurations: smtpConfigurations, ApiKeys: apiKeys, ApiMethods: apiMethods); } }