Graph access with only JS and Fetch
Postman is a popular tool for testing APIs. However, when testing an API like Optimizely Graph that I will be consuming in the front-end I prefer to do this with JS + Fetch.
This allows me to identify any quirks involved in consuming the API in JavaScript. The code can then be passed to the front-end team for use in the relevant framework, and JSON results can be saved for Storybook mock data.
Example
import projectConfig from "../../CMS12/appsettings.json" assert { type: "json" };
// Define the GraphQL query to fetch StandardPages in the "Acme" category with their associated form templates
const formQuery =
`fragment FormContainerBlock on FormContainerBlock { __typename FormRenderTemplate }
query MyQuery {
StandardPage( where: { PrimaryCategory: { Name: { eq: "Acme" } } } )
{ items { Name Url MainContent { ContentLink { Expanded { ...FormContainerBlock } } } } }
}`
// Wrap the query in the expected JSON structure for the Content Graph API Request Body
const queryWrapper = `{"query":${JSON.stringify(formQuery)}}`;
// Public key access - Review security implications before using outside of local development
const publicKey = projectConfig.Optimizely.ContentGraph.SingleKey;
const graphUrl = `https://cg.optimizely.com/content/v2?auth=${publicKey}`;
const outputData = (data) => {
const formPages = data.StandardPage.items
.map(i => {
return {
name: i.Name,
url: i.Url,
forms: i.MainContent
.filter(b => b.ContentLink.Expanded.FormRenderTemplate)
.map(b => b.ContentLink.Expanded.FormRenderTemplate)
};
})
.filter(i => i.forms?.length);
formPages.forEach(page => page.forms.forEach(f => console.log(f)));
}
const executeQuery = async () => {
return fetch(graphUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: queryWrapper
})
.then((response) => {
if (response.status >= 400) {
throw new Error(`Error fetching data`, {cause: response});
} else {
return response.json();
}
})
.then((data) => outputData(data.data))
.catch(async error => {
if (error.cause.status === 400) {
// Graph returns JSON on most errors. Normally this is caused by a query syntax error.
const result = await error.cause.json();
console.log(`CODE: ${result.code}`);
console.log(`DETAILS:\n${JSON.stringify(result.details)}`);
}
})
}
const result = await executeQuery();
console.log("Completed");
Skip to breakdown
How to run
First of all there are some prerequisites:
- Access to an Optimizely Graph index.
- Request a developer index.
We only need a public key for this example.
How to debug in VS Code
We can setup VS Code to debug the JavaScript. This gives us the usual access to variables to inspect and review.
Debug Current File
This should be added to the launch.json file in the .vscode folder in your source root. This allows you to run and debug the currently viewed JavaScript file.
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Current Opened File",
"program": "${file}"
}
]
}
If you are running a front-end framework then you will typically need to run the framework and attach the debugging using a standard configuration.
Breakdown
So I will do a breakdown of what is going on in this code.
Settings and Variables
import projectConfig from "../../CMS12/appsettings.json" assert { type: "json" };
// Define the GraphQL query to fetch StandardPages in the "Acme" category with their associated form templates
const formQuery =
`fragment FormContainerBlock on FormContainerBlock { __typename FormRenderTemplate }
query MyQuery {
StandardPage( where: { PrimaryCategory: { Name: { eq: "Acme" } } } )
{ items { Name Url MainContent { ContentLink { Expanded { ...FormContainerBlock } } } } }
}`
// Wrap the query in the expected JSON structure for the Content Graph API Request Body
const queryWrapper = `{"query":${JSON.stringify(formQuery)}}`;
// Public key access - Review security implications before using outside of local development
const publicKey = projectConfig.Optimizely.ContentGraph.SingleKey;
const graphUrl = `https://cg.optimizely.com/content/v2?auth=${publicKey}`;
First I am importing the config file from a project that contains the usual Optimizely Graph settings. You will want to target one with the developer settings if you have multiple configs.
Then we have the GraphQL query. This can be copy and pasted from the GraphiQL page. This example query finds the Form Blocks within the Main Content of the Standard Page Type. This was written for the Headless Forms BETA which nested the form data in a property of the Form Container Block.
The wrapper builds the standard request body that Optimizely Graph expects.
Finally we extract the single key from the shared config and build the URL.
CAUTION: See security concerns
Render Function
const outputData = (data) => {
const formPages = data.StandardPage.items
.map(i => {
return {
name: i.Name,
url: i.Url,
forms: i.MainContent
.filter(b => b.ContentLink.Expanded.FormRenderTemplate)
.map(b => b.ContentLink.Expanded.FormRenderTemplate)
};
})
.filter(i => i.forms?.length);
formPages.forEach(page => page.forms.forEach(f => console.log(f)));
}
This function handles the Graph results data. As we are working in JavaScript and the results are JSON we can work with this dynamically. Using the debugger we can inspect the data structure then access it as needed. In this example I am drilling down to the FromRenderTemplate. In the Headless Forms beta this was a blob of JSON containing all of the form elements and form settings.
For my testing I normally output to the console which will show up in VS Code. I would experiment mapping the data here using JSON.stringify() to dump data to console or view via the debugger inspector.
Fetch -> Then -> Catch Chain Function
const executeQuery = async () => {
return fetch(graphUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: queryWrapper
})
.then((response) => {
if (response.status >= 400) {
throw new Error(`Error fetching data`, {cause: response});
} else {
return response.json();
}
})
.then((data) => outputData(data.data))
.catch(async error => {
if (error.cause.status === 400) {
// Graph returns JSON on most errors. Normally this is caused by a query syntax error.
const result = await error.cause.json();
console.log(`CODE: ${result.code}`);
console.log(`DETAILS:\n${JSON.stringify(result.details)}`);
}
})
}
This is a fairly standard API call chain. If you drop the debugging it's very simple but we will get GraphQL syntax errors while experimenting, so this detects and logs these.
Fetch
The fetch sends the POST call with the wrapped query to a public URL. If you have secured your Graph index then you will also need to add an authentication header in the Fetch call.
For example:
return fetch(graphUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${projectConfig.Authorization_Token}`
},
body: queryWrapper
})
If you are working with an API that has a seperate call to request authorization then do that Fetch call, extract the token and pass to the main Fetch call. In production code you will want to cache the key based on lifetime the API states.
Then - Response
The first then checks the status code. Most APIs will return data on 4xx responses but this will often be a different format to the working response. So here we raise an error so we can handle it separately.
response.JSON() grabs the body for the success path.
Then - Data
Here we delegate the data to the output function.
Catch
In the catch function we expect to read the GraphQL errors so we log the data to the console.
Summary
This process can be used for any API you would consume on the front-end.
Graph is a good example as both the queries and data responses can be complicated and verbose. You will find nested data has multiple wrappers before you hit the expected properties. Testing from JS you can also see how missing data affects the use of the data.
Next Steps
Next time I will expand this explanation out to TypeScript with simple types that allow dynamic processing of child objects in data and strong typing for the rendering in templates.
References
Security Considerations
Note that many production implementations will want to control and obfuscate access to the graph data. This can be done in a number of ways. For example pushing the keys and access to Graph to server components or SSG the page or site.
Comments