I want to provide a directive similar to @date(format: “mmmm d, yyyy”) for our supergraph users. Users of Apollo Explorer should be able to use this directive. I want to convert the value at the router level, possibly using a co-processor, without requiring each subgraph team to implement the directive in their subgraphs. I’d appreciate any thoughts and idea, is it even possible?
Just to make sure I understand, you want to have a schema like this:
directive @date(format: String) on FIELD_DEFINITION
type Query {
now: String! @date(format: "mmmm d, yyyy")
}
Schema
The service would have to return a String to make it compatible with the SDL. So maybe something like an ISO-8601 date string. You could maybe use a custom scalar for this, but it might be a little confusing when reviewing the Schema docs.
Formatting with Directive
I think you can transform the data that you get back as long as it will fulfill the type defined in the SDL, but you’ll need to have the schema (with directives) in the coprocessor to do it.
The way I see it, you could do it at one of two levels:
- The subgraph level — This would handle formatting when subgraphs respond. This would ensure that any field that uses the directive would be formatted before being sent to another subgraph when required via the
@requires
directive or in a@key
- The supergraph level — This would handle the formatting all at once, but unformatted dates would be sent to subgraphs
To make this choice easy, I would have dedicated fields for the formatted and unformatted dates, updating my example schema to:
directive @date(format: String) on FIELD_DEFINITION
"An ISO-8601 date string."
scalar DateTime @specifiedBy(url: "https://www.iso.org/iso-8601-date-and-time-format.html")
type Query {
now: DateTime!
nowFormatted: String! @date(format: "mmmm d, yyyy")
}
This way a subgraph can @requires
whichever they need and we can format at the subgraph request level.
What you’ll need
To do the formatting, you would need:
- The supergraph schema with the directives (so you know what needs to be formatted)
- The Operation document (so you know about aliases and their original field name when aliases are used, more on this later)
- The response data from the subgraph
Supergraph Schema
I’m not 100% sure how I would get the supergraph schema. You may be able to get it by tapping into the RouterRequest
stage and requesting the SDL, but that will push the SDL to the coprocessor for every router request. There may be another way, but it would require custom code to do it, I think.
coprocessor:
url: ...
timeout: 2s
router:
request:
sdl: false
Operation Document
To get the operation document, you’ll tap into the SubgraphRequest
stage:
coprocessor:
# ...
subgraph:
all:
request:
body: true
context: true
At this stage, you can capture the subgraph operation document and push it into the request context for use down the line. You can do this in the coprocessor, but I would recommend using Rhai instead because it will run in the Router context (and be one less request to your coprocessor):
rhai:
scripts: "/rhai"
main: "main.rhai"
// /rhai/main.rhai
fn subgraph_service(service, subgraph) {
const request_callback = Fn("process_subgraph_request");
service.map_request(request_callback);
}
fn process_subgraph_request(request) {
// Add the subgraph operation document to the Router context so it is accessible
// during the subgraph_response stage in the coprocessor.
request.context["custom::subgraph_operation::document"] = request.body.query;
}
Doing the Formatting
To format the data, you’ll tap into the SubgraphResponse
stage and do your actual work:
coprocessor:
# ...
subgraph:
all:
# ...
response:
body: true
context: true
To do the formatting itself, you’ll crawl the operation document (which you’ll get from context under the context.entries["custom::subgraph_operation::document"]
) to find any fields that have the directive.
If you’re using something like the graphql JS library in a node.js coprocessor service, you can use the visit function and tie the operation to the schema using TypeInfo.
import { TypeInfo, visit, visitWithTypeInfo, parse } from 'graphql';
const operation = parse(`
query ExampleQuery {
nowFormatted
}
`);
const typeInfo = new TypeInfo(supergraphSchema);
visit(operation, visitWithTypeInfo(typeInfo, {
enter(node) {
// Access current type and field information using typeInfo
const type = typeInfo.getType();
const parentType = typeInfo.getParentType();
const fieldDef = typeInfo.getFieldDef();
// Perform actions based on the current context...
},
leave(node) {
// Clean up or perform actions after visiting a node...
},
}));
If a field has the directive, you’ll look in the subgraph response data to find that field and format its value.
Handling Field Aliases
When crawling the operation, you’ll need to watch our for aliases. For example, a query for the example schema could be:
{
nowFormatted
}
or it could also be:
query ExampleQuery {
currentDate: nowFormatted
}
When the data is returned by the subgraph for that second operation with the alias, the JSON will have:
query ExampleQueryWithAlias {
"data": {
"currentDate": "2025-04-30T00:00:01Z"
}
}
This is why the operation is necessary so you can make the correct data to operation to schema connection to format properly.
Final YAML
coprocessor:
url: ...
timeout: 2s
router:
request:
sdl: false
subgraph:
all:
request:
body: true
context: true
response:
body: true
context: true
rhai:
scripts: "/rhai"
main: "main.rhai"
There might be other ways to do this (I’m curious what the community can come up with). But hopefully this is helpful.
Thanks for the detailed answer @greg-apollo.
Currently we have fields that that returns DateTime in ISO format as you suggested:
query Nodes {
assetSearch {
nodes {
lastBootedAt
}
}
}
# Response #
{
"lastBootedAt": "2025-04-15T14:42:08Z"
}
Now, let’s say I want add custom date formatting using directives like this in Apollo Explorer:
query Nodes {
assetSearch {
nodes {
lastBootedAt @date(format: "mmmm d, yyyy")
}
}
}
Now the catch:
I want to implement this without asking our subgraph teams to modify their subgraphs code. I’d like our front-end teams to use the directive in their code when querying the supergraph. Similarly, I want give our PowerShell users also the ability to specify the directive in their PS Script like this:
$query = @"
query Nodes {
assetSearch {
nodes {
lastBootedAt @date(format: "mmmm d, yyyy")
}
}
}
"@
I want create as many directives we want without any dependency on our subgraphs teams modifying their code each time we create a directive. This way as the Graph Platform Team, we can handle the conversion for our end users and subgraph teams, so maybe your 2nd option maybe the path we need to take?
I assume I’ll have to create a subgraph with all the directives we want maybe using @composeDirective
to make sure it’s available in the supergraph for external use, and then we can intercept at router level and modify the response based on the format?
Apologies, if this is really confusing.
Thank you.
Ah, gotcha. That’s a very different approach.
There’s a distinction between a Schema directive and a client directive.
In this case, you would define the directive as:
directive @date(format: String!) on FIELD
I believe @composeDirective
would be used to include it in the supergraph so clients can use it and at least one subgraph would need to define the directive in its SDL.
Typically, the catch is that all subgraphs will need to include the directive definition to accept operations that include it (although they may all need it for the supergraph to compose properly, I’m not sure).
But, assuming the supergraph composes, you could use your coprocessor to scrub the directive from the queries on the way to subgraphs in the SubgraphRequest
stage and then format the response data in the SubgraphResponse
stage. To do that you would need:
- To store the un-scrubbed subgraph operation in context (I again recommend a Rhai script)
- To scrub the
@date
directive from the subgraph query in theSubgraphRequest
stage - To read the un-scrubbed operation from context and use it to format the values of the fields it is applied to in the
SubgraphResponse
stage