Search code examples
pythondjangoreactjsgraphqlapollo

How to return PDF file in an Graphql mutation?


I'm using React and Graphql on the frontend and Django and Graphene on the backend.

I want to be able to download a pdf file of a report. I try to do it using mutation as follows:

const [createPdf, {loading: createPdfLoading, error: createPdfError}] = useMutation(CREATE_PDF)
const handleCreatePDF = async (reportId) => {
        const res = await createPdf({variables: {reportId: parseInt(reportId) }})
        debugger;
    };

export const CREATE_PDF = gql`
    mutation ($reportId: Int!) {
        createPdf (reportId: $reportId){
            reportId
        }
    }
`;

On the backend I have something like this:

class CreatePDFFromReport(graphene.Mutation):
    report_id = graphene.Int()

    class Arguments:
        report_id = graphene.Int(required=True)

    def mutate(self, info, report_id):
        user = info.context.user

        if user.is_anonymous:
            raise GraphQLError("You are not logged in!")

        report = Report.objects.get(id=report_id)
        if not report:
            raise GraphQLError("Report not found!")

        if user != report.posted_by:
            raise GraphQLError("You are not permitted to do that!")

        html_string = render_to_string('report.html', {'report_id': 1})

        pdf_file = HTML(string=html_string)
        response = HttpResponse(pdf_file, content_type='application/pdf')
        response['Content-Disposition'] = 'attachment; filename="rapport_{}"'.format(report_id)
        return response


        # return CreatePDFFromReport(report_id=report_id)

When I uncomment return CreatePDFFromReport(report_id=report_id) it works fine.

But I want to return pdf file.

Is there any possibility to do that?

Thanks.


Solution

  • It can't be done by mutation only.

    You can't return file(binary)/headers(mime)/etc (needed to be handled as download request behaviour by browser) using json'ed communication. GraphQL request and response are in json format.

    Solution

    Workflow:

    • call a mutation (passing parameter in variables)
    • resolver creates file content and saves it somewhere (file storage on server or s3)
    • resolver returns a file id to be a part of download url or full url (string) to target file

    id or url in this case should be a part of required mutation response

     mutation ($reportId: Int!) {
         createPdf (reportId: $reportId){
            reportDownloadId
        }
     }
    
    • mutation result (and our reportDownloadId) is available in data

    data (result/response) from mutation can be accessed in two ways:

    ... by { data } :

    const [addTodo, { data }] = useMutation(ADD_TODO);
    

    ... or by onCompleted handler:

    addTodo({ 
      variables: { type: input.value },
      onCompleted = {(data) => {
        // some action with 'data'
        // f.e. setDownloadUrl(data.reportDownloadId)
        // from 'const [downloadUrl, setDownloadUrl] = useState(null)'
      }}
    });
    

    Both methods are described in docs

    Both methods will allow you to render (conditionally) a download button/link/component, f.e.

    {downloadUrl && <DownloadReport url={downloadUrl}/>}
    
    {downloadUrl && <a href={downloadUrl}>Report ready - download it</a>}
    
    // render download, share, save in cloud, etc.
    

    Handler method can be used to automatically invoke downloading file request, without rendering additional button. This will look like one action while technically there are two following requests (graphql mutation and download). This thread describes how to handle downloads using js.