'Create nested file nodes Gatsby 4

Since Gatsby 4 doesn't accept remoteFileNodes to be created within the createResolvers API inside gatsby-node.js, i'm looking for another solution to create File's from our remote (graphql) source.

Creating files on the upper level of an object works just fine, however i can't find a way to create these files inside nested objects in my schema.

Although there is a File object created with the provided name inside Sections, all the data inside of it results in null. The given URL is checked and is valid.

The following code is inside my gatsby-node.js file:

sourceNodes

exports.sourceNodes = async ({ actions, createContentDigest, createNodeId }) => {
  const { createNode } = actions;

  const { data } = await client.query({
    query: gql`
      query {
        
        PageContent {
          id
          main_image_url
          blocks {
            title
            sections {
              title
              nested_image_url
            }
          }
        }
      }
    `,
  });

  data.PageContent.forEach((pageContent) => {
    createNode({
      ...pageContent,
      id: createNodeId(`${PAGE_CONTENT}-${pageContent.id}`),
      parent: null,
      children: [],
      internal: {
        type: PAGE_CONTENT,
        content: JSON.stringify(pageContent),
        contentDigest: createContentDigest(pageContent),
      }
    })
  });

  return;
};

onCreateNode

exports.onCreateNode = async ({
  node,
  actions: { createNode, createNodeField },
  createNodeId,
  getCache,
}) => {

  if (node.internal.type === PAGE_CONTENT) {

This works just fine

    if (node.main_image_url) {
      const fileNode = await createRemoteFileNode({
        url: node.main_image_url,
        parentNodeId: node.id,
        createNode,
        createNodeId,
        getCache,
      });
  
      if (fileNode) {
        createNodeField({ node, name: "main_image", value: fileNode.id });
      }
    }

But this won't

    if (node.blocks && node.blocks.length > 0) {
      node.blocks.map(async({ sections }) => {

        if (sections.length > 0) {

          sections.map(async(section) => {

            if (section.nested_image_url) {

              const fileNode = await createRemoteFileNode({
                url: section.nested_image_url,
                parentNodeId: node.id,
                createNode,
                createNodeId,
                getCache,
              });
  
              if (fileNode) {
                createNodeField({ node, name: "nested_image", value: fileNode.id });
              }
            }
          })
        }
      })
    }
  }
};

createSchema

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;

  createTypes(`
    type PageContent implements Node {
      main_image: File @link(from: "fields.main_image")
      blocks: [Block]
    }

    type Block {
      sections: [Section]
    }

    type Section {
      nested_image: File @link(from: "fields.nested_image")
    }
  `);
};

Would be really grateful if someone has a clue!



Solution 1:[1]

Meanwhile I've come to a solution, which includes the use of the onCreateNode and createSchemaCustomization API's

onCreateNode

  // Single Node

  const fileNode = await createRemoteFileNode({
     url: node.image,
     parentNodeId: node.id,
     createNode,
     createNodeId: (id) => `${node.unique_identifier_prop}-image`,
     getCache,
   });

    // Array

    await Promise.all(
      node.images.map((url, index) => (
        createRemoteFileNode({
          url: url,
          parentNodeId: node.id,
          createNode,
          createNodeId: id => `${node.unique_identifier_prop}-images-${index}`,
          getCache,
        })
      ))
    )

First you can create a FileNode to your own liking, and instead of the API's createNodeId function. We replace it by a unique and retrievable identifier, so we can locate the File Node in our Schema.

createSchemaCustomization

exports.createSchemaCustomization = async({ actions, schema }) => {
  const { createTypes } = actions;

  const typeDefs = [
    schema.buildObjectType({
      name: `**target_typename**`,
      fields: {
        // Single Node
        imageFile: {
          type: 'File',
          resolve: (source, args, context, info) => {
            return context.nodeModel.getNodeById({
              id: `${source.unique_identifier_prop}-image`,
              type: 'File',
            })
          }
        },
        // Array
        imageFiles: {
          type: '[File]',
          resolve: (source, args, context, info) => {
            const images = source.images.map((img, index) => (
              context.nodeModel.getNodeById({
                id: `${source.unique_identifier_prop}-images-${index}`,
                type: 'File',
              })
            ))
            return images
          }
        },
      }
    })
  ];

  createTypes(typeDefs)
};

In the createSchemaCustomization we now can define our custom type with the buildObjectType function provided by schema, which is available in this API.

In the resolver, we can retrieve the node's values with the source parameter, which holds our unique_identifier_prop. Now, with the context parameter, we can use the getNodeById function to retrieve the File Node that is bound to our provided ID. Finally, we can return the found File Node and attach it to our Node.

Solution 2:[2]

I had the same problem, the solution I found was to use createResolvers.

    exports.createResolvers = ({ createResolvers }) => {
    const resolvers = {
        HomePageValuesBannerImage: {
            bannerImageFile: {
                type: "File",
                resolve: async (source, args, context, info) => {
                    const homePage = await context.nodeModel.findRootNodeAncestor(source)
                    const imageId = homePage.fields.bannerImage
                    const imageNode = await context.nodeModel.getNodeById({
                        id: imageId,
                    })

                    return imageNode
                },
            },
        },
    }
    createResolvers(resolvers)
}

So the first thing I needed was to access the fields field on my parent object, because whenever you use createNodeField it creates a field with the name you provide inside the node on fields.

To access the node file id that is stored on fields, in my case is called bannerImage on your example nested_image, you first need a reference of the parent node so in order to do that I use context.nodeModel.findRootNodeAncestor(source) source is just a reference of the nested property. In my case is HomePageValuesBannerImage because my data looks like this:

HomePage {
 Values {
  banner {
   image
  }
 }
}

and in my schema the banner property's type is HomePageValuesBannerImage.

Then you need to create a new field, on the documentation says that is better to create a new field than updating one.

The field I created is called bannerImageFile and inside that field is going to be the File Node.

After making the query for the parent of my nested node I just extract the file id that is stored inside fields in my custom field called bannerImage.

And finally I made a query to find a node by its ID and return it.

const imageNode = await context.nodeModel.getNodeById({
                        id: imageId,
                    })

Im still looking for a different approach to this problem, but this is the best solutions I could find.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jeremy Caney
Solution 2 José Luna