Free computer code on screen

GraphQL Query Generation In Salesforce Apex

GraphQL has the capability to fetch data from numerous sources within a solitary query. This diminishes the need for multiple HTTP requests, thereby lowering latency and enhancing performance, particularly on mobile devices and those with low bandwidth. To retrieve data, we need to create SQL queries in GraphQL format. In this article, we’ll construct a GraphQL Query Generator within Salesforce Apex, enabling the generation of valid GraphQL queries. Additionally, we’ll utilize the generated SOQL to fetch data through the GraphQL API.

Steps to Develop a GraphQL Query Generator:

  • Create Apex Classes for the Query Generator.
  • Utilize the “Generate GraphQL Query” function to retrieve data.

Step 1: Develop Apex Classes for the Query Generator

In order to employ the GraphQL API, it’s necessary to provide a valid GraphQL query. Below is a fundamental query designed to fetch Account record data, extracting only the ID and Name fields.

query accounts
{
  uiapi
  {
    query 
    {
      Account 
      {
        edges
        {
          node 
          {
            Id
            Name {
              value
            }
          }
        }
      }
    }
  }
}

Let’s craft classes that dynamically generate a query resembling the one mentioned above, based on provided parameters, during runtime.

Apex Class Overview

GraphQLArgument

This class will create the filter condition for a graphQL query. Example is Account(where:{Name:{eq:"Demo App Test"}})

GraphQLNode

This class will create an object node and add fields and conditions to the query.

GraphQLQuery

This class will generate a query for a given object and fields.

