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.
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: