diff --git a/TALXIS.CLI.sln b/TALXIS.CLI.sln index d0a7c51..b102964 100644 --- a/TALXIS.CLI.sln +++ b/TALXIS.CLI.sln @@ -19,6 +19,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0AB3BF05 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TALXIS.CLI.IntegrationTests", "tests\TALXIS.CLI.IntegrationTests\TALXIS.CLI.IntegrationTests.csproj", "{EDB2D38C-8601-43BD-AC88-165E822986C7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TALXIS.CLI.DataVisualizer", "src\TALXIS.CLI.DataVisualizer\TALXIS.CLI.DataVisualizer.csproj", "{AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +103,18 @@ Global {EDB2D38C-8601-43BD-AC88-165E822986C7}.Release|x64.Build.0 = Release|Any CPU {EDB2D38C-8601-43BD-AC88-165E822986C7}.Release|x86.ActiveCfg = Release|Any CPU {EDB2D38C-8601-43BD-AC88-165E822986C7}.Release|x86.Build.0 = Release|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Debug|x64.ActiveCfg = Debug|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Debug|x64.Build.0 = Debug|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Debug|x86.ActiveCfg = Debug|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Debug|x86.Build.0 = Debug|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Release|Any CPU.Build.0 = Release|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Release|x64.ActiveCfg = Release|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Release|x64.Build.0 = Release|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Release|x86.ActiveCfg = Release|Any CPU + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -111,6 +125,7 @@ Global {047E218E-A6A2-1C66-58E1-AFEF0AD34E7F} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {DFE5EC2E-21E2-42D6-B9C6-3111CE00FD0B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {0914E284-15E7-4215-B72F-7195F0EB8EEA} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {AFF26BEA-6C69-7440-9F51-7C0E8F3CDBC0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} {EDB2D38C-8601-43BD-AC88-165E822986C7} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/TALXIS.CLI.DataVisualizer/DataVisualizer.cs b/src/TALXIS.CLI.DataVisualizer/DataVisualizer.cs new file mode 100644 index 0000000..d9f9948 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/DataVisualizer.cs @@ -0,0 +1,608 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Serialization; +using DotMake.CommandLine; +using TALXIS.CLI.DataVisualizer.Extensions; +using TALXIS.CLI.DataVisualizer.Model; +using TALXIS.CLI.DataVisualizer.Translators; + +namespace TALXIS.CLI.DataVisualizer; + +[CliCommand( + Name = "visualize", + Description = "Convert the entity model to various formats such as DBML, SQL, EDMX" +)] + +public class DataVisualizer +{ + + [CliOption( + Name = "--input", + Description = "Path to the input. It can be a path to a .zip file of a build solution or a folder with declarations", + Required = true + )] + public string? InputPath { get; set; } + + [CliOption( + Name = "--target", + Description = "Target format for the conversion", + AllowedValues = new[] { "dbml", "sql", "edmx", "ribbon" }, + Required = true + )] + public string? TargetFormat { get; set; } + + [CliOption( + Name = "--output", + Description = "Path to the output file to be saved", + Required = true + )] + public string? OutputPath { get; set; } + + public int Run() + { + if (string.IsNullOrWhiteSpace(InputPath) || string.IsNullOrWhiteSpace(TargetFormat) || string.IsNullOrWhiteSpace(OutputPath)) + { + throw new ArgumentException("All options --input, --target, and --output must be specified."); + } + + if (!new[] { "dbml", "sql", "edmx", "ribbon" }.Contains(TargetFormat.ToLower())) + { + throw new ArgumentException($"Unsupported target format '{TargetFormat}'. Supported formats are: dbml, sql, edmx, ribbon."); + } + + var parsedModel = new ParsedModel(); + + // If input path is folder or file + if (Directory.Exists(InputPath)) + { + // Parse all files in the folder + parsedModel = ParseModelFolder(InputPath); + + } + else if (File.Exists(InputPath)) + { + // Get base64 encoded content from the input file + using var fileStream = new FileStream(InputPath, FileMode.Open, FileAccess.Read); + using var memoryStream = new MemoryStream(); + fileStream.CopyTo(memoryStream); + var base64Content = Convert.ToBase64String(memoryStream.ToArray()); + + parsedModel = ParseModel(base64Content); + } + else + { + throw new FileNotFoundException($"Input path '{InputPath}' does not exist."); + } + + + + var resultString = TargetFormat.ToLower() switch + { + "edmx" => ConvertToEDMX(parsedModel), + "sql" => ConvertToEDSSQL(parsedModel), + "ribbon" => ConvertToRibbonDiff(parsedModel), + _ => ConvertToDBML(parsedModel) + }; + + // Write the result to the output file + using var writer = new StreamWriter(OutputPath + "." + TargetFormat); + writer.Write(resultString); + + return 0; + } + private static string ConvertToDBML(ParsedModel model) + { + string result = string.Empty; + + foreach (Table entityText in model.tables) + { + result += entityText.ToDbDiagramNotation(); + } + foreach (Relationship relText in model.relationships) + { + result += relText.ToDbDiagramNotation(); + result += "\n"; + } + foreach (OptionsetEnum optionsetText in model.optionSets) + { + result += optionsetText.ToDbDiagramNotation(); + result += "\n"; + } + + return result; + } + + private static string ConvertToSQL(ParsedModel model) + { + string result = string.Empty; + + foreach (Table entityText in model.tables) + { + result += entityText.ToSQLNotation(model.optionSets); + } + foreach (Relationship relText in model.relationships) + { + result += relText.ToSQLNotation(); + result += "\n"; + } + + return result; + } + + private static string ConvertToEDSSQL(ParsedModel model) + { + string result = string.Empty; + + foreach (Table entityText in model.tables) + { + result += entityText.ToEDSSQLNotation(model.optionSets, model.relationships.Where(x => x.LeftSideTable.LogicalName == entityText.LogicalName || x.RighSideTable.LogicalName == entityText.LogicalName).ToList()); + } + foreach (Relationship relText in model.relationships) + { + result += relText.ToSQLNotation(); + result += "\n"; + } + + return result; + } + + private static string ConvertToRibbonDiff(ParsedModel model) + { + RibbonDiffXml result = new RibbonDiffXml(); + + var ribbondiffs = model.tables.Where(x => x.ribbonDiff != null); + + XmlSerializer xmlSerializer = new XmlSerializer(typeof(RibbonDiffXml)); + + using StringWriter textWriter = new StringWriter(); + + foreach (var table in ribbondiffs) + { + result.Merge(table.ribbonDiff); + } + + xmlSerializer.Serialize(textWriter, result); + + return textWriter.ToString(); + } + + private static string ConvertToEDMX(ParsedModel model) + { + string result = string.Empty; + result += ""; + result += ""; + result += ""; + result += ""; + result += ""; + result += ""; + result += ""; + result += ""; + foreach (Table entityText in model.tables) + { + result += entityText.ToEDMXNotation(); + + var relevantRelationships = model.relationships.Where(x => x.LeftSideTable == entityText || x.RighSideTable == entityText); + + foreach (Relationship relationship in relevantRelationships) + { + result += relationship.ToEDMXNotation(entityText); + } + + result += ""; + + } + + result += ""; + + foreach (Table entityText in model.tables) + { + var relevantRelationships = model.relationships.Where(x => x.LeftSideTable == entityText || x.RighSideTable == entityText); + + result += $"containsendswithstartswith"; + + result += ""; + + + result += ""; + + result += ""; + + result += ""; + + result += ""; + result += ""; + + + using (var reader = new StringReader(result)) + { + var edmmodel = Microsoft.OData.Edm.Csdl.CsdlReader.Parse(XmlReader.Create(reader)); + } + + + return result; + + } + + private static ParsedModel ParseModelFolder(string folderPath) + { + Module module = new(); + + // Get files named Entity.xml in subfolders + var entityFiles = Directory.GetFiles(folderPath, "Entity.xml", SearchOption.AllDirectories); + + foreach (var file in entityFiles) + { + try + { + var doc = XDocument.Load(file); + module.entities.Add(doc.Root); + + // We need to save inline optionsets and state/status optionsets + foreach (var item in doc.Root.Descendants().Where(x => x.Name == "optionset").ToList()) + { + module.optionsets.Add(item); + } + + } + catch (Exception ex) + { + Console.WriteLine($"Error loading {file}: {ex.Message}"); + } + } + + // Get files in folder Other/Relationships + var relationshipFiles = Directory.GetFiles(Path.Combine(folderPath, "Other", "Relationships"), "*.xml", SearchOption.AllDirectories); + foreach (var file in relationshipFiles) + { + try + { + var doc = XDocument.Load(file); + module.relationships.AddRange(doc.Root.Descendants().Where(x => x.Name == "EntityRelationship").ToList()); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading {file}: {ex.Message}"); + } + } + + // Get files in folder called OptionSets + var optionsetFiles = Directory.GetFiles(Path.Combine(folderPath, "OptionSets"), "*.xml", SearchOption.AllDirectories); + foreach (var file in optionsetFiles) + { + try + { + var doc = XDocument.Load(file); + module.optionsets.Add(doc.Root); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading {file}: {ex.Message}"); + } + } + + return ParseModules([module]); + + } + + private static ParsedModel ParseModel(string? base64solution) + { + + if (string.IsNullOrWhiteSpace(base64solution)) + { + throw new ArgumentException("Base64 solution content cannot be null or empty."); + } + + return ParseModel([base64solution]); + } + + private static ParsedModel ParseModel(List base64solution) + { + List modules = []; + + foreach (var solution in base64solution) + { + using ZipArchive archive = new(new MemoryStream(Convert.FromBase64String(solution))); + + var customizationsxml = archive.Entries.FirstOrDefault(x => x.FullName.Equals("customizations.xml", StringComparison.OrdinalIgnoreCase)); + var solutionxml = archive.Entries.FirstOrDefault(x => x.FullName.Equals("solution.xml", StringComparison.OrdinalIgnoreCase)); + + if (customizationsxml == null || solutionxml == null) + { + throw new FileNotFoundException("The solution archive does not contain the required customizations.xml or solution.xml files."); + } + + Module foundModule = new(XDocument.Load(solutionxml.Open()).Descendants().First(x => x.Name == "UniqueName").Value, XDocument.Load(customizationsxml.Open())); + + modules.Add(foundModule); + } + + return ParseModules(modules); + } + + private static ParsedModel ParseModules(List modules) + { + + List EntityTables = ParseEntities(modules); + List EntityOptionSets = ParseOptionSets(modules); + + // Remove optionset rows without optionsets defined + var validOptionSetNames = EntityOptionSets.Select(x => x.LocalizedName).ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var entity in EntityTables) + { + entity.Rows = [.. entity.Rows + .Where(row => + row.RowType is not (RowType.Picklist or RowType.Multiselectoptionset or RowType.State or RowType.Status or RowType.Bit) + || validOptionSetNames.Contains(row.OptionSetName) + )]; + } + + // Fill in setnames where missing with placeholder logical names + foreach (var entity in EntityTables.Where(entity => string.IsNullOrEmpty(entity.SetName))) + { + entity.SetName = entity.LogicalName; + } + + List EntityRelationships = ParseRelationships(modules, EntityTables); + + return new ParsedModel() + { + tables = EntityTables, + relationships = EntityRelationships, + optionSets = EntityOptionSets + }; + + } + + private static List ParseRelationships(List modules, List
EntityTables) + { + + List EntityRelationships = new(); + + foreach (var module in modules) + { + Console.WriteLine($"Parsing {module.ModuleName} with {module.relationships.Count} relationships"); + + foreach (var relationship in module.relationships) + { + + //Console.WriteLine($"--Relationship {relationship.Attribute("Name").Value} parsing"); + if (relationship.Element("EntityRelationshipType").Value == "ManyToMany") + { + //Console.WriteLine($"---ManyToMany"); + var firstEntityTable = EntityTables.Find(relationship.Element("FirstEntityName").Value); + if (firstEntityTable == null) + { + firstEntityTable = TableExtension.CreateTable(relationship.Element("FirstEntityName").Value, TableType.NotInSolution); + EntityTables.Add(firstEntityTable); + } + + var secondEntityTable = EntityTables.Find(relationship.Element("SecondEntityName").Value); + if (secondEntityTable == null) + { + secondEntityTable = TableExtension.CreateTable(relationship.Element("SecondEntityName").Value, TableType.NotInSolution); + EntityTables.Add(secondEntityTable); + } + + var intersectEntityName = relationship.Element("IntersectEntityName").Value; + + var connectionTable = new Table + { + Type = TableType.ConnectionTable, + LocalizedName = relationship.Attribute("Name").Value, + LogicalName = intersectEntityName, + SetName = intersectEntityName + "s", + Rows = { + new TableRow(intersectEntityName + "id", RowType.Primarykey), + new TableRow(firstEntityTable.LogicalName + "id", RowType.Lookup), + new TableRow(secondEntityTable.LogicalName + "id", RowType.Lookup), + } + }; + + + EntityTables.Add(connectionTable); + + var firstToMid = new Relationship(relationship.Attribute("Name").Value, + "ManyToOne", + firstEntityTable, + firstEntityTable.Rows.FirstOrDefault(x => x.RowType == RowType.Primarykey), + connectionTable, + connectionTable.Rows.FirstOrDefault(x => x.Name == firstEntityTable.LogicalName + "id")); + + + var secondToMid = new Relationship(relationship.Attribute("Name").Value, + "ManyToOne", + secondEntityTable, + secondEntityTable.Rows.FirstOrDefault(x => x.RowType == RowType.Primarykey), + connectionTable, + connectionTable.Rows.FirstOrDefault(x => x.Name == secondEntityTable.LogicalName + "id")); + + EntityRelationships.Add(firstToMid); + EntityRelationships.Add(secondToMid); + } + else + { + //Console.WriteLine($"---OneToMany"); + var leftSideTable = EntityTables.Find(relationship.Element("ReferencingEntityName").Value); + if (leftSideTable == null) + { + var missingEntityLogicalName = relationship.Element("ReferencingEntityName").Value; + + if (missingEntityLogicalName != "FileAttachment") + { + leftSideTable = TableExtension.CreateTable(missingEntityLogicalName, TableType.NotInSolution); + EntityTables.Add(leftSideTable); + } + } + + var rightSideTable = EntityTables.Find(relationship.Element("ReferencedEntityName").Value); + if (rightSideTable == null) + { + var missingEntityLogicalName = relationship.Element("ReferencedEntityName").Value; + + if (missingEntityLogicalName != "FileAttachment") + { + rightSideTable = TableExtension.CreateTable(missingEntityLogicalName, TableType.NotInSolution); + EntityTables.Add(rightSideTable); + } + } + + if (rightSideTable != null && leftSideTable != null) + { + var entityRelationship = new Relationship(relationship.Attribute("Name").Value, + relationship.Element("EntityRelationshipType").Value, + leftSideTable, + leftSideTable.GetOrCreateRow(relationship.Element("ReferencingAttributeName").Value, RowType.Lookup), + rightSideTable, + rightSideTable.Rows.FirstOrDefault(x => x.RowType == RowType.Primarykey)); + + if (EntityRelationships.FirstOrDefault(x => x.LeftSideTable == entityRelationship.LeftSideTable && x.RighSideTable == entityRelationship.RighSideTable) == default) + { + EntityRelationships.Add(entityRelationship); + } + } + + } + + } + + } + + foreach (var relText in EntityRelationships.Where(relText => relText.GetType().GetProperties().Any(p => p.GetValue(relText) == null))) + { + throw new Exception($"Something is missing in the {EntityRelationships.IndexOf(relText)} relationship"); + } + + return EntityRelationships; + } + + private static List ParseOptionSets(List modules) + { + List EntityOptionSets = new(); + + foreach (var module in modules) + { + Console.WriteLine($"Parsing {module.ModuleName} with {module.optionsets.Count} option sets"); + foreach (var optionsetXElement in module.optionsets) + { + + var optionsetRows = new List(); + List options = []; + switch (optionsetXElement.Element("OptionSetType")?.Value) + { + case "status": + case "state": + options = optionsetXElement.Descendants(optionsetXElement.Element("OptionSetType")?.Value).ToList(); + break; + default: + options = optionsetXElement.Descendants("option").ToList(); + break; + } + + foreach (var item in options) + { + var value = item.Attribute("value")?.Value; + var labelElement = item.Descendants("label").FirstOrDefault(x => x.Attribute("languagecode")?.Value == "1033" || x.Attribute("languagecode")?.Value == "1029"); + var label = labelElement != null ? labelElement.Attribute("description")?.Value.NormalizeString() : value; + + if (optionsetRows.Where(x => x.Value == int.Parse(value)).Count() == 0) optionsetRows.Add(new OptionsetRow(label, int.Parse(value))); + } + + if (EntityOptionSets.FirstOrDefault(x => x.LocalizedName == optionsetXElement.Attribute("Name")?.Value) != default) + { + EntityOptionSets.FirstOrDefault(x => x.LocalizedName == optionsetXElement.Attribute("Name")?.Value)?.MergeOptions(optionsetRows); + } + else + { + var optionsetEnum = new OptionsetEnum(optionsetXElement.Attribute("Name")!.Value, optionsetRows); + if (optionsetEnum.Values.Count > 0 && !EntityOptionSets.Where(x => x.LocalizedName == optionsetEnum.LocalizedName).Any()) EntityOptionSets.Add(optionsetEnum); + } + + //Console.WriteLine($"-- {optionset.Attribute("Name").Value} parsed"); + } + + } + + return EntityOptionSets; + } + + private static List
ParseEntities(List modules) + { + + var EntityTables = new List
(); + + foreach (var module in modules) + { + Console.WriteLine($"Parsing {module.ModuleName} with {module.entities.Count} entities"); + + foreach (var entityXmlElement in module.entities) + { + var entityTable = new Table(); + + if (EntityTables.FirstOrDefault(x => x.LogicalName == entityXmlElement.Element("Name")?.Value) != default) + { + entityTable = EntityTables.FirstOrDefault(x => x.LogicalName == entityXmlElement.Element("Name")?.Value); + + if (string.IsNullOrEmpty(entityTable.SetName)) + { + entityTable.SetName = entityXmlElement.Elements("EntityInfo").Elements("entity").Elements("EntitySetName").ToList().Count != 0 ? entityXmlElement.Elements("EntityInfo").Elements("entity").Elements("EntitySetName").FirstOrDefault()?.Value : string.Empty; + } + + } + else + { + entityTable = new Table(entityXmlElement) + { + ParentModule = module, + Type = TableType.InSolution + }; + EntityTables.Add(entityTable); + } + + if (entityXmlElement.Element("RibbonDiffXml") != null) + { + entityTable.ParseRibbonDiffXml(entityXmlElement.Element("RibbonDiffXml")!); + } + + var attributeXElements = entityXmlElement.Elements("EntityInfo").Elements("entity").Elements("attributes").Elements("attribute").ToList(); + + entityTable.ParseMultipleRowsFromXml(attributeXElements); + + if (!entityTable.Rows.Any(x => x.RowType == RowType.Primarykey)) + { + entityTable.Rows.Add(new TableRow(entityTable.LogicalName + "id", RowType.Primarykey)); + } + + //Console.WriteLine($"-- {entityTable.LocalizedName} parsed"); + + } + + } + + return EntityTables; + } +} diff --git a/src/TALXIS.CLI.DataVisualizer/Extensions/StringExtension.cs b/src/TALXIS.CLI.DataVisualizer/Extensions/StringExtension.cs new file mode 100644 index 0000000..2d855ee --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Extensions/StringExtension.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace TALXIS.CLI.DataVisualizer.Extensions; + +public static class StringExtension +{ + public static string FirstCharToUpper(this string input) => input switch + { + null => throw new ArgumentNullException(nameof(input)), + "" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)), + _ => string.Concat(input.First().ToString().ToUpper(), input.AsSpan(1)) + }; + + /// + /// Remove diacritics, pascal case, remove spaces and replace "-" with "_" + /// + /// + /// + public static string NormalizeString(this string text) + { + var normalizedString = text.Normalize(NormalizationForm.FormD); + var stringBuilder = new StringBuilder(); + + foreach (var c in normalizedString) + { + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + { + stringBuilder.Append(c); + } + } + + return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(stringBuilder.ToString().Normalize(NormalizationForm.FormC)).Replace(" ", "").Replace("-", "_"); + + } +} diff --git a/src/TALXIS.CLI.DataVisualizer/Extensions/TableExtension.cs b/src/TALXIS.CLI.DataVisualizer/Extensions/TableExtension.cs new file mode 100644 index 0000000..89ebbc4 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Extensions/TableExtension.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using TALXIS.CLI.DataVisualizer.Model; + +namespace TALXIS.CLI.DataVisualizer.Extensions; + +public static class TableExtension +{ + public static Table Find(this List
list, string logicalName) + { + return list.FirstOrDefault(x => x.LogicalName.Equals(logicalName, StringComparison.InvariantCultureIgnoreCase)); + } + + public static Table CreateTable(string tableName, TableType type) + { + return new Table + { + Type = type, + LocalizedName = tableName, + LogicalName = tableName, + SetName = tableName + "s", + Rows = { new TableRow(tableName + "id", RowType.Primarykey) } + }; + } +} + diff --git a/src/TALXIS.CLI.DataVisualizer/Model/DataverseToSqlTypeMapper.cs b/src/TALXIS.CLI.DataVisualizer/Model/DataverseToSqlTypeMapper.cs new file mode 100644 index 0000000..dda73d3 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/DataverseToSqlTypeMapper.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; + +namespace TALXIS.CLI.DataVisualizer.Model; + +public class DataverseToSqlTypeMapper +{ + private readonly Dictionary translationTable = new Dictionary() + { + { "nvarchar", "varchar" }, + { "lookup", "uniqueidentifier" }, + { "primarykey", "uniqueidentifier [primary key]"}, + { "partylist", "varchar"}, + { "file", "varchar" }, + { "customer", "uniqueidentifier" }, + { "ntext", "varchar" } + }; + + public string this[string key] + { + get + { + if (translationTable.ContainsKey(key)) return translationTable[key]; + return key; + } + } + +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/Module.cs b/src/TALXIS.CLI.DataVisualizer/Model/Module.cs new file mode 100644 index 0000000..d6de904 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/Module.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace TALXIS.CLI.DataVisualizer.Model; + +public class Module +{ + public Module() + { + var random = new Random(); + Colorhex = string.Format("#{0:X6}", random.Next(0x1000000)); + } + + public Module(string module, XDocument xml) + { + ModuleName = module; + XmlDoc = xml; + + var random = new Random(); + Colorhex = string.Format("#{0:X6}", random.Next(0x1000000)); + + entities = XmlDoc.Descendants().Where(x => x.Name == "Entity").ToList(); + relationships = XmlDoc.Descendants().Where(x => x.Name == "EntityRelationship").ToList(); + optionsets = XmlDoc.Descendants().Where(x => x.Name == "optionset").ToList(); + } + + public string ModuleName { get; set; } = ""; + public XDocument XmlDoc { get; set; } = new XDocument(); + + public List entities = []; + public List relationships = []; + public List optionsets = []; + + public string Colorhex { get; } +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/OptionsetEnum.cs b/src/TALXIS.CLI.DataVisualizer/Model/OptionsetEnum.cs new file mode 100644 index 0000000..59b52f6 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/OptionsetEnum.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TALXIS.CLI.DataVisualizer.Model; + +public class OptionsetEnum +{ + public string LocalizedName { get; set; } + + public List Values = []; + + public OptionsetEnum(string localizedName, List values) + { + LocalizedName = localizedName; + Values = values; + } + + public void Add(string label, int value) + { + Values.Add(new OptionsetRow(label, value)); + } + + public void MergeOptions(List options) + { + foreach (var newoption in options) + { + OptionsetRow optionsetRow = Values.FirstOrDefault(x => x.Value == newoption.Value); + if (optionsetRow == default) + { + Values.Add(newoption); + } + else + { + if (optionsetRow.Label != newoption.Label) optionsetRow.Label = $"{newoption.Label}"; + } + } + } + + public override string ToString() + { + return LocalizedName; + } + +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/OptionsetRow.cs b/src/TALXIS.CLI.DataVisualizer/Model/OptionsetRow.cs new file mode 100644 index 0000000..c0470ee --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/OptionsetRow.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TALXIS.CLI.DataVisualizer.Model; + +public class OptionsetRow +{ + public OptionsetRow(string label, int value) + { + Label = label; + Value = value; + } + + public string Label { get; set; } + public int Value { get; set; } + + +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/ParsedModel.cs b/src/TALXIS.CLI.DataVisualizer/Model/ParsedModel.cs new file mode 100644 index 0000000..908918a --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/ParsedModel.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TALXIS.CLI.DataVisualizer.Model; + +public class ParsedModel +{ + public List
tables = []; + public List relationships = []; + public List optionSets = []; + +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/Relationship.cs b/src/TALXIS.CLI.DataVisualizer/Model/Relationship.cs new file mode 100644 index 0000000..7c55175 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/Relationship.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace TALXIS.CLI.DataVisualizer.Model; + +public class Relationship +{ + private static Dictionary CardinalityLookup = new Dictionary() + { + {"OneToOne", "-"}, + {"OneToMany", ">" }, + {"ManyToOne", "<" } + }; + + public Relationship(string name, string cardinality, Table leftSideTable, TableRow leftSideRow, Table righSideTable, TableRow righSideRow) + { + Name = name; + Cardinality = cardinality; + LeftSideTable = leftSideTable; + LeftSideRow = leftSideRow; + RighSideTable = righSideTable; + RighSideRow = righSideRow; + } + + public string Name { get; set; } + public string Cardinality { get; set; } + public Table LeftSideTable { get; set; } + public TableRow LeftSideRow { get; set; } + public Table RighSideTable { get; set; } + public TableRow RighSideRow { get; set; } + + public override string ToString() + { + return Name; + } + +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/RibbonDiff.cs b/src/TALXIS.CLI.DataVisualizer/Model/RibbonDiff.cs new file mode 100644 index 0000000..a48d1cb --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/RibbonDiff.cs @@ -0,0 +1,263 @@ +using System.Xml.Serialization; +using System.Collections.Generic; +namespace TALXIS.CLI.DataVisualizer.Model; + +[XmlRoot(ElementName = "Button")] +public class Button +{ + [XmlAttribute(AttributeName = "Command")] + public string Command { get; set; } + [XmlAttribute(AttributeName = "Id")] + public string Id { get; set; } + [XmlAttribute(AttributeName = "LabelText")] + public string LabelText { get; set; } + [XmlAttribute(AttributeName = "Sequence")] + public string Sequence { get; set; } + [XmlAttribute(AttributeName = "TemplateAlias")] + public string TemplateAlias { get; set; } + [XmlAttribute(AttributeName = "ModernImage")] + public string ModernImage { get; set; } +} + +[XmlRoot(ElementName = "CommandUIDefinition")] +public class CommandUIDefinition +{ + [XmlElement(ElementName = "Button")] + public Button Button { get; set; } +} + +[XmlRoot(ElementName = "CustomAction")] +public class CustomAction +{ + [XmlElement(ElementName = "CommandUIDefinition")] + public CommandUIDefinition CommandUIDefinition { get; set; } + [XmlAttribute(AttributeName = "Id")] + public string Id { get; set; } + [XmlAttribute(AttributeName = "Location")] + public string Location { get; set; } + [XmlAttribute(AttributeName = "Sequence")] + public string Sequence { get; set; } +} + +[XmlRoot(ElementName = "CustomActions")] +public class CustomActions +{ + [XmlElement(ElementName = "CustomAction")] + public List CustomAction { get; set; } +} + +[XmlRoot(ElementName = "RibbonTemplates")] +public class RibbonTemplates +{ + [XmlAttribute(AttributeName = "Id")] + public string Id { get; set; } +} + +[XmlRoot(ElementName = "Templates")] +public class Templates +{ + [XmlElement(ElementName = "RibbonTemplates")] + public RibbonTemplates RibbonTemplates { get; set; } +} + +[XmlRoot(ElementName = "EnableRule")] +public class EnableRule +{ + [XmlAttribute(AttributeName = "Id")] + public string Id { get; set; } + [XmlElement(ElementName = "CustomRule")] + public CustomRule CustomRule { get; set; } + [XmlElement(ElementName = "SelectionCountRule")] + public SelectionCountRule SelectionCountRule { get; set; } +} + +[XmlRoot(ElementName = "EnableRules")] +public class EnableRules +{ + [XmlElement(ElementName = "EnableRule")] + public List EnableRule { get; set; } +} + +[XmlRoot(ElementName = "CrmParameter")] +public class CrmParameter +{ + [XmlAttribute(AttributeName = "Value")] + public string Value { get; set; } +} + +[XmlRoot(ElementName = "JavaScriptFunction")] +public class JavaScriptFunction +{ + [XmlElement(ElementName = "CrmParameter")] + public List CrmParameter { get; set; } + [XmlAttribute(AttributeName = "FunctionName")] + public string FunctionName { get; set; } + [XmlAttribute(AttributeName = "Library")] + public string Library { get; set; } +} + +[XmlRoot(ElementName = "Actions")] +public class Actions +{ + [XmlElement(ElementName = "JavaScriptFunction")] + public JavaScriptFunction JavaScriptFunction { get; set; } +} + +[XmlRoot(ElementName = "CommandDefinition")] +public class CommandDefinition +{ + [XmlElement(ElementName = "EnableRules")] + public EnableRules EnableRules { get; set; } + [XmlElement(ElementName = "DisplayRules")] + public string DisplayRules { get; set; } + [XmlElement(ElementName = "Actions")] + public Actions Actions { get; set; } + [XmlAttribute(AttributeName = "Id")] + public string Id { get; set; } +} + +[XmlRoot(ElementName = "CommandDefinitions")] +public class CommandDefinitions +{ + [XmlElement(ElementName = "CommandDefinition")] + public List CommandDefinition { get; set; } +} + +[XmlRoot(ElementName = "CustomRule")] +public class CustomRule +{ + [XmlElement(ElementName = "CrmParameter")] + public List CrmParameter { get; set; } + [XmlAttribute(AttributeName = "FunctionName")] + public string FunctionName { get; set; } + [XmlAttribute(AttributeName = "Library")] + public string Library { get; set; } + [XmlAttribute(AttributeName = "Default")] + public string Default { get; set; } + [XmlAttribute(AttributeName = "InvertResult")] + public string InvertResult { get; set; } +} + +[XmlRoot(ElementName = "SelectionCountRule")] +public class SelectionCountRule +{ + [XmlAttribute(AttributeName = "AppliesTo")] + public string AppliesTo { get; set; } + [XmlAttribute(AttributeName = "Minimum")] + public string Minimum { get; set; } + [XmlAttribute(AttributeName = "Maximum")] + public string Maximum { get; set; } + [XmlAttribute(AttributeName = "Default")] + public string Default { get; set; } + [XmlAttribute(AttributeName = "InvertResult")] + public string InvertResult { get; set; } +} + +[XmlRoot(ElementName = "RuleDefinitions")] +public class RuleDefinitions +{ + [XmlElement(ElementName = "TabDisplayRules")] + public string TabDisplayRules { get; set; } + [XmlElement(ElementName = "DisplayRules")] + public string DisplayRules { get; set; } + [XmlElement(ElementName = "EnableRules")] + public EnableRules EnableRules { get; set; } +} + +[XmlRoot(ElementName = "Title")] +public class Title +{ + [XmlAttribute(AttributeName = "description")] + public string Description { get; set; } + [XmlAttribute(AttributeName = "languagecode")] + public string Languagecode { get; set; } +} + +[XmlRoot(ElementName = "Titles")] +public class Titles +{ + [XmlElement(ElementName = "Title")] + public Title Title { get; set; } +} + +[XmlRoot(ElementName = "LocLabel")] +public class LocLabel +{ + [XmlElement(ElementName = "Titles")] + public Titles Titles { get; set; } + [XmlAttribute(AttributeName = "Id")] + public string Id { get; set; } +} + +[XmlRoot(ElementName = "LocLabels")] +public class LocLabels +{ + [XmlElement(ElementName = "LocLabel")] + public List LocLabel { get; set; } +} + +[XmlRoot(ElementName = "RibbonDiffXml")] +public class RibbonDiffXml +{ + [XmlElement(ElementName = "CustomActions")] + public CustomActions CustomActions { get; set; } + [XmlElement(ElementName = "Templates")] + public Templates Templates { get; set; } + [XmlElement(ElementName = "CommandDefinitions")] + public CommandDefinitions CommandDefinitions { get; set; } + [XmlElement(ElementName = "RuleDefinitions")] + public RuleDefinitions RuleDefinitions { get; set; } + [XmlElement(ElementName = "LocLabels")] + public LocLabels LocLabels { get; set; } + + public void Merge(RibbonDiffXml diff) + { + if (diff.CustomActions != null) + { + if (CustomActions != null) + { + CustomActions.CustomAction.AddRange(diff.CustomActions.CustomAction); + } + else + { + CustomActions = new CustomActions() { CustomAction = diff.CustomActions.CustomAction }; + } + } + + if (diff.CommandDefinitions != null) + { + if (CommandDefinitions != null) + { + CommandDefinitions.CommandDefinition.AddRange(diff.CommandDefinitions.CommandDefinition); + } + else + { + CommandDefinitions = new CommandDefinitions() { CommandDefinition = diff.CommandDefinitions.CommandDefinition }; + } + } + + if (diff.RuleDefinitions != null) + { + if (RuleDefinitions != null) + { + RuleDefinitions.EnableRules.EnableRule.AddRange(diff.RuleDefinitions.EnableRules.EnableRule); + } + else + { + RuleDefinitions = new RuleDefinitions() { EnableRules = new EnableRules() { EnableRule = diff.RuleDefinitions.EnableRules.EnableRule } }; + } + } + + if (diff.LocLabels != null) + { + if (LocLabels != null) + { + LocLabels.LocLabel.AddRange(diff.LocLabels.LocLabel); + } + else + { + LocLabels = new LocLabels() { LocLabel = diff.LocLabels.LocLabel }; + } + } + } +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/RowType.cs b/src/TALXIS.CLI.DataVisualizer/Model/RowType.cs new file mode 100644 index 0000000..4adcb27 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/RowType.cs @@ -0,0 +1,34 @@ +namespace TALXIS.CLI.DataVisualizer.Model; + +public enum RowType +{ + Bit, + Managedproperty, + Multiselectoptionset, + Picklist, + State, + Status, + Virtual, + Owner, + Lookup, + Date, + Datetimeoffset, + Timestamp, + Varbinary, + Image, + Primarykey, + Smallint, + Tinyint, + Int, + Long, + Bigint, + Money, + Nvarchar, + Decimal, + Float, + Ntext, + Uniqueidentifier, + File, + Partylist, + Customer +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/Table.cs b/src/TALXIS.CLI.DataVisualizer/Model/Table.cs new file mode 100644 index 0000000..a0afa3c --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/Table.cs @@ -0,0 +1,84 @@ +using DocumentFormat.OpenXml.Vml.Office; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Xml.Linq; +using System.Xml.Serialization; +using TALXIS.CLI.DataVisualizer.Extensions; + +namespace TALXIS.CLI.DataVisualizer.Model; + + +public enum TableType +{ + InSolution, + NotInSolution, + ConnectionTable +} + +public class Table +{ + public Table() { } + + public Table(XElement element) + { + LocalizedName = element.Elements("Name").FirstOrDefault(x => x.Name == "Name").Attribute("LocalizedName").Value.Replace(" ", "_").NormalizeString(); + LogicalName = element.Element("Name")?.Value; + SetName = element.Elements("EntityInfo").Elements("entity").Elements("EntitySetName").ToList().Count != 0 ? element.Elements("EntityInfo").Elements("entity").Elements("EntitySetName").FirstOrDefault().Value : string.Empty; + } + + public string LocalizedName { get; set; } + public string LogicalName { get; set; } + public string SetName { get; set; } + [JsonIgnore] + public Module ParentModule { get; set; } + public RibbonDiffXml ribbonDiff { get; set; } + public List Rows = []; + public TableType Type { get; set; } + + public TableRow GetOrCreateRow(string rowName, RowType rowType, int maxLength = 0, string optionsetname = "") + { + var row = Rows.FirstOrDefault(x => string.Compare(x.Name, rowName, true) == 0); + if (row == null) + { + var tableRow = new TableRow(rowName, rowType); + + if (maxLength > 0) tableRow.MaxLenght = maxLength; + if (!string.IsNullOrEmpty(optionsetname)) tableRow.OptionSetName = optionsetname; + + Rows.Add(tableRow); + } + return Rows.FirstOrDefault(x => string.Compare(x.Name, rowName, true) == 0); + } + + public void ParseMultipleRowsFromXml(List xElements) + { + foreach (var element in xElements) + { + Rows.Add(TableRow.ParseXElement(element)); + } + } + + public void ParseRibbonDiffXml(XElement ribbonDiffElement) + { + var serializer = new XmlSerializer(typeof(RibbonDiffXml)); + using var reader = ribbonDiffElement.CreateReader(); + var root = serializer.Deserialize(reader) as RibbonDiffXml ?? throw new InvalidOperationException("Failed to deserialize RibbonDiffXml."); + + if (ribbonDiff != null) + { + ribbonDiff.Merge(root); + } + else + { + ribbonDiff = root; + } + } + + public override string ToString() + { + return LogicalName; + } +} diff --git a/src/TALXIS.CLI.DataVisualizer/Model/TableRow.cs b/src/TALXIS.CLI.DataVisualizer/Model/TableRow.cs new file mode 100644 index 0000000..b478fa6 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Model/TableRow.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Xml.Linq; +using TALXIS.CLI.DataVisualizer.Extensions; + +namespace TALXIS.CLI.DataVisualizer.Model; + + +public class TableRow +{ + public TableRow(string name, RowType rowType) + { + Name = name; + RowType = rowType; + } + + public string Name { get; set; } + public int MaxLenght { get; set; } + public string OptionSetName { get; set; } + public RowType RowType { get; set; } + + internal static TableRow ParseXElement(XElement attribute) + { + string optionsetName = string.Empty; + RowType rowType; + int maxLength = 0; + switch (attribute.Elements("Type")?.FirstOrDefault()?.Value) + { + case "bit": + rowType = RowType.Bit; + optionsetName = attribute.Element("OptionSetName") == null ? attribute.Element("optionset") == null ? attribute.Elements("Type").FirstOrDefault().Value : attribute.Element("optionset").Attribute("Name").Value : attribute.Element("OptionSetName").Value; + break; + case "multiselectpicklist": + rowType = RowType.Multiselectoptionset; + optionsetName = attribute.Element("OptionSetName") == null ? attribute.Element("optionset") == null ? attribute.Elements("Type").FirstOrDefault().Value : attribute.Element("optionset").Attribute("Name").Value : attribute.Element("OptionSetName").Value; + break; + case "picklist": + rowType = RowType.Picklist; + optionsetName = attribute.Element("OptionSetName") == null ? attribute.Element("optionset") == null ? attribute.Elements("Type").FirstOrDefault().Value : attribute.Element("optionset").Attribute("Name").Value : attribute.Element("OptionSetName").Value; + break; + case "state": + rowType = RowType.State; + optionsetName = attribute.Element("OptionSetName") == null ? attribute.Element("optionset") == null ? attribute.Elements("Type").FirstOrDefault().Value : attribute.Element("optionset").Attribute("Name").Value : attribute.Element("OptionSetName").Value; + break; + case "status": + rowType = RowType.Status; + optionsetName = attribute.Element("OptionSetName") == null ? attribute.Element("optionset") == null ? attribute.Elements("Type").FirstOrDefault().Value : attribute.Element("optionset").Attribute("Name").Value : attribute.Element("OptionSetName").Value; + break; + case "virtual": //in case of default entities + rowType = RowType.Virtual; + break; + case "datetime": + if (attribute.Elements("Behavior").FirstOrDefault()?.Value == "2" || attribute.Elements("Behavior").FirstOrDefault()?.Value.ToLower() == "dateonly") + { + rowType = RowType.Date; + } + else + { + rowType = RowType.Datetimeoffset; + } + break; + case "primarykey": + rowType = RowType.Primarykey; + break; + case "lookup": + rowType = RowType.Lookup; + break; + case "uniqueidentifier": + rowType = RowType.Uniqueidentifier; + break; + case "owner": + rowType = RowType.Owner; + break; + case "nvarchar": + rowType = RowType.Nvarchar; + + if (attribute.Elements("MaxLength").FirstOrDefault() != default) + { + maxLength = int.Parse(attribute.Elements("MaxLength").FirstOrDefault()?.Value ?? "0"); + } + else if (attribute.Elements("Length").FirstOrDefault() != default) + { + maxLength = int.Parse(attribute.Elements("Length").FirstOrDefault()?.Value ?? "0"); + } + + break; + case "ntext": + rowType = RowType.Ntext; + if (attribute.Elements("MaxLength").FirstOrDefault() != default) + { + maxLength = int.Parse(attribute.Elements("MaxLength").FirstOrDefault()?.Value ?? "0"); + } + break; + default: + rowType = Enum.Parse(attribute.Elements("Type")?.FirstOrDefault()?.Value.FirstCharToUpper()); + break; + + } + + return new TableRow(attribute.Attribute("PhysicalName").Value.ToLower(), rowType) + { + MaxLenght = maxLength, + OptionSetName = optionsetName + }; + + } + + public override string ToString() + { + return Name; + } +} diff --git a/src/TALXIS.CLI.DataVisualizer/TALXIS.CLI.DataVisualizer.csproj b/src/TALXIS.CLI.DataVisualizer/TALXIS.CLI.DataVisualizer.csproj new file mode 100644 index 0000000..ef99175 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/TALXIS.CLI.DataVisualizer.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + diff --git a/src/TALXIS.CLI.DataVisualizer/Translators/DBDiagramTranslator.cs b/src/TALXIS.CLI.DataVisualizer/Translators/DBDiagramTranslator.cs new file mode 100644 index 0000000..f5e00ee --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Translators/DBDiagramTranslator.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Text; +using TALXIS.CLI.DataVisualizer.Model; + +namespace TALXIS.CLI.DataVisualizer.Translators; + +public static class DBDiagramTranslator +{ + + public static Dictionary CardinalityLookup = new Dictionary() + { + {"OneToOne", "-"}, + {"OneToMany", ">" }, + {"ManyToOne", "<" } + }; + + public static string ToDbDiagramNotation(this Table table) + { + var result = $"\ntable {table.LogicalName} "; + + switch (table.Type) + { + case TableType.InSolution: + result += $"[headercolor: {table.ParentModule?.Colorhex}] //{table.ParentModule?.ModuleName} \n"; + break; + case TableType.NotInSolution: + result += "[headercolor: #c0392b] "; + break; + case TableType.ConnectionTable: + result += "[headercolor: #27ae60] "; + break; + default: + break; + } + + result += "{\n"; + + foreach (var row in table.Rows) + { + result += row.ToDbDiagramNotation(); + } + + result += "}"; + + return result; + + } + + public static string ToDbDiagramNotation(this TableRow row) + { + return $" {row.Name} {row.RowType} \n"; + + + } + + public static string ToDbDiagramNotation(this Relationship relationship) + { + return $"\nRef: \"{relationship.LeftSideTable?.LogicalName}\".\"{relationship.LeftSideRow?.Name}\" {CardinalityLookup[relationship.Cardinality]} \"{relationship.RighSideTable?.LogicalName}\".\"{relationship.RighSideRow?.Name}\""; + + } + + public static string ToDbDiagramNotation(this OptionsetRow row) + { + return $"\"{row.Value}\" [note:'{row.Label}']"; + } + + public static string ToDbDiagramNotation(this OptionsetEnum optionset) + { + var result = $"\nEnum {optionset.LocalizedName} {{"; + + foreach (var value in optionset.Values) + { + result += $"\n {value.ToDbDiagramNotation()}"; + } + + result += "\n}"; + + return result; + + } +} + diff --git a/src/TALXIS.CLI.DataVisualizer/Translators/EDMXTranslator.cs b/src/TALXIS.CLI.DataVisualizer/Translators/EDMXTranslator.cs new file mode 100644 index 0000000..77cba25 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Translators/EDMXTranslator.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TALXIS.CLI.DataVisualizer.Model; + +namespace TALXIS.CLI.DataVisualizer.Translators; + +public static class EDMXTranslator +{ + public static string ToEDMXNotation(this Table table) + { + var result = $" x.RowType == RowType.Primarykey); + + if (primaryKey.Name == "activityid") + { + result += " BaseType =\"mscrm.activitypointer\">"; + } + else + { + result += " BaseType =\"mscrm.crmbaseentity\">"; + } + + if (primaryKey != default) + { + result += $""; + } + + foreach (var row in table.Rows) + { + result += row.ToEDMXNotation(); + } + + return result; + + } + + public static string ToEDMXNotation(this TableRow row) + { + + string result = ""; + break; + case RowType.Bit: + result += " Type=\"Edm.Boolean\"/>"; + break; + case RowType.Managedproperty: + result += " Type = \"mscrm.BooleanManagedProperty\" />"; + break; + case RowType.Smallint: + case RowType.Tinyint: + case RowType.Int: + case RowType.State: + case RowType.Status: + case RowType.Picklist: + result += " Type=\"Edm.Int32\"/>"; + break; + case RowType.Lookup: + case RowType.Owner: + result = ""; + break; + case RowType.Customer: + // Solve + case RowType.Nvarchar: + case RowType.Ntext: + result += " Type=\"Edm.String\" Unicode=\"false\"/>"; + break; + case RowType.File: + return $" \n "; + case RowType.Virtual: + return string.Empty; + case RowType.Datetimeoffset: + result += " Type=\"Edm.DateTimeOffset\"/>"; + break; + case RowType.Date: + result += " Type=\"Edm.Date\"/>"; + break; + case RowType.Primarykey: + case RowType.Uniqueidentifier: + result += " Type=\"Edm.Guid\"/>"; + break; + case RowType.Money: + case RowType.Decimal: + result += " Type=\"Edm.Decimal\" Scale=\"Variable\"/>"; + break; + case RowType.Float: + result += " Type=\"Edm.Double\"/>"; + break; + case RowType.Long: + case RowType.Timestamp: + case RowType.Bigint: + result += " Type=\"Edm.Int64\"/>"; + break; + case RowType.Varbinary: + case RowType.Image: + result += " Type=\"Edm.Binary\"/>"; + break; + default: + break; + } + + return string.Format(result, row.Name.ToLower()); + } + + public static string ToEDMXNotation(this Relationship relationship, Table table) + { + if (relationship.LeftSideTable == table) + { + return $""; + } + else + { + return $""; + } + } + + public static string ToEDMXNotationBinding(this Relationship relationship, Table table) + { + + if (relationship.LeftSideTable == table) + { + return $""; + } + else + { + return $""; + } + + } +} diff --git a/src/TALXIS.CLI.DataVisualizer/Translators/SQLTranslator.cs b/src/TALXIS.CLI.DataVisualizer/Translators/SQLTranslator.cs new file mode 100644 index 0000000..489bb2c --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/Translators/SQLTranslator.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using TALXIS.CLI.DataVisualizer.Model; + +namespace TALXIS.CLI.DataVisualizer.Translators; + +public static class SQLTranslator +{ + private static Dictionary translationTable = new Dictionary() + { + { RowType.Primarykey, "uniqueidentifier PRIMARY KEY"}, + { RowType.Partylist, "nvarchar (1000)"}, + }; + + + public static string TranslateToSql(RowType key) + { + if (translationTable.ContainsKey(key)) return translationTable[key]; + return key.ToString().ToLower(); + } + + public static string ToSQLNotation(this Table table, List optionsets) + { + string result = string.IsNullOrEmpty(table.SetName) ? $"\nCREATE TABLE [{table.LogicalName}] (\n" : $"\nCREATE TABLE [{table.SetName}] (\n"; + + List rows = []; + foreach (var row in table.Rows) + { + rows.Add(row.ToSQLNotation(optionsets)); + } + + result += string.Join(",\n", rows.Where(x => !string.IsNullOrEmpty(x))); + + result += "\n)\nGO\n"; + + return result; + + } + + public static string ToEDSSQLNotation(this Table table, List optionsets, List relationships) + { + string result = string.IsNullOrEmpty(table.SetName) ? $"\nCREATE TABLE [{table.LogicalName}] (\n" : $"\nCREATE TABLE [{table.SetName}] (\n"; + + List rows = []; + foreach (var row in table.Rows) + { + rows.Add(row.ToEDSSQLNotation(optionsets, relationships)); + } + + result += string.Join(",\n", rows.Where(x => !string.IsNullOrEmpty(x))); + + result += "\n)\nGO\n"; + + return result; + + } + + public static string ToSQLNotation(this Relationship relationship) + { + + var cardinality = DBDiagramTranslator.CardinalityLookup[relationship.Cardinality]; + var result = string.Empty; + + if (relationship.RighSideRow.RowType == RowType.Primarykey && relationship.LeftSideRow.RowType == RowType.Primarykey) return string.Empty; + + if (cardinality == ">") + { + + if (relationship.LeftSideRow.RowType == RowType.Customer) + { + result += $"\nALTER TABLE [{(string.IsNullOrEmpty(relationship.LeftSideTable?.SetName) ? relationship.LeftSideTable?.LogicalName : relationship.LeftSideTable?.SetName)}] ADD FOREIGN KEY ([_{relationship.LeftSideRow?.Name}_{relationship.RighSideTable.LogicalName.ToLower()}]) REFERENCES [{(string.IsNullOrEmpty(relationship.RighSideTable?.SetName) ? relationship.RighSideTable?.LogicalName : relationship.RighSideTable?.SetName)}] ([{relationship.RighSideRow?.Name}])"; + } + else + { + result += $"\nALTER TABLE [{(string.IsNullOrEmpty(relationship.LeftSideTable?.SetName) ? relationship.LeftSideTable?.LogicalName : relationship.LeftSideTable?.SetName)}] ADD FOREIGN KEY ([_{relationship.LeftSideRow?.Name}_value]) REFERENCES [{(string.IsNullOrEmpty(relationship.RighSideTable?.SetName) ? relationship.RighSideTable?.LogicalName : relationship.RighSideTable?.SetName)}] ([{relationship.RighSideRow?.Name}])"; + } + + } + + if (cardinality == "<") + { + if (relationship.RighSideRow.RowType == RowType.Customer) + { + result += $"\nALTER TABLE [{(string.IsNullOrEmpty(relationship.LeftSideTable?.SetName) ? relationship.LeftSideTable?.LogicalName : relationship.LeftSideTable?.SetName)}] ADD FOREIGN KEY ([_{relationship.RighSideRow?.Name}_{relationship.LeftSideTable.LogicalName.ToLower()}]) REFERENCES [{(string.IsNullOrEmpty(relationship.RighSideTable?.SetName) ? relationship.RighSideTable?.LogicalName : relationship.RighSideTable?.SetName)}] ([{relationship.RighSideRow?.Name}])"; + } + + else + { + result += $"\nALTER TABLE [{(string.IsNullOrEmpty(relationship.RighSideTable?.SetName) ? relationship.RighSideTable?.LogicalName : relationship.RighSideTable?.SetName)}] ADD FOREIGN KEY ([_{relationship.RighSideRow?.Name}_value]) REFERENCES [{(string.IsNullOrEmpty(relationship.LeftSideTable?.SetName) ? relationship.LeftSideTable?.LogicalName : relationship.LeftSideTable?.SetName)}] ([{relationship.LeftSideRow?.Name}])"; + + } + } + + result += "\nGO\n"; + + return result; + } + + public static string ToEDSSQLNotation(this TableRow row, List optionsets, List relationships) + { + switch (row.RowType) + { + case RowType.Multiselectoptionset: + return $" [{row.Name}] nvarchar(255)"; + case RowType.Bit: + return $" [{row.Name}] bit"; + case RowType.State: + case RowType.Status: + case RowType.Picklist: + var relevantPicklist = optionsets.First(x => x.LocalizedName == row.RowType.ToString()); + return $" [{row.Name}] nvarchar(255) CHECK ([{row.Name}] IN ({string.Join(',', relevantPicklist.Values.Select(x => "'" + x.Value + "'"))}))"; + case RowType.Managedproperty: + return $" [{row.Name}] nvarchar(255) CHECK ([{row.Name}] IN ('0','1'))"; + case RowType.Customer: + var result = $" [_{row.Name}_value] nvarchar(255)"; + + foreach (var item in relationships.Where(x => x.LeftSideRow.Name == row.Name)) + { + result += $",\n [_{row.Name}_{item.RighSideTable.LogicalName.ToLower()}] uniqueidentifier"; + } + + return result; + case RowType.Lookup: + case RowType.Owner: + case RowType.Uniqueidentifier: + return $" [_{row.Name}_value] uniqueidentifier"; + case RowType.Nvarchar: + case RowType.Ntext: + return $" [{row.Name}] nvarchar({(row.MaxLenght > 4000 ? "max" : row.MaxLenght.ToString())})"; + case RowType.File: + return $" [{row.Name}] uniqueidentifier,\n [{row.Name}_name] nvarchar (max)"; + case RowType.Virtual: + case RowType.Varbinary: + case RowType.Timestamp: + return string.Empty; + case RowType.Long: + case RowType.Int: + case RowType.Smallint: + case RowType.Tinyint: + return $" [{row.Name}] int"; + case RowType.Date: + return $" [{row.Name}] datetime"; + case RowType.Datetimeoffset: + return $" [{row.Name}] datetimeoffset"; + case RowType.Money: + case RowType.Decimal: + return $" [{row.Name}] decimal"; + case RowType.Primarykey: + case RowType.Float: + case RowType.Partylist: + default: + return $" [{row.Name}] {TranslateToSql(row.RowType)}"; + } + + } + + public static string ToSQLNotation(this TableRow row, List optionsets) + { + switch (row.RowType) + { + case RowType.Multiselectoptionset: + return $" [{row.Name}] nvarchar(255)"; + case RowType.Bit: + return $" [{row.Name}] bit"; + case RowType.State: + case RowType.Status: + case RowType.Picklist: + var relevantPicklist = optionsets.First(x => x.LocalizedName == row.RowType.ToString()); + return $" [{row.Name}] nvarchar(255) CHECK ([{row.Name}] IN ({string.Join(',', relevantPicklist.Values.Select(x => "'" + x.Value + "'"))}))"; + case RowType.Managedproperty: + return $" [{row.Name}] nvarchar(255) CHECK ([{row.Name}] IN ('0','1'))"; + case RowType.Lookup: + case RowType.Owner: + case RowType.Customer: + return $" [{row.Name}] uniqueidentifier"; + case RowType.Nvarchar: + case RowType.Ntext: + return $" [{row.Name}] nvarchar({(row.MaxLenght > 4000 ? "max" : row.MaxLenght.ToString())})"; + case RowType.File: + return $" [{row.Name}] uniqueidentifier,\n [{row.Name}_name] nvarchar (max)"; + case RowType.Virtual: + case RowType.Varbinary: + case RowType.Timestamp: + return string.Empty; + case RowType.Long: + case RowType.Int: + case RowType.Smallint: + case RowType.Tinyint: + return $" [{row.Name}] int"; + case RowType.Date: + return $" [{row.Name}] datetime"; + case RowType.Datetimeoffset: + return $" [{row.Name}] datetimeoffset"; + case RowType.Money: + case RowType.Decimal: + return $" [{row.Name}] decimal"; + case RowType.Primarykey: + case RowType.Float: + case RowType.Uniqueidentifier: + case RowType.Partylist: + default: + return $" [{row.Name}] {TranslateToSql(row.RowType)}"; + } + + } + +} diff --git a/src/TALXIS.CLI.DataVisualizer/XMLSchemas/OptionSetXmlSchema.cs b/src/TALXIS.CLI.DataVisualizer/XMLSchemas/OptionSetXmlSchema.cs new file mode 100644 index 0000000..ee225e8 --- /dev/null +++ b/src/TALXIS.CLI.DataVisualizer/XMLSchemas/OptionSetXmlSchema.cs @@ -0,0 +1,324 @@ + +// NOTE: Generated code may require at least .NET Framework 4.5 or .NET Core/Standard 2.0. +using System.Xml.Linq; + +using System.Xml.Serialization; + +/// +[System.SerializableAttribute()] +[System.ComponentModel.DesignerCategoryAttribute("code")] +[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] +[System.Xml.Serialization.XmlRootAttribute(Namespace = "", IsNullable = false)] +public partial class Optionset +{ + + public static Optionset ParseOptionsetFromXElement(XElement element) + { + var serializer = new XmlSerializer(typeof(Optionset)); + using var reader = element.CreateReader(); + if (serializer.Deserialize(reader) is not Optionset result) + throw new InvalidOperationException("Failed to deserialize Optionset from XElement."); + return result; + } + + private string optionSetTypeField; + + private byte isGlobalField; + + private string introducedVersionField; + + private byte isCustomizableField; + + private optionsetDisplayname[] displaynamesField; + + private optionsetDescription[] descriptionsField; + + private optionsetOption[] optionsField; + + private string nameField; + + private string localizedNameField; + + /// + public string OptionSetType + { + get + { + return this.optionSetTypeField; + } + set + { + this.optionSetTypeField = value; + } + } + + /// + public byte IsGlobal + { + get + { + return this.isGlobalField; + } + set + { + this.isGlobalField = value; + } + } + + /// + public string IntroducedVersion + { + get + { + return this.introducedVersionField; + } + set + { + this.introducedVersionField = value; + } + } + + /// + public byte IsCustomizable + { + get + { + return this.isCustomizableField; + } + set + { + this.isCustomizableField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("displayname", IsNullable = false)] + public optionsetDisplayname[] displaynames + { + get + { + return this.displaynamesField; + } + set + { + this.displaynamesField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("Description", IsNullable = false)] + public optionsetDescription[] Descriptions + { + get + { + return this.descriptionsField; + } + set + { + this.descriptionsField = value; + } + } + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("option", IsNullable = false)] + public optionsetOption[] options + { + get + { + return this.optionsField; + } + set + { + this.optionsField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Name + { + get + { + return this.nameField; + } + set + { + this.nameField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string localizedName + { + get + { + return this.localizedNameField; + } + set + { + this.localizedNameField = value; + } + } +} + +/// +[System.SerializableAttribute()] +[System.ComponentModel.DesignerCategoryAttribute("code")] +[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] +public partial class optionsetDisplayname +{ + + private string descriptionField; + + private ushort languagecodeField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string description + { + get + { + return this.descriptionField; + } + set + { + this.descriptionField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public ushort languagecode + { + get + { + return this.languagecodeField; + } + set + { + this.languagecodeField = value; + } + } +} + +/// +[System.SerializableAttribute()] +[System.ComponentModel.DesignerCategoryAttribute("code")] +[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] +public partial class optionsetDescription +{ + + private string descriptionField; + + private ushort languagecodeField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string description + { + get + { + return this.descriptionField; + } + set + { + this.descriptionField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public ushort languagecode + { + get + { + return this.languagecodeField; + } + set + { + this.languagecodeField = value; + } + } +} + +/// +[System.SerializableAttribute()] +[System.ComponentModel.DesignerCategoryAttribute("code")] +[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] +public partial class optionsetOption +{ + + private optionsetOptionLabel[] labelsField; + + private uint valueField; + + /// + [System.Xml.Serialization.XmlArrayItemAttribute("label", IsNullable = false)] + public optionsetOptionLabel[] labels + { + get + { + return this.labelsField; + } + set + { + this.labelsField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public uint value + { + get + { + return this.valueField; + } + set + { + this.valueField = value; + } + } +} + +/// +[System.SerializableAttribute()] +[System.ComponentModel.DesignerCategoryAttribute("code")] +[System.Xml.Serialization.XmlTypeAttribute(AnonymousType = true)] +public partial class optionsetOptionLabel +{ + + private string descriptionField; + + private ushort languagecodeField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string description + { + get + { + return this.descriptionField; + } + set + { + this.descriptionField = value; + } + } + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public ushort languagecode + { + get + { + return this.languagecodeField; + } + set + { + this.languagecodeField = value; + } + } +} + diff --git a/src/TALXIS.CLI/TALXIS.CLI.csproj b/src/TALXIS.CLI/TALXIS.CLI.csproj index b1da299..111ad18 100644 --- a/src/TALXIS.CLI/TALXIS.CLI.csproj +++ b/src/TALXIS.CLI/TALXIS.CLI.csproj @@ -1,39 +1,40 @@  - - - + + + - - - - - + + + + + + - - - + + + - - Exe - net9.0 - enable - enable - - true - txc - TALXIS.CLI - TALXIS CLI (txc) - NETWORG - TALXIS CLI - TALXIS CLI (txc) is a .NET global tool for developer automation and Power Platform scripting. - cli;dotnet-tool;talxis;automation;powerplatform;txc - https://github.com/TALXIS/tools-cli - MIT - README.md - false - true - snupkg - © 2025 NETWORG Corporation - MIT License - + + Exe + net9.0 + enable + enable + + true + txc + TALXIS.CLI + TALXIS CLI (txc) + NETWORG + TALXIS CLI + TALXIS CLI (txc) is a .NET global tool for developer automation and Power Platform scripting. + cli;dotnet-tool;talxis;automation;powerplatform;txc + https://github.com/TALXIS/tools-cli + MIT + README.md + false + true + snupkg + © 2025 NETWORG Corporation - MIT License + diff --git a/src/TALXIS.CLI/TxcCliCommand.cs b/src/TALXIS.CLI/TxcCliCommand.cs index 651a92e..f02d87f 100644 --- a/src/TALXIS.CLI/TxcCliCommand.cs +++ b/src/TALXIS.CLI/TxcCliCommand.cs @@ -3,8 +3,7 @@ namespace TALXIS.CLI; [CliCommand( - Description = "Tool for automating development loops in Power Platform", - Children = new[] { typeof(Data.DataCliCommand), typeof(Workspace.WorkspaceCliCommand) }, + Children = new[] { typeof(Data.DataCliCommand), typeof(Docs.DocsCliCommand), typeof(DataVisualizer.DataVisualizer), typeof(Workspace.WorkspaceCliCommand) }, ShortFormAutoGenerate = CliNameAutoGenerate.None )] public class TxcCliCommand