SimonCropp / GraphQL.Attachments

Provides access to a HTTP stream in GraphQL

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

GraphQL.Attachments

Build status NuGet Status

Provides access to a HTTP stream (via JavaScript on a web page) in GraphQL Mutations or Queries. Attachments are transferred via a multipart form.

See Milestones for release notes.

NuGet package

https://nuget.org/packages/GraphQL.Attachments/

PM> Install-Package GraphQL.Attachments

Usage in Graphs

Incoming and Outgoing attachments can be accessed via the ResolveFieldContext:

Field<ResultGraph>("withAttachment")
    .Argument<NonNullGraphType<StringGraphType>>("argument")
    .Resolve(context =>
    {
        var incomingAttachments = context.IncomingAttachments();
        var outgoingAttachments = context.OutgoingAttachments();

        foreach (var incoming in incomingAttachments.Values)
        {
            // For sample purpose echo the incoming request
            // stream to the outgoing response stream
            var memoryStream = new MemoryStream();
            incoming.CopyTo(memoryStream);
            memoryStream.Position = 0;
            outgoingAttachments.AddStream(incoming.Name, memoryStream);
        }

        return new Result
        {
            Argument = context.GetArgument<string>("argument"),
        };
    });

snippet source | anchor

Server-side Middleware

RequestReader instead of binding

When using Attachments the incoming request also requires the incoming form data to be parse. To facilitate this RequestReader is used.:

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    var cancel = context.RequestAborted;
    var response = context.Response;
    var request = context.Request;
    var isGet = HttpMethods.IsGet(request.Method);
    var isPost = HttpMethods.IsPost(request.Method);

    if (isGet)
    {
        var (query, inputs, operation) = readerWriter.ReadGet(request);
        await Execute(response, query, operation, null, inputs, cancel);
        return;
    }

    if (isPost)
    {
        var (query, inputs, attachments, operation) = await readerWriter.ReadPost(request, cancel);
        await Execute(response, query, operation, attachments, inputs, cancel);
        return;
    }

    response.Headers["Allow"] = "GET, POST";
    response.StatusCode = (int) HttpStatusCode.BadRequest;
}

snippet source | anchor

Query Execution

To expose the attachments to the queries, the attachment context needs to be added to the IDocumentExecuter. This is done using AttachmentsExtensions.ExecuteWithAttachments:

var result = await executer.ExecuteWithAttachments(options, attachments);

snippet source | anchor

Result Writing

As with RequestReader for the incoming data, the outgoing data needs to be written with any resulting attachments. To facilitate this ResponseWriter is used.

await readerWriter.WriteResult(response, result, cancel);

snippet source | anchor

Client - JavaScript

The JavaScript that submits the query does so through by building up a FormData object and POSTing that via the Fetch API.

Helper method for builgin post settings

function BuildPostSettings() {
    var data = new FormData();
    var files = document.getElementById("files").files;
    for (var i = 0; i < files.length; i++) {
        data.append('files[]', files[i], files[i].name);
    }
    data.append(
        "query",
        'mutation{ withAttachment (argument: "argumentValue"){argument}}'
    );

    return {
        method: 'POST',
        body: data
    };
}

snippet source | anchor

Post mutation and download result

function PostMutationAndDownloadFile() {

    var postSettings = BuildPostSettings();
    return fetch('graphql', postSettings)
        .then(function (data) {
            return data.formData().then(x => {
                var resultContent = '';
                x.forEach(e => {
                    // This is the attachments
                    if (e.name) {
                        var a = document.createElement('a');
                        var blob = new Blob([e]);
                        a.href = window.URL.createObjectURL(blob);
                        a.download = e.name;
                        a.click();
                    }
                    else {
                        resultContent += JSON.stringify(e);
                    }
                });
                result.innerHTML = resultContent;
            });
        });
}

snippet source | anchor

Post mutation and display text result

function PostMutationWithTextResult() {
    var postSettings = BuildPostSettings();
    return fetch('graphql', postSettings)
        .then(function (data) {
            return data.text().then(x => {
                result.innerHTML = x;
            });
        });
}

snippet source | anchor

Client - .NET

Creating and posting a multipart form can be done using a combination of MultipartFormDataContent and HttpClient.PostAsync. To simplify this action the ClientQueryExecutor class can be used:

namespace GraphQL.Attachments;

public class QueryExecutor
{
    HttpClient client;
    string uri;

    public QueryExecutor(HttpClient client, string uri = "graphql")
    {
        Guard.AgainstNullWhiteSpace(uri);

        this.client = client;
        this.uri = uri;
    }

    public Task<QueryResult> ExecutePost(string query, Cancel cancel = default)
    {
        Guard.AgainstNullWhiteSpace(query);
        return ExecutePost(new PostRequest(query), cancel);
    }

    public async Task<QueryResult> ExecutePost(PostRequest request, Cancel cancel = default)
    {
        using var content = new MultipartFormDataContent();
        content.AddQueryAndVariables(request.Query, request.Variables, request.OperationName);

        if (request.Action != null)
        {
            var postContext = new PostContext(content);
            request.Action?.Invoke(postContext);
            postContext.HeadersAction?.Invoke(content.Headers);
        }

        var response = await client.PostAsync(uri, content, cancel);
        var result = await response.ProcessResponse(cancel);
        return new(result.Stream, result.Attachments, response.Content.Headers, response.Headers, response.StatusCode);
    }

    public Task<QueryResult> ExecuteGet(string query, Cancel cancel = default)
    {
        Guard.AgainstNullWhiteSpace(query);
        return ExecuteGet(new GetRequest(query), cancel);
    }

    public async Task<QueryResult> ExecuteGet(GetRequest request, Cancel cancel = default)
    {
        var compressed = Compress.Query(request.Query);
        var variablesString = RequestAppender.ToJson(request.Variables);
        var getUri = UriBuilder.GetUri(uri, variablesString, compressed, request.OperationName);

        using var getRequest = new HttpRequestMessage(HttpMethod.Get, getUri);
        request.HeadersAction?.Invoke(getRequest.Headers);
        var response = await client.SendAsync(getRequest, cancel);
        return await response.ProcessResponse(cancel);
    }
}

snippet source | anchor

This can be useful when performing Integration testing in ASP.NET Core.

Icon

memory designed by H Alberto Gongora from The Noun Project

About

Provides access to a HTTP stream in GraphQL

License:MIT License


Languages

Language:C# 96.8%Language:HTML 3.2%