Skip to content

Commit 305c3ca

Browse files
TomProkopCopilot
andauthored
feat: add txc data model convert command (#7)
- Rename DataVisualizer → DataModelConverter, move into TALXIS.CLI.Data project - Command: txc data model convert --input --target --output - Smart input resolution: project folder (.csproj/.cdsproj + SolutionRootPath), declarations folder, or .zip - Fix bug: Relationships dir may not exist → add Directory.Exists() guard - Fix bug: TableRow.ParseXElement returns null for unknown/missing <Type> - Fix bug: SQLTranslator optionset lookup used RowType name instead of OptionSetName - Fix DBML output: column types now reference enum names for dbdiagram.io linking - Fix MCP SDK v1.x breaking change: IMcpClient removed → use McpClient directly - Add integration tests: TempWorkspaceFixture + 8 DataModelConvertTests - Bring in master-only files: ProjectCliCommand, ConfigCliCommand, updated ComponentParameterListCliCommand - Remove SolutionCliCommand from Workspace (moved to Data as DataModelConvertCliCommand) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1fe92f5 commit 305c3ca

29 files changed

Lines changed: 2556 additions & 67 deletions

src/TALXIS.CLI.Data/DataModelCliCommand.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ namespace TALXIS.CLI.Data;
44

55
[CliCommand(
66
Name = "model",
7-
Description = "Data modeling utilities"
7+
Description = "Data modeling utilities",
8+
Children = new[] { typeof(DataModelConvertCliCommand) }
89
)]
910
public class DataModelCliCommand
1011
{
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using DotMake.CommandLine;
2+
using TALXIS.CLI.Data.DataModelConverter;
3+
4+
namespace TALXIS.CLI.Data;
5+
6+
[CliCommand(
7+
Name = "convert",
8+
Description = "Convert a Power Platform solution data model to various formats such as DBML, SQL, EDMX or Ribbon"
9+
)]
10+
public class DataModelConvertCliCommand
11+
{
12+
private const string ExportsFolderName = "exports";
13+
14+
[CliOption(
15+
Name = "--input",
16+
Description = "Path to the input: a solution project folder (.cdsproj/.csproj with SolutionRootPath), a declarations folder, or a .zip solution file. Defaults to the current directory.",
17+
Required = false
18+
)]
19+
public string? InputPath { get; set; }
20+
21+
[CliOption(
22+
Name = "--target",
23+
Description = "Target format for the conversion.",
24+
AllowedValues = new[] { "dbml", "sql", "plainsql", "edmx", "ribbon" },
25+
Required = true
26+
)]
27+
public string? TargetFormat { get; set; }
28+
29+
[CliOption(
30+
Name = "--output",
31+
Description = $"Directory to write the output file into. Defaults to the '{ExportsFolderName}/' folder in the current directory (auto-created and gitignored).",
32+
Required = false
33+
)]
34+
public string? OutputDirectory { get; set; }
35+
36+
public int Run()
37+
{
38+
var inputPath = InputPath ?? Directory.GetCurrentDirectory();
39+
var outputDir = OutputDirectory ?? Path.Combine(Directory.GetCurrentDirectory(), ExportsFolderName);
40+
41+
Directory.CreateDirectory(outputDir);
42+
EnsureGitIgnored(outputDir);
43+
44+
var extension = TargetFormat!.ToLower() == "plainsql" ? "sql" : TargetFormat.ToLower();
45+
var outputFilePath = Path.Combine(outputDir, $"solution.{extension}");
46+
47+
DataModelConverterService.ConvertModel(inputPath, TargetFormat!, outputFilePath);
48+
49+
Console.WriteLine($"Output written to: {outputFilePath}");
50+
return 0;
51+
}
52+
53+
/// <summary>
54+
/// Ensures the exports folder is listed in the nearest .gitignore,
55+
/// adding an entry if it is not already present.
56+
/// </summary>
57+
private static void EnsureGitIgnored(string exportsDirPath)
58+
{
59+
var gitIgnorePath = FindGitIgnore(exportsDirPath);
60+
if (gitIgnorePath == null)
61+
return;
62+
63+
var entry = $"{ExportsFolderName}/";
64+
var lines = File.ReadAllLines(gitIgnorePath);
65+
if (lines.Any(l => l.Trim() == entry))
66+
return;
67+
68+
File.AppendAllText(gitIgnorePath, $"{Environment.NewLine}{entry}{Environment.NewLine}");
69+
}
70+
71+
private static string? FindGitIgnore(string startPath)
72+
{
73+
var dir = new DirectoryInfo(startPath);
74+
while (dir != null)
75+
{
76+
var candidate = Path.Combine(dir.FullName, ".gitignore");
77+
if (File.Exists(candidate))
78+
return candidate;
79+
80+
// Stop at the git root
81+
if (Directory.Exists(Path.Combine(dir.FullName, ".git")))
82+
break;
83+
84+
dir = dir.Parent;
85+
}
86+
return null;
87+
}
88+
}

src/TALXIS.CLI.Data/DataModelConverter/DataModelConverterService.cs

Lines changed: 617 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.Linq;
5+
using System.Text;
6+
7+
namespace TALXIS.CLI.Data.DataModelConverter.Extensions;
8+
9+
public static class StringExtension
10+
{
11+
public static string FirstCharToUpper(this string input) => input switch
12+
{
13+
null => throw new ArgumentNullException(nameof(input)),
14+
"" => throw new ArgumentException($"{nameof(input)} cannot be empty", nameof(input)),
15+
_ => string.Concat(input.First().ToString().ToUpper(), input.AsSpan(1))
16+
};
17+
18+
/// <summary>
19+
/// Remove diacritics, pascal case, remove spaces and replace "-" with "_"
20+
/// </summary>
21+
/// <param name="text"></param>
22+
/// <returns></returns>
23+
public static string NormalizeString(this string text)
24+
{
25+
var normalizedString = text.Normalize(NormalizationForm.FormD);
26+
var stringBuilder = new StringBuilder();
27+
28+
foreach (var c in normalizedString)
29+
{
30+
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
31+
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
32+
{
33+
stringBuilder.Append(c);
34+
}
35+
}
36+
37+
return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(stringBuilder.ToString().Normalize(NormalizationForm.FormC)).Replace(" ", "").Replace("-", "_");
38+
39+
}
40+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using TALXIS.CLI.Data.DataModelConverter.Model;
5+
6+
namespace TALXIS.CLI.Data.DataModelConverter.Extensions;
7+
8+
public static class TableExtension
9+
{
10+
public static Table Find(this List<Table> list, string logicalName)
11+
{
12+
return list.FirstOrDefault(x => x.LogicalName.Equals(logicalName, StringComparison.InvariantCultureIgnoreCase));
13+
}
14+
15+
public static Table CreateTable(string tableName, TableType type)
16+
{
17+
return new Table
18+
{
19+
Type = type,
20+
LocalizedName = tableName,
21+
LogicalName = tableName,
22+
SetName = tableName + "s",
23+
Rows = { new TableRow(tableName + "id", RowType.Primarykey) }
24+
};
25+
}
26+
}
27+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Collections.Generic;
2+
3+
namespace TALXIS.CLI.Data.DataModelConverter.Model;
4+
5+
public class DataverseToSqlTypeMapper
6+
{
7+
private readonly Dictionary<string, string> translationTable = new Dictionary<string, string>()
8+
{
9+
{ "nvarchar", "varchar" },
10+
{ "lookup", "uniqueidentifier" },
11+
{ "primarykey", "uniqueidentifier [primary key]"},
12+
{ "partylist", "varchar"},
13+
{ "file", "varchar" },
14+
{ "customer", "uniqueidentifier" },
15+
{ "ntext", "varchar" }
16+
};
17+
18+
public string this[string key]
19+
{
20+
get
21+
{
22+
if (translationTable.ContainsKey(key)) return translationTable[key];
23+
return key;
24+
}
25+
}
26+
27+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using System.Xml.Linq;
7+
8+
namespace TALXIS.CLI.Data.DataModelConverter.Model;
9+
10+
public class Module
11+
{
12+
public Module()
13+
{
14+
var random = new Random();
15+
Colorhex = string.Format("#{0:X6}", random.Next(0x1000000));
16+
}
17+
18+
public Module(string module, XDocument xml)
19+
{
20+
ModuleName = module;
21+
XmlDoc = xml;
22+
23+
var random = new Random();
24+
Colorhex = string.Format("#{0:X6}", random.Next(0x1000000));
25+
26+
entities = XmlDoc.Descendants().Where(x => x.Name == "Entity").ToList();
27+
relationships = XmlDoc.Descendants().Where(x => x.Name == "EntityRelationship").ToList();
28+
optionsets = XmlDoc.Descendants().Where(x => x.Name == "optionset").ToList();
29+
}
30+
31+
public string ModuleName { get; set; } = "";
32+
public XDocument XmlDoc { get; set; } = new XDocument();
33+
34+
public List<XElement> entities = [];
35+
public List<XElement> relationships = [];
36+
public List<XElement> optionsets = [];
37+
38+
public string Colorhex { get; }
39+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace TALXIS.CLI.Data.DataModelConverter.Model;
8+
9+
public class OptionsetEnum
10+
{
11+
public string LocalizedName { get; set; }
12+
13+
public List<OptionsetRow> Values = [];
14+
15+
public OptionsetEnum(string localizedName, List<OptionsetRow> values)
16+
{
17+
LocalizedName = localizedName;
18+
Values = values;
19+
}
20+
21+
public void Add(string label, int value)
22+
{
23+
Values.Add(new OptionsetRow(label, value));
24+
}
25+
26+
public void MergeOptions(List<OptionsetRow> options)
27+
{
28+
foreach (var newoption in options)
29+
{
30+
OptionsetRow optionsetRow = Values.FirstOrDefault(x => x.Value == newoption.Value);
31+
if (optionsetRow == default)
32+
{
33+
Values.Add(newoption);
34+
}
35+
else
36+
{
37+
if (optionsetRow.Label != newoption.Label) optionsetRow.Label = $"{newoption.Label}";
38+
}
39+
}
40+
}
41+
42+
public override string ToString()
43+
{
44+
return LocalizedName;
45+
}
46+
47+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace TALXIS.CLI.Data.DataModelConverter.Model;
6+
7+
public class OptionsetRow
8+
{
9+
public OptionsetRow(string label, int value)
10+
{
11+
Label = label;
12+
Value = value;
13+
}
14+
15+
public string Label { get; set; }
16+
public int Value { get; set; }
17+
18+
19+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
5+
namespace TALXIS.CLI.Data.DataModelConverter.Model;
6+
7+
public class ParsedModel
8+
{
9+
public List<Table> tables = [];
10+
public List<Relationship> relationships = [];
11+
public List<OptionsetEnum> optionSets = [];
12+
13+
}

0 commit comments

Comments
 (0)