gatsbyjs / gatsby

The best React-based framework with performance, scalability and security built in.
https://www.gatsbyjs.com
MIT License
55.27k stars 10.31k forks source link

[bug] ☂️ umbrella issue for schema customization issues #12272

Closed freiksenet closed 4 years ago

freiksenet commented 5 years ago

This is a meta issue for all the issues with 2.2.0, that were introduced by the schema refactoring.

What?

See the blog post for the details about why we did the refactoring and what's it all about.

See the release blog post for release notes and final updates.

How?

Install latest version of Gatsby and try running your site. Hopefully it will all just work. If you want, you could also try the two new APIs (createTypes and createResolvers).

yarn add gatsby

Changelog

gatsby@2.5.0

gatsby@2.5.0-rc.1

gatsby@2.4.0-alpha.2

gatsby@2.4.0-alpha.1

gatsby@2.2.0

gatsby@2.2.0-rc.2

gatsby@2.2.0-rc.1

gatsby@2.2.0-alpha.6

gatsby@2.2.0-alpha.5

gatsby@2.2.0-alpha.4

gatsby@2.2.0-alpha.3

exports.sourceNodes = ({ actions, schema }) => {
  const { createTypes } = actions
  createTypes([
    schema.buildObjectType({
      name: `CommentJson`,
      fields: {
        text: `String!`,
        blog: {
          type: `BlogJson`,
          resolve(parent, args, context) {
            return context.nodeModel.getNodeById({
              id: parent.author,
              type: `BlogJson`,
            })
          },
        },
        author: {
          type: `AuthorJson`,
          resolve(parent, args, context) {
            return context.nodeModel.getNodeById({
              id: parent.author,
              type: `AuthorJson`,
            })
          },
        },
      },
      interfaces: [`Node`],
    }),
  ])
}

gatsby@2.2.0-alpha.2

gatsby@2.2.0-alpha.1

gatsby@2.2.0-alpha.0

gatsby@2.1.20-alpha.0

pauleveritt commented 5 years ago

@stefanprobst I understand your point, but I think if the goal is to do less inferred schema and more declared schema, Markdown frontmatter should be in scope. Gatsby with Markdown files is a popular combo. I

Having different types/schema in Markdown would be easier if gatsby-transformer-remark didn't make the surface area of forking so big. There's a lot going on in the arrow function that does the node type assignment.

Let's close out this conversation. I'm likely in a minority of those that want different content types in Markdown. Thanks a bunch for the sample code, that's the direction I'll take for now.

pauleveritt commented 5 years ago

Regarding runQuery and its query parameter...I have filter working correctly but I can't get an example of sort working. The tests that use sort seem to use runQuery from a template string. When I try sort: { order: 'DESC', fields: ['frontmatter___date'] } (or with 'desc' as the string) the resolve bails out.

stefanprobst commented 5 years ago

@pauleveritt Does it work with an Array on order?

pauleveritt commented 5 years ago

@stefanprobst Alas, no. This:

sort: { order: ['DESC'], fields: ['frontmatter___date'] }

...leads to:

error gatsby-node.js returned an error

  TypeError: Cannot read property 'resolve' of undefined

  - prepare-nodes.js:34 awaitSiftField
    [gatsby-theme-bulmaio]/[gatsby]/dist/redux/prepare-nodes.js:34:13

  - prepare-nodes.js:52 
    [gatsby-theme-bulmaio]/[gatsby]/dist/redux/prepare-nodes.js:52:69

  - Array.map

  - prepare-nodes.js:52 resolveRecursive
    [gatsby-theme-bulmaio]/[gatsby]/dist/redux/prepare-nodes.js:52:44
...
stefanprobst commented 5 years ago

@pauleveritt unfortunately I was not able to reproduce with this basic setup -- would you be able to provide a small repro for this issue to look into? This would be very helpful, thanks!

pauleveritt commented 5 years ago

@stefanprobst Found the cause: it's 2.8.x -- want me to file a new ticket?

Imagine your gatsby-blog for YAML repo. Add this to gatsby-node.js:

exports.createResolvers = ({ createResolvers, schema }) => {
  createResolvers({
    Query: {
      allResourcesByType: {
        type: ['MarkdownRemark'],
        args: {
          resourceType: 'String'
        },
        resolve(source, args, context, info) {
          return context.nodeModel.runQuery({
            query: {
              filter: {
                frontmatter: {}
              },
              sort: { fields: ["frontmatter___title"], order: ["ASC"] },
            },
            type: `MarkdownRemark`
          })
        }
      }
    }
  })
}

In gatsby 2.7.6 you can query for allResourcesByType in the explorer. In 2.8.0-2.8.2 you get a resolve error.

janosh commented 5 years ago

