Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Research how best to handle schema customization #12

Open
thompsonsj opened this issue Jun 5, 2023 · 2 comments
Open

Research how best to handle schema customization #12

thompsonsj opened this issue Jun 5, 2023 · 2 comments

Comments

@thompsonsj
Copy link
Owner Author

Generating schemas from Payload and passing to Gatsby

Terminology

  • SDL The GraphQL Schema Definition Language, or SDL.

graphql-js types

TODO: Investigate.

From https://www.gatsbyjs.com/docs/reference/graphql-data-layer/schema-customization/#gatsby-type-builders:

Note that the createTypes action also accepts graphql-js types directly, but usually either SDL or Type Builders are the better alternatives.

SDL

SDL can be generated from Payload CMS. In the following example, a route is added to server.ts to generate Gatsby schema customizations on demand:

import { printSchema, GraphQLSchema } from 'graphql'
import buildObjectType from 'payload/dist/graphql/schema/buildObjectType'

// Collection
import Testimonials from './collections/Logos/Testimonials'

app.get('/gatsby-schema-types', async (_, res) => {
  const gatsbyNodeType = "PayloadTestimonials"

  const testimonials = buildObjectType({
    payload,
    name: gatsbyNodeType,
    parentName: gatsbyNodeType,
    fields: Testimonials.fields
  })
  const schema = new GraphQLSchema({query: testimonials})

  const report = printSchema(schema)

  res.setHeader('content-type', 'application/json')
  res.send(report)
})

This will print the schema for collection fields (as defined).

schema {
  query: PayloadTestimonials
}

type PayloadTestimonials {
  locales: [PayloadTestimonials_locales!]
  name: String!
  jobTitle: String
}

"""
Additional definitons....
"""

A more complicated example that:

  • stiches multiple types together into a single SDL string; and
  • modifies the resulting SDL as necessary with rudimentary (brittle but works!) text substitution.
import { Field } from 'payload/types'
import { buildSchema, printSchema, GraphQLSchema } from 'graphql'
import { mergeSchemas } from '@graphql-tools/schema'
import buildObjectType from 'payload/dist/graphql/schema/buildObjectType'

import Testimonials from './collections/Logos/Testimonials'
import ATS from './global/ATS'

app.get('/gatsby-schema-types', async (_, res) => {
  interface IgatsbyNodeType {
    gatsbyNodeType: string
    fields: Field[]
  }
  
  const gatsbyNodeTypes: IgatsbyNodeType[] =[
    {
      gatsbyNodeType: "PayloadTestimonials",
      fields: Testimonials.fields,
    },
    {
      gatsbyNodeType: "PayloadAts",
      fields: ATS.fields,
    }
  ]

  const schemas = gatsbyNodeTypes.map((node) => {
    const query = buildObjectType({
      payload,
      name: node.gatsbyNodeType,
      parentName: node.gatsbyNodeType,
      fields: Testimonials.fields
    })
    const schema = new GraphQLSchema({query})
    const sdl = printSchema(schema)
      // Payload's JSON scalar conflicts with a built-in/Gatsby scalar
      .replaceAll('JSON', 'PayloadJSON')
      // replace backticks so we can paste the result into a string literal
      .replaceAll('`', "\"")
      // Gatsby won't load in the schema/query definition
      .replace('schema {', '')
      .replace(`  query: ${node.gatsbyNodeType}`, '')
      .replace('}', '')
    return buildSchema(sdl)
  })

  const merged = mergeSchemas({
    schemas
  })

  let report = printSchema(merged)

  gatsbyNodeTypes.forEach(node => {
    // Add Gatsby's Node interface to each type
    report = report.replace(`type ${node.gatsbyNodeType} {`, `type ${node.gatsbyNodeType} implements Node {`)
  })

  res.setHeader('content-type', 'application/json')
  res.send(report)
})

Required changes to output

The generated SDL needs to be modified before use with Gatsby's createTypes action.

TODO: Investigate an automated approach - are there any Gatsby/Payload/graphql classes/functions that can help?

Add implements Node

On each type that corresponds to a Gatsby Node type, implement the Node interface.

type PayloadTestimonials implements Node

See https://www.gatsbyjs.com/docs/reference/graphql-data-layer/schema-customization/#the-node-interface.

Remove schema from the top

schema {
  query: PayloadTestimonial
}

If this is not removed, the following error is produced on gatsby develop:

Missing onError handler for invocation 'building-schema', error was 'TypeError: Cannot read properties of undefined (reading 'value')'.
Stacktrace was 'TypeError: Cannot read properties of undefined (reading 'value')

Remove backticks in the generated output

The generated SDL include comments (wrapped in """) that can use backticks - making it impossible to use a string literal. Replace with standard quotes or remove.

Build-in scalar type error for the JSON scalar

Missing onError handler for invocation 'building-schema', error was 'Invariant Violation: The GraphQL type `JSON` is reserved for internal use
by built-in scalar types.'. Stacktrace was 'Invariant Violation: The GraphQL type `JSON` is reserved for internal use by built-in scalar types.

@gigaSproule
Copy link

gigaSproule commented Dec 3, 2024

I tried using the generated GraphQL schema from payload-graphql generate:schema. After making the expected changes so it's compatible with Gatsby, it seems to be happy, but the blocks have a type in the schema, but the plugin doesn't have a type defined for it, so it prints out the error

Cannot read properties of undefined (reading 'type')

If I remove the custom schema and let Gatsby generate what it thinks is the schema, the blocks are a flat set of fields. Whilst this isn't an issue functionally, it does mean that the page-data.json is going to be full of null fields, which is problematic when you have various complex objects in the same field.

EDIT: I was able to get around this by utilising schema.buildUnionType. It would be great to have this done automatically, but at least there's a way to solve it.

As the Gatsby docs don't show this at all, here's an example.

createTypes([
  ...
  schema.buildUnionType({
    name: `PayloadBlockUnionABC`,
    types: [
      `PayloadBlockTypeA`,
      `PayloadBlockTypeB`,
      `PayloadBlockTypeC`
    ],
    resolveType: (value, info) => {
      if (value.blockType === "block-type-a") {
        return `PayloadBlockTypeA`;
      } else if (value.blockType === "block-type-b") {
        return `PayloadBlockTypeB`;
      }
      return "PayloadBlockTypeC";
    }
  }),
  ...
]);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants