Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public FusionCommand() : base("fusion")

AddCommand(new FusionComposeCommand());
AddCommand(new FusionDownloadCommand());
AddCommand(new FusionMigrateCommand());
AddCommand(new FusionPublishCommand());
AddCommand(new FusionRunCommand());
AddCommand(new FusionSettingsCommand());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System.Text.Json;
using ChilliCream.Nitro.CommandLine.Helpers;
using ChilliCream.Nitro.CommandLine.Options;

namespace ChilliCream.Nitro.CommandLine.Commands.Fusion;

internal sealed class FusionMigrateCommand : Command
{
public FusionMigrateCommand() : base("migrate")
{
Description = "Migrate Fusion configuration files";

var targetArgument = new Argument<string>("TARGET")
.FromAmong(Targets.SubgraphConfig);

AddArgument(targetArgument);
AddOption(Opt<WorkingDirectoryOption>.Instance);

Comment thread
tobias-tengler marked this conversation as resolved.
this.SetHandler(async context =>
{
var target = context.ParseResult.GetValueForArgument(targetArgument);
var workingDirectory = context.ParseResult.GetValueForOption(Opt<WorkingDirectoryOption>.Instance)!;
var console = context.BindingContext.GetRequiredService<IAnsiConsole>();

context.ExitCode = target switch
{
Targets.SubgraphConfig => await MigrateSubgraphConfigAsync(
console,
workingDirectory,
context.GetCancellationToken()),
_ => throw new ArgumentOutOfRangeException(nameof(target))
};
});
}

private static async Task<int> MigrateSubgraphConfigAsync(
IAnsiConsole console,
string workingDirectory,
CancellationToken cancellationToken)
{
const string sourceFileName = "subgraph-config.json";
const string targetFileName = "schema-settings.json";

console.WriteLine($"Searching for '{sourceFileName}' files in '{workingDirectory}'...");

var sourceFiles = Directory.GetFiles(
workingDirectory,
sourceFileName,
SearchOption.AllDirectories);

if (sourceFiles.Length == 0)
{
console.ErrorLine($"No {sourceFileName} files found.");
return ExitCodes.Error;
}

var migratedFiles = new List<string>();

foreach (var sourceFile in sourceFiles)
{
cancellationToken.ThrowIfCancellationRequested();

var directory = Path.GetDirectoryName(sourceFile)!;
var targetFile = Path.Combine(directory, targetFileName);

if (File.Exists(targetFile))
{
var relativePath = Path.GetRelativePath(workingDirectory, targetFile);
console.MarkupLineInterpolated(
$"[yellow]Skipping[/] [grey]{relativePath}[/] (already exists)");
continue;
}

var sourceJson = await File.ReadAllBytesAsync(sourceFile, cancellationToken);

using var document = JsonDocument.Parse(sourceJson);
var root = document.RootElement;

await using var stream = File.Create(targetFile);
await using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
{
Comment thread
tobias-tengler marked this conversation as resolved.
Indented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
Comment thread
tobias-tengler marked this conversation as resolved.
});

writer.WriteStartObject();

// Enable backwards compatibility
writer.WriteString("version", "1.0.0");

// "subgraph" -> "name"
if (root.TryGetProperty("subgraph", out var subgraphElement))
{
writer.WritePropertyName("name");
subgraphElement.WriteTo(writer);
}
else
{
writer.WriteString("name", "");

var relativePath = Path.GetRelativePath(workingDirectory, targetFile);
console.MarkupLineInterpolated(
$"[grey]{relativePath}[/] [yellow]needs to define a 'name'.[/]");
}

Comment thread
tobias-tengler marked this conversation as resolved.
// "http" -> "transports.http" with "baseAddress" -> "url"
if (root.TryGetProperty("http", out var httpElement))
{
writer.WriteStartObject("transports");
writer.WriteStartObject("http");

foreach (var httpProperty in httpElement.EnumerateObject())
{
if (httpProperty.NameEquals("baseAddress"))
{
writer.WritePropertyName("url");
httpProperty.Value.WriteTo(writer);
}
else
{
httpProperty.WriteTo(writer);
}
}

writer.WriteEndObject();
writer.WriteEndObject();
}

// Copy any other top-level properties except "subgraph", "http", and "websocket"
foreach (var property in root.EnumerateObject())
{
if (property.NameEquals("subgraph")
|| property.NameEquals("http")
|| property.NameEquals("websocket"))
{
Comment thread
tobias-tengler marked this conversation as resolved.
continue;
}

property.WriteTo(writer);
}

writer.WriteEndObject();
await writer.FlushAsync(cancellationToken);

migratedFiles.Add(sourceFile);
}

if (migratedFiles.Count == 0)
{
console.MarkupLine("[yellow]No files were migrated.[/]");
return ExitCodes.Success;
}

console.Success($"Migrated {migratedFiles.Count} file(s) to {targetFileName}!");

foreach (var sourceFile in migratedFiles)
{
var relativePath = Path.GetRelativePath(workingDirectory, sourceFile);
console.MarkupLineInterpolated(
$"[grey]{relativePath}[/] -> [green]{targetFileName}[/]");
}
Comment thread
tobias-tengler marked this conversation as resolved.

return ExitCodes.Success;
}