@stefanprobst I'm trying to convert tags that I specify as an array of strings in the frontmatter of my posts into a type Tag with fields title and slug where title is just the string I wrote and slug = _.kebabCase(title). I threw together this snippet

exports.sourceNodes = ({ actions, schema }) => {
  actions.createTypes([
    `type MarkdownRemark implements Node {
      frontmatter: MarkdownRemarkFrontmatter
    }`,
    `type Tag { title: String!, slug: String! }`,
    schema.buildObjectType({
      name: `MarkdownRemarkFrontmatter`,
      fields: {
        tags: {
          type: `[Tag!]`,
          resolve(source) {
            if (!source.tags) return null
            return source.tags.map(tag => ({
              title: tag,
              slug: kebabCase(tag),
            }))
          },
        },
      },
    }),
  ])
}

which works for adding the Tag type to MarkdownRemarkFrontmatter.tags. But of course, when I try to group all tags with

tags: allMarkdownRemark {
  group(field: frontmatter___tags) {
    title: fieldValue
    count: totalCount
  }
}

I get only the string I wrote into the frontmatter. I tried writing a resolver for allTags like so

exports.createResolvers = ({ createResolvers }) => {
  const resolvers = {
    Query: {
      allTags: {
        type: [`Tag`],
        resolve(source, args, context) {
          return context.nodeModel.getAllNodes({
            type: `MarkdownRemarkFrontmatterTags`,
          })
        },
      },
    },
  }
  createResolvers(resolvers)
}

but can't get it to work. Any advice?

stefanprobst commented 5 years ago

@janosh

Fixing this is on my todo list -- i'll get on it asap
* The issue with your `createResolvers` snippet is this: `getAllNodes` will retrieve nodes by type, where "node" means objects with a unique ID created by source or transformer plugins (with the `createNode` action). So you would have to retrieve `MarkdownRemark` nodes, and then further manipulate the results in the resolver.
* Depending on your usecase, you might not even need to use `createTypes`, but could simply add a custom root query field to group posts by tag, and include a slug field:
```js
// gatsby-node.js
const { kebabCase } = require(`lodash`)

exports.createResolvers = ({ createResolvers }) => {
  createResolvers({
    Query: {
      allMarkdownRemarkGroupedByTag: {
        type: [
          `type MarkdownRemarkGroup {
            nodes: [MarkdownRemark]
            count: Int
            tag: String
            slug: String
          }`,
        ],
        resolve(source, args, context, info) {
          const allMarkdownRemarkNodes = context.nodeModel.getAllNodes({
            type: `MarkdownRemark`,
          })

          const groupedByTag = allMarkdownRemarkNodes.reduce((acc, node) => {
            const { tags } = node.frontmatter
            tags.forEach(tag => {
              acc[tag] = (acc[tag] || []).concat(node)
            })
            return acc
          }, {})

          return Object.entries(groupedByTag).map(([tag, nodes]) => {
            return {
              nodes,
              count: nodes.length,
              tag,
              slug: kebabCase(tag),
            }
          })
        },
      },
    },
  })
}
janosh commented 5 years ago

@stefanprobst Thanks for the quick reply!

So you would have to retrieve MarkdownRemark nodes, and then further manipulate the results in the resolver.

I thought about doing that but suspected that I was overlooking something and hence approaching this from the wrong angle. Thanks for clearing this up and for planning to add support for field resolvers for group fields!

Maybe another solution would be to let Tag implement Node such that Gatsby creates an allTag query automatically.

type Tag implements Node { title: String!, slug: String! }

Then all I would have to do is to actually create nodes of type Tag every time I come across a new one in the call to schema.buildObjectType for MarkdownRemarkFrontmatter. Is it possible to apply side effects like this from within resolvers? I.e. in this case check if Tag with title MyTag exists and if not createNode({type: `Tag`, title: `MyTag, slug: `my-tag` }).

stefanprobst commented 5 years ago

@pauleveritt sorry for the late reply!

In your example, sorting should work with this:

exports.createResolvers = ({ createResolvers }) => {
  createResolvers({
    Query: {
      allResourcesByType: {
        type: ["MarkdownRemark"],
        args: {
          resourceType: "String",
        },
        resolve(source, args, context, info) {
          return context.nodeModel.runQuery({
            query: {
              sort: { fields: ["frontmatter.post_title"], order: ["DESC"] },
            },
            type: "MarkdownRemark",
          })
        },
      },
    },
  })
}

