Cerulean
Cerulean

Reputation: 6013

How can one upload an image to a KeystoneJS GraphQL endpoint?

I'm using TinyMCE in a custom field for the KeystoneJS AdminUI, which is a React app. I'd like to upload images from the React front to the KeystoneJS GraphQL back. I can upload the images using a REST endpoint I added to the Keystone server -- passing TinyMCE an images_upload_handler callback -- but I'd like to take advantage of Keystone's already-built GraphQL endpoint for an Image list/type I've created.

enter image description here

I first tried to use the approach detailed in this article, using axios to upload the image

const getGQL = (theFile) => {
    const query = gql`
  mutation upload($file: Upload!) {
    createImage(file: $file) {
      id
      file {
        path
        filename
      }
    }
  }
`;
// The operation contains the mutation itself as "query"
    // and the variables that are associated with the arguments
    // The file variable is null because we can only pass text
    // in operation variables
    const operation = {
        query,
        variables: {
            file: null
        }
    };
    // This map is used to associate the file saved in the body
    // of the request under "0" with the operation variable "variables.file"
    const map = {
        '0': ['variables.file']
    };

    // This is the body of the request
    // the FormData constructor builds a multipart/form-data request body
    // Here we add the operation, map, and file to upload
    const body = new FormData();
    body.append('operations', JSON.stringify(operation));
    body.append('map', JSON.stringify(map));
    body.append('0', theFile);

    // Create the options of our POST request
    const opts = {
        method: 'post',
        url: 'http://localhost:4545/admin/api',
        body
    };
// @ts-ignore
    return axios(opts);

};

but I'm not sure what to pass as theFile -- TinyMCE's images_upload_handler, from which I need to call the image upload, accepts a blobInfo object which contains functions to give me

enter image description here

The file name doesn't work, neither does the blob -- both give me server errors 500 -- the error message isn't more specific.

I would prefer to use a GraphQL client to upload the image -- another SO article suggests using apollo-upload-client. However, I'm operating within the KeystoneJS environment, and Apollo-upload-client says

Apollo Client can only have 1 “terminating” Apollo Link that sends the GraphQL requests; if one such as apollo-link-http is already setup, remove it.

I believe Keystone has already set up Apollo-link-http (it comes up multiple times on search), so I don't think I can use Apollo-upload-client.

Upvotes: 3

Views: 2429

Answers (3)

Ralexrdz
Ralexrdz

Reputation: 833

The easies way for me was to use graphql-request. The advantage is that you don't need to set manually any header prop and it uses the variables you need from the images_upload_handler as de docs describe.

I did it this way:

const { request, gql} = require('graphql-request')

const query = gql`
  mutation IMAGE ($file: Upload!) {
    createImage (data:
      file: $file,
    }) {
      id
      file {
        publicUrl
      }
    }
  }
`

images_upload_handler = (blobInfo, success) => {
//                             ^        ^   varibles you get from tinymce
  const variables = {
    file: blobInfo.blob()
  }

  request(GRAPHQL_API_URL, query, variables)
    .then( data => {
      console.log(data)
      success(data.createImage.fileRemote.publicUrl)
    })
}

For Keystone 5 editorConfig would stripe out functions, so I clone the field and set the function in the views/Field.js file.

Good luck ( ^_^)/*

Upvotes: 1

Ken Kauksi
Ken Kauksi

Reputation: 11

I used to have the same problem and solved it with Apollo upload link. Now when the app got into the production phase I realized that Apollo client took 1/3rd of the gzipped built files and I created minimal graphql client just for keystone use with automatic image upload. The package is available in npm: https://www.npmjs.com/package/@sylchi/keystone-graphql-client

Usage example that will upload github logo to user profile if there is an user with avatar field set as file:

import {  mutate } from  '@sylchi/keystone-graphql-client'

const  getFile  = () =>  fetch('https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png',
{
    mode:  "cors",
    cache:  "no-cache"
})
.then(response  =>  response.blob())
.then(blob  => {
    return  new  File([blob], "file.png", { type:  "image/png" })
});

getFile().then(file  => {

const  options  = {
    mutation: `
        mutation($id: ID!, $data: UserUpdateInput!){
            updateUser(id: $id, data: $data){
                id
            }
        }
    `,
    variables: {
        id:  "5f5a7f712a64d9db72b30602", //replace with user id
        data: {
            avatar:  file
        }
    }
}

mutate(options).then(result  =>  console.log(result));

});

The whole package is just 50loc with 1 dependency :)

Upvotes: 1

Daniel Rearden
Daniel Rearden

Reputation: 84687

The UploadLink is just a drop-in replacement for HttpLink. There's no reason you shouldn't be able to use it. There's a demo KeystoneJS app here that shows the Apollo Client configuration, including using createUploadLink.

Actual usage of the mutation with the Upload scalar is shown here.

Looking at the source code, you should be able to use a custom image handler and call blob on the provided blobInfo object. Something like this:

tinymce.init({
  images_upload_handler: async function (blobInfo, success, failure) {
    const image = blobInfo.blob()
    try {
      await apolloClient.mutate(
        gql` mutation($image: Upload!) { ... } `,
        {
          variables: { image }
        } 
      )
      success()
    } catch (e) {
      failure(e)
    }
  }
})

Upvotes: 1

Related Questions