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
42 changes: 35 additions & 7 deletions src/HotChocolate/Fusion/src/Fusion.Aspire/SchemaComposition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -269,16 +269,25 @@ private List<IResourceWithEndpoints> GetReferencedResources(
{
var sourceSchemaName = resource.GetGraphQLSourceSchemaName() ?? resource.Name;

var schemaFromFile = await ReadSchemaFromProjectDirectoryAsync(resource, annotation.SchemaPath, cancellationToken);
if (schemaFromFile == null)
var schemaPath = annotation.SchemaPath ?? "schema.graphql";

if (IsExtensionsSchemaPath(schemaPath))
{
logger.LogWarning(
"Schema extensions file '{SchemaPath}' cannot be used as a source schema file. Provide the base schema file instead.",
schemaPath);
return null;
}

var schemaFromFile = await ReadSchemaFromProjectDirectoryAsync(resource, schemaPath, cancellationToken);
if (schemaFromFile is not { } schemaFiles)
{
return null;
}

// For file schemas, settings file is named after the schema file
// e.g., "foo.graphql" -> "foo-settings.json"
var schemaFileName = annotation.SchemaPath ?? "schema.graphql";
var settingsFileName = $"{IOPath.GetFileNameWithoutExtension(schemaFileName)}-settings.json";
var settingsFileName = $"{IOPath.GetFileNameWithoutExtension(schemaPath)}-settings.json";

var schemaSettings = await GetSourceSchemaSettingsAsync(resource, settingsFileName, cancellationToken);
if (schemaSettings == null)
Expand All @@ -291,7 +300,7 @@ private List<IResourceWithEndpoints> GetReferencedResources(
Name = sourceSchemaName,
ResourceName = resource.Name,
HttpEndpointUrl = null, // No HTTP endpoint for file-based schemas
Schema = new SourceSchemaText(sourceSchemaName, schemaFromFile),
Schema = new SourceSchemaText(sourceSchemaName, schemaFiles.Schema, schemaFiles.Extensions),
SchemaSettings = schemaSettings
};
}
Expand Down Expand Up @@ -359,7 +368,7 @@ private List<IResourceWithEndpoints> GetReferencedResources(
}
}

private async Task<string?> ReadSchemaFromProjectDirectoryAsync(
private async Task<(string Schema, string? Extensions)?> ReadSchemaFromProjectDirectoryAsync(
IResourceWithEndpoints resource,
string? fileName,
CancellationToken cancellationToken)
Expand All @@ -383,7 +392,21 @@ private List<IResourceWithEndpoints> GetReferencedResources(
return null;
}

return await File.ReadAllTextAsync(schemaFile, cancellationToken);
var schemaText = await File.ReadAllTextAsync(schemaFile, cancellationToken);

var extensionsFile = IOPath.Combine(
IOPath.GetDirectoryName(schemaFile)!,
IOPath.GetFileNameWithoutExtension(schemaFile)
+ "-extensions"
+ IOPath.GetExtension(schemaFile));

string? extensionsText = null;
if (File.Exists(extensionsFile))
{
extensionsText = await File.ReadAllTextAsync(extensionsFile, cancellationToken);
}

return (schemaText, extensionsText);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -518,4 +541,9 @@ private async Task<bool> ComposeSchemaAsync(

return false;
}

private static bool IsExtensionsSchemaPath(string filePath)
=> IOPath.GetFileNameWithoutExtension(filePath).EndsWith(
"-extensions",
StringComparison.OrdinalIgnoreCase);
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,11 @@ await archive.GetSourceSchemaNamesAsync(cancellationToken),
}

var sourceText = await ReadSchemaSourceTextAsync(configuration, cancellationToken);
var extensionsSourceText = await TryReadSchemaExtensionsTextAsync(configuration, cancellationToken);

sourceSchemas[schemaName] = (new SourceSchemaText(schemaName, sourceText), configuration.Settings);
sourceSchemas[schemaName] = (
new SourceSchemaText(schemaName, sourceText, extensionsSourceText),
configuration.Settings);
}

