Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,17 @@ Use `dotnet` CLI to search NuGet packages, for example:
```bash
dotnet package search HotChocolate
```

### Nitro persisted operations (Fusion Aspire)

After adding or editing any `.graphql` document under `src/HotChocolate/Fusion/src/Fusion.Aspire/Nitro/Operations`, regenerate the `.sha256` sidecars and verify them:

```bash
.github/scripts/nitro-aspire-operations.sh update \
--source src/HotChocolate/Fusion/src/Fusion.Aspire/Nitro/Operations
.github/scripts/nitro-aspire-operations.sh verify \
--source src/HotChocolate/Fusion/src/Fusion.Aspire/Nitro/Operations \
--output /tmp/nitro-aspire-operations.json
```

Never hand-write or hand-edit a `.sha256` sidecar. The `update` command is the only source of sidecar content, and `verify` must pass before handoff.
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,17 @@ This is framework code — performance matters. Aim for zero allocations on hot
### C# / .NET

If you need to search for packages on nuget.org use the `dotnet` cli, eg `dotnet package search HotChocolate`.

### Nitro persisted operations (Fusion Aspire)

After adding or editing any `.graphql` document under `src/HotChocolate/Fusion/src/Fusion.Aspire/Nitro/Operations`, regenerate the `.sha256` sidecars and verify them:

```bash
.github/scripts/nitro-aspire-operations.sh update \
--source src/HotChocolate/Fusion/src/Fusion.Aspire/Nitro/Operations
.github/scripts/nitro-aspire-operations.sh verify \
--source src/HotChocolate/Fusion/src/Fusion.Aspire/Nitro/Operations \
--output /tmp/nitro-aspire-operations.json
```

Never hand-write or hand-edit a `.sha256` sidecar. The `update` command is the only source of sidecar content, and `verify` must pass before handoff.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Aspire.Hosting.ApplicationModel;

namespace HotChocolate.Fusion.Aspire;

