Search code examples
node.jsgraphqlgatsbygraphql-schemagatsby-source-graphql

Gatsby v5 and GraphQL: 'Error: Schema must contain uniquely named types but contains multiple types named "File".'


I'm developing my website using Gatsby v5 and I'm currently struggling with a GraphQL issue.

Problem

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.

Setup

gatsby-config.ts

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;

gatsby-node.js

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);
};

code.tsx

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);
}

TL;DR

Here is the rest of the related code, for completeness.

code.tsx (continued)

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;

Update

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.


Solution

  • 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:

    gatsby-config.ts

    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;
    

    gatsby-node.js

    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);
    };
    

    code.tsx

    Type definitions

    type Repository = {
      openGraphImageUrl?: string | null;
      image?: {
        childImageSharp?: {
          gatsbyImageData?: any | null;
        } | null;
      } | null;
    };
    
    type Data = {
      githubData: {
        data: {
          user: {
            pinnedItems: {
              edges: {
                node: Repository;
              }[];
            };
          };
        };
      };
    };
    

    Query

    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;
    }