// This class will use to add Where Condition in Query
public virtual class GraphQLArgument{
    public String name;
    public Object value; //string | GraphQLArgument | GraphQLArgument[]
    public Boolean isVariable;
    
    public GraphQLArgument(String name, Object value){
        this.name = name;
        this.value = value;
        this.isVariable = false;
    }
    
    public GraphQLArgument(String name, Object value, Boolean isVariable){
        this.name = name;
        this.value = value;
        this.isVariable = isVariable;
    }
    
    public virtual string build(){
        String qry = this.name + ':{eq:';
        if(isVariable || this.value instanceOf Integer || this.value instanceOf Decimal || this.value instanceOf Boolean){
            qry += String.valueOf(this.value);
        }else if(this.value instanceOf DateTime){
            qry += '\\"' + ((DateTime) this.value).format('yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'') + '\\"';
        }else if(this.value instanceOf GraphQLArgument){
            qry += '{' + ((GraphQLArgument) this.value).build() + '}';
        }else if(this.value instanceOf GraphQLArgument[]){
            String[] argsStrings = new String[]{};
                for(GraphQLArgument arg : (GraphQLArgument[]) this.value){
                    argsStrings.add(arg.build());
                    qry += '{' + String.join(argsStrings, ', ') + '}';
                }
            
        }else { 
            qry += '\\"' + String.valueOf(this.value) + '\\"';
        }
        qry +='}';
        return qry;
    }
}
public virtual class GraphQLNode {
    public String id;
    public String operation; 
    public Boolean typeFrament; 
    public Boolean isSubSelection; 
    public String alias;
    public GraphQLArgument[] args; 
    public Object[] children; 
    

    public GraphQLNode(String id){
        this.typeFrament = false;
        this.isSubSelection=false;
        this.args = new GraphQLArgument[]{};
        this.children = new Object[]{};
        this.id = id;
    }

    //returns the current node
    public GraphQLNode setId(String id){
        this.id = id;
        return this;
    }

    //returns the current node
    public GraphQLNode setOperation(String operation){
        this.operation = operation;
        return this;
    }

    //returns the current node
    public GraphQLNode setAlias(String alias){
        this.alias = alias;
        return this;
    }

    //returns the current node
    public GraphQLNode addFilter(GraphQLArgument arg){
        this.args.add(arg);
        return this;
    }

    public GraphQLNode addFilters(GraphQLArgument[] args){
        this.args.addAll(args);
        return this;
    }

    //returns the current node
    public GraphQLNode addSOQLField(Object children,boolean  isSubSelection){
        this.isSubSelection=isSubSelection;
        if(children instanceOf Object[]){
            this.children.addAll((Object[])children);
        }else{
            this.children.add(children);
        }
        return this;
    }

    public GraphQLNode setTypeFragment(Boolean isTypeFrag){
        this.typeFrament = isTypeFrag;
        return this;
    }

    public virtual string build(){
       String qry = '';

       if(string.isNotBlank(this.id)){
            qry+='query '+ this.id+'s'+' ';
			qry+='{';
            qry+='uiapi ';
            qry+='{';
			qry+='query ';
            qry+='{';
			qry+=this.id;
        }
        if(this.args.size() > 0){
            qry+='(where:';
			qry+='{';
            String[] argsStrings = new String[]{};
            for(GraphQLArgument arg : this.args){
                argsStrings.add(arg.build());
            }
           	qry += String.join(argsStrings, ', ') + '})';
        }
        qry+=' { edges ';
        qry+='{';
        qry+='node ';
        if(this.children.size() > 0){
            qry += '{';
            for(Object child : this.children){
                if(child instanceOf GraphQLNode){
                    qry += ((GraphQLNode) child).build();
                }else{
                    qry += ((String) child);
                    if(isSubSelection)
                    {
                        qry += '{value}';
                    }
                    qry += + '';
                }
            }
            qry += '}';
        }
        qry+='}}}}}';
        return qry;
    }
}
public class GraphQLQuery {
    public string query;
    public Object variables;
    public GraphQLQuery(string query, Object variables){
        this.query = query;
        this.variables = variables;
    }

    public GraphQLQuery(GraphQLNode node, Object variables){
        this.query = buildQuery(node);
        this.variables = variables;
    }

    public GraphQLQuery(GraphQLNode[] nodes, Object variables){
        this.query = buildQuery(nodes);
        this.variables = variables;
    }

    private static string buildQuery(GraphQLNode node){
        if(node.operation != null){
            return node.build();
        }
        return node.build();
        //return '{\n' + node.build() + '\n}';
    }

    private static string buildQuery(GraphQLNode[] nodes){
        String[] nodeStrings = new String[]{};
        for(GraphQLNode node : nodes){
            nodeStrings.add(node.build());
        }
        return '{\n' + String.join(nodeStrings, '\n') + '\n}';
    }
}

2. Implement the Generate GraphQL Query function to fetch data.


Let’s utilize the previously created classes to extract data from the GraphQL API. To initiate a GraphQL call, we must first generate an authentication token. Subsequently, we can access the GraphQL API to retrieve records.

The GraphQL API URL is contingent on the organization being used. For instance, a sample URL might appear as https://vagminecodex-dev-ed.my.salesforce.com/services/data/v57.0/graphql. During runtime, we can dynamically generate a GraphQL API URL employing an instance URL, which is created when the access token is generated.

Within the GraphQLController class, the following actions are performed:

  • Generation of an authentication token
  • Creation of a GraphQL query
  • Invocation of the GraphQL API to retrieve records using the query

To acquire the client ID and secret required for the token API call, it’s essential to generate a connected app within the Salesforce org. For guidance on creating a connected app, you can refer to the article “Generate Salesforce Authentication Token using Postman.”

The ExternalCallout class is a versatile template used for making external API calls. You can access the code for this class in the article titled “Generic Apex class for Calling External System.”

public class GraphQLController {
    
    public static SalesforceToken generateToken()
    {
        //Map of request parameters
            Map<String, String> ncParams=new Map<String, String> {
                'grant_type' => 'password',
                    'client_id' => '<connected api client id>', 
                    'client_secret' => '<connected api client secret>',
                    'username' => 'salesforcecodex@gmail.com',
                    'password' => '<password+security token>'
                    };
                        
      //Header Setting
        Map<String, String> headers=new Map<String, String> {
            'Content-Type' => 'application/x-www-form-urlencoded'
       	};
        // You can use Named Credential instead of direct Url
        // When using direct Url - put url in Remote Site Setting
        HttpResponse response=new ExternalCallout().post('https://login.salesforce.com/services/oauth2/token', ncParams,headers);
        
        try{
            SalesforceToken token =SalesforceToken.parse(response.getBody());
            return token; 
        }
        catch(Exception e){
            system.debug('Error :'+e);
        }   
        return null;
    }
    
    public static string buildGraphSOQL()
    {
      // Build Graph QL Query
      // It can be take input from dynamic values
       GraphQLNode node = new GraphQLNode('Account')
        .addFilter(new GraphQLArgument('Id', '0012v00003BVfn5AAD'))
        .addFilter(new GraphQLArgument('Name', 'Demo App Test'))
        .addSOQLField('Name',true)
        .addSOQLField('Website',true)
        .addSOQLField('Phone',true)
        .addSOQLField('BillingCity',true);
        GraphQLQuery qry = new GraphQLQuery(node, null);
       
        SalesforceToken token=generateToken();
        try
        {
            //Put URL in Remote Site Setting
            string url=token.instance_url+'/services/data/v57.0/graphql';
            HTTP http = new HTTP();
            HTTPRequest r = new HTTPRequest();
            r.setEndpoint(url);
            r.setMethod('POST'); 
            r.setHeader('Content-Type', 'application/json');
            r.setHeader('Authorization', 'Bearer '+token.access_token);
           	r.setBody('{"query":"{query}"}'.replace('{query}',qry.query));
            HTTPResponse response =http.send(r);
           	return response.getBody();
        }
        catch(Exception ex)
        {
            throw new CalloutException('Error in GraphQL API Call');
        }
    }
}

Test Output

The above code will generate a response similar to the below image.

Expected Errors

You may encounter errors similar to [Unexpected character (‘0’ (code 48)): was expecting a comma to separate OBJECT entries at [line:1, column:80] or a Validation error of type FieldUndefined: Field ‘query’ in type ‘Query’ is undefined @ ‘query’. These errors occur due to issues with the JSON format of the GraphQL query. Consider removing any unnecessary characters that may appear in the dynamically generated query.

Please ensure that the API URLs are added in the Remote Site Settings or configure a Named Credential for seamless access.