menu
announcement

Spectrum is now read-only. Learn more about the decision in our official announcement.

gatsby-drupal-wg

Gatsby Working Group: Drupal - Drupal module: https://www.drupal.org/project/gatsby - Gatsby source plugin: https://www.gatsbyjs.org/packages/gatsby-source-drupal/

Channels
Team

Gatsby with Drupal GraphQL module (instead of JSONAPI) and Simple Oauth

April 27, 2019 at 4:27pm

Gatsby with Drupal GraphQL module (instead of JSONAPI) and Simple Oauth

April 27, 2019 at 4:27pm (Edited 3 years ago)

Drupal GraphQL Module

Being unable to connect gatsby to drupal via the gatsby-source-drupal plugin which uses jsonapi as a backend, I went with gatsby-source-graphql and installed the drupal graphql module instead. The module exposes an api endpoint at /graphql as well as a GraphiQL UI at /graphql/explorer. Out of the box, the graphql module provides practically all you need to return content from Drupal such as nodeQuery and nodeById schema queries. They allow you to return any specific content type or filter or sort by content type fields. Authentication is needed to have full graphql functionality from the gatsby client endpoint, and I setup simple oauth to accomplish this.

On the Gatsby side

On the gatsby side, I defined two env variables to store the url to my drupal server and the oauth bearer token required for authentication. here is how i configured gatsby-config:
{
resolve: "gatsby-source-graphql",
options: {
typeName: "Drupal",
fieldName: "drupal",
// Url to the drupal server eg. http://example.com/graphql
url: `${process.env.GATSBY_API_URL}`,
headers: {
"Authorization": `Bearer ${process.env.GATSBY_API_TOKEN}`
}
// refetchInterval: 30,
},
GATSBY_API_TOKEN is my simple oauth access token, see explanation below. The fieldName value can be really anything you like. The fieldName will be what you use in your graphql queries to reference your drupal server schema. It could be any random word, its just how gatsby's graphql will generate the schema. This allows you to have multiple graphql connections to various servers each with their own unique fieldName value. (eg:
query {
drupal {
nodeById(id: "2136") {
.....
}
}
}
Finally, a simple implementation of gatsby-node.js of createPages() allows you to have full control over how you generate content.
const path = require(`path`)
const { createArticles, articleFragment } = require('./src/controllers/Article')
const { createRecipes, recipeFragment } = require('./src/controllers/Recipe')
exports.createPages = async ({ actions, graphql }) => {
const articleTemplate = require.resolve('./src/templates/article.js')
const recipeTemplate = require.resolve('./src/templates/recipe.js')
const { createPage } = actions
const { data } = await graphql(`
query {
drupal {
articles: nodes(filter: {type: "article"}) {
results {
nid
...${articleFragment}
}
}
recipes: nodes(filter: {type: "recipe"}) {
results {
nid
...${recipeFragment}
}
}
}
}
`)
createArticles(data.drupal.articles.results, createPage, articleTemplate)
createRecipes(data.drupal.recipes.results, createPage, recipeTemplate)
}
The nodes query you see above is driven by a simple graphql view I setup using the graphql_views module although you can also use the nodeQuery query provided by the drupal graphql module. The filtering for nodeQuery is a bit more complex but also more powerful. I will likely switch over to using nodeQuery eventually, so as not to maintain a separate view nor require the graphql_views module. If you choose to use graphql_views, the process is straightforward, just like creating any view, just make sure to add a GraphQL display for it and render Entity instead of rendering Fields. That will allow you access to all entity fields instead of having to add them one-by-one in your view.

Performance

For performance reasons, I went against the usual strategy of having graphql queries within each of my content templates, where you pass in a simple nid value. Instead, I am pulling back all content in gatsby-node and passing data via context props to my content templates. This allows me to generate about 4000 pages in less than 30 seconds via gatsby build.
I setup controllers for my content types that exposed a pseudo fragment and a createPages function so that I could support future content type implementations without blowing up my gatsby-node.js. Pseudo fragments? But doesn't gatsby support regular fragments out of the box? Turns out it does, except in the gatsby-node.js file. You can't pull in fragments into gatsby-node. So I had to use string interpolation as suggested by LekoArts. Here is an example of my Article controller:
const articleFragment = `
on Drupal_NodeArticle{
title
type { targetId }
body{
processed
}
}
`
const createArticles = (articles, createPage, template) =>
articles.forEach(article => {
if (article) {
createPage({
path: `/${article.type.targetId}/${article.nid}`,
component: template,
context: {
nid: article.nid,
title: article.title,
body: article.body ? article.body.processed: ""
},
})
}
});
module.exports = {articleFragment, createArticles}
A few things to note for the controller example: for wysiwyg fields, pull back the processed field instead of value, as it will support entity embeds and other types of drupal server side rendering. Also, in query you see type { targetId }, which you expect would be a number, but graphql coercion will return enum values so the targetId actually returns the bundle name as a string. The path could also have been hardcoded in this controller, but it helped to reference the type dynamically this way for future controller implementations.

Authentication with Simple Oauth

As for authentication, if you don't implement it you will find that some graphql queries simply don't work on your gatsby side (eg nodeById or nodeQuery). Some data will come back null or "Inaccessible". It is simple enough to implement simple_oauth, and here are some pointers:
To get authentication working enable the simple_oauth module and simple_oauth_extras. These should have been installed via composer. Once you enable them go to Configuration->People>Simple Oauth and set Access token expiration time and Refresh token expiration time as you wish. You will also need to generate rsa keys and upload them to your server. The following commands will do this easily.
openssl genrsa -out private.key 2048
openssl rsa -in private.key -pubout > public.key
Once they're on your drupal server you can reference them in the public and private key fields on the simple oauth settings screen. Next click "Save Configuration", then click "Add Client" and enter something like gatsby as the label, the User field should be a valid user with enough permissions to view published content, New Secret should be a password you will remember (although not the user's password but a hashing secret password) then press Save.
At this point you're ready to get an access token which you will store in your environmental variables as the value to GATSBY_API_TOKEN. You can generate the token using something like Postman or even curl, for example
curl -i -k -X POST -d "client_id=XXXXXXXX&grant_type=password&client_secret=XXXXXX&username=XXXXXX&password=XXXXX" http://example.com/oauth/token
An explanation of the params sent in that curl commmand:
client_id: The UUID of the consumer/client you created from /admin/config/services/consumer.
client_secret: The client secret provided during the addition of the consumer.
username: The username of the account associated with the consumer.
password: The password for the account associated with the consumer.
Once you have that you copy the access_token value from the curl response into however you like to set env variables, whether via dotenv, .bashrc, .bash_profile, or in windows environmental variables gui.
Once you have GATSBY_API_TOKEN in your environmental variables with the value being the access_token then your next gatsby develop graphql queries will be authenticated (assuming you have updated gatsby-config.js to use ${process.env.GATSBY_API_TOKEN} )