var existingCompositionSettings = await GetCompositionSettingsAsync(archive, cancellationToken);
Expand Down Expand Up @@ -134,10 +137,15 @@ await archive.GetSourceSchemaNamesAsync(cancellationToken),

foreach (var (schemaName, (schema, settings)) in sourceSchemas)
{
var schemaExtensions = schema.ExtensionsSourceText is null
? default
: Encoding.UTF8.GetBytes(schema.ExtensionsSourceText);

await archive.SetSourceSchemaConfigurationAsync(
schemaName,
Encoding.UTF8.GetBytes(schema.SourceText),
settings,
schemaExtensions,
cancellationToken);
}

Expand Down Expand Up @@ -194,4 +202,19 @@ private static async Task<string> ReadSchemaSourceTextAsync(
using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadToEndAsync(cancellationToken);
}

private static async Task<string?> TryReadSchemaExtensionsTextAsync(
SourceSchemaConfiguration configuration,
CancellationToken cancellationToken)
{
await using var stream = await configuration.TryOpenReadSchemaExtensionsAsync(cancellationToken);

if (stream is null)
{
return null;
}

using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadToEndAsync(cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -536,10 +536,17 @@ public static LogEntry InvalidFieldSharing(
.Build();
}

public static LogEntry InvalidGraphQL(string exceptionMessage, MutableSchemaDefinition schema)
public static LogEntry InvalidGraphQL(
string exceptionMessage,
MutableSchemaDefinition schema,
bool inExtensions = false)
{
return LogEntryBuilder.New()
.SetMessage(LogEntryHelper_InvalidGraphQL, exceptionMessage)
.SetMessage(
inExtensions
? LogEntryHelper_InvalidGraphQLInExtensions
: LogEntryHelper_InvalidGraphQL,
exceptionMessage)
.SetCode(LogEntryCodes.InvalidGraphQL)
.SetSeverity(LogSeverity.Error)
.SetSchema(schema)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@
<data name="LogEntryHelper_InvalidGraphQL" xml:space="preserve">
<value>Invalid GraphQL in source schema. Exception message: {0}.</value>
</data>
<data name="LogEntryHelper_InvalidGraphQLInExtensions" xml:space="preserve">
<value>Invalid GraphQL in source schema extensions. Exception message: {0}.</value>
</data>
<data name="LogEntryHelper_InvalidShareableUsage" xml:space="preserve">
<value>The field '{0}' in schema '{1}' must not be marked as shareable.</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public CompositionResult<MutableSchemaDefinition> Parse()
schema.AddBuiltInFusionTypes();
schema.AddBuiltInFusionDirectives();

// Parse source schema.
try
{
SchemaParser.Parse(
Expand All @@ -34,22 +35,42 @@ public CompositionResult<MutableSchemaDefinition> Parse()
IgnoreExistingTypes = true,
IgnoreExistingDirectives = true
});
}
catch (Exception ex)
{
log.Write(LogEntryHelper.InvalidGraphQL(ex.Message, schema));
}

// Schema validation.
if (_options.EnableSchemaValidation)
// Parse optional source schema extensions.
if (sourceSchemaText.ExtensionsSourceText is not null)
{
try
{
var validationLog = new ValidationLog();
s_schemaValidator.Validate(schema, validationLog);

if (validationLog.HasErrors)
{
log.WriteValidationLog(validationLog, schema);
}
SchemaParser.Parse(
schema,
sourceSchemaText.ExtensionsSourceText,
new SchemaParserOptions
{
IgnoreExistingTypes = true,
IgnoreExistingDirectives = true
});
}
catch (Exception ex)
{
log.Write(LogEntryHelper.InvalidGraphQL(ex.Message, schema, inExtensions: true));
}
}
catch (Exception ex)

// Schema validation.
if (_options.EnableSchemaValidation)
{
log.Write(LogEntryHelper.InvalidGraphQL(ex.Message, schema));
var validationLog = new ValidationLog();
s_schemaValidator.Validate(schema, validationLog);

if (validationLog.HasErrors)
{
log.WriteValidationLog(validationLog, schema);
}
}

return log.HasErrors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ namespace HotChocolate.Fusion;
/// </summary>
public readonly record struct SourceSchemaText(
string Name,
string SourceText);
string SourceText,
string? ExtensionsSourceText = null);
67 changes: 67 additions & 0 deletions src/HotChocolate/Fusion/src/Fusion.Packaging/ArchiveSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ public IEnumerable<string> GetFiles()

foreach (var entry in _archive.Entries)
{
// Skip entries that are explicitly marked Deleted in this session;
// they are still in the underlying ZipArchive but logically gone.
if (_files.TryGetValue(entry.FullName, out var tracked)
&& tracked.State is FileState.Deleted)
{
continue;
}

files.Add(entry.FullName);
}

Expand Down Expand Up @@ -119,6 +127,57 @@ public Stream OpenWrite(string path)
return stream;
}

