Custom Directive with a Co-processor at Router level

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?

1 Like

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:

  1. 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
  2. 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.

1 Like

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:

  1. To store the un-scrubbed subgraph operation in context (I again recommend a Rhai script)
  2. To scrub the @date directive from the subgraph query in the SubgraphRequest stage
  3. 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
1 Like