Conclusion

Overall, I am pleased with the out-of-the-box functionality provided by the drupal grapqhl module. There are all kinds of nice graphql queries provided by it like reverse field lookups which let you pull back content related to a common taxonomy field. It also feels more native to use the drupal graphql module instead of json endpoints. I'm sure there are things I missed here but I'm happy to discuss any issues/questions you may run into.

April 27, 2019 at 4:27pm
as requested
like-fill
2

April 29, 2019 at 4:27pm
Fabio, is the articles: nodes(filter: {type: "article"}) { something you did custom? I do not have that query available, but do have nodeQuery() and nodeById() available in my GraphiQL interfaces (and my drupal explorer) but nothing that is just "nodes()" and when I try to filter, type is not an option (on the nodeQuery). Would love to use the efficiency of the Graphql module but right now I keep spinning my wheels.
right that is a custom view done with graphql_views module. the syntax for nodeQuery would be something slightly different like nodeQuery(filter: {conditions: {field: "type", operator: EQUAL, value: "article"}}) {
Edited
Ah Ok.. that makes more sense.. Edit: I must of gleaned over that in your post..
Edited

April 30, 2019 at 4:58pm
I am curious to see your templates. I hadn't thought about passing the information through contexts before. One thing about NodeQuery(), it has a limit of 10 set by default, and when I tried to pull more than 200 items it would return a parsing error. So I worked around that by getting the total count and doing a little while statement
Edit: Spectrum didn't like my snippet.. so have a gist: https://gist.github.com/mark-casias/97a8e874c2cd175f4e63dd8b07aaf737
Edited
here is an example of a simple Article template
import React from "react"
import Layout from "../components/Layout/layout"
const Content = ({ pageContext })=> (
<Layout>
<h1>{pageContext.title}</h1>
<div dangerouslySetInnerHTML={{ __html: pageContext.body }} />
</Layout>
)
export default Content
nodeQuery does allow you to increase the limit
nodeQuery(limit: 9999){
entities{
entityId
}
}
yeah weird.. anytime I increase my limit to above 200, it chokes on me.
ah, yes i see in your gist. i had to tweak my apache config to allow for pulling so much data without resetting the connection (see my other post )
I'm getting: error Plugin gatsby-source-graphql returned an error
ServerParseError: Unexpected token < in JSON at position 0
  • Object.parse
  • index.js:35 [road]/[apollo-link-http-common]/lib/index.js:35:25
  • next_tick.js:109 process._tickCallback internal/process/next_tick.js:109:7
any ideas?
that means the connection is not returning data, possibly due to bad permissions. Is your GATSBY_API_URL pointing to your Drupal server /graphql ? Is your GATSBY_API_TOKEN a valid value?
I'll pull another token and give a go
Edit: No luck.
Edited
You can see what header values are getting passed by gatsby if you open this file node_modules\gatsby-source-graphql\gatsby-node.js and put a console.log(headers) in line ~56 it should dump the authorization header with a valid token
Edited
I'm really baffled with the GraphQL auth, I've tried Simple OAuth and JWT, and while both authorize the user I'm testing with (uid 1 in this case) I still get a 403 at /graphql. I can use the Authorization header with the respective tokens from each module to load any other page in the admin section, etc, but the /graphql endpoint still returns 403 even though it works when I login with cookies.
It looks like the version of the GraphQL module I'm using (3.0-rc3) only allows basic_auth and cookie auth on the query endpoint, in graphql/src/Routing/QueryRoutes.php:
/**
* Alters existing routes for a specific collection.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The route collection for adding routes.
*/
protected function alterRoutes(RouteCollection $collection) {
$routes = new RouteCollection();
foreach ($this->schemaManager->getDefinitions() as $key => $definition) {
$routes->add("graphql.query.$key", new Route($definition['path'], [
'schema' => $key,
'_graphql' => TRUE,
'_controller' => '\Drupal\graphql\Controller\RequestController::handleRequest',
'_disable_route_normalizer' => 'TRUE',
], [
'_graphql_query_access' => 'TRUE',
], [
'_auth' => ['basic_auth', 'cookie'],
]));
}
$collection->addCollection($routes);
}
You want to make sure whatever user you setup with your authentication module has permission to "Execute arbitrary GraphQL requests" and "Execute persisted GraphQL requests"
Edited

May 1, 2019 at 2:10pm
It appears I have an issue with my CORS policy. Have to get that fixed, then it should be working.
ah, yes your Access-Control-Allow-Origin header should allow connections from wherever you're pulling from, i set it to Access-Control-Allow-Origin: * while i'm working locally. you can do this in settings.php or via http_response_headers module

May 2, 2019 at 5:57pm
For "Authorization": Bearer ${process.env.GATSBY_API_TOKEN} are we just using the access_token hash for the GATSBY_API_TOKEN or does it need the headers as well?
no just the access_token value

May 3, 2019 at 6:00am
Absolutely gold! I'll try this out over the weekend. Great work

May 3, 2019 at 8:12pm
I'm also getting nothing but 403s now. I haven't set up gatsby-node yet or any content-type templates, just trying to get the two to talk. Do I have to have a query in place for the endpoint to respond with a 200?

May 4, 2019 at 12:15pm
If you turn on dblog module in drupal are you getting any auth errors?

May 13, 2019 at 4:40pm
I just get an access denied. It's almost as if it's not even trying to use the oauth. There are no other errors. I can't verify that it's even trying to authenticate.
Show more messages