Leonardo Losoviz Freelance developer and writer, with an ongoing quest to integrate innovative paradigms into existing PHP frameworks, and unifying all of them into a single mental model.

Adding directives to the schema in code-first GraphQL servers

8 min read 2398

Adding Directives To The Schema In Code-first GraphQL Servers

There are two approaches to creating a GraphQL server: the SDL-first (for schema definition language) approach, and the code-first approach, which each have benefits and drawbacks. And there are two types of directives: schema-type directives (such as @deprecated), which are declared on the schema via the SDL; and query-type directives (such as @include and @skip), which are added to the query in the client.

One of the drawbacks of the code-first approach is that, because it doesn’t have an SDL, it can’t naturally support schema-type directives. A code-first server could provide the alternative to declare schema-type directives via code instead of the SDL. However, the maintainers of graphql-js (which is the reference implementation for GraphQL) have decided to not support registering directives via code.

As a consequence, code-first servers depending on graphql-js may be unable to offer features that depend on these directives unless they find some workaround. For instance, Nexus can’t be integrated with Apollo Federation, which requires you to define a @key directive on the schema.

But other GraphQL servers do not need to emulate graphql-js in this respect and may offer support to declare schema-type directives via code. After all, the GraphQL spec does not concern itself with how directives are to be implemented.

In this article, I’ll describe the strategy I’ve implemented for GraphQL by PoP (the code-first GraphQL server written in PHP that I authored) to provide support for schema-type directives via code.

Contrasting schema- and query-type directives

@deprecated is a schema-type directive, so it must be applied on the schema. However, what would happen if we pretend for a moment that it is a query-type directive and add @deprecated on some field directly in the query?

For instance, when executing this query:

query {
  posts {
    id
    title
    content @deprecated(reason: "Use newContent instead")
  }
}

Well, it would work, too! Because, after all, a directive is just some functionality to execute on the field; declaring that functionality via the schema, or directly in the query, does not make the functionality behave any different.

The thing is that, even though it works, it doesn’t make any sense; you can’t force your clients to add @deprecated to their queries. This is functionality decided by the application on the server side, not on the client.

However, the takeaway is that the functionality itself still works. Hence, whether the directive is added to the schema or to the query doesn’t matter from a functional point of view. Moreover, every directive will eventually end up being present in the query since that’s where it is executed.

We made a custom demo for .
No really. Click here to check it out.

Thus, if we don’t have an SDL, we can still embed the directive into the query on runtime.

Decoupling the requested and executable queries

In the article Treating GraphQL directives as middleware, I described the directive pipeline, an architecture that enables the server’s engine to resolve, validate, and execute the query. In order to make the engine as simple as possible, every action concerning the resolution of the query takes place within the pipeline through directives.

The Directive Pipeline
The directive pipeline.

Calling the resolver to validate and resolve a field, and merge its output into the response, is accomplished through a couple of special directives: @validate and @resolveValueAndMerge. These directives are of a special type; they are not added by the application (on either the query or the schema) but by the engine itself. These two directives are implicit, and they are always added on every field of every query.

From this strategy, we can see that when executing a query on the GraphQL server, there are actually two queries involved:

  1. The requested query
  2. The executable query

The executable query, which is the one that’s ultimately resolved by the server, is produced from applying transformations on the requested query, among them the inclusion of the directives @validate and @resolveValueAndMerge for every field.

The Inner Process Within The GraphQL Server
Inner process within the GraphQL server.

For instance, if the requested query is this one:

{
  posts {
    url
    title @uppercase
    content @include(if: $addContent)
  }
}

The executable query will be this one:

{
  posts @validate @resolveValueAndMerge {
    url @validate @resolveValueAndMerge
    title @validate @resolveValueAndMerge @uppercase
    content @validate @include(if: $addContent) @resolveValueAndMerge
  }
}

Indeed, we can transform the query to add any directive, not only the special-type ones. This way, even though there is no SDL, we can still insert a @deprecated directive in the field on runtime.

As a side note, decoupling the query into requested and executable instances has other potential uses. For instance, it could provide a solution for the flat chain syntax issue to resolve a shortcut programs.shortName into an array of the strings (["name1", "name2", ...]) instead of an array of objects ([{shortName: "name1"}, {shortName: "name2"}, ...]) through these steps:

  • Transforming field programs.shortName from the requested query into the corresponding connection programs { shortName } in the executable query
  • Resolving this part of the executable query
  • Copying the resolved data upwards the nodes of the graph through some directive @copyConnectionDataUpwards
  • Converting the results from an array of objects to the expected array of strings through some @flatten directive
  • Placing this result on the response for the queried object under entry with name programs.shortName

IFTTT through directives

Next, we must produce a mechanism to tell the server when and how to add the directives onto the query. The mechanism I’ve developed is based on the concept of IFTTT (if this, then that), and I’ve called it IFTTT through directives.

In general, IFTTT are rules that trigger actions whenever a specified event happens. In our situation, the pairs of event/action are:

  • If “field X found on the query,” then “attach directive Y to field X”
  • If “directive Z found on the query,” then “execute directive Y before/after directive Z”

How do we add directives to the schema via IFTTT? Say, for instance, we want to create a custom directive @authorize(role: String!) to validate the that user executing field myPosts has the expected role author, or show an error otherwise.

If we could work with the SDL, we would create the schema like this:

directive @authorize(role: String!) on FIELD_DEFINITION

type User {
  myPosts: [Post] @authorize(role: "author")
}

The IFTTT rule defines the same intent that the SDL above is declaring: whenever requesting field myPosts, execute directive @authorize(role: "author") on it.

Being coded in PHP, this rule for GraphQL by PoP looks something like this:

$iftttManager = IFTTTManagerFacade::getInstance();
$iftttManager->addEntriesForFields(
  'authorize'
  [
    [RootTypeResolver::class, 'myPosts', ['role' => 'author']],
  ]
);

Now, whenever field myPosts is found on the query, the engine will automatically attach @authorize(role: 'author') to that field on the executable query.

IFTTT rules can also be triggered when encountering a directive, not just a field. For instance, rule “whenever directive @translate is found on the query, execute directive @cache(time: 3600) on that field” is coded like this:

$iftttManager = IFTTTManagerFacade::getInstance();
$iftttManager->addEntriesForDirectives(
  'translate'
  [
    ['cache', ['time' => 3600]],
  ]
);

Adding IFTTT directives to the query is a recursive process: it will trigger a new event to be processed by IFTTT rules, potentially attaching other directives to the query, and so on.

For instance, rule “Whenever directive @cache is found, execute directive @log” would log an entry about the execution of the field, and then trigger a new event concerning this newly added directive.

IFTTT as configuration, not code

An interesting side effect of using the IFTTT approach to execute functionality through directives is that instead of hardcoding a directive into the schema, which is suitable for executing predetermined actions, we can now add directives through configuration, which is suitable for making the API more flexible.

We could conveniently invoke actions from plug-and-play third parties, for example, or grant our users (not just our developers!) the chance to modify the behavior of the service.

For instance, for the GraphQL by PoP-powered GraphQL API for WordPress I have built an interface for users to configure what access control rules are applied on the schema:

Providing Access Control To The Schema
Providing access control to the schema through IFTTT.

The engine is executing a chain of directives on the field, but it doesn’t know in advance which directives to execute. These are defined by the user through the interface and are consequently stored as IFTTT rules.

Some possible directives from third parties to be plugged into the service through configuration could be:

  • After mutating data through createPost, send a notification to the user via email/Slack
  • After requesting forgotPassword, send SMS for 2FA, if enabled
  • After calling addComment on a static Jamstack site, execute a webhook to regenerate the site
  • After calling uploadImage, invoke a cloud service to compress the image
  • Source imageSrc through a CDN

Dealing with multiple configurations through field aliases

As we’ve seen, we can add directives to our query on runtime through coded IFTTT rules, in this way bypassing the SDL. But this alone is not enough to build a versatile schema.

For instance, say that our site is in English, and we need to translate it to French. We can then create rule “Whenever requesting fields Post.title and Post.content, attach directive @translate(from: "en", to: "fr") to the field.”

Now, whenever requesting this query:

{
  posts {
    id
    title
    content
  }
}

The server will execute this query:

{
  posts {
    id
    title @translate(from: "en", to: "fr")
    content @translate(from: "en", to: "fr")
  }
}

So far, so good. But then, what happens if we want to retrieve the data without any processing, back in English? Now we can’t do it anymore because fields Post.title and Post.content will always have the directive @translate attached to them.

The solution is to create field aliases, but on the server side (the concept of field aliases on GraphQL is executed on the client, not on the server). Then, we can create the following field aliases:

  • Post.title => Post.frenchTitle
  • Post.content => Post.frenchContent

A field and its aliases are all available in the schema and are resolved exactly the same way by the resolver, hence both Post.title and Post.frenchTitle will be resolved as "Hello world!". But then, we can define the IFTTT rule on the aliased fields only so that only the aliased fields are translated to French:

$directiveArgs = ['from' => 'en', 'to' => 'fr'];
$iftttManager = IFTTTManagerFacade::getInstance();
$iftttManager->addEntriesForFields(
  'translate'
  [
    [PostTypeResolver::class, 'frenchTitle', $directiveArgs],
    [PostTypeResolver::class, 'frenchContent', $directiveArgs],
  ]
);

Now, this query:

{
  posts {
    id
    title
    frenchTitle
    content
    frenchContent
  }
}

Will be executed by the server like this:

{
  posts {
    id
    title
    frenchTitle @translate(from: "en", to: "fr")
    content
    frenchContent @translate(from: "en", to: "fr")
  }
}

In the same way, we can create directive aliases.

Improving the legibility of the schema through aliases

This section is a side note, to show how aliases on the server-side can have other great uses too.

In the article Versioning fields in GraphQL, I described how we can provide field- or directive-based versioning for our schema (as contrasted with evolving the schema), where we pass a field (or directive) argument versionConstraint to indicate which version of the field to use.

Field aliases can be a convenient mechanism to expose all versions of the fields in the schema; we could “tag” an alias to a specific version of a field, like this:

  • Post.v1Title => Post.title(versionConstraint: 1.2.5)

Another use is to avoid having to namespace custom directives, a practice recommended by the spec:

When defining a directive, it is recommended to prefix the directive’s name to make its scope of usage clear and to prevent a collision with directives which may be specified by future versions of this document (which will not include _ in their name). For example, a custom directive used by Facebook’s GraphQL service should be named @fb_auth instead of @auth.

The issue with this practice is that it makes the schema ugly, where @fb_auth is not as elegant as the simple @auth. Even worse, it is not 100 percent reliable in avoiding conflicts since companies may use the same namespace to identify themselves. For instance, a library offering directive @fb_auth may be produced not just by Facebook, but also by Google’s Firebase.

An alternative solution offered by aliases is to generate an aliased version of the directive only when the conflict actually arises.

For instance, if we are using directive @auth provided by Facebook, and we later on also need to use directive @auth provided by Firebase, only then would we create aliases for them, such as @fb_auth and @g_fb_auth.

It is true that the possibility of breaking changes with this strategy, in theory, goes up: if we call the directive @auth and then the GraphQL spec mandates that @auth is a spec-required directive (such as @include and @skip), then namespacing our directive will not be enough; we must also change the queries to use the new, namespaced name.

But what are the chances of this situation actually happening? Given the favor no change guideline to modify the spec, and the reticence to introduce official directives (other than a potential few exceptions, such as @stream, @defer, and maybe @export), the chances of coming across a naming conflict are pretty much nil.

With directive aliases, our schema can be elegant and legible by default, and namespacing is introduced only in the slight chance it is ever needed, and not always.

Conclusion

By now, you might have concluded that I love directives. If that’s the case, you are right. In my opinion, directives are among the most powerful features of GraphQL, and I believe that providing good access to them should be one of the top priorities of any GraphQL server.

In this article, I have described how code-first servers (which have no SDL) can manage to provide support for schema-type directives. The idea is simple: instead of defining them on the schema, attach them to the query, on runtime, through IFTTT rules. This is certainly not the only way to do it, but that’s how I have implemented it for my GraphQL server, and it works pretty well.

Keep reading more

This article is part of an ongoing series on conceptualizing, designing, and implementing a GraphQL server. The previous articles from the series are:

  1. Designing a GraphQL server for optimal performance
  2. Simplifying the GraphQL data model
  3. Schema-first vs. code-first development in GraphQL
  4. Speeding up changes to the GraphQL schema
  5. Versioning fields in GraphQL
  6. GraphQL directives are underrated
  7. Treating GraphQL directives as middleware
  8. Creating an @export GraphQL directive

 

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    Monitor failed and slow GraphQL requests in production

    While GraphQL has some features for debugging requests and responses, making sure GraphQL reliably serves resources to your production app is where things get tougher. If you’re interested in ensuring network requests to the backend or third party services are successful, try LogRocket.https://logrocket.com/signup/

    LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic GraphQL requests to quickly understand the root cause. In addition, you can track Apollo client state and inspect GraphQL queries' key-value pairs.

    LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
    Leonardo Losoviz Freelance developer and writer, with an ongoing quest to integrate innovative paradigms into existing PHP frameworks, and unifying all of them into a single mental model.

    Leave a Reply