6

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.

Boky
  • 11,554
  • 28
  • 93
  • 163
  • @xadm And what is the best way to do that? Add it as an answer and I will accept it. – Boky Apr 15 '20 at 06:10

2 Answers2

11

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.

Allan Hortle
  • 2,435
  • 2
  • 20
  • 22
xadm
  • 8,219
  • 3
  • 14
  • 25
2

You can encode your PDF to Base64 and send it as a string. Then just decode it on the frontend.

Note that the Base64 uses 4 bytes to encode every 3 bytes (3MB file becomes 4MB string).

mr.Bondar
  • 21
  • 1