I'm developing my website using Gatsby v5 and I'm currently struggling with a GraphQL issue.
I use a static GraphQL query to pull the openGraphImageUrl from some GitHub repositories and display each in a card component. In order to have more control over the images, I wrote a resolver that downloads the files behind the openGraphImageUrl and adds it them as File nodes to the GraphQL data layer so that I can use them with the <GatsbyImage>
component.
This approach generally works, I can build the website and the static query provides the information from the repository. The resolver correctly adds the image
node with the downloaded file, which I can use with <GatsbyImage>
as expected (see further down).
The problem that I'm facing is the following error message, which only occurs when I make changes to a page and then save it (e.g. index.tsx) after successfully running gatsby develop
, but it does not occur when modifying individual components that are not pages (e.g. code.tsx - see below):
Missing onError handler for invocation 'building-schema', error was 'Error: Schema must contain uniquely named types but contains multiple types named "File".'
This happens when the building schema
step runs again (triggered by saving changes to pages), which is where the build process then gets stuck after the error occurs.
I have searched hours on end to figure out the problem and I also consulted ChatGPT, but to no avail. I'm currently still a beginner with regards to Gatsby, React and Typescript.
The error message suggests that the "File" type is redefined, I just don't understand why that happens and what I'm doing wrong. How can I avoid that the "File" type is redefined in the schema? My setup is as follows below.
This is where I have set up the access to GitHub's GraphQL API:
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
});
import type { GatsbyConfig } from "gatsby";
const config: GatsbyConfig = {
graphqlTypegen: true,
plugins: [
{
resolve: "gatsby-source-graphql",
options: {
typeName: "GitHub",
fieldName: "github",
url: "https://api.github.com/graphql",
headers: {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
},
fetchOptions: {},
},
},
//... skipping the rest
],
};
export default config;
The resolver is created as shown below. On a GitHub_Repository
node, it adds an image
node of type File
and downloads the image data from the openGraphImageUrl of the repository. It then saves that as a temporary file, reads the blob into a buffer and then creates the actual node from the buffer by calling createFileNodeFromBuffer()
.
const fs = require("fs");
const path = require("path");
const fetch = require("node-fetch");
const { createFileNodeFromBuffer } = require("gatsby-source-filesystem");
exports.createResolvers = async ({
actions: { createNode },
createNodeId,
cache,
store,
createResolvers,
}) => {
const resolvers = {
GitHub_Repository: {
image: {
type: "File",
resolve: async (source) => {
const imageUrl = source.openGraphImageUrl;
const imageNodeId = createNodeId(imageUrl);
const response = await fetch(imageUrl);
const buffer = await response.buffer();
const dirPath = path.join(process.cwd(), "public", "tmp");
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
const filePath = path.join(
dirPath,
imageNodeId,
);
fs.writeFileSync(filePath, buffer);
const fileNode = await createFileNodeFromBuffer({
buffer,
store,
cache,
createNode,
createNodeId,
name: imageNodeId,
});
return fileNode;
},
},
},
};
createResolvers(resolvers);
};
This is what the query looks like:
function QueryGitHubRepositories(): Repository[] {
const data: Data = useStaticQuery(graphql`
{
github {
viewer {
pinnedItems(first: 6, types: REPOSITORY) {
nodes {
... on GitHub_Repository {
id
name
url
openGraphImageUrl
image {
childImageSharp {
gatsbyImageData(layout: FIXED, width: 336, height: 168)
}
}
}
}
}
}
}
}
`);
return data.github.viewer.pinnedItems.nodes.map((node) => node);
}
Here is the rest of the related code, for completeness.
I defined the following types for the query method (also in code.tsx):
type Repository = {
name?: string | null;
url?: string | null;
openGraphImageUrl?: string | null;
image?: {
childImageSharp: {
gatsbyImageData?: any | null;
};
} | null;
};
type Data = {
github: {
viewer: {
pinnedItems: {
nodes: Repository[];
};
};
};
};
The query data is used here to build the code section with the repository cards (also in code.tsx):
import * as React from "react";
import { graphql, useStaticQuery } from "gatsby";
import { GatsbyImage } from "gatsby-plugin-image";
//skipping type definitions and query here, since they're already shown above
const RepositoryCard = ({ repository }: { repository: Repository }) => {
const imageItem = repository.image?.childImageSharp.gatsbyImageData ?? "";
const altText = repository.name ?? "repository";
return (
<div>
<a href={repository.url ?? ""} target="_blank">
<div className="flex h-fit flex-col">
<GatsbyImage image={imageItem} alt={altText} />
</div>
</a>
</div>
);
};
const CodeSection = () => {
const repositories: Repository[] = QueryGitHubRepositories();
return (
<div className="w-full">
<div className="flex flex-col">
{repositories.map((repository) => (
<RepositoryCard key={repository.name} repository={repository} />
))}
</div>
</div>
);
};
export default CodeSection;
I also tried different implementations and I had a look at Paul Scanlon's two blog posts about adding data to Gatsby's GraphQL data layer and modifying Gatsby's GraphQL data types using createSchemaCustomization. However, I couldn't get that to work properly, probably because in his blog posts, the nodes are added without any source plugins, while I am using gatsby-plugin-graphql
.
Suggestions for alternative implementations are welcome.
I found a working solution. I just had to replace gatsby-source-graphql
with gatsby-source-github-api
, because the former apparently doesn't support incremental builds:
require("dotenv").config({
path: `.env.${process.env.NODE_ENV}`,
});
import type { GatsbyConfig } from "gatsby";
const config: GatsbyConfig = {
graphqlTypegen: true,
plugins: [
{
resolve: `gatsby-source-github-api`,
options: {
url: "https://api.github.com/graphql",
token: `${process.env.GITHUB_TOKEN}`,
graphQLQuery: `
query{
user(login: "someUserName") {
pinnedItems(first: 6, types: [REPOSITORY]) {
edges {
node {
... on Repository {
name
openGraphImageUrl
}
}
}
}
}
}`
}
},
//... skipping the rest
],
};
export default config;
const fs = require("fs");
const path = require("path");
const fetch = require("node-fetch");
const { createFileNodeFromBuffer } = require("gatsby-source-filesystem");
exports.createResolvers = async ({
actions: { createNode },
createNodeId,
cache,
store,
createResolvers,
}) => {
const resolvers = {
GithubDataDataUserPinnedItemsEdgesNode: {
image: {
type: "File",
resolve: async (source) => {
const imageUrl = source.openGraphImageUrl;
const imageNodeId = createNodeId(imageUrl);
const response = await fetch(imageUrl);
const buffer = await response.buffer();
const dirPath = path.join(process.cwd(), "public", "tmp");
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
const filePath = path.join(
dirPath,
imageNodeId,
);
fs.writeFileSync(filePath, buffer);
const fileNode = await createFileNodeFromBuffer({
buffer,
store,
cache,
createNode,
createNodeId,
name: imageNodeId,
});
return fileNode;
},
},
},
};
createResolvers(resolvers);
};
type Repository = {
openGraphImageUrl?: string | null;
image?: {
childImageSharp?: {
gatsbyImageData?: any | null;
} | null;
} | null;
};
type Data = {
githubData: {
data: {
user: {
pinnedItems: {
edges: {
node: Repository;
}[];
};
};
};
};
};
function QueryGitHubRepositories(): Repository[] {
const data: Data = useStaticQuery(graphql`
{
githubData {
data {
user {
pinnedItems {
edges {
node {
openGraphImageUrl
image {
childImageSharp {
gatsbyImageData
}
}
}
}
}
}
}
}
}
`);
const repos = data.githubData.data.user.pinnedItems.edges.map(
(edge) => edge.node
);
return repos;
}