private static class Targets
{
public const string SubgraphConfig = "subgraph-config";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
using System.CommandLine;
using System.CommandLine.Builder;
using System.CommandLine.Parsing;

namespace ChilliCream.Nitro.CommandLine.Tests.Commands.Fusion;

public sealed class FusionMigrateCommandTests : IDisposable
{
private readonly string _tempDir;

public FusionMigrateCommandTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(_tempDir);
}

[Fact]
public async Task Migrate_SubgraphConfig()
{
// arrange
await File.WriteAllTextAsync(
Path.Combine(_tempDir, "subgraph-config.json"),
"""
{
"subgraph": "Order",
"http": {
"clientName": "order-client",
"baseAddress": "http://localhost:59093/graphql",
"timeout": 30
},
"websocket": { "baseAddress": "ws://localhost:59093/graphql" },
"extensions": {
"nitro": {
"apiId": "blah"
}
}
}
""");

var builder = GetCommandLineBuilder();

// act
var exitCode = await builder.Build().InvokeAsync(
["fusion", "migrate", "subgraph-config", "--working-directory", _tempDir]);

// assert
Assert.Equal(0, exitCode);

var json = await File.ReadAllTextAsync(Path.Combine(_tempDir, "schema-settings.json"));
json.MatchInlineSnapshot(
"""
{
"version": "1.0.0",
"name": "Order",
"transports": {
"http": {
"clientName": "order-client",
"url": "http://localhost:59093/graphql",
"timeout": 30
}
},
"extensions": {
"nitro": {
"apiId": "blah"
}
}
}
""");
}

[Fact]
public async Task Migrate_SubgraphConfig_SkipsIfTargetExists()
{
// arrange
const string existingContent = """{ "existing": true }""";
await File.WriteAllTextAsync(
Path.Combine(_tempDir, "subgraph-config.json"),
"""
{
"subgraph": "Order",
"http": { "baseAddress": "http://localhost:5001/graphql" }
}
""");
await File.WriteAllTextAsync(
Path.Combine(_tempDir, "schema-settings.json"),
existingContent);

var builder = GetCommandLineBuilder();

// act
var exitCode = await builder.Build().InvokeAsync(
["fusion", "migrate", "subgraph-config", "--working-directory", _tempDir]);

// assert
Assert.Equal(0, exitCode);

var json = await File.ReadAllTextAsync(Path.Combine(_tempDir, "schema-settings.json"));
Assert.Equal(existingContent, json);
}

[Fact]
public async Task Migrate_SubgraphConfig_NoFilesFound_ReturnsError()
{
// arrange
var builder = GetCommandLineBuilder();

// act
var exitCode = await builder.Build().InvokeAsync(
["fusion", "migrate", "subgraph-config", "--working-directory", _tempDir]);

// assert
Assert.Equal(-1, exitCode);
}

[Fact]
public async Task Migrate_SubgraphConfig_MissingSubgraph()
{
// arrange
await File.WriteAllTextAsync(
Path.Combine(_tempDir, "subgraph-config.json"),
"""
{
"http": { "baseAddress": "http://localhost:5001/graphql" }
}
""");

var builder = GetCommandLineBuilder();

// act
var exitCode = await builder.Build().InvokeAsync(
["fusion", "migrate", "subgraph-config", "--working-directory", _tempDir]);

// assert
Assert.Equal(0, exitCode);
var json = await File.ReadAllTextAsync(Path.Combine(_tempDir, "schema-settings.json"));
json.MatchInlineSnapshot(
"""
{
"version": "1.0.0",
"name": "",
"transports": {
"http": {
"url": "http://localhost:5001/graphql"
}
}
}
""");
}

[Fact]
public async Task Migrate_SubgraphConfig_MultipleFiles()
{
// arrange
var subDir1 = Path.Combine(_tempDir, "subgraph1");
var subDir2 = Path.Combine(_tempDir, "subgraph2");
Directory.CreateDirectory(subDir1);
Directory.CreateDirectory(subDir2);

await File.WriteAllTextAsync(
Path.Combine(subDir1, "subgraph-config.json"),
"""
{
"subgraph": "Order",
"http": { "baseAddress": "http://localhost:5001/graphql" }
}
""");

await File.WriteAllTextAsync(
Path.Combine(subDir2, "subgraph-config.json"),
"""
{
"subgraph": "Product",
"http": { "baseAddress": "http://localhost:5002/graphql" }
}
""");

var builder = GetCommandLineBuilder();

// act
var exitCode = await builder.Build().InvokeAsync(
["fusion", "migrate", "subgraph-config", "--working-directory", _tempDir]);

// assert
Assert.Equal(0, exitCode);
Assert.True(File.Exists(Path.Combine(subDir1, "schema-settings.json")));
Assert.True(File.Exists(Path.Combine(subDir2, "schema-settings.json")));
}

private static CommandLineBuilder GetCommandLineBuilder()
{
var rootCommand = new Command("nitro");
rootCommand.AddNitroCloudCommands();
return new CommandLineBuilder(rootCommand)
.UseExtendedConsole()
.UseExceptionMiddleware()
.UseDefaults();
}

public void Dispose()
{
try
{
Directory.Delete(_tempDir, recursive: true);
}
catch
{
// ignore
}
}
}
Loading