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 += $"";
+ }
+ else // populate relationships in EntitySet
+ {
+ result += ">";
+
+ foreach (Relationship relationship in relevantRelationships)
+ {
+ result += relationship.ToEDMXNotationBinding(entityText);
+ }
+
+ result += "";
+ }
+ }
+
+ 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