The power of GraphQL: optimizing performance - jcmings/sn GitHub Wiki
Alright. Let's talk GraphQL (in the context of ServiceNow). I am not an expert on GraphQL, but I have seen it save minutes in load times. Let me explain how it can be helpful:
The benefit of GraphQL
GraphQL allows you to make server-side calls on demand. It also allows you to specify exactly which fields you need. Think about this for a second. We'll apply it to two real world scenarios that I have faced in two separate ServiceNow implementations. I'll describe each in a section, and then talk about how GraphQL did, and can, optimize performance.
In a career pathing application
One of the implementations I supported was for a career pathing application. You navigate to a service portal page, where you can see a list of 900+ skills that you can tag to your profile. After you tag the skills to your profile, some behind-the-scenes magic performs some calculations, and recommends a few different "job functions" that may be relevant to you. For example, if I tagged that I was good at 508 Compliance and User Experience, I might be recommended a "job function" of being a 508 Compliance Tester. Or, if I said that I was good at JavaScript and HTML, I might be recommended a Developer job function.
Seems simple enough. But let's break this down. When you load that service portal page, a GlideRecord query is made to load 900+ skills. Immediately, because that GlideRecord query is in the server-script of a widget on the page. That's a pretty big call to the server, to get and return 900 records. Now imagine there are 20,000 users who are all loading this page at the same time -- you can probably imagine the performance implications.
So to get around this potential performance issue, we started using GraphQL. Now, when you load the page, maybe you click on a button that says "show me the skills," and the skills get loaded. This puts the user in control of when that query to the database is made. You are basically calling an API to get the information on-demand instead of having that information load no matter what. To take this example further -- after I've tagged the skills that are relevant to me -- I click on a button that says "show me my relevant job functions." We are able to execute the behind-the-scenes magic on-demand here and get our results when we need them, instead of when we don't.
In a records page
Another implementation is hoping to show the user a big list of records that they can work on. The data is displayed in a data table with three tabs:
- Assigned to me
- Assigned to my team
- Unassigned
Presently, when the page is loaded, a GlideRecord query grabs all of the cases and divides the results up into the tabs based on the assignee. Simple enough. However, once this starts to scale, it's likely going to run into performance issues. Imagine if there are 1000 unassigned cases -- this means we have to load and return 1000+ records every time.
So what's a way to get around this big call to the server every time the page loads? Make the server-side call happen on-demand (i.e. when a tab is clicked). Using an onClick event, we can send the server-side call to the database, and return those 1000 unassigned cases (or even set a lower limit to return) only when they are needed.
So let's see this in action...
Setting up GraphQL
Fortunately, GraphQL is already set up in your instance on the GraphQL APIs [sys_graphql_schema]
table. All you have to do is create a few records and we'll be good as gold. So let's use a fictional example, where we want to click a button and see a list of cases, and actually implement GraphQL.
All of the code that powers the below will be included in a separate wiki post. You can find the link at the bottom of the document.
What this will look like
Here's what our future solution will look like, on page load:
And once we click the button, we'll see a list of Incidents [incident]
:
Setting this up: first, the GraphQL API
First things first, I'm setting up my resolver on the GraphQL API [sys_graphql_schema]
table.
I've chosen a name -- Incident Return GraphQL -- for this. The Schema namespace and Application namespace will auto-populate. We'll need to know the Schema namespace and Application namespace later.
The Schema itself -- we'll touch on in a sec. Before we do that, let's talk through the bottom-half of the page:
I am not requiring ACL authorization for this. You could, and you may want to.
There are a few related lists. We will talk through the first and third tabs later in this post.
Setting up the schema
There are two types of requests we can make using GraphQL: a query and a mutation. Remember, this is an API, so we're able to retrieve and modify information if we want to. Simply put, a query will GET information, and a mutation can do everything else (update, insert, delete). We aren't going to demo any mutations in this post, but they follow a pretty similar structure to the query stuff.
I've left comments along each of these lines with some more info. Read through it, and I'll meet you below the screenshot.
The syntax of the schema is basically input: output
. For the examples towards the bottom of the screenshot, our inputs are field names. These always match system name, AKA column name, for a field in the database. On line 37, for example, we see for the sys_id field, we are getting back an object (DisplayableString) of its value and display value. (In this example with sys_id, both the value and display_value will obviously be the same. But for our assigned_to input, on line 24, we will eventually get a value and display_value that are different: a sys_id and a user's display name.)
The output can have varying types. In the example above, on line 16, you can see we created a type called DisplayableString. This is so we can access both a value and display_value. Both of these are of type String. It isn't necessary to use DisplayableString like I did, but that's what I was taught to do, and I like it.
On line 8, we are saying that we have a query set up called getAllRecords. That query will return a list of Incidents. The type Incident gets defined on line 21. So when we query getAllRecords, we'll essentially receive back an array of objects with sys_id's, numbers, assignees, and short descriptions.
One last thing to touch on here is why we have a UserReference object AND a User object. When we query for our incidents in the getAllRecords query, we're GlideRecord-ing into Incident [incident]
. So we have all of the fields (that we asked for) with their values and display values. This includes assigned_to -- we have its value and display value, thanks to lines 32 and 33. These two lines (32 and 33) will typically be enough -- the sys_id and name of the user. Pretend for a second that line 31, reference: User
, was not there. If this was the case, that means we've only made one hit onto the database -- one GlideRecord, into Incident [incident]
.
Sometimes we may need to go a layer deeper and get values from the actual User [sys_user]
record. Maybe we need just their first name -- we don't want it combined with their last name. This is why we have line 31. Line 31 pretty much signifies that we're going to hit the database a second time. So we'll GlideRecord into both Incident [incident]
and User [sys_user]
. If we didn't have that reference: User
field, we wouldn't have to make this second hit on the database. In the example we're using for this post, we actually don't need to GlideRecord further into the user, since all we need is the user's sys_id (read: line 32), but I'm including it as an example anyway.
Now, let's talk through how we actually make line 8, the getAllRecords query, work.
Setting up a GraphQL Scripted Resolver
Alright, so we defined some schema early on. We thought about what types of info we want to eventually get back. Ultimately, we're returning a list of incidents, and we want certain fields from that incident. As you saw in the final-state screenshot, all we really need are the incident's number and short description, but we're actually grabbing more than that (as evidenced by the schema just above this section). Anyway, we will give power to our query by creating a scripted resolver:
Opening that record, we see...
Server-side script! This is where we plug in the server-side code that we'd normally put into a widget. So this kinda makes GraphQL a script include, if you think about it.
In this example, I'm looking for P4 incidents (just to cut down the list) and returning the GlideRecord. You could create an object if you wanted and push in the value of different fields from the incident, but this works just fine for me, and saves me some typing.
Setting up a GraphQL Resolver Mapping
So we've created the resolver, but now we need to connect the pieces. On the third tab of the related lists on our GraphQL API record, called GraphQL Resolver Mappings, we'll create a relationship between the resolver we just created and the query in our schema.
That record just looks like this:
So it basically just tells the system: when you see the query getAllRecords, run this server script, and output the Incident data.
Setting up the client script
So now we're in a custom widget, in the Widget Editor, and we're going to bring this to life. We have a button in our HTML which has an ng-click:
And that triggers the function lookupRecords:
This function calls the GraphQL API, passing through a few parameters, and gets the data back in the variable returnedData. The screenshot is cropped on line 8 because it's really long. Also, note that we are passing $http into the controller.
So what's going into line 8? Well, we have the Schema namespace and Application namespace from earlier, we have the query getAllRecords, and then we have all of the fields we wanted to get info on in the GraphQL grammar. Note: if we wanted to do a mutation instead, we'd replace 'query { x700455 { incidentReturnGraphQL...
with 'mutation { x700455 { incidentReturnGraphQL...
.
Here's my full query string, in a beautified format, which ultimately is condensed into one line for the client script. A quick note -- you can use a tool like GraphQL Beautifier to convert this from one line to multiple (for readability purposes), and then you can paste the formatted output into your URL bar, re-copy it, and paste it into a client script to return it to one line.
query {
x700455 {
incidentReturnGraphQL {
getAllRecords {
sys_id {
value
display_value
}
number {
value
display_value
}
assigned_to {
reference {
sys_id {
value
display_value
}
user_name {
value
display_value
}
first_name {
value
display_value
}
last_name {
value
display_value
}
}
value
display_value
}
short_description {
value
display_value
}
}
}
}
}
This looks like a lot, but all it really is is the schema we worked on earlier. We just walk through each field, and the types of outputs we want for each. Value, display_value. We want the sys_id returned, with its value and display_value. Same thing with number and short_description. With assigned_to, we needed to go a few layers deeper, because in our schema we defined that we would return a UserReference and within that, a User.
I made this graphic to try to help explain the layers. We have our greater Incident object, which is supported to return a field called assigned_to, which has its own output type of UserReference. UserReference itself will return its own reference, a value, and display_value (as pictured in green). That reference to User (in orange) has its own set of data that it will return.
But how do we get the user data?
So our getAllRecords query is supposed to return Incidents [incident]
. That much should be clear by now. But it's also supposed to return the person assigned_to the case. Because this is a reference field, we need to create another scripted resolver and mapping.
So we're creating a scripted resolver to getUser. In this resolver, we're using a function called env.getSource()
to extract the sys_id of the assigned_to field, which we'll use to perform a GlideRecord lookup on the User [sys_user]
table. The getSource()
function essentially jumps up one level in the schema to get you a value. We are using .value
here as well -- which I'll explain in a section below.
And we also create a mapping to say that whenever a query indicates we want the reference UserReference, run the server script in the above scripted resolver. Essentially, we're building in a cascade. So now, when we call our original query, getAllRecords, we'll automatically trigger the getUser query as well.
getSource()
More on getSource()
allows us to jump one layer up and grab a value. In our particular example, we're doing a getSource().value
, but you won't necessarily always have to append .value
to your function. Because of how I set up my schema, I did, and I'll explain how you can figure out what you need to do below.
First things first, I am in my getUser resolver. I know I need to be here because this query cascades from my original query, getAllRecords* (where we get the Incident data, including... you guessed it, the assigned_to user). On line 2, I've created a variable called test
and set it to env.getSource()
. I've also set a breakpoint on this line by clicking to the left of the line marker.
The next thing I'm going to do is click on the little bug icon -- far right, highlighted in a red box. That's going to pop-up our script debugger which will help us identify the objects returned to us.
I'll then go to my widget and click on the Load records button:
When I do that, my script debugger will show that the script was fired. Because we set that breakpoint, it will break on line 2, and we'll need to step into the next function by clicking the down arrow highlighted in a red box here:
Once we open up our test object, we can see that the value field is storing a sys_id. This is what we want, as this is what the getUser function is expecting in the variable userId
. Therefore, this tells us that we need to append .value
to getSource()
.
Now that we've figured this out, we can remove our breakpoint, remove our line 2, and save our record. We'll be good to go using getSource().value
for our function.
(Optional) Passing through a parameter
In the example for getUser above, we saw the function env.getSource()
being used. There is also a function called env.getArguments()
that we can use if we wanted to pass something in via the client script. In our main example, we don't need this function, since we're just clicking a button. But what if we wanted to choose something from a dropdown, and pass that value through to the GraphQL API?
In our career pathing application example, this could look like the following: a manager wants to look up the suggested job functions for a specific user. They choose the user in the dropdown. Then, the suggested job functions appear. How would this work?
Well, in our widget, we would have an ng-click
event where we'd pass through a parameter. In our client script, where we actually trigger the GraphQL with the long query string, we'd also pass through that parameter, using some clever quotations and concatenations to pass through the value as a string (which we'll touch on just below this screenshot):
We'd also have to update our schema in our GraphQL API record so that it accepts a parameter. We include the parameter name and define its type. Note that this parameter name must match what we used in the client script (well technically, vice versa, but I'm showing you out of order here).
And then in our getAllRecords resolver, we'd receive that parameter using getArguments()
.
Note: I am skipping an important if
check here to check if the user_sys_id was even passed through. I'd recommend including that.
Getting this to actually display
Ok, we've got all the back-end pieces stood up. Now we just need to set up the ng-repeat
on our table to make this actually display in our widget.
In the client script, we're getting the data back as returnedData. This is an array of objects. We're storing that in c.incidents and then accessing that array in our HTML. For cleanliness, we've set our table up so that if there's no data, we see a nice "No data" message. We do this by checking for the presence or length of c.incidents.
In our ng-repeat
, we're looping through all of the items in the array. Each incident in c.incidents. We then say we want to display the display_value of the number. Remember, we set up a DisplayableString earlier, so we need to dot-walk one layer deeper than just incident.number.
That's pretty much it... if you are running into issues, I'd recommend checking out the debugging tips below. If you're not running into any issues, still check that out.
Debugging tips
Developer console
Probably the most helpful tool for you when you're running into issues will be the Network tab of your Developer Console. When you trigger the GraphQL -- like I have done by clicking the button -- you will see a row called graphql appear in the console.
Click into that row and you can see a bunch more details. I like to pay particular attention to the Preview tab. This is where you can quickly see if you have any errors. Usually, the response will tell you exactly what your error is (and if you're unsure, you can Google it). For instance, you can see I have an error here because I spelled my query name wrong in my client script:
Once that's corrected (and the page has been reloaded), my Preview tab looks different. In this screenshot, you can see my query was a success, and I have my response object:
You can also refer to the Response tab to see the formatted response object:
Script debugger
Similar to the developer console, this one will be super helpful for most folks. I explain how to use the debugger in detail in the more on getSource()
section above. You can set breakpoints and see objects returned in real-time.
This is particularly helpful if you have a cascaded query, like my getUser query.
GraphQL records
Sometimes, your Network tab may show you getting a response object, but there may be an issue with one of the fields in the object. For example, while I was building this out, I got response objects that showed no errors at the top-level, but when I expanded the object tree/viewed the data in the Response tab of the developer console, I saw null
as the response in my assigned_to field.
What this told me is that I needed to validate that my resolvers and mappings were all set up correctly. So I double-checked my resolver was there and then realized that I didn't create a mapping yet. This is a super simple mistake, but it can drive you crazy. So make sure to check you've created mappings!
Also, make sure to check that your schema makes sense. This is for both the schema of the GraphQL API record and for your query string in your client script. Remember that you can use the GraphQL Beautifier tool to see if your grammar is correct. You'll also probably see a warning in your client script code editor if you're missing a }
or have an extra "
somewhere. When in doubt, know that you can use a tool like ChatGPT to create your query string for you. Simply paste in your schema and ask it to convert to a GraphQL query string.
Server script
Check to see if your server script works in Background Scripts! This is another prominent source of error.
Google is your friend.
ChatGPT
ChatGPT is your best friend.
The code for all of this
I've pasted all of the code for this project in this wiki post. Know that you will have to swap out things like the Schema namespace and Application namespace with your own values.
Good luck! And a special shout out to my colleague Robert P. for his expertise and guidance.