|
using System; |
|
using System.Collections.Generic; |
|
using System.Collections.Immutable; |
|
using System.Linq; |
|
using System.Threading.Tasks; |
|
using HotChocolate; |
|
using HotChocolate.Execution; |
|
using HotChocolate.Language; |
|
using HotChocolate.Resolvers; |
|
using HotChocolate.Stitching; |
|
using HotChocolate.Stitching.Delegation; |
|
using HotChocolate.Stitching.Requests; |
|
using HotChocolate.Stitching.Utilities; |
|
using HotChocolate.Types; |
|
using HotChocolate.Types.Relay; |
|
|
|
namespace AutoGuru.GraphQLService.Stitching |
|
{ |
|
/// <summary> |
|
/// Temporary node resolver until something is built-in to HC. |
|
/// </summary> |
|
/// <remarks> |
|
/// Heavily inspired by: |
|
/// https://github.com/ChilliCream/hotchocolate/blob/main/src/HotChocolate/Stitching/src/Stitching/Delegation/DelegateToRemoteSchemaMiddleware.cs |
|
/// </remarks> |
|
public class NodeResolverMiddleware |
|
{ |
|
private readonly FieldDelegate _next; |
|
private readonly IIdSerializer _idSerializer; |
|
|
|
// ../WellKnownProperties.cs |
|
internal static class WellKnownProperties |
|
{ |
|
public const string IsAutoGenerated = "__hc_IsAutoGenerated"; |
|
public const string SchemaName = "__hc_SchemaName"; |
|
} |
|
|
|
internal static class WellKnownContextData |
|
{ |
|
public const string NameLookup = "HotChocolate.Stitching.NameLookup"; |
|
} |
|
|
|
public NodeResolverMiddleware(FieldDelegate next, IIdSerializer idSerializer) |
|
{ |
|
_next = next; |
|
_idSerializer = idSerializer; |
|
} |
|
|
|
public async Task InvokeAsync(IMiddlewareContext context) |
|
{ |
|
var idRaw = context.ArgumentValue<string>("id"); |
|
var id = _idSerializer.Deserialize(idRaw); |
|
|
|
var schemaName = id.SchemaName; |
|
if (string.IsNullOrWhiteSpace(schemaName)) |
|
{ |
|
throw new ArgumentException("id"); |
|
} |
|
|
|
var path = ImmutableStack<SelectionPathComponent>.Empty.Push( |
|
new SelectionPathComponent( |
|
new NameNode(context.Selection.SyntaxNode.Location, "node"), |
|
new List<ArgumentNode>(0))); |
|
|
|
var request = CreateQuery(context, schemaName, path); |
|
var result = await ExecuteQueryAsync(context, request, schemaName); |
|
context.RegisterForCleanup(result.Dispose); |
|
|
|
UpdateContextData(context, result, schemaName); |
|
|
|
var data = ExtractData(result.Data, path.Count()); |
|
|
|
if (data is { }) |
|
{ |
|
context.Result = new SerializedData(data); |
|
} |
|
|
|
if (result.Errors is { Count: > 0 }) |
|
{ |
|
ReportErrors(schemaName, context, result.Errors); |
|
} |
|
|
|
await _next(context); |
|
} |
|
|
|
private static IReadOnlyQueryRequest CreateQuery( |
|
IMiddlewareContext context, |
|
NameString schemaName, |
|
IImmutableStack<SelectionPathComponent> path) |
|
{ |
|
var fieldRewriter = new ExtractFieldQuerySyntaxRewriter( |
|
context.Schema, |
|
context.Service<IEnumerable<IQueryDelegationRewriter>>()); |
|
|
|
var extractedField = fieldRewriter.ExtractField( |
|
schemaName, context.Document, context.Operation, |
|
context.Selection, context.ObjectType); |
|
|
|
var queryBuilder = RemoteQueryBuilder.New() |
|
.SetRequestField(extractedField.SyntaxNodes[0]) |
|
.SetOperation(context.Operation.Name, OperationType.Query) |
|
.SetSelectionPath(path) |
|
.AddFragmentDefinitions(extractedField.Fragments); |
|
|
|
queryBuilder.AddVariables(extractedField.Variables); |
|
|
|
var query = queryBuilder |
|
.Build(schemaName, GetNameLookup(context.Schema)); |
|
|
|
var requestBuilder = QueryRequestBuilder.New() |
|
.SetQuery(query); |
|
|
|
foreach (var extractedFieldVariable in extractedField.Variables) |
|
{ |
|
var variableName = extractedFieldVariable.Variable.Name.Value; |
|
var namedType = context.Schema.GetType<INamedInputType>( |
|
extractedFieldVariable.Type.NamedType().Name.Value); |
|
|
|
if (!context.Variables.TryGetVariable<IValueNode>(variableName, out var value)) |
|
{ |
|
value = NullValueNode.Default; |
|
} |
|
|
|
value = fieldRewriter.RewriteValueNode( |
|
schemaName, |
|
(IInputType)extractedFieldVariable.Type.ToType(namedType), |
|
value); |
|
|
|
requestBuilder.SetVariableValue(variableName, value); |
|
} |
|
|
|
requestBuilder.SetProperties(context.ContextData); |
|
requestBuilder.SetProperty(WellKnownProperties.IsAutoGenerated, true); |
|
|
|
return requestBuilder.Create(); |
|
} |
|
|
|
private static async Task<IReadOnlyQueryResult> ExecuteQueryAsync( |
|
IResolverContext context, |
|
IReadOnlyQueryRequest request, |
|
string schemaName) |
|
{ |
|
var stitchingContext = context.Service<IStitchingContext>(); |
|
var executor = stitchingContext.GetRemoteRequestExecutor(schemaName); |
|
var result = await executor.ExecuteAsync(request); |
|
|
|
if (result is IReadOnlyQueryResult queryResult) |
|
{ |
|
return queryResult; |
|
} |
|
|
|
throw new QueryException("Only query results"); |
|
} |
|
|
|
private static object? ExtractData( |
|
IReadOnlyDictionary<string, object?>? data, |
|
int levels) |
|
{ |
|
if (data == null || data.Count == 0) |
|
{ |
|
return null; |
|
} |
|
|
|
var obj = data.Count == 0 ? null : data.First().Value; |
|
|
|
if (obj != null && levels > 1) |
|
{ |
|
for (var i = levels - 1; i >= 1; i--) |
|
{ |
|
var current = obj as IReadOnlyDictionary<string, object>; |
|
obj = current!.Count == 0 ? null : current.First().Value; |
|
if (obj is null) |
|
{ |
|
return null; |
|
} |
|
} |
|
} |
|
|
|
return obj; |
|
} |
|
|
|
private void UpdateContextData( |
|
IResolverContext context, |
|
IReadOnlyQueryResult result, |
|
NameString schemaName) |
|
{ |
|
if (result.ContextData is { Count: > 0 }) |
|
{ |
|
var builder = ImmutableDictionary.CreateBuilder<string, object?>(); |
|
builder.AddRange(context.ScopedContextData); |
|
builder[WellKnownProperties.SchemaName] = schemaName; |
|
builder.AddRange(result.ContextData); |
|
context.ScopedContextData = builder.ToImmutableDictionary(); |
|
} |
|
else |
|
{ |
|
context.SetScopedValue(WellKnownProperties.SchemaName, schemaName); |
|
} |
|
} |
|
|
|
private const string _originalMsgErrorField = "originalMessage"; |
|
private const string _remoteErrorField = "remote"; |
|
private const string _schemaNameErrorField = "schemaName"; |
|
|
|
private static void ReportErrors( |
|
NameString schemaName, |
|
IResolverContext context, |
|
IEnumerable<IError> errors) |
|
{ |
|
foreach (var error in errors) |
|
{ |
|
var builder = ErrorBuilder.FromError(error) |
|
.SetMessage($"Node resolution failed for some reason. See logs in DB. Schema: {schemaName.Value}.") |
|
.SetExtension(_originalMsgErrorField, error.Message) |
|
.SetExtension(_remoteErrorField, error.RemoveException()) |
|
.SetExtension(_schemaNameErrorField, schemaName.Value); ; |
|
|
|
context.ReportError(builder.Build()); |
|
} |
|
} |
|
|
|
private static IReadOnlyDictionary<(NameString, NameString), NameString> GetNameLookup( |
|
ISchema schema) |
|
{ |
|
if (schema.ContextData.TryGetValue(WellKnownContextData.NameLookup, out var value) && |
|
value is IReadOnlyDictionary<(NameString, NameString), NameString> dict) |
|
{ |
|
return dict; |
|
} |
|
|
|
throw RequestExecutorBuilder_NameLookupNotFound(); |
|
} |
|
|
|
private static InvalidOperationException RequestExecutorBuilder_NameLookupNotFound() => |
|
new InvalidOperationException("A stitched schema must provide a name lookup"); |
|
} |
|
} |
@benmccallum There is a typo in the readme - should be 12.9 and 13