public void Delete(string path)
{
if (_mode is FusionArchiveMode.Read)
{
throw new InvalidOperationException("Cannot delete from a read-only archive.");
}

if (_files.TryGetValue(path, out var file))
{
if (file.State is FileState.Deleted)
{
return;
}

if (file.State is FileState.Created)
{
// File was added in this uncommitted session and never existed
// in the original archive: drop it entirely.
TryDeleteTempFile(file);
_files.Remove(path);
return;
}

// File was previously read or replaced (extracted to a temp file).
// Clean up the temp file now since Dispose skips Deleted entries.
TryDeleteTempFile(file);
file.MarkDeleted();
return;
Comment thread
glen-84 marked this conversation as resolved.
}

if (_mode is not FusionArchiveMode.Create && _archive.GetEntry(path) is not null)
{
_files.Add(path, FileEntry.Deleted(path));
}
}

private static void TryDeleteTempFile(FileEntry file)
{
if (File.Exists(file.TempPath))
{
try
{
File.Delete(file.TempPath);
}
catch
{
// ignore
}
}
}

public void SetMode(FusionArchiveMode mode)
{
_mode = mode;
Expand Down Expand Up @@ -262,6 +321,11 @@ public void MarkMutated()
}
}

public void MarkDeleted()
{
State = FileState.Deleted;
}

public void MarkRead()
{
State = FileState.Read;
Expand All @@ -273,6 +337,9 @@ public static FileEntry Created(string path)
public static FileEntry Read(string path)
=> new(path, GetRandomTempFileName(), FileState.Read);

public static FileEntry Deleted(string path)
=> new(path, GetRandomTempFileName(), FileState.Deleted);

private static string GetRandomTempFileName()
{
var tempDir = System.IO.Path.GetTempPath();
Expand Down
5 changes: 5 additions & 0 deletions src/HotChocolate/Fusion/src/Fusion.Packaging/FileNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal static class FileNames
private const string GatewaySchemaFormat = "gateway/{0}/gateway.graphqls";
private const string GatewaySettingsFormat = "gateway/{0}/gateway-settings.json";
private const string SourceSchemaFormat = "source-schemas/{0}/schema.graphqls";
private const string SourceSchemaExtensionsFormat = "source-schemas/{0}/schema-extensions.graphqls";
private const string SourceSchemaSettingsFormat = "source-schemas/{0}/schema-settings.json";

public const string ArchiveMetadata = "archive-metadata.json";
Expand All @@ -22,6 +23,9 @@ public static string GetGatewaySettingsPath(Version version)
public static string GetSourceSchemaPath(string schemaName)
=> string.Format(SourceSchemaFormat, schemaName);

public static string GetSourceSchemaExtensionsPath(string schemaName)
=> string.Format(SourceSchemaExtensionsFormat, schemaName);

public static string GetSourceSchemaSettingsPath(string schemaName)
=> string.Format(SourceSchemaSettingsFormat, schemaName);

Expand All @@ -31,6 +35,7 @@ public static FileKind GetFileKind(string fileName)
{
case "gateway.graphqls":
case "schema.graphqls":
case "schema-extensions.graphqls":
return FileKind.Schema;

case "schema-settings.json":
Expand Down
Loading
Loading