Documenting Source Service in Stitched Schemas

Subschemas in this project often share types, meaning these types need to be merged into the superschema for the gateway. When these types are merged, we have no easy way of knowing which service each field comes from.

Documenting Source Service in Stitched Schemas
Image from: https://the-guild.dev/graphql/stitching/docs/approaches/type-merging

I work on a project that uses schema stitching and type merging to merge subschemas into a gateway superschema.

Subschemas in this project often share types, meaning these types need to be merged into the superschema for the gateway. When these types are merged, we have no easy way of knowing which service each field comes from.

For example, shirt-service has a type that looks like this:

type Shirt {
  """
  The length of the sleeve in inches
  """
  sleeveLength: Integer
}

Similarly, clothing-service has a Shirt type:

type Shirt {
  """
  The length of the torso in inches
  """
  torsoLength: Integer
}

Why is this type distributed across services, you ask? Well, lets just wave our hands vaguely towards the past and say "its legacy code." These kinds of distributed types are so easy to accidentally create when teams are working in similar directions but haven't established good communication lines yet.

The Shirt type in the gateway's superschema is now going to have fields from both subschemas. But the trouble is, it's hard to know which service sleeveLength and torsoLength come from just by looking at the schema.

type Shirt {
  """
  The length of the sleeve in inches
  """
  sleeveLength: Integer
  """
  The length of the torso in inches
  """
  torsoLength: Integer
}

Our teams have found ways of following traces in DataDog to their source service, but this requires a lot of manual work and is not intuitive.

Adding Resolving Service to the Stitched Schema

When we stitch schemas we can use transformations add documentation to each field to note it's resolving service.

Let's start by defining a schema transformer that can add documentation to fields for us. You can find this code on GitHub too: https://github.com/cassidycodes/stitching-docs-example/blob/main/src/app.ts#L32-L59

 const documentSourceService = ({
    serviceName = 'unknown',
  }: documentSourceServiceOptions) => {
    // Create a new transformer that is returned by this function
    return new TransformObjectFields((_typeName, _fieldName, fieldConfig) => {
      let service = serviceName;

      const commentToAdd = `Resolved by ${service}.`;

      // Build a copy of the AST Node with a new description
      if (fieldConfig.astNode) {
        fieldConfig.astNode = {
          ...fieldConfig.astNode,
          description: {
            ...fieldConfig.astNode?.description,
            kind: Kind.STRING,
            // Don't forget to keep the original description if there is one!
            value: fieldConfig.astNode?.description
              ? fieldConfig.astNode?.description.value.concat(`\n${commentToAdd}`)
              : commentToAdd,
          },
        };
      }

      // Add the description to the fieldConfig as well
      fieldConfig.description = fieldConfig.description
        ? fieldConfig.description.concat(`\n${commentToAdd}`)
        : commentToAdd;

      return fieldConfig;
    });
  };

The transformer accepts a service name, maps over object fields, and rebuilds the AST Node to add a description. It also adds the description to the fieldConfig. The AST Node is largely copied over from the original one with the value of the documentation field getting an update.

The astNode description is used when we are printing the schema to a file, the fieldConfig.description is used in things like the Yoga GraphQL explorer.

Now we can stitch the schemas and apply the transformations:

stitchSchemas({
    subschemas: [
      {
        schema: await schemaFromExecutor(clothingExec, adminContext),
        executor: clothingExec,
        merge: {
          Shirt: {
            fieldName: 'shirt',
            selectionSet: '{ id }',
            args: originalObject => ({ id: originalObject.id })
          },
        },
        transforms: [
          documentSourceService({ serviceName: 'clothing-service' })
        ],
      },
      {
        schema: await schemaFromExecutor(shirtExec, adminContext),
        executor: shirtExec,
        merge: {
          Shirt: {
            fieldName: 'shirt',
            args: originalObject => ({ id: originalObject.id })
          },
        },
        transforms: [
          documentSourceService({ serviceName: 'shirt-service' })
        ],
      },
    ],
    mergeTypes: true,
  });

And now we have documentation strings in our schema telling us which service resolves each field!

You can find a working example of this on my GitHub:

GitHub - cassidycodes/stitching-docs-example: Example of adding docs during schema stitching
Example of adding docs during schema stitching. Contribute to cassidycodes/stitching-docs-example development by creating an account on GitHub.