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 @@ -20,15 +20,17 @@ public ExportCommand(IHost host) : base("export")

Options.Add(Opt<OutputOption>.Instance);
Options.Add(Opt<SchemaNameOption>.Instance);
Options.Add(Opt<SemanticNonNullOption>.Instance);

SetAction(
(parseResult, cancellationToken) =>
{
var output = parseResult.InvocationConfiguration.Output;
var outputFile = parseResult.GetValue(Opt<OutputOption>.Instance);
var schemaName = parseResult.GetValue(Opt<SchemaNameOption>.Instance);
var semanticNonNull = parseResult.GetValue(Opt<SemanticNonNullOption>.Instance);

return ExecuteAsync(output, host, outputFile, schemaName, cancellationToken);
return ExecuteAsync(output, host, outputFile, schemaName, semanticNonNull, cancellationToken);
});
}

Expand All @@ -37,6 +39,7 @@ private static async Task ExecuteAsync(
IHost host,
FileInfo? outputFile,
string? schemaName,
bool semanticNonNull,
CancellationToken cancellationToken)
{
var provider = host.Services.GetRequiredService<IRequestExecutorProvider>();
Expand All @@ -58,7 +61,11 @@ private static async Task ExecuteAsync(

var executor = await provider.GetExecutorAsync(schemaName, cancellationToken);
outputFile ??= new FileInfo(System.IO.Path.Combine(Environment.CurrentDirectory, "schema.graphqls"));
var result = await SchemaFileExporter.Export(outputFile.FullName, executor, cancellationToken);
var result = await SchemaFileExporter.Export(
outputFile.FullName,
executor,
semanticNonNull,
cancellationToken);

await output.WriteLineAsync("Exported Files:");
await output.WriteLineAsync($"- {result.SchemaFileName}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.CommandLine;

namespace HotChocolate.AspNetCore.CommandLine;

/// <summary>
/// An option for the schema command. The option is used to rewrite non-null
/// output fields to use the @semanticNonNull directive in the exported schema.
/// </summary>
internal sealed class SemanticNonNullOption : Option<bool>
{
public SemanticNonNullOption() : base("--semantic-non-null")
{
Description = "Rewrite the exported schema to strip non-null wrappers from output "
+ "fields and apply the @semanticNonNull directive instead.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -914,14 +914,14 @@ private sealed class CachedSemanticNonNullSchemaOutput

public CachedSemanticNonNullSchemaOutput(ISchemaDefinition schema, ulong version, DateTimeOffset lastModifiedTime)
{
var document = SchemaFormatter.FormatAsDocument(
schema,
new SchemaFormatterOptions
{
IncludeInternalDirectives = false
});
var rewritten = SemanticNonNullSchemaRewriter.Rewrite(document);
_schema = Encoding.UTF8.GetBytes(rewritten.ToString(indented: true));
_schema = Encoding.UTF8.GetBytes(
SchemaFormatter.FormatAsString(
schema,
new SchemaFormatterOptions
{
IncludeInternalDirectives = false,
RewriteToSemanticNonNull = true
}));
FileName = GetSchemaFileName(schema);
ETag = CreateETag(_schema, version);
LastModified = lastModifiedTime.ToString("R");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ public static IRequestExecutorBuilder AddWarmupTask<
/// <param name="schemaFileName">
/// The file name of the schema file.
/// </param>
/// <param name="semanticNonNull">
/// If <c>true</c>, non-null wrappers on output fields are stripped and
/// replaced with the @semanticNonNull directive in the exported schema.
/// </param>
/// <param name="skipIf">
/// If <c>true</c>, the schema file will not be exported.
/// </param>
Expand All @@ -383,14 +387,15 @@ public static IRequestExecutorBuilder AddWarmupTask<
public static IRequestExecutorBuilder ExportSchemaOnStartup(
this IRequestExecutorBuilder builder,
string? schemaFileName = null,
bool semanticNonNull = false,
bool skipIf = false)
{
ArgumentNullException.ThrowIfNull(builder);

if (!skipIf)
{
schemaFileName ??= System.IO.Path.Combine(Environment.CurrentDirectory, "schema.graphqls");
builder.AddWarmupTask(new SchemaFileExporterWarmupTask(schemaFileName));
builder.AddWarmupTask(new SchemaFileExporterWarmupTask(schemaFileName, semanticNonNull));
}

return builder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

namespace HotChocolate.AspNetCore.Warmup;

internal sealed class SchemaFileExporterWarmupTask(string schemaFileName) : IRequestExecutorWarmupTask
internal sealed class SchemaFileExporterWarmupTask(
string schemaFileName,
bool rewriteToSemanticNonNull) : IRequestExecutorWarmupTask
{
public bool ApplyOnlyOnStartup => false;

public async Task WarmupAsync(IRequestExecutor executor, CancellationToken cancellationToken)
{
await SchemaFileExporter.Export(schemaFileName, executor, cancellationToken);
await SchemaFileExporter.Export(
schemaFileName,
executor,
rewriteToSemanticNonNull,
cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,34 @@ public async Task App_Should_WriteSchemaToFile_When_OutputOptionIsSpecified()
await snapshot.MatchMarkdownAsync();
}

[Fact]
public async Task App_Should_WriteSemanticNonNullSchemaToFile_When_SemanticNonNullOptionIsSpecified()
{
// arrange
var snapshot = new Snapshot();
var services = new ServiceCollection();
services.AddGraphQL()
.AddQueryType(x => x.Name("Query").Field("foo").Type<NonNullType<StringType>>().Resolve("bar"));

var hostMock = new Mock<IHost>();
hostMock
.Setup(x => x.Services)
.Returns(services.BuildServiceProvider());

var host = hostMock.Object;
var output = new StringWriter();
var app = new App(host);
var tempFile = CreateSchemaFileName();

// act
await app.InvokeAsync($"schema export --output {tempFile} --semantic-non-null", output);

// assert
snapshot.Add(await File.ReadAllTextAsync(tempFile + ".graphqls"), "Schema", markdownLanguage: "graphql");
snapshot.Add(await File.ReadAllTextAsync(tempFile + "-settings.json"), "Settings", markdownLanguage: "json");
await snapshot.MatchMarkdownAsync();
}

[Fact]
public async Task App_Should_WriteNamedSchemaToOutput_When_SchemaNameIsSpecified()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ Usage:
Options:
--output <output> The path to the file where the schema should be exported to. If no output path is specified the schema will be printed to the console.
--schema-name <schema-name> The name of the schema that should be exported. If no schema name is specified the default schema will be exported.
--semantic-non-null Rewrite the exported schema to strip non-null wrappers from output fields and apply the @semanticNonNull directive instead.
-?, -h, --help Show help and usage information

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# App_Should_WriteSemanticNonNullSchemaToFile_When_SemanticNonNullOptionIsSpecified

## Schema

```graphql
schema {
query: Query
}

type Query {
foo: String @semanticNonNull
}

directive @semanticNonNull(levels: [Int!] = [
0
]) on FIELD_DEFINITION

```

## Settings

```json
{
"name": "_Default",
"transports": {
"http": {
"url": "http://localhost:5000/graphql"
}
}
}

```
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<InternalsVisibleTo Include="HotChocolate.Types.Tests" />
<InternalsVisibleTo Include="HotChocolate.Types.Abstractions.Tests" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ public static DocumentNode FormatAsDocument(
document = postProcessor.Format(schema, document);
}

if (options.RewriteToSemanticNonNull)
{
document = SemanticNonNullSchemaRewriter.Rewrite(document);
}

return document;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,12 @@ public sealed class SchemaFormatterOptions
/// Default: <c>true</c>.
/// </summary>
public bool IncludeInternalDirectives { get; set; } = true;

/// <summary>
/// Controls whether the formatted document is post-processed to strip
/// non-null wrappers from output fields and apply the @semanticNonNull
/// directive instead.
/// Default: <c>false</c>.
/// </summary>
public bool RewriteToSemanticNonNull { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using HotChocolate.Language;
using HotChocolate.Types;

namespace HotChocolate.AspNetCore.Formatters;
namespace HotChocolate.Serialization;

/// <summary>
/// Rewrites a GraphQL schema document by stripping non-null wrappers from
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using HotChocolate.Serialization;

namespace HotChocolate.Execution.Internal;

Expand All @@ -9,9 +10,12 @@ internal static class SchemaFileExporter
public static async Task<SchemaFileInfo> Export(
string schemaFileName,
IRequestExecutor executor,
bool rewriteToSemanticNonNull,
CancellationToken cancellationToken)
{
var sdl = executor.Schema.ToString();
var sdl = SchemaFormatter.FormatAsString(
executor.Schema,
new SchemaFormatterOptions { RewriteToSemanticNonNull = rewriteToSemanticNonNull });

if (Directory.Exists(schemaFileName))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using HotChocolate.AspNetCore.Formatters;
using HotChocolate.Language;

namespace HotChocolate.AspNetCore;
namespace HotChocolate.Serialization;

public class SemanticNonNullSchemaRewriterTests
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,72 @@ There is no 1:1 mapping for all old methods. In most cases:

Also note that `SubscriptionTransportError(...)` is no longer exposed separately in the fusion diagnostics API; use `SourceSchemaTransportError(...)`. -->

## Experimental @semanticNonNull support removed

Hot Chocolate v15 included experimental support for the `@semanticNonNull` directive, which let you mark fields as semantically non-null while still returning `null` (rather than propagating to the parent) when a resolver errored. We've removed this feature in v16 in favor of the [`onError` proposal](https://github.com/graphql/graphql-spec/pull/1163).

If you previously opted in to this feature, remove the option:

```diff
builder.AddGraphQL()
.ModifyOptions(o =>
{
- o.EnableSemanticNonNull = true;
});
```

If you still need to keep the behavior of not propagating nulls for errors on non-null fields, set the `DefaultErrorHandlingMode` to `ErrorHandlingMode.Null`:

```csharp
builder
.AddGraphQL()
.ModifyOptions(o => o.DefaultErrorHandlingMode = ErrorHandlingMode.Null);
```

### Clients that still need a schema with @semanticNonNull annotations

If you have a client that still relies on the schema being annotated with `@semanticNonNull`, you have a few options to obtain such a schema.

**Schema snapshot tests**

If you're producing a schema string for snapshot tests like this:

```csharp
ISchemaDefinition schema = await new ServiceCollection()
.AddGraphQL()
// ...
.BuildSchemaAsync();

string schemaStr = schema.ToString();

// assert schemaStr ...
```

Switch to `SchemaFormatter` with `RewriteToSemanticNonNull` enabled:

```csharp
string schemaStr = SchemaFormatter.FormatAsString(
schema,
new SchemaFormatterOptions { RewriteToSemanticNonNull = true });
```

**Downloading the schema from the server**

If you're using `MapGraphQLSchema()` to expose the schema at `/graphql/schema`, you can additionally call `MapGraphQLSemanticNonNullSchema()` to expose a variant annotated with `@semanticNonNull` at `/graphql/semantic-non-null-schema.graphql`:

```csharp
app.MapGraphQLSchema();
app.MapGraphQLSemanticNonNullSchema();
```

**Exporting the schema via the CLI**

If you're using the schema export command, add the `--semantic-non-null` flag to emit the schema with `@semanticNonNull` annotations:

```bash
dotnet run -- schema export --output schema.graphql --semantic-non-null
```
Comment thread
tobias-tengler marked this conversation as resolved.

# Deprecations

Things that will continue to function this release, but we encourage you to move away from.
Expand Down
1 change: 1 addition & 0 deletions website/src/docs/hotchocolate/v16/server/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dotnet run -- schema export --output schema.graphql

- `--output`: The path to the file where the schema is exported. If no output path is specified, the schema prints to the console.
- `--schema-name`: The name of the schema to export. If no schema name is specified, the default schema is exported.
- `--semantic-non-null`: Rewrites the exported schema to strip non-null wrappers from output fields and apply the `@semanticNonNull` directive instead. Useful for clients that still rely on `@semanticNonNull` annotations after the experimental support was removed in v16.

# Next Steps

Expand Down
Loading