internal sealed class GatewayCompositionCommandCoordinator
{
private readonly Dictionary<string, Func<CancellationToken, Task<ExecuteCommandResult>>> _commands =
[with(StringComparer.Ordinal)];
private readonly Lock _sync = new();

public void Register(
string resourceName,
Func<CancellationToken, Task<ExecuteCommandResult>> command)
{
ArgumentException.ThrowIfNullOrWhiteSpace(resourceName);
ArgumentNullException.ThrowIfNull(command);

lock (_sync)
{
_commands[resourceName] = command;
}
}

public Task<ExecuteCommandResult> ExecuteAsync(
string resourceName,
CancellationToken cancellationToken)
{
Func<CancellationToken, Task<ExecuteCommandResult>>? command;
lock (_sync)
{
_commands.TryGetValue(resourceName, out command);
}

return command is null
? Task.FromResult(CommandResults.Failure("Schema composition is not ready."))
: command(cancellationToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Fusion.Aspire;

Expand Down Expand Up @@ -92,6 +94,33 @@ public static IResourceBuilder<T> WithGraphQLSchemaComposition<T>(
Settings = settings
});

if (!builder.Resource.Annotations
.OfType<ResourceCommandAnnotation>()
.Any(command => command.Name == "recompose"))
{
builder.WithCommand(
"recompose",
"Recompose",
context => context.ServiceProvider
.GetService<GatewayCompositionCommandCoordinator>()?
.ExecuteAsync(context.ResourceName, context.CancellationToken)
?? Task.FromResult(
CommandResults.Failure("Schema composition is not ready.")),
new CommandOptions
{
Description = "Recompose and install the gateway schema.",
IconName = "ArrowSync",
UpdateState = context =>
{
var state = context.ResourceSnapshot.State?.Text;
return state == KnownResourceStates.Running
|| state == KnownResourceStates.RuntimeUnhealthy
? ResourceCommandState.Enabled
: ResourceCommandState.Disabled;
}
});
}

return builder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,20 @@

<ItemGroup Condition="'$(Configuration)' != 'Release'">
<None Remove="Nitro\Operations\ResolveNitroApiName.graphql" />
<None Remove="Nitro\Operations\ValidateNitroSchema.graphql" />
<None Remove="Nitro\Operations\PollNitroSchemaValidation.graphql" />
<EmbeddedResource Include="Nitro\Operations\ResolveNitroApiName.graphql" />
<EmbeddedResource Include="Nitro\Operations\ValidateNitroSchema.graphql" />
<EmbeddedResource Include="Nitro\Operations\PollNitroSchemaValidation.graphql" />
</ItemGroup>

<ItemGroup Condition="'$(Configuration)' == 'Release'">
<None Remove="Nitro\Operations\ResolveNitroApiName.graphql.sha256" />
<None Remove="Nitro\Operations\ValidateNitroSchema.graphql.sha256" />
<None Remove="Nitro\Operations\PollNitroSchemaValidation.graphql.sha256" />
<EmbeddedResource Include="Nitro\Operations\ResolveNitroApiName.graphql.sha256" />
<EmbeddedResource Include="Nitro\Operations\ValidateNitroSchema.graphql.sha256" />
<EmbeddedResource Include="Nitro\Operations\PollNitroSchemaValidation.graphql.sha256" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace HotChocolate.Fusion.Aspire.Nitro;

internal interface INitroSchemaValidator
{
Task<NitroSchemaValidationReport> ValidateAsync(
NitroConnection connection,
string apiId,
string stage,
byte[] schema,
string schemaHash,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ internal sealed class NitroCompositionOptions
/// the distributed application does not add Nitro.
/// </summary>
public NitroSeedCoordinator? Coordinator { get; set; }

/// <summary>
/// Gets or sets the caller-supplied Nitro portal URL.
/// </summary>
public Uri? PortalUrl { get; set; }
}
27 changes: 27 additions & 0 deletions src/HotChocolate/Fusion/src/Fusion.Aspire/Nitro/NitroDefaults.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,33 @@ internal static class NitroDefaults
/// </summary>
public static readonly Uri ApiUrl = new("https://api.chillicream.com");

/// <summary>
/// Creates the unauthenticated Nitro portal URL for an effective Nitro API URL.
/// </summary>
public static Uri CreatePortalUrl(Uri apiUrl)
{
ArgumentNullException.ThrowIfNull(apiUrl);

if (!apiUrl.IsAbsoluteUri
|| (!string.Equals(apiUrl.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(apiUrl.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException("The Nitro API URL must be an absolute HTTP URL.", nameof(apiUrl));
}

if (!NitroApiUrl.TryNormalize(apiUrl.ToString(), out var normalized))
{
throw new ArgumentException("The Nitro API URL must be an absolute HTTP URL.", nameof(apiUrl));
}

return normalized!.Host.ToLowerInvariant() switch
{
"api.chillicream.com" => new Uri("https://nitro.chillicream.com"),
"api.chillicream.cloud" => new Uri("https://nitro.chillicream.cloud"),
_ => new Uri(normalized, "ui")
};
}

/// <summary>
/// The delay before the single re-read of the session file that resolves a read which raced
/// with <c>nitro login</c>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,53 @@ internal static class NitroOperationDocuments
private const string ResourceNamespace = "HotChocolate.Fusion.Aspire.Nitro.Operations";
#if NITRO_PERSISTED_OPERATIONS
private const string ResolveApiNameHashFile = "ResolveNitroApiName.graphql.sha256";
private const string ValidateSchemaHashFile = "ValidateNitroSchema.graphql.sha256";
private const string PollSchemaValidationHashFile = "PollNitroSchemaValidation.graphql.sha256";

private static string? s_resolveApiNameHash;
private static string? s_validateSchemaHash;
private static string? s_pollSchemaValidationHash;
#else
private const string ResolveApiNameFile = "ResolveNitroApiName.graphql";
private const string ValidateSchemaFile = "ValidateNitroSchema.graphql";
private const string PollSchemaValidationFile = "PollNitroSchemaValidation.graphql";

private static string? s_resolveApiName;
private static string? s_validateSchema;
private static string? s_pollSchemaValidation;
#endif

/// <summary>
/// The operation name of the document that resolves the name of an api by its id.
/// </summary>
public const string ResolveApiNameOperationName = "ResolveNitroApiName";
public const string ValidateSchemaOperationName = "ValidateNitroSchema";
public const string PollSchemaValidationOperationName = "PollNitroSchemaValidation";

#if NITRO_PERSISTED_OPERATIONS
/// <summary>
/// Gets the persisted operation id of the document that resolves the name of an api by its id.
/// </summary>
public static string GetResolveApiNameOperationId()
=> s_resolveApiNameHash ??= ReadDocument(ResolveApiNameHashFile).Trim();

public static string GetValidateSchemaOperationId()
=> s_validateSchemaHash ??= ReadDocument(ValidateSchemaHashFile).Trim();

public static string GetPollSchemaValidationOperationId()
=> s_pollSchemaValidationHash ??= ReadDocument(PollSchemaValidationHashFile).Trim();
#else
/// <summary>
/// Gets the document that resolves the name of an api by its id.
/// </summary>
public static string GetResolveApiNameDocument()
=> s_resolveApiName ??= ReadDocument(ResolveApiNameFile);

public static string GetValidateSchemaDocument()
=> s_validateSchema ??= ReadDocument(ValidateSchemaFile);

public static string GetPollSchemaValidationDocument()
=> s_pollSchemaValidation ??= ReadDocument(PollSchemaValidationFile);
#endif

private static string ReadDocument(string fileName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Aspire.Hosting.ApplicationModel;

namespace HotChocolate.Fusion.Aspire.Nitro;

internal sealed class NitroSchemaValidationAnnotation : IResourceAnnotation;
Loading
Loading