Code Shopify About

Shopify App and Theme Development tutorials for those who are familiar with code and want to dive into Shopify.

Sign up for Shopify

Using the Shopify GraphQL Admin API with Rails

Wednesday, Jan 12, 2022

Using the GraphQL Admin API to Upload files

I found this awesome blog post about creating staged uploads with the GraphQL admin api. (it also has a github for reference). The React portion of the tutorial was great, but it assumed that the backend was node.js created by the Shopify CLI. I realized I had not yet used the GraphQL Admin API in my Rails app. So this tutorial will be building off of the Staged Uploads tutorial and explaining the Rails side of the equation.

Required Gems

This tutorial is assuming that you have a Shopify app built on Ruby on Rails using the shopify_app gem.

The 3 gems you'll need:

			
 gem 'shopify_app', '15.0.0'
 gem 'graphql-client'
 gem 'graphql'
			
		

You'll likely have the shopify_app and the graphl-client gem already in your gemfile. So the one you'll need to add is the graphl gem. Here's the github for reference.

Once you add the graphl gem to your gem file and run a bundle install you'll want to run the generator. Run the following command in your terminal.

			
 $ rails generate graphql:install
			
		

This generator creates a bunch of files. We'll be looking at the app > controllers > graphql_controller specifically. The first thing we want to do is make sure this controller is inheriting from the Authenticated controller rather than the regular Application controller.

We'll also want to uncomment the protect_from_forger line as we'll be accessing this controller from the front end.

			
 class GraphqlController < ShopifyApp::AuthenticatedController

  # If accessing from outside this domain, nullify the session
  # This allows for outside API access while preventing CSRF attacks,
  # but you'll have to authenticate your user separately
  protect_from_forgery with: :null_session

  def execute
    ...
  end

  ... 

 end
	
		

Another thing to note is that a graphql route has been added to the config > routes file. Note that this routes to the Graphql Controller and the execute method.

			
 post "/graphql", to: "graphql#execute"
			
		

Create Shopify Graphql Schema

Next we'll need to create a Shopify graphql schema that corresponds with the version of the api you're using. The file gets created in the db folder db > shopify_graphql_schemas > 2021-07.json (where the json file name corresponds to the api version you're using in your app). Here's the reference in the docs.

To create this schema run the following command in the terminal. Note that the Shop domain can be any shop - most likely the one you're testing on. If you're following convention the ACCESS_TOKEN is likely saved in your database in Shop.shop_token.

			
  $ rake shopify_api:graphql:dump SHOP_DOMAIN=SHOP_NAME.myshopify.com ACCESS_TOKEN=shpat_xxxxx API_VERSION=2021-07
			
		

Required Scopes

In order to be able to access the files we'll need to make sure we have set the read_files, write_files scopes. We set this in the config > initializers > shopify_app.rb file

			
 config.scope = "read_files, write_files"
			
		

Don't forget to restart your server for the initializer. Also, you'll need to uninstall and re-install the app to accept the new permissions.

The Execute Method

The generator created the execute method with the first 4 variables already build in and parsed the standard params coming into the app. The rest of the generator assumes the queries are coming from inside the app. This is where the graphql-client gem comes in. You'd use that syntax to make a call outside of the Rails app. However, the shopify_api gem is built upon that gem and you can use that to make the call.

First we create a client, then we parse the query. We then run the query method on the client with the parsed query and the variable (if applicable) as the parameters and then return the data as the result.

			
def execute

  variables = prepare_variables(params[:variables])
  query = params[:query]
  operation_name = params[:operationName]
  context = {
    # Query context goes here, for example:
    # current_user: current_user,
  } 

  client = ShopifyAPI::GraphQL.client

  incoming_query = ShopifyAPI::GraphQL.client.parse(query)

  result = client.query(incoming_query, variables: variables).data

  render json: result

rescue StandardError => e
  raise e unless Rails.env.development?
  handle_error_in_development(e)
end
			
		

Back to the Frontend

Here's the font upload component for the React portion of the app. I just want to point out a few things that differ from the original tutorial and call out a few things.

We're using @apollo/client here rather the the apollo boost, and now gql and useMutation are apart of that node module.

Here we're uploading to the files rather than a collection.

In the original tutorial the actual queries were named. For example mutation generateStagedInput($input: [StagedUploadInput!]!). This seems to break the Rails queries, so I took them out and now it's just mutation ($input: [StagedUploadInput!]!).

Note, when calling our queries, we add an input object called variables for the query inputs. This corresponds with the variables variable in the controller and so it must be called variables.

			
import React, { useState } from "react";
import { DropZone, Thumbnail, Spinner } from "@shopify/polaris";
import { gql, useMutation } from '@apollo/client';
import { NoteMinor } from '@shopify/polaris-icons'


const STAGED_UPLOADS_CREATE = gql`
  mutation ($input: [StagedUploadInput!]!) {
    stagedUploadsCreate(input: $input) {
      stagedTargets {
        url
        resourceUrl
        parameters {
          name
          value
        }
      }
      userErrors {
        field
        message
      }
    }
  }
`;

const FILE_UPDATE = gql`
  mutation ($files: [FileCreateInput!]!) {
    fileCreate(files: $files) {
      files {
        fileStatus
      }
      userErrors {
        field
        message
      }
    }
  }
`

function FontUpload(props) {
  const [loading, setLoading] = useState(false);
  const [generateStagedUploads] = useMutation(STAGED_UPLOADS_CREATE);
  const [fileUpdate] = useMutation(FILE_UPDATE);

  const handleDropZoneDrop = async ([file]) => {

    setLoading(true)

    try {
      let { data } = await generateStagedUploads({
        variables: {
          "input": [
            {
              "fileSize": file.size.toString(),
              "filename": file.name,
              "httpMethod": "POST",
              "mimeType": file.type,
              "resource": "FILE"
            }
          ]
        }
      })

      const [{ url, parameters, resourceUrl }] = data.stagedUploadsCreate.stagedTargets
      
      const formData = new FormData()

      parameters.forEach(({ name, value }) => {
        formData.append(name, value)
      })

      formData.append('file', file)


      try {
        const response = await fetch(url, {
          headers: props.headers,
          method: 'POST',
          body: formData,
        })

        if (!response.ok) {
          throw ('File could not be uploaded.')
        }

        const key = parameters.find(p => p.name === 'key')

        let { newdata } = await fileUpdate({
          variables: {
            "files": [
              {
                "alt": "test",
                "contentType": "FILE",
                "originalSource": resourceUrl
              }
            ]
          }
        })


        if (newdata) {
          throw ('Collection image could not be updated.')
        }
      } catch (err) {
        props.setToastMessage(err)
      }

      setLoading(false)

    } catch (e) {
      // do something with the error here
      console.log("ERROR", e)
    }
		
  }

  return (
    
      {loading ?  : }
    
  );
}
export default FontUpload;
			
		

How it works

The STAGED_UPLOADS_CREATE query takes the file and saves it to a Shopify AWS bucket, the FILE_UPDATE query saves the file to a specific shop via the AWS file url. This is a big πŸŽ‰winπŸŽ‰ because it takes the file processing off our plate and according to the article is more performant.

more posts