GatsbyJs - How to handle empty graphql node from Contentful plugin - graphql

I am building a site in Gatsbyjs that pulls information in via the gatsby-source-contentful plugin, but I'm struggling with the graphql side of things.
If I have a Content model in Contentful that contains a field to override the default description for example - then if none of the content uses that yet graphql throws an error if I try to include it in my query.
Is there anyway to short circuit the graphql queries?
Examples
{
allContentfulPage {
edges {
node {
title
description {
description
}
}
}
}
}
This will break if there is no Page model that exists with a description, but as soon as one page gets a description it works.

Gatsby pulls data from Contentful and then it builds an internal "model" of what the data from Contenful looks like. The Contentful API is REST but internally Gatsby uses GraphQL.
If a field does not have a value on Contentful then it will not be a part of the generated GraphQL query in Gatsby. The solution is to push a single value in one of your records.

The other solution would be to create and define the ContentfulPage type in your gatsby-node.js file. here is the link to gatsby documentation if you need to read more .
To create types you could add this to your gatsby-node.js file:
const createSchemaCustomization = ({ actions }) => {
const types = [
`
type ContentfulPage implements Node {
description : ContentfullDescription
}
type ContentfullDescription implements Node {
description : String
}
`,
];
actions.createTypes(types);
};
const gatsbyNode = {
createSchemaCustomization,
};
export default gatsbyNode;
this is only the case if the description is a string, if it is some other type such as rich text you should add its type the createTypes api. then it would be:
const createSchemaCustomization = ({ actions }) => {
const types = [
`
type ContentfulPage implements Node {
description : ContentfullDescription
}
type ContentfullDescription implements Node {
description : ContentfulRichText
}
type ContentfulRichText {
raw: String
references: [Node] #link(from: "references___NODE")
}
`,
];
actions.createTypes(types);
};
const gatsbyNode = {
createSchemaCustomization,
};
export default gatsbyNode;
Please take note that I am just guessing the type of description it might be some other types that you need to create yourself.

Related

Gatsby - Cannot query field on type Error when field in CMS is empty

I found out that this is known problem with Strapi + Gatsby setup, but I found some articles about how to get rid of that error like the following ones:
https://www.virtualbadge.io/blog-articles/nullable-relational-fields-strapi-gatsbyjs-graphql
https://medium.com/swlh/gatsby-graphql-and-the-missing-but-necessary-explanation-about-type-definitions-87a5ef83e759
And indeed it helped with the bug itself, but caused another. At the moment when I fill the field inside the CMS graphql cannot see it and interprets it as null.
Without gatsby-node.ts:
And With gatsby-node.ts:
import type { GatsbyNode } from 'gatsby'
export const sourceNodes: GatsbyNode['sourceNodes'] = async ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type STRAPI__COMPONENT_BASE_HERO implements Node {
backgroundVideo: STRAPI__MEDIA
}
`
createTypes(typeDefs)
}

Issues with Gatsby & Strapi optional data

I have a model made in Strapi which contains a specific component which can be used to add social media links. Each link contains a text field and a link field. Everything works as expected, except when I leave it empty. If there are 0 links I get an error which is shown below.
This is how the component looks inside Strapi:
Gatsby GraphQL trying to access the links:
strapiWebsiteSetting {
footerSocialLinks {
text
link
}
footerOtherLinks {
text
link
}
}
The error I get when there is 0 links added:
Is there a way of making GraphQL work even if there is 0 links added. I've tried
adding the following code to gatsby-node.js but that didn't work:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions;
const typeDefs = `
type STRAPI__COMPONENT_LINK_FOOTER_OTHER_LINK implements Node {
id: ID!
parent: Node
children: [Node!]!
internal: Internal!
text: String
link: String
strapi_id: Int
}
type STRAPI__COMPONENT_LINK_FOOTER_SOCIAL_MEDIA_LINK implements Node {
id: ID!
parent: Node
children: [Node!]!
internal: Internal!
text: String
link: String
strapi_id: Int
}
`;
createTypes(typeDefs);
};
A less painful way to make gatsby and strapi work with optional attributes and even optional records is the following:
Install the gatsby-plugin-schema-snapshot plugin.
Set the "update" option to "true" in gatsby-config.js (could also be handled by environment variable):
{
resolve: `gatsby-plugin-schema-snapshot`,
options: {
update: true,
},
}
Build your app with all content-types having at least one record (e.g. a dummy record) and all attributes present. That creates a schema.gql file in the root of your app folder.
Set the plugin's "update" option to "false" and remove your dummy record.
Build again. No complaints anymore :)
So, I managed to get it working. Creating the custom types in the gatsby-node.js probably did the trick, but I haven't really tested it, I just know it works.

Optional queries in Gatsby / GraphQL

I have a Gatsby site with a contentful source, the initial page build GraphQL looks like this:
{
allContentfulProduct {
edges {
node {
slug
contentfulid
}
}
}
}
this works fine when using the preview API, but when using the production one it fails unless I have at least 1 Product entry published:
There was an error in your GraphQL query:
Cannot query field "allContentfulProduct" on type "Query". Did you mean ... [suggested entry names] ?
I'm pretty sure that when I publish a Product things will work as expected, but is there any way to make this query optional. The query should return zero results, and thus no Product pages will be created (expected outcome if no Product entries are published)
Try to add into your gatsby-node.js file and play around that.
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type allContentfulProduct implements Node{
slug: String,
contentfulid: String
}`