Note that both sort fields and order are arrays, and fields uses the dot-notation instead of triple underscore to separate fields. (We have to document this! EDIT: #14681) There are two reasons why this is different in runQuery than it is when using a query string (i.e. in the Graph_i_ql explorer):

stefanprobst commented 5 years ago

@janosh grouping on resolved fields should now work with gatsby@2.8.7

janosh commented 5 years ago

@stefanprobst Wow, that was fast! Thanks for the update.

Is there a way to get both the title and slug of a tag in a single grouping? I can only figure out how to group by either and then only have access to one of them.

{
  tags: allMarkdownRemark {
    group(field: frontmatter___tags___(slug|title)) {
     (slug|title): fieldValue
      count: totalCount
    }
  }
}

If I try to group by frontmatter___tags instead,

{
  tags: allMarkdownRemark {
    group(field: frontmatter___tags) {
      tag: fieldValue
      count: totalCount

    }
  }
}

I get only a single result

{
  "data": {
    "tags": {
      "group": [
        {
          "tag": "[object Object]",
          "count": 52
        }
      ]
    }
  }
}
stefanprobst commented 5 years ago

@janosh you can only group by one field (hierarchical sub-groupings are not possible). if you need the values of both title and slug, you can group by one, and get the value of the other from the results (maybe I'm misunderstanding?):

{
  allMarkdownRemark {
    group(field: frontmatter___tags___title) {
      fieldValue
      totalCount
      nodes {
        frontmatter {
          tags {
            slug
            title
          }
        }
      }
    }
  }
}
janosh commented 5 years ago

@stefanprobst That doesn't fit my use case but it's only a minor inconvenience. Thanks for all the awesome work you're putting into schema customization!

laradevitt commented 5 years ago

I've been having an issue since 2.2.0 which seems to be OS-related. I would be grateful to get another Windows 10 user's eyes on it for a test so I can either confirm or rule it out: https://github.com/escaladesports/gatsby-plugin-netlify-cms-paths/issues/10

Thanks!

stefanprobst commented 5 years ago

@laradevitt I don't currently have access to a windows machine, and I'm also not familiar with gatsby-plugin-netlify-cms-paths -- but does it work if you replace the path on frontmatter.image to a relative path, i.e. ../static/media/gatsby-astronaut.png ?

laradevitt commented 5 years ago

@stefanprobst - Thanks for your reply! Yes, it does, but the point of the plugin is that you should not have to use relative paths:

A gatsby plugin to change file paths in your markdown files to Gatsby-friendly paths when using Netlify CMS to edit them.

I've created a pull request with a fix that makes the returned relative path platform agnostic.

I still don't know why it broke with 2.2.0. 🤷‍♀

stefanprobst commented 5 years ago

@laradevitt ah, sorry, should have checked the readme.

I still don't know why it broke with 2.2.0.

This is very probably a regression in Gatsby, as with 2.2.0 type inference changed a bit -- but normalizing paths in gatsby-plugin-netlify-cms-paths definitely seems more correct :+1:

ryanwiemer commented 5 years ago

@stefanprobst

I just created a new ticket related to schema customization. If it is better for me to close that and put it in this umbrella thread just let me know.

https://github.com/gatsbyjs/gatsby/issues/16099

Thanks!

NickyMeuleman commented 5 years ago

Description

Arguments in a graphQL query don't work when using schema customization.

After watching the learn with Jason stream on advanced GraphQL(broken up in multiple parts due to technical difficulties) I incorporated a lot of what was talked about into my theme. However using graphQL arguments (like filter and sort) that rely on values from the customized schema doesn't work.

Steps to reproduce

run this branch of my theme locally. Fire up /___graphql and query allBlogPosts. Now try to query allBlogPosts again with a sort by descending date. RESULT: no change in order (ASC, or DESC)

Example 2

Query for all posts and show their titles.

query MyQuery {
  allBlogPost {
    nodes {
      title
    }
  }
}

RESULT: works

now query for all posts that are have a tag with slug "lorem-ipsum"

query MyQuery {
  allBlogPost(filter: {tags: {elemMatch: {slug: {eq: "lorem-ipsum"}}}}) {
    nodes {
      title
    }
  }
}

RESULT: empty nodes array.

temporary workaround, possible reason why it doesn't work?

Try to query a single BlogPost (picks first one by default if no arguments are given). Now try to query blogPost(slug: {eq: "herp-derpsum"}). This works, I think because I added the slug to the MdxBlogPost node. https://github.com/NickyMeuleman/gatsby-theme-nicky-blog/blob/d29c966e639f4733caf9ee43e9f5755df42db71d/theme/gatsby-node.js#L209-L210

It seems like the graphql arguments use data from the MdxBlogPost node rather than the eventual result after it runs its resolvers. Is my suspicion close?

Environment

System: OS: Linux 4.19 Ubuntu 18.04.2 LTS (Bionic Beaver) CPU: (4) x64 Intel(R) Core(TM) i5-2500K CPU @ 3.30GHz Shell: 4.4.19 - /bin/bash Binaries: Node: 12.4.0 - /tmp/yarn--1565124765846-0.45931526383359134/node Yarn: 1.16.0 - /tmp/yarn--1565124765846-0.45931526383359134/yarn npm: 6.9.0 - ~/.nvm/versions/node/v12.4.0/bin/npm Languages: Python: 2.7.15+ - /usr/bin/python

Additional notes

Relevant parts of code are in gatsby-node.js in the theme directory. specifically the createSchemaCustomization and onCreateNode lifecycles. I tried to heavily comment to show my though process.

stefanprobst commented 5 years ago

@NickyMeuleman sorry haven't had time yet to look closely but it seems you are missing a link extension on the BlogPost interface.

You define your MdxBlogPost.tags field to link by name:

tags: {
  type: "[Tag]",
  extensions: {
    link: { by: "name" },
  },
},

in the typedefs for the BlogPost interface you have:

  interface BlogPost @nodeInterface {
    tags: [Tag]
  }

Does it work with

  interface BlogPost @nodeInterface {
    tags: [Tag] @link(by: "name")
  }
stefanprobst commented 5 years ago

Related: #16466

NickyMeuleman commented 5 years ago

After adding your suggestion, querying allBlogPost with a filter based on a tag did work! 🎉 Thanks!

n the future, will I be able to remove the @link from the interface, since Tags might be linked differently per type that implements the BlogPost interface? (same question for Author, filtering there doesn't work, even with the @link moved up to the interface level.

stefanprobst commented 5 years ago

@NickyMeuleman thanks for experimenting! after looking at this i think it's something we need to fix in core: both issues have to with the fact that we use the interfaces's resolver when manually preparing nodes so we can run a filter on them. instead, we should run the resolver of the type we get from the interface's resolveType, which is also what happens when graphql processes the selection set.

NickyMeuleman commented 5 years ago

After https://github.com/gatsbyjs/gatsby/pull/17284 got merged I tried removing the logic in my theme to work around this.

Used Gatsby version: 2.15.14 It doesn't appear to be working. https://github.com/NickyMeuleman/gatsby-theme-nicky-blog/blob/bbc782332e6938daaa2fca1b25d6df7e78f19c6c/theme/gatsby-node.js#L278-L282 When you comment out the lines linked above, filtering in GraphQL on these fields is no longer possible.

Everspace commented 5 years ago

LekoArts suggested I ask this question more publicly than in the discord, since it might be a good topic for documentation.

How do I describe Many to Many relationships with createSchemaCustomization?

The use cases I have are the following:

  1. I have a Product and a Store. Stores can hold any number of products, and products are non exclusive to any one store. What is the best way to express these two types of nodes?
  2. I am trying to make a page for a game system. I have expressed each Ability as a Node. Abilities have any number of prerequisites, and any number of dependant skills. Preferably I would like to link to those pages (prerequisites and dependants) in the page for the skill itself. How do I model these relationships in graphQL, and furthermore, in Gatsby's api for that?
Wolfsun commented 5 years ago

@Everspace Try this for 1. add to gatsby-node.js. I've not tried this but I am doing something similar linking from a custom type (using the node interface) to an image, and it works. It does rely on the link extension so you will need to use IDs assigned by Gatsby. I guess you could map whatever IDs you are using in your datasource to Gatsby IDs during createPages?

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions
  const typeDefs = [
    schema.buildObjectType({
      name: 'Store',
      fields: {
        products: {
          type: "[Product]",
          extensions: {
            link: {},
          },
        },
      },
      interfaces: ['Node'],
    }),
    schema.buildObjectType({
      name: 'Product',
      fields: {
        stores: {
          type: "[Store]",
          extensions: {
            link: {},
          },
        },
      },
      interfaces: ['Node'],
    }),
  ]
  createTypes(typeDefs)
}
freiksenet commented 5 years ago

@NickyMeuleman Do you mean you don't get results or that you don't see fields in input object?

NickyMeuleman commented 5 years ago

When I comment out the date key in that link, sorting by date no longer has any effect.

query MyQuery {
  allBlogPost(sort: {fields: date, order: DESC}) {
    nodes {
      date(fromNow: true)
    }
  }
}

has the same (non-empty) result as the same query with the order set to ASC. Was hoping that wouldn't be necessary since there is a custom date resolver

ryanwiemer commented 4 years ago

Are there any examples out there for how to handle schema customization for an optional image coming from Contentful?

sidharthachatterjee commented 4 years ago

Been a while since Schema Customisation has been released and things are stable. I think it's time to close this issue 🙂

Incredible work @freiksenet and @stefanoverna ❤️

Folks, if you're having trouble related to Schema Customisation, let's open independent issues. Thanks!