apollographql / apollo-tooling

✏️ Apollo CLI for client tooling (Mostly replaced by Rover)
https://apollographql.com
MIT License
3.04k stars 467 forks source link

Support JSDoc to Generate Types for GQL Scalars #2544

Open nalchevanidze opened 2 years ago

nalchevanidze commented 2 years ago

Support JSDoc to Generate Types for GQL Scalars

Note: This is a brief summary of Post: typed-scalars.

Motivation

One of the fundamental strengths of GraphQL is that we have control over the depth of the structure by selecting fields in your query. This limitation stops graph databases and recursive data types with independent resolvers running into the loop.

Nevertheless, this design can be obstructive in some cases. The first example is introspection queries, where the client has to query the fields nine levels deep to cover the most useful types.

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}

Hypothetically, we could create a schema with type [[[[[User]!]!]!]! where we could break clients. Since this scenario is doubtful, it was never a big issue in GraphQL. However, there are other recursive types (Tree Types), where this issue can be challenging. They can have hundreds or even thousands of nesting levels before reaching the leaf nodes, which sometimes makes them impossible to query. Let us consider one instance: the RichText. We want to create RichText in our WebApp where we use GraphQL BFF.

enum RichTextNodeType {
  Label
  Paragraph
  Image
}

type RichTextNode {
  type: RichTextNodeType!
  src: String
  text: String
  children: [RichTextNode!]
}

For this case, the solution presented above (see TypeRef) is no longer applicable, as it can have hundreds of nesting levels depending on the content.

Solution in GraphQL

The straightforward solution to this problem is to represent RichText by custom scalar and map it to the specific type. However, this works well when server and client are packages in the same Monorepo and use the same language. If we target third-party clients, we need to define the library "@types/rich-text" and publish it on npm for them.

// apollo.config.yaml

config:
  scalars:
    RichTextNode: import('@types/rich-text').RichTextNode

However, this approach has the following problems:

A general solution in GraphQL is typed scalars (which we have in Iris as data types). A typed scalar will represent JSON values without getting its dedicated resolvers. GraphQL compiler will only check if the values match type definitions and will not automatically resolve their fields. That way, we would not run into the loop but still have type safety guaranteed by the compiler.

One attempt of solving this problem in GraphQL is to provide type annotations with JSDoc in the scalar description, where a type generator could parse annotations and generate corresponding types. In addition, a server with the directive @JSDoc could use these annotations to validate scalar (inputs/outputs) values.

enum RichTextNodeType = {
  Label
  Paragraph
  Image
}

"""
@type {{  
  type: RichTextNodeType,
  src: ?string,
  text: ?string,
  children: ?RichTextNode[]
  }}
"""
scalar @JSDoc RichTextNode
// __generated__/globalTypes.ts

export type RichTextNodeType = "Label" | "Paragraph" | "Image"

export type RichTextNode = {
  type: RichTextNodeType,
  src: string | undefined,
  text: string | undefined,
  children: RichTextNode[] | undefined
}
nalchevanidze commented 2 years ago

Furthermore, this would allow my language Iris to function without dedicated code-gen CLI.

PR for this feature is already in progress #2545