Use absolute path for featured image in markdown post with Gatsby

I've followed Gatsby tutorial for Working With Images in Markdown Posts and Pages which is working well but what I want to achieve is to fetch image from a static location instead of using a relative path for the image.
Would like to reference image like this (in frontmatter)
featuredImage: img/IMG_20190621_112048_2.jpg
Where IMG_20190621_112048_2.jpg is in /src/data/img instead of same directory as markdown file under /src/posts
I've tried to setup gatsby-source-filesystem like this :
{
resolve: `gatsby-source-filesystem`,
options: {
name: `posts`,
path: `${__dirname}/src/posts`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `data`,
path: `${__dirname}/src/data/`,
},
},
but graphQL query in post template fails :
export const query = graphql`
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
featuredImage {
childImageSharp {
fluid(maxWidth: 800) {
...GatsbyImageSharpFluid
}
}
}
}
}
}
GraphQL Error Field "featuredImage" must not have a selection since
type "String" has no subfields.
Any idea how I could fetch image from a location distinct to the post markdown directory ?
Achieving this in Gatsby used to be pretty troublesome, but thanks to the new createSchemaCustomization Node API docs (since Gatsby 2.5) it's relatively easy.
Here's a demo where I replicate your repo structure: github
Here's where the relevant code lives: github
Here's the code to make it work:
// gatsby-node.js
const path = require('path')
exports.createSchemaCustomization = ({ actions }) => {
const { createFieldExtension, createTypes } = actions
createFieldExtension({
name: 'fileByDataPath',
extend: () => ({
resolve: function (src, args, context, info) {
const partialPath = src.featureImage
if (!partialPath) {
return null
}
const filePath = path.join(__dirname, 'src/data', partialPath)
const fileNode = context.nodeModel.runQuery({
firstOnly: true,
type: 'File',
query: {
filter: {
absolutePath: {
eq: filePath
}
}
}
})
if (!fileNode) {
return null
}
return fileNode
}
})
})
const typeDefs = `
type Frontmatter #infer {
featureImage: File #fileByDataPath
}
type MarkdownRemark implements Node #infer {
frontmatter: Frontmatter
}
`
createTypes(typeDefs)
}
How it works:
There are 2 parts to this:
Extend markdownRemark.frontmatter.featureImage so graphql resolves to a File node instead of a string via createTypes
Create a new field extension #fileByDataPath via createFieldExtension
createTypes
Right now Gatsby's inferring frontmatter.featureImage as a string. We'll ask Gatsby to read featureImage as a string instead, by modifying its parent type:
type Frontmatter {
featureImage: File
}
This is not enough however, we'll also need to pass this Frontmatter type to its parent as well:
type Frontmatter {
featureImage: File
}
type MarkdownRemark implements Node {
frontmatter: Frontmatter
}
We'll also add the #infer tag, which lets Gatsby know that it can infer other fields of these types, i.e frontmatter.title, markdownRemark.html, etc.
Then pass these custom type to createTypes:
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
const typeDefs = `
type Frontmatter #infer {
featureImage: File
}
type MarkdownRemark implements Node #infer {
frontmatter: Frontmatter
}
`
createTypes(typeDefs)
}
Now, we can fire up localhost:8000/___graphql and try to query the image
query Post {
markdownRemark {
frontmatter {
featureImage {
id
}
}
}
}
and we get...
Error: Cannot return null for non-nullable field File.id.
That is because while Gatsby now understands featureImage should be a File node, it has no idea where to get that file.
At this point, we can either use createResolvers to manually resolve the field to a File node, or createFileExtension to do the same thing. I choose createFileExtension because it allows more code reuse (you can extend any fields), while createResolvers, in this case, is more useful for a specific field. Seeing that all you want is to resolve a file from the src/data directory, I'll call this extension fieldByDataPath.
createFileExtension
Let's just look at the resolve attribute. It is a function that takes in the following:
source: The data of the parent field (in this case, frontmatter)
args: The arguments passed to featureImage in a query. We won't need this
context: contains nodeModel, which we'll use to get nodes from Gatsby node store
info: metadata about this field + the whole schema
We will find the original path (img/photo.jpg) from src.featureImage, then glue it to src/data to get a complete absolute path. Next, we query the nodeModel to find a File node with the matching absolute path. Since you have already pointed gatsby-source-filesystem to src/data, the image (photo.jpg) will be in Gatsby node store.
In case we can't find a path or a matching node, return null.
resolve: async function (src, args, context) {
// look up original string, i.e img/photo.jpg
const partialPath = src.featureImage
if (!partialPath) {
return null
}
// get the absolute path of the image file in the filesystem
const filePath = path.join(__dirname, 'src/data', partialPath)
// look for a node with matching path
const fileNode = await context.nodeModel.runQuery({
firstOnly: true,
type: 'File',
query: {
filter: {
absolutePath: {
eq: filePath
}
}
}
})
// no node? return
if (!fileNode) {
return null
}
// else return the node
return fileNode
}
We've done 99% of the work. The last thing to do is to move this to pass this resolve function to createFieldExtension; as well as add the new extension to createTypes
createFieldExtension({
name: 'fileByDataPath' // we'll use it in createTypes as `#fileByDataPath`
extend: () => ({
resolve, // the resolve function above
})
})
const typeDef = `
type Frontmatter #infer {
featureImage: File #fileByDataPath // <---
}
...
`
With that, you can now use relative path from src/data/ in frontmatter.
Extra
The way fileByDataPath implemented, it'll only work with fields named featureImage. That's not too useful, so we should modify it so that it'll work on any field that, say, whose name ended in _data; or at the very least accept a list of field names to work on.
Edit had a bit of time on my hand, so I wrote a plugin that does this & also wrote a blog on it.
Edit 2 Gatsby has since made runQuery asynchronous (Jul 2020), updated the answer to reflect this.
In addition to Derek Answer which allow assets of any type to be use anywhere (sound, video, gpx, ...), if looking for a solution only for images, one can use :
https://www.gatsbyjs.org/packages/gatsby-remark-relative-images/
The reason in your server schema you may have declared the featuredImage variable as string and in your client graphql query you are trying to call subobjects of the featuredImage variable and that subobjects is not existing.
You may have to check the graphql schema definition and align the query with the schema definition
you current schema might be like this
featuredImage: String
and you need to change it by declaring the proper types based on the requirements in the server side.
For more information about graphql types. please refer this url - https://graphql.org/learn/schema/#object-types-and-fields
Thanks
Rigin Oommen

Cannot get Relay to make GraphQL Call over the network

I am new to Relay, and I am having problems making it work with a GraphQL server.
I have adapted the Tea sample from the relay homepage to the SWAPI relay service. I cloned swapi-graphql, and added cors to the express server. I tested the link with this code:
var query = `
{
allFilms {
edges {
node {
id
}
}
}
}
`
fetch("http://localhost:50515/graphiql?query=" + query)
.then(response=>response.json())
.then(json=>console.log(json))
I got a response from the server, I saw some network action, it worked! I can communicate with the graphiql service.
Next, I created a query that was structured similar to the TeaStoreQuery. I tested it, and it returned the expected results.
query AllFilmQuery {
allFilms {
...filmListFragment
}
}
fragment filmListFragment on FilmsConnection {
edges {
node {
...filmFragment
}
}
}
fragment filmFragment on Film {
id
title
releaseDate
}
HOW DO YOU MAKE THIS WORK WITH RELAY??
I cannot figure out how to use Relay to query the server. Here is the code that I adapted from the Tea sample.
import { render } from 'react-dom'
import {
RootContainer,
createContainer,
Route,
injectNetworkLayer
} from 'react-relay'
// React component for each star wars film
const Film = ({ id, title, releaseDate }) =>
<li key={id}>
{title} (<em>{releaseDate}</em>)
</li>
// Relay container for each film
const FilmContainer = createContainer(Film, {
fragments: {
film: () => Relay.QL`
fragment on Film {
id,
title,
releaseDate
}
`
}
})
// React component for listing films
const FilmList = ({ films=[] }) =>
<ul>
{films.map(
film => <Film {...film} />
)}
</ul>
// Relay container for Listing all Films
const FilmListContainer = createContainer(FilmList, {
fragments: {
films: () => Relay.QL`
fragment on FilmsConnection {
edges {
node {
${ Film.getFragment('film') }
}
}
}
`
}
})
// The Home Route
class FilmHomeRoute extends Route {
static routeName = 'Home'
static queries = {
allFilms: (Component) => Relay.QL`
query AllFilmQuery {
allFilms {
${Component.getFragment('allFilms')}
}
}
`
}
}
// Is this how you setup a network layer
// I am using CORS, and I testing the graphql service with fetch
// The graphql service works but Relay never seems to try to connect
Relay.injectNetworkLayer(
new Relay.DefaultNetworkLayer('http://localhost:50515/graphiql')
)
render(
<RootContainer
Component={FilmListContainer}
route={new FilmHomeRoute()}
/>,
document.getElementById('react-container')
)
When I run this sample (source | output) I do not see any attempts at making network requests. I do see an error "Cannot render map of null". It seems like it cannot map the allfilms data.
What am I doing wrong?
According to section 5.1 of this document, the "Relay Object Identification Specification":
Relay‐compliant servers may expose root fields that are not plural identifying root fields; the Relay client will just be unable to use those fields as root fields in its queries.
Based on this specification, Relay can not query plural fields at the root of a query unless the field takes a list of arguments that exactly maps to the results. That means the allFilms field cannot be used with Relay. You can read more about the limitations in this other StackOverflow answer: How to get RelayJS to understand that a response from GraphQL is an array of items, not just a single item
If you would like to have a GraphQL schema with root fields that return arrays, you might want to use a different GraphQL client, since Relay is particularly restrictive in what kinds of schemas it works with. graphql.org has a list: http://graphql.org/code/#graphql-clients

Resources