diff --git a/Foundation.Data.Doublets.Cli.Tests/LinoGraphQLProcessorTests.cs b/Foundation.Data.Doublets.Cli.Tests/LinoGraphQLProcessorTests.cs new file mode 100644 index 0000000..a4e1cc2 --- /dev/null +++ b/Foundation.Data.Doublets.Cli.Tests/LinoGraphQLProcessorTests.cs @@ -0,0 +1,145 @@ +using Xunit; +using Foundation.Data.Doublets.Cli; + +namespace Foundation.Data.Doublets.Cli.Tests +{ + public class LinoGraphQLProcessorTests + { + [Fact] + public void ProcessLinoGraphQLQuery_WithSimpleLinksQuery_ReturnsExpectedFormat() + { + // Arrange + var tempDb = Path.GetTempFileName(); + var links = new NamedLinksDecorator(tempDb, false); + var processor = new LinoGraphQLProcessor(links); + + // Create some test links using Update method + links.Update(null, new uint[] { 1, 1 }, null); + links.Update(null, new uint[] { 2, 2 }, null); + + // Act + var result = processor.ProcessLinoGraphQLQuery("(query (links (id source target)))"); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Data.Contains("links")); + Assert.Null(result.Errors); + + // Cleanup + File.Delete(tempDb); + } + + [Fact] + public void ProcessLinoGraphQLQuery_WithSingleLinkQuery_ReturnsExpectedFormat() + { + // Arrange + var tempDb = Path.GetTempFileName(); + var links = new NamedLinksDecorator(tempDb, false); + var processor = new LinoGraphQLProcessor(links); + + // Create a test link + var linkId = links.Update(null, new uint[] { 1, 1 }, null); + + // Act + var result = processor.ProcessLinoGraphQLQuery($"(query (link (id: {linkId}) (id source target)))"); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Data.Contains("link")); + Assert.Null(result.Errors); + + // Cleanup + File.Delete(tempDb); + } + + [Fact] + public void ProcessLinoGraphQLQuery_WithSchemaIntrospection_ReturnsSchemaInfo() + { + // Arrange + var tempDb = Path.GetTempFileName(); + var links = new NamedLinksDecorator(tempDb, false); + var processor = new LinoGraphQLProcessor(links); + + // Act + var result = processor.ProcessLinoGraphQLQuery("(query (__schema))"); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Data.Contains("__schema")); + Assert.Null(result.Errors); + + // Cleanup + File.Delete(tempDb); + } + + [Fact] + public void ProcessLinoGraphQLQuery_WithInvalidQuery_ReturnsError() + { + // Arrange + var tempDb = Path.GetTempFileName(); + var links = new NamedLinksDecorator(tempDb, false); + var processor = new LinoGraphQLProcessor(links); + + // Act + var result = processor.ProcessLinoGraphQLQuery(""); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Errors); + Assert.Single(result.Errors); + Assert.Contains("Empty query", result.Errors[0].Message); + + // Cleanup + File.Delete(tempDb); + } + + [Fact] + public void ProcessLinoGraphQLQuery_WithCustomSchemaQuery_ReturnsSchemaInLino() + { + // Arrange + var tempDb = Path.GetTempFileName(); + var links = new NamedLinksDecorator(tempDb, false); + var processor = new LinoGraphQLProcessor(links); + + // Act + var result = processor.ProcessLinoGraphQLQuery("(query (schema))"); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.True(result.Data.Contains("schema")); + Assert.True(result.Data.Contains("Link")); + Assert.True(result.Data.Contains("fields")); + Assert.Null(result.Errors); + + // Cleanup + File.Delete(tempDb); + } + + [Fact] + public void ProcessLinoGraphQLQuery_WithVariables_ProcessesCorrectly() + { + // Arrange + var tempDb = Path.GetTempFileName(); + var links = new NamedLinksDecorator(tempDb, false); + var processor = new LinoGraphQLProcessor(links); + + var linkId = links.Update(null, new uint[] { 1, 1 }, null); + var variables = new Dictionary { { "linkId", linkId } }; + + // Act + var result = processor.ProcessLinoGraphQLQuery("(query (link (id source target)))", variables); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Data); + Assert.Null(result.Errors); + + // Cleanup + File.Delete(tempDb); + } + } +} \ No newline at end of file diff --git a/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj b/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj index 1d8f9ab..7443099 100644 --- a/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj +++ b/Foundation.Data.Doublets.Cli/Foundation.Data.Doublets.Cli.csproj @@ -15,7 +15,7 @@ link-foundation A CLI tool for links manipulation. clink - 2.2.2 + 2.3.0 Unlicense https://github.com/link-foundation/link-cli @@ -26,6 +26,7 @@ + diff --git a/Foundation.Data.Doublets.Cli/LinoGraphQLProcessor.cs b/Foundation.Data.Doublets.Cli/LinoGraphQLProcessor.cs new file mode 100644 index 0000000..77bb2cf --- /dev/null +++ b/Foundation.Data.Doublets.Cli/LinoGraphQLProcessor.cs @@ -0,0 +1,296 @@ +using Platform.Data.Doublets; +using Platform.Protocols.Lino; +using System.Text; +using LinoLink = Platform.Protocols.Lino.Link; +using DoubletLink = Platform.Data.Doublets.Link; + +namespace Foundation.Data.Doublets.Cli +{ + public class LinoGraphQLProcessor + { + private readonly NamedLinksDecorator _links; + private readonly Parser _parser; + + public LinoGraphQLProcessor(NamedLinksDecorator links) + { + _links = links; + _parser = new Parser(); + } + + public class GraphQLQuery + { + public string Query { get; set; } = ""; + public Dictionary? Variables { get; set; } + public string? OperationName { get; set; } + } + + public class GraphQLResponse + { + public string? Data { get; set; } + public List? Errors { get; set; } + } + + public class GraphQLError + { + public string Message { get; set; } = ""; + public List? Locations { get; set; } + public string[]? Path { get; set; } + } + + public class GraphQLLocation + { + public int Line { get; set; } + public int Column { get; set; } + } + + public GraphQLResponse ProcessLinoGraphQLQuery(string queryString, Dictionary? variables = null) + { + try + { + // Parse the LINO GraphQL query + var parsedQuery = ParseLinoGraphQLQuery(queryString); + + // Execute the query + var result = ExecuteQuery(parsedQuery, variables); + + return new GraphQLResponse + { + Data = result + }; + } + catch (Exception ex) + { + return new GraphQLResponse + { + Errors = new List + { + new GraphQLError + { + Message = ex.Message + } + } + }; + } + } + + private LinoGraphQLQueryAst ParseLinoGraphQLQuery(string queryString) + { + // Parse LINO notation to extract GraphQL-like structure + var parsedLinks = _parser.Parse(queryString); + + if (parsedLinks.Count == 0) + { + throw new ArgumentException("Empty query provided"); + } + + // For simplicity, assume the query structure is: + // (query (fieldName (selection))) + var rootLink = parsedLinks[0]; + + return new LinoGraphQLQueryAst + { + Operation = rootLink.Id ?? "query", + Fields = ExtractFields(rootLink.Values?.ToList() ?? new List()) + }; + } + + private List ExtractFields(List links) + { + var fields = new List(); + + foreach (var link in links) + { + var field = new LinoGraphQLField + { + Name = link.Id ?? "unknown", + Arguments = new Dictionary(), + SelectionSet = link.Values?.Any() == true ? ExtractFields(link.Values.ToList()) : null + }; + fields.Add(field); + } + + return fields; + } + + private string ExecuteQuery(LinoGraphQLQueryAst query, Dictionary? variables) + { + var result = new StringBuilder(); + result.Append("("); + + foreach (var field in query.Fields) + { + var fieldResult = ExecuteField(field, variables); + if (!string.IsNullOrEmpty(fieldResult)) + { + result.Append(fieldResult); + } + } + + result.Append(")"); + return result.ToString(); + } + + private string ExecuteField(LinoGraphQLField field, Dictionary? variables) + { + return field.Name switch + { + "links" => ExecuteLinksQuery(field), + "link" => ExecuteLinkQuery(field, variables), + "schema" => ExecuteSchemaQuery(), + "__schema" => ExecuteIntrospectionQuery(), + _ => ExecuteCustomField(field, variables) + }; + } + + private string ExecuteLinksQuery(LinoGraphQLField field) + { + var result = new StringBuilder(); + result.Append($"({field.Name} "); + + var any = _links.Constants.Any; + var query = new DoubletLink(index: any, source: any, target: any); + var links = new List(); + + _links.Each(query, link => + { + var doubletLink = new DoubletLink(link); + var formattedLink = FormatLinkForGraphQL(doubletLink, field.SelectionSet); + links.Add(formattedLink); + return _links.Constants.Continue; + }); + + result.Append(string.Join(" ", links)); + result.Append(")"); + + return result.ToString(); + } + + private string ExecuteLinkQuery(LinoGraphQLField field, Dictionary? variables) + { + // Extract link ID from arguments or variables + uint linkId = 1; // Default + + if (field.Arguments?.ContainsKey("id") == true) + { + if (uint.TryParse(field.Arguments["id"].ToString(), out var id)) + { + linkId = id; + } + } + + // Check if link exists by trying to query it + var exists = false; + _links.Each(new DoubletLink(linkId, _links.Constants.Any, _links.Constants.Any), link => + { + exists = true; + return _links.Constants.Break; + }); + + if (!exists) + { + return ""; + } + + // Get the actual link using the Each method + DoubletLink? actualLink = null; + _links.Each(new DoubletLink(linkId, _links.Constants.Any, _links.Constants.Any), link => + { + actualLink = new DoubletLink(link); + return _links.Constants.Break; + }); + + if (actualLink == null) + { + return ""; + } + + return $"({field.Name} {FormatLinkForGraphQL(actualLink.Value, field.SelectionSet)})"; + } + + private string ExecuteSchemaQuery() + { + return "(schema (types (Link (fields (id source target)))))"; + } + + private string ExecuteIntrospectionQuery() + { + return "(__schema (__type (name: \"Link\") (fields (id source target))))"; + } + + private string ExecuteCustomField(LinoGraphQLField field, Dictionary? variables) + { + // For custom fields, try to match against existing link names or IDs + if (uint.TryParse(field.Name, out var linkId)) + { + // Check if link exists by trying to query it + var linkExists = false; + _links.Each(new DoubletLink(linkId, _links.Constants.Any, _links.Constants.Any), link => + { + linkExists = true; + return _links.Constants.Break; + }); + + if (linkExists) + { + // Get the actual link using the Each method + DoubletLink? actualLink = null; + _links.Each(new DoubletLink(linkId, _links.Constants.Any, _links.Constants.Any), link => + { + actualLink = new DoubletLink(link); + return _links.Constants.Break; + }); + + if (actualLink != null) + { + return FormatLinkForGraphQL(actualLink.Value, field.SelectionSet); + } + } + } + + return $"({field.Name})"; + } + + private string FormatLinkForGraphQL(DoubletLink link, List? selectionSet) + { + if (selectionSet == null || !selectionSet.Any()) + { + return _links.Format(link); + } + + var result = new StringBuilder(); + result.Append("("); + + foreach (var field in selectionSet) + { + var value = field.Name switch + { + "id" => link.Index.ToString(), + "source" => link.Source.ToString(), + "target" => link.Target.ToString(), + _ => "" + }; + + if (!string.IsNullOrEmpty(value)) + { + result.Append($"({field.Name}: {value})"); + } + } + + result.Append(")"); + return result.ToString(); + } + } + + public class LinoGraphQLQueryAst + { + public string Operation { get; set; } = "query"; + public List Fields { get; set; } = new(); + } + + public class LinoGraphQLField + { + public string Name { get; set; } = ""; + public Dictionary Arguments { get; set; } = new(); + public List? SelectionSet { get; set; } + } +} \ No newline at end of file diff --git a/Foundation.Data.Doublets.Cli/LinoGraphQLServer.cs b/Foundation.Data.Doublets.Cli/LinoGraphQLServer.cs new file mode 100644 index 0000000..bd2b4dc --- /dev/null +++ b/Foundation.Data.Doublets.Cli/LinoGraphQLServer.cs @@ -0,0 +1,326 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System.Text; +using System.Text.Json; + +namespace Foundation.Data.Doublets.Cli +{ + public class LinoGraphQLServer + { + private readonly NamedLinksDecorator _links; + private readonly LinoGraphQLProcessor _processor; + private readonly int _port; + private readonly bool _trace; + + public LinoGraphQLServer(NamedLinksDecorator links, int port = 5000, bool trace = false) + { + _links = links; + _processor = new LinoGraphQLProcessor(links); + _port = port; + _trace = trace; + } + + public async Task StartAsync(CancellationToken cancellationToken = default) + { + var builder = WebApplication.CreateBuilder(); + + // Configure services + builder.Services.AddLogging(logging => + { + if (_trace) + { + logging.SetMinimumLevel(LogLevel.Debug); + } + logging.AddConsole(); + }); + + builder.Services.AddCors(options => + { + options.AddPolicy("AllowAll", policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); + }); + + // Configure the web host + builder.WebHost.UseUrls($"http://localhost:{_port}"); + + var app = builder.Build(); + + // Configure middleware + app.UseCors("AllowAll"); + + // GraphQL endpoint + app.MapPost("/graphql", HandleGraphQLRequest); + app.MapGet("/graphql", HandleGraphQLRequest); + + // GraphQL playground/introspection endpoint + app.MapGet("/", ServePlayground); + + // Schema introspection + app.MapGet("/schema", HandleSchemaRequest); + + if (_trace) + { + Console.WriteLine($"LINO GraphQL API server starting on http://localhost:{_port}"); + Console.WriteLine("Endpoints:"); + Console.WriteLine(" POST /graphql - Main GraphQL endpoint"); + Console.WriteLine(" GET /graphql - GraphQL GET queries"); + Console.WriteLine(" GET / - GraphQL playground"); + Console.WriteLine(" GET /schema - Schema introspection"); + } + + await app.RunAsync(cancellationToken); + } + + private async Task HandleGraphQLRequest(HttpContext context) + { + try + { + string queryString = ""; + Dictionary? variables = null; + string? operationName = null; + + if (context.Request.Method == "POST") + { + using var reader = new StreamReader(context.Request.Body); + var requestBody = await reader.ReadToEndAsync(); + + if (_trace) + { + Console.WriteLine($"[GraphQL POST] Request body: {requestBody}"); + } + + // Try to parse as JSON first (for compatibility) + if (requestBody.TrimStart().StartsWith("{")) + { + var jsonRequest = JsonSerializer.Deserialize(requestBody); + queryString = jsonRequest?.Query ?? ""; + variables = jsonRequest?.Variables; + operationName = jsonRequest?.OperationName; + } + else + { + // Treat as pure LINO notation + queryString = requestBody; + } + } + else if (context.Request.Method == "GET") + { + queryString = context.Request.Query["query"].ToString(); + var variablesParam = context.Request.Query["variables"].ToString(); + if (!string.IsNullOrEmpty(variablesParam)) + { + variables = JsonSerializer.Deserialize>(variablesParam); + } + operationName = context.Request.Query["operationName"].ToString(); + + if (_trace) + { + Console.WriteLine($"[GraphQL GET] Query: {queryString}"); + } + } + + if (string.IsNullOrEmpty(queryString)) + { + return Results.BadRequest(new LinoGraphQLProcessor.GraphQLResponse + { + Errors = new List + { + new LinoGraphQLProcessor.GraphQLError + { + Message = "No query provided" + } + } + }); + } + + var result = _processor.ProcessLinoGraphQLQuery(queryString, variables); + + if (_trace) + { + Console.WriteLine($"[GraphQL Response] Data: {result.Data}"); + if (result.Errors?.Any() == true) + { + Console.WriteLine($"[GraphQL Response] Errors: {string.Join(", ", result.Errors.Select(e => e.Message))}"); + } + } + + // Return response in LINO format by default, but support JSON for compatibility + var acceptHeader = context.Request.Headers["Accept"].ToString(); + if (acceptHeader.Contains("application/json")) + { + return Results.Json(result); + } + else + { + // Return as LINO notation + var linoResponse = FormatResponseAsLino(result); + return Results.Content(linoResponse, "text/plain; charset=utf-8"); + } + } + catch (Exception ex) + { + if (_trace) + { + Console.WriteLine($"[GraphQL Error] {ex.Message}"); + Console.WriteLine($"[GraphQL Error] Stack trace: {ex.StackTrace}"); + } + + var errorResponse = new LinoGraphQLProcessor.GraphQLResponse + { + Errors = new List + { + new LinoGraphQLProcessor.GraphQLError + { + Message = ex.Message + } + } + }; + + return Results.Json(errorResponse); + } + } + + private static IResult HandleSchemaRequest(HttpContext context) + { + var schema = @"(schema + (types + (Link + (fields + (id (type: ID)) + (source (type: ID)) + (target (type: ID)) + ) + ) + (Query + (fields + (links (type: (List Link))) + (link (args (id (type: ID))) (type: Link)) + ) + ) + (Mutation + (fields + (createLink (args (source: ID) (target: ID)) (type: Link)) + (updateLink (args (id: ID) (source: ID) (target: ID)) (type: Link)) + (deleteLink (args (id: ID)) (type: Boolean)) + ) + ) + ) +)"; + + return Results.Content(schema, "text/plain; charset=utf-8"); + } + + private static IResult ServePlayground(HttpContext context) + { + var playgroundHtml = @" + + + LINO GraphQL Playground + + + +
+

LINO GraphQL API Playground

+ +
+

Try LINO GraphQL Queries

+ +

+ +
+ +
+

Response

+
No query executed yet.
+
+ +
+

Example Queries

+
(query (links (id source target)))
+
(query (link (id: 1) (id source target)))
+
(query (__schema))
+
(query (schema))
+
+ +
+

About LINO GraphQL API

+

This API uses LINO (Links Notation) instead of JSON for GraphQL queries and responses.

+

LINO uses parentheses to represent linked data structures, making it natural for graph operations.

+
    +
  • POST /graphql - Main GraphQL endpoint
  • +
  • GET /graphql?query=... - GraphQL GET queries
  • +
  • GET /schema - Schema introspection
  • +
+
+
+ + + +"; + + return Results.Content(playgroundHtml, "text/html"); + } + + private static string FormatResponseAsLino(LinoGraphQLProcessor.GraphQLResponse response) + { + if (response.Errors?.Any() == true) + { + var errors = string.Join(" ", response.Errors.Select(e => $"(error: \"{e.Message}\")")); + return $"(response (errors ({errors})))"; + } + + return $"(response (data {response.Data ?? "()"}))"; + } + } +} \ No newline at end of file diff --git a/Foundation.Data.Doublets.Cli/Program.cs b/Foundation.Data.Doublets.Cli/Program.cs index 1f9bfed..af2e99d 100644 --- a/Foundation.Data.Doublets.Cli/Program.cs +++ b/Foundation.Data.Doublets.Cli/Program.cs @@ -70,8 +70,40 @@ afterOption.AddAlias("--links"); afterOption.AddAlias("-a"); +// Server command options +var portOption = new Option( + name: "--port", + description: "Port number for the GraphQL server", + getDefaultValue: () => 5000 +); +portOption.AddAlias("-p"); + +// Create server command +var serverCommand = new Command("serve", "Start LINO GraphQL API server") +{ + dbOption, + portOption, + traceOption +}; + +// Create query command (existing functionality) +var queryCommand = new Command("query", "Execute LINO query") +{ + dbOption, + queryOption, + queryArgument, + traceOption, + structureOption, + beforeOption, + changesOption, + afterOption +}; + var rootCommand = new RootCommand("LiNo CLI Tool for managing links data store") { + serverCommand, + queryCommand, + // Keep backward compatibility - add options directly to root too dbOption, queryOption, queryArgument, @@ -82,73 +114,110 @@ afterOption }; -rootCommand.SetHandler( - (string db, string queryOptionValue, string queryArgumentValue, bool trace, uint? structure, bool before, bool changes, bool after) => +// Server command handler +serverCommand.SetHandler( + async (string db, int port, bool trace) => { var decoratedLinks = new NamedLinksDecorator(db, trace); - - // If --structure is provided, handle it separately - if (structure.HasValue) + var server = new LinoGraphQLServer(decoratedLinks, port, trace); + + Console.WriteLine($"Starting LINO GraphQL API server on port {port}..."); + Console.WriteLine("Press Ctrl+C to stop the server"); + + try { - var linkId = structure.Value; - try - { - var structureFormatted = decoratedLinks.FormatStructure(linkId, link => decoratedLinks.IsFullPoint(linkId), true, true); - Console.WriteLine(Namify(decoratedLinks, structureFormatted)); - } - catch (Exception ex) - { - Console.Error.WriteLine($"Error formatting structure for link ID {linkId}: {ex.Message}"); - Environment.Exit(1); - } - return; // Exit after handling --structure + await server.StartAsync(); } - - if (before) + catch (Exception ex) { - PrintAllLinks(decoratedLinks); + Console.Error.WriteLine($"Server error: {ex.Message}"); + Environment.Exit(1); } + }, + dbOption, portOption, traceOption +); - var effectiveQuery = !string.IsNullOrWhiteSpace(queryOptionValue) ? queryOptionValue : queryArgumentValue; +// Query command handler +queryCommand.SetHandler( + (string db, string queryOptionValue, string queryArgumentValue, bool trace, uint? structure, bool before, bool changes, bool after) => + { + ExecuteQueryCommand(db, queryOptionValue, queryArgumentValue, trace, structure, before, changes, after); + }, + dbOption, queryOption, queryArgument, traceOption, structureOption, beforeOption, changesOption, afterOption +); - var changesList = new List<(DoubletLink Before, DoubletLink After)>(); +// Root command handler (backward compatibility) +rootCommand.SetHandler( + (string db, string queryOptionValue, string queryArgumentValue, bool trace, uint? structure, bool before, bool changes, bool after) => + { + ExecuteQueryCommand(db, queryOptionValue, queryArgumentValue, trace, structure, before, changes, after); + }, + dbOption, queryOption, queryArgument, traceOption, structureOption, beforeOption, changesOption, afterOption +); + +static void ExecuteQueryCommand(string db, string queryOptionValue, string queryArgumentValue, bool trace, uint? structure, bool before, bool changes, bool after) +{ + var decoratedLinks = new NamedLinksDecorator(db, trace); - if (!string.IsNullOrWhiteSpace(effectiveQuery)) + // If --structure is provided, handle it separately + if (structure.HasValue) + { + var linkId = structure.Value; + try { - var options = new QueryProcessor.Options - { - Query = effectiveQuery, - Trace = trace, - ChangesHandler = (beforeLink, afterLink) => - { - changesList.Add((new DoubletLink(beforeLink), new DoubletLink(afterLink))); - return decoratedLinks.Constants.Continue; - } - }; - - QueryProcessor.ProcessQuery(decoratedLinks, options); + var structureFormatted = decoratedLinks.FormatStructure(linkId, link => decoratedLinks.IsFullPoint(linkId), true, true); + Console.WriteLine(Namify(decoratedLinks, structureFormatted)); } - - if (changes && changesList.Any()) + catch (Exception ex) { - // Simplify the collected changes - var simplifiedChanges = SimplifyChanges(changesList); + Console.Error.WriteLine($"Error formatting structure for link ID {linkId}: {ex.Message}"); + Environment.Exit(1); + } + return; // Exit after handling --structure + } + + if (before) + { + PrintAllLinks(decoratedLinks); + } + + var effectiveQuery = !string.IsNullOrWhiteSpace(queryOptionValue) ? queryOptionValue : queryArgumentValue; - // Print the simplified changes - foreach (var (linkBefore, linkAfter) in simplifiedChanges) + var changesList = new List<(DoubletLink Before, DoubletLink After)>(); + + if (!string.IsNullOrWhiteSpace(effectiveQuery)) + { + var options = new QueryProcessor.Options + { + Query = effectiveQuery, + Trace = trace, + ChangesHandler = (beforeLink, afterLink) => { - PrintChange(decoratedLinks, linkBefore, linkAfter); + changesList.Add((new DoubletLink(beforeLink), new DoubletLink(afterLink))); + return decoratedLinks.Constants.Continue; } - } + }; + + QueryProcessor.ProcessQuery(decoratedLinks, options); + } + + if (changes && changesList.Any()) + { + // Simplify the collected changes + var simplifiedChanges = SimplifyChanges(changesList); - if (after) + // Print the simplified changes + foreach (var (linkBefore, linkAfter) in simplifiedChanges) { - PrintAllLinks(decoratedLinks); + PrintChange(decoratedLinks, linkBefore, linkAfter); } - }, - // Explicitly specify the type parameters - dbOption, queryOption, queryArgument, traceOption, structureOption, beforeOption, changesOption, afterOption -); + } + + if (after) + { + PrintAllLinks(decoratedLinks); + } +} await rootCommand.InvokeAsync(args);