Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Abstraction for reusable schema @directive implementations. #640

Merged
merged 80 commits into from
Mar 14, 2018

Conversation

benjamn
Copy link
Contributor

@benjamn benjamn commented Feb 16, 2018

This PR implements a class called SchemaDirectiveVisitor that enables writing reusable implementations of any @directives that appear in a GraphQL schema written in Schema Definition Language.

Note: I would not recommend reviewing this PR commit-by-commit. Instead, just have a look at the combined changes, which are amply commented. There were a lot of detours as I figured out exactly which abstractions I needed.

By overriding one or more visit{Object,Union,...} methods, a subclass of SchemaDirectiveVisitor registers interest in certain schema types, such as GraphQLObjectType, GraphQLUnionType, etc. When SchemaDirectiveVisitor.visitSchemaDirectives is called with a GraphQLSchema object and a map of visitor subclasses, the overridden visitor methods of those subclasses are able to obtain references to any schema type objects that have @directives attached to them, enabling the visitors to inspect or modify the schema as appropriate.

For example, if a directive called @rest(url: "...") appears after a field definition, a SchemaDirectiveVisitor subclass could provide meaning to that directive by overriding the visitFieldDefinition method (which receives a GraphQLField parameter), and then the body of that visitor method could manipulate the field's resolver function to fetch data from a REST endpoint:

const typeDefs = `
type Query {
  people: [Person] @rest(url: "/api/v1/people")
}`;

const schema = makeExecutableSchema({ typeDefs });

SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
  rest: class extends SchemaDirectiveVisitor {
    public visitFieldDefinition(field: GraphQLField<any, any>) {
      const { url } = this.args;
      field.resolve = () => fetch(url);
    }
  }
});

The subclass in this example is defined as an anonymous class expression, for brevity. A truly reusable SchemaDirectiveVisitor would most likely be defined in a library using a named class declaration, and then exported for consumption by other modules and packages.

See my comments in the code below for more explanation of how this all works.

Next steps:

public visitInputObject(object: GraphQLInputObjectType) {}
public visitInputFieldDefinition(field: GraphQLInputField, details: {
objectType: GraphQLInputObjectType,
}) {}
Copy link
Contributor Author

@benjamn benjamn Feb 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much of the complexity of this implementation was necessary so that these visit* methods could have meaningful names (compared to just visit) and concrete parameter types, rather than a generic type like GraphQLNamedType (which would not even be generic enough for GraphQLField<any, any> or GraphQLEnumValue). This makes writing SchemaDirectiveVisitor subclasses a breeze in an editor with TypeScript auto-completion, yet also works just fine if you're consuming the SchemaDirectiveVisitor class in pure JavaScript.

visitorSelector: (
type: VisitableSchemaType,
methodName: string,
) => SchemaVisitor[],
Copy link
Contributor Author

@benjamn benjamn Feb 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After digesting the comment above this visitorSelector parameter, I would suggest checking out this very simple visitorSelector implementation and then having a look at the more powerful visitorSelector used by SchemaDirectiveVisitor.visitSchemaDirectives.

args = getArgumentValues(decl, directiveNode);
} else {
// If this directive was not explicitly declared, just convert the
// argument nodes to their corresponding JavaScript values.
Copy link
Contributor Author

@benjamn benjamn Feb 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the SchemaDirectiveVisitor abstraction works even for @directives that have not been declared in the schema with the syntax

directive @myDirective(param: Type = defaultValue) on OBJECT | UNION | ...

However, declaring your directives using SDL might still be a good idea, since the parameter types and default values will be enforced by this code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm, if I import a directive from somewhere, will its arguments/locations get enforced?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually super useful info for third party packages that may not have access to the sdl!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stubailo Yes, if a GraphQLDirective node with the same name is in the schema at the time visitSchemaDirectives happens (usually at the end of makeExecutableSchema), then it will be enforced. Regarding locations, though, we only enforce that the SchemaDirectiveVisitor actually implements appropriate visitor methods for the locations declared by the declaration. It's ok if the visitor implements additional locations not mentioned by the declaration.

| GraphQLArgument
| GraphQLUnionType
| GraphQLEnumType
| GraphQLEnumValue;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was no meaningful supertype of this hodgepodge of types that SchemaVisitor can visit, hence the union.


Object.keys(directiveResolvers).forEach(directiveName => {
directiveVisitors[directiveName] = class extends SchemaDirectiveVisitor {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The directiveResolvers option supported by makeExecutableSchema works exactly as it did before, but is now implemented in terms of SchemaDirectiveVisitor.visitSchemaDirectives.

@@ -78,6 +82,7 @@ export interface IExecutableSchemaDefinition {
allowUndefinedInResolve?: boolean;
resolverValidationOptions?: IResolverValidationOptions;
directiveResolvers?: IDirectiveResolvers<any, any>;
directiveVisitors?: { [name: string]: typeof SchemaDirectiveVisitor };
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure whether folks would want to pass directiveVisitors to makeExecutableSchema, a la directiveResolvers, or just call SchemaDirectiveVisitor.visitSchemaDirectives(schema, directiveVisitors) after creating the schema, so both styles are supported.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we want to have it here. The way I'm imagining the most common case is something like:

// We should come up with a naming convention for these
import { adminOnlyDirective } from 'graphql-tools-directive-admin-only';

// ...
makeExecutableSchema({
  typeDefs,
  resolvers,
  directives: [adminOnlyDirective] // could also be an object?
})

I definitely do think that we shouldn't use the word "visitor" in the name of the option though, since probably most users won't be aware of the fact that the implementation is a visitor pattern (and knowing that doesn't really help)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamn I think for the most part people would pass them in when creating the schema. Are there any added usecases to delaying that addition? Like create the schema, do something async, add directives?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought of a potential use case here: if you're stitching schemas together, you might want to apply directives to one or both of the schemas before you merge them, or you might want to wait until after merging, so the directives get to operate on the combined schema. In the second case, you'd probably want to call visitSchemaDirectives yourself, after the merge, rather than passing directiveVisitors to makeExecutableSchema.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stubailo I'm open to naming the option directives, but the value does have to be an object, since the keys of the object specify the names of the directives.

}

// Generic function for visiting GraphQLSchema objects.
export function visitSchema(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this visitSchema function is useful for any kind of schema visitor pattern, not just visiting @directive syntax. Since reinventing schema visitor abstractions seems pretty common (e.g. this PR and #527, and probably elsewhere), I'm hopeful that we can consolidate our efforts into one shared abstraction. That said, I don't think consolidation should be a blocker for shipping developer-facing features like SchemaDirectiveVisitor or schema stitching / field renaming.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@freiksenet Looking forward to chatting about this with you and @stubailo!

// More complicated visitor implementations might use the
// visitorSelector function more selectively, but this SimpleVisitor
// class always volunteers itself to visit any schema type.
visitSchema(this.schema, () => [this]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test demonstrates how easy it is to use the visitSchema function even if you don't need to do something as complicated as what SchemaDirectiveVisitor does.

// SchemaDirectiveVisitor can be instantiated multiple times to visit
// directives of different names. In other words, SchemaDirectiveVisitor
// implementations are effectively anonymous, and it's up to the caller of
// SchemaDirectiveVisitor.visitSchemaDirectives to assign names to them.
Copy link
Contributor Author

@benjamn benjamn Feb 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a pretty important comment. Essentially, implementors of SchemaDirectiveVisitors don't have to commit to any specific names for the directives they're implementing.

export abstract class SchemaVisitor {
// All SchemaVisitor instances are created while visiting a specific
// GraphQLSchema object, so this property holds a reference to that object,
// in case a vistor method needs to refer to this.schema.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo vistor

@giautm
Copy link
Contributor

giautm commented Feb 17, 2018

cool! 😄

@freiksenet
Copy link
Contributor

Copy link

@jbaxleyiii jbaxleyiii left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamn still reviewing the tests, but here are a few comments

@@ -78,6 +82,7 @@ export interface IExecutableSchemaDefinition {
allowUndefinedInResolve?: boolean;
resolverValidationOptions?: IResolverValidationOptions;
directiveResolvers?: IDirectiveResolvers<any, any>;
directiveVisitors?: { [name: string]: typeof SchemaDirectiveVisitor };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamn I think for the most part people would pass them in when creating the schema. Are there any added usecases to delaying that addition? Like create the schema, do something async, add directives?

public visitFieldDefinition(field: GraphQLField<any, any>) {
const resolver = directiveResolvers[directiveName];
const originalResolver = field.resolve || defaultFieldResolver;
const directiveArgs = this.args;
field.resolve = (...args: any[]) => {
const [source, , context, info] = args;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is with the extra comma here? I'm assuming that for the arguments, but we should probably name it or note what the empty space is doing to split the args

}

function visit(type: VisitableSchemaType) {
if (type instanceof GraphQLSchema) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so right now I think this is fine since graphql-js does this, but in practice I really hate using instanceof since it makes dependency trees harder.

Copy link
Contributor Author

@benjamn benjamn Feb 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed! Not sure what to do instead, though. I wish graphql-js tagged prototypes like GraphQLObjectType.prototype with a Symbol that was unique to that class, but identical across all copies of the package.

}

const decl = declaredDirectives[directiveName];
let args: { [key: string]: any };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some places we use Record<string, any> vs this syntax. I'm honestly not sure of the difference though

args = getArgumentValues(decl, directiveNode);
} else {
// If this directive was not explicitly declared, just convert the
// argument nodes to their corresponding JavaScript values.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually super useful info for third party packages that may not have access to the sdl!

// subclasses (not instances) to visitSchemaDirectives.
protected constructor(config: {
name: string
args: { [name: string]: any }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice for args and context to be type parameters on this so they can be validated in the implementing functions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, then they should probably be type parameters on the SchemaDirectiveVisitor class itself, right?

}

enum Gender @enumTypeDirective {
NONBINARY @enumValueDirective

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤗

@benjamn benjamn force-pushed the wip-schema-directives branch 3 times, most recently from b76f0a2 to cd83f38 Compare February 27, 2018 03:26
@@ -0,0 +1,175 @@

## Directive example
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the old documentation, just to be clear. The new docs are in schema-directives.md.

Copy link
Contributor

@stubailo stubailo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

outdated comments from a week ago

@@ -78,6 +82,7 @@ export interface IExecutableSchemaDefinition {
allowUndefinedInResolve?: boolean;
resolverValidationOptions?: IResolverValidationOptions;
directiveResolvers?: IDirectiveResolvers<any, any>;
directiveVisitors?: { [name: string]: typeof SchemaDirectiveVisitor };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we want to have it here. The way I'm imagining the most common case is something like:

// We should come up with a naming convention for these
import { adminOnlyDirective } from 'graphql-tools-directive-admin-only';

// ...
makeExecutableSchema({
  typeDefs,
  resolvers,
  directives: [adminOnlyDirective] // could also be an object?
})

I definitely do think that we shouldn't use the word "visitor" in the name of the option though, since probably most users won't be aware of the fact that the implementation is a visitor pattern (and knowing that doesn't really help)


// Determine if this SchemaVisitor (sub)class implements a particular
// visitor method.
public static implementsVisitorMethod(methodName: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason to have these be static members rather than functions exported by this module separately? I don't have anything against it, but there doesn't seem to be any benefit of attaching them here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method refers to this.prototype below, so it's important that it's a static method inherited by all subclasses. That's the benefit.

// Determine if this SchemaVisitor (sub)class implements a particular
// visitor method.
public static implementsVisitorMethod(methodName: string) {
if (! methodName.startsWith('visit')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the goal of this return false? Should it throw an error maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just a shortcut because we know this method name can't correspond to a visitor method that this class implements.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - I just think this would be more like "you used this function wrong probably" and not "nope we don't implement this"

args = getArgumentValues(decl, directiveNode);
} else {
// If this directive was not explicitly declared, just convert the
// argument nodes to their corresponding JavaScript values.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm, if I import a directive from somewhere, will its arguments/locations get enforced?

Copy link
Contributor

@stubailo stubailo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions

---

## Simple Directive example
## Schema directives
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this heading, if you look at it in the docs preview it's just duplicative with the title


To start, let's grab the schema definition string from the `makeExecutableSchema` example [in the "Generating a schema" article](/tools/graphql-tools/generate-schema.html#example).
The [GraphQL specification](http://facebook.github.io/graphql/October2016/#sec-Type-System.Directives) requires every server implementation to support at least two directives, `@skip(if: Boolean)` and `@include(if: Boolean)`, which can be used during query execution to conditionally omit or include certain fields. The [GraphQL.js reference implementation](https://github.com/graphql/graphql-js) provides one additional built-in [`@deprecated`](https://github.com/graphql/graphql-js/blob/master/src/type/directives.js) directive, which is useful for indicating that a field or `enum` value should no longer be used.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think talking about @skip and @include here is confusing since those appear on queries. People often confuse the two and they have very little to do with each other.

Often when people hear "schema directive" they think it's something that lets them implement a new directive they can write in their queries, which is not the case; so we should try not to confuse them further.

BTW a good place to link to is maybe the PR for adding the schema language to the spec: graphql/graphql-spec#90

Also, here's the deployed draft version that has it: http://facebook.github.io/graphql/draft/#sec-Type-System.Directives

It's pretty relevant:

image

import { graphql } from 'graphql';
However, the formal syntax of the GraphQL query and schema languages allows arbitrary user-defined `@directive`s to appear as modifiers following almost any kind of type, field, or argument. Unless the server ascribes special meaning to these annotations, they are typically ignored after parsing, almost as if they were comments. But if the server knows how to interpret them, `@directive` annotations can be a powerful tool for preventing repetition, specifying extra behavior, enforcing additional type or value restrictions, and enabling static analysis.

This document focuses on `@directive`s that appear in GraphQL _schemas_ written in Schema Definition Language (SDL), as described in the [Schemas and Types](http://graphql.org/learn/schema/) section of the GraphQL.org documentation. These `@directive`s can be used to modify the structure and behavior of a GraphQL schema in ways that would not be possible using SDL syntax alone.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should basically just start here.


The possible applications of `@directive` syntax are numerous: enforcing access permissions, formatting date strings, auto-generating resolver functions for a particular backend API, marking strings for internationalization, synthesizing globally unique object identifiers, specifying caching behavior, skipping or including or deprecating fields, and just about anything else you can imagine.

## Implementing schema directives
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a section above this maybe called "using schema directives"? I think people should have a super easy guide for using directives pre-implemented by someone else.


If you're using Apollo Server, you are also likely to be using the [`graphql-tools`](https://github.com/apollographql/graphql-tools) npm package, which provides a convenient yet powerful tool for implementing `@directive` syntax: the [`SchemaDirectiveVisitor`](https://github.com/apollographql/graphql-tools/blob/wip-schema-directives/src/schemaVisitor.ts) class.

To implement a schema `@directive` using `SchemaDirectiveVisitor`, simply create a subclass of `SchemaDirectiveVisitor` that overrides one or more of the following visitor methods:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we lead with a simple example up front? This part looks a bit intimidating, but I think it would look more approachable if we had a small example that shows it's not rocket science.

const schema = makeExecutableSchema({ typeDefs });

SchemaDirectiveVisitor.visitSchemaDirectives(schema, {
rest: class extends SchemaDirectiveVisitor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better if these examples clearly separated the implementation, like so:

class RestDirective extends SchemaDirectiveVisitor {
   ...
}

SchemaDirectiveVisitor.visitSchemaDirectives(schema, { rest: RestDirective });

That way it's clear that these are reusable across schemas, and I bet most people would want to define them in separate files and import them into where they use them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up doing that in the examples down below, but I agree it would help here too.

},
},
};
The subclass in this example is defined as an anonymous `class` expression, for brevity. A truly reusable `SchemaDirectiveVisitor` would most likely be defined in a library using a named class declaration, and then exported for consumption by other modules and packages.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think documenting the anonymous class thing is worthwhile - I bet most people will find the existence of anonymous classes surprising

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, no need to mention it if the example isn't doing it anymore.


## Declaring schema directives

While the above examples should be sufficient to implement any `@directive` used in your schema, SDL syntax also supports declaring the names, argument types, default argument values, and permissible locations of any available directives:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd lead with why someone would want to do this if it's not necessary.

Copy link
Contributor

@stubailo stubailo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments


* The `@deprecated` directive _follows_ the field that it pertains to (`oldField`), even though its syntax might remind you of "decorators" in other languages, which usually appear on the line above.
* Directives are often _declared_ in the schema, as in this example, though it's up to the GraphQL server to enforce the argument types (`reason: String`) and locations (`FIELD_DEFINITION | ENUM_VALUE`) of the declaration.
* The `@deprecated(reason: ...)` syntax is legal even without the `directive @deprecated ...` declaration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I find this confusing - maybe we can just have some advanced section about defining directives at the bottom of the page? It's weird that defining them is optional and maybe we should take a stance on whether or not people should do it.


Since the GraphQL specification does not discuss any specific implementation strategy for directives, it's up to each GraphQL server framework to expose an API for implementing new directives.

If you're using Apollo Server, you are also likely to be using the [`graphql-tools`](https://github.com/apollographql/graphql-tools) npm package, which provides a convenient yet powerful tool for implementing directive syntax: the [`SchemaDirectiveVisitor`](https://github.com/apollographql/graphql-tools/blob/wip-schema-directives/src/schemaVisitor.ts) class.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page is in the graphql-tools docs, so referencing Apollo Server isn't really necessary in this way: https://deploy-preview-640--graphql-tools-docs.netlify.com/docs/graphql-tools/schema-directives.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's amazing!


* `visitSchema(schema: GraphQLSchema)`
* `visitScalar(scalar: GraphQLScalarType)`
* `visitObject(object: GraphQLObjectType)`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like these are intended to match the SDL types here: http://facebook.github.io/graphql/draft/#sec-Type-System.Directives

But it's a bit funny that they don't match the TypeScript/Flow types. Maybe we should link to the spec to explain where we got these names? IDK

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the names come directly from the DirectiveLocation enum values. I think they're pretty reasonable as method names, even if you don't know where they came from. I'd rather not throw GraphQL or Type into the names, though the relationship there is also fairly predictable. I'll add a link.

const schema = makeExecutableSchema({
typeDefs,
directives: {
deprecated: DeprecatedDirective
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should update the API here: https://deploy-preview-640--graphql-tools-docs.netlify.com/docs/graphql-tools/generate-schema.html#makeExecutableSchema

Also we should add an API section at the bottom with an API doc for SchemaDirectiveVisitor


To appreciate the range of possibilities enabled by `SchemaDirectiveVisitor`, let's examine a variety of practical examples.

> Note that these examples are written in JavaScript rather than TypeScript, though either language should work.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is important to call out. Our libraries are written in TypeScript, but my understanding is that the majority of our users don't use it.


### Uppercasing strings

Suppose you want to ensure a string-valued field is converted to uppercase:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd call out that this is an example of wrapping a resolver.

Also, fun fact: This directive won't work on fields that return an array of values, but maybe that's reasonable since it would only be expected to work on String fields.

```

> Note: next() always return a Promise for consistency, resolved with original resolver value or rejected with an error.
There are many more issues to consider when implementing a real GraphQL wrapper over a REST endpoint (such as how to do caching or pagination), but this example demonstrates the basic structure.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"demonstrates the basic structure" -> "demonstrates one basic approach"

this.wrapType(field);
}

wrapType(field) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a minute to realize that this actually replaces the field type with a custom scalar - maybe we should add a comment to show that's what is happening.

Also, this might have undesirable side effects, for example:

mutation ($bookTitle: String!) {
  createBook(book: { title: bookTitle }) { title }
}

After this transformation it will no longer be a valid mutation because the String type won't match. So it might be better to actually wrap the resolver and do the check at runtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would that work with input fields?

@benjamn benjamn force-pushed the wip-schema-directives branch 2 times, most recently from 4329811 to b3f11aa Compare March 1, 2018 20:53
name: { value: 'Human' }
});
this.schema.getTypeMap()[object.name] = HumanType;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snippet is a good demonstration of the value of this functionality. Mutating the type map doesn't work here, because it has no effect on the rest of the traversal, whereas returning a replacement object immediately determines what gets visited next.

This is a major ergonomic improvement for SchemaDirectiveVisitor
implementors, since it means named types can be replaced without worrying
about references to those types elsewhere in the schema (fields,
arguments, or union members).
Returning null removes the object from the schema. Returning undefined
leaves it untouched (most common). Returning a non-null object replaces
the original object in the schema.

References to replaced objects will be automatically updated during
healing, so implementors don't have to worry about that.

cc @jbaxleyiii
Implementing the visitSchema method can be a good idea if you want to
modify the existing GraphQLSchema object, but replacing the entire schema
is pointless, because you're basically disregarding the result of
makeExecutableSchema.

Also, SchemaDirectiveVisitor.visitSchemaDirectives relies on the schema
object remaining the same throughout the traversal, and it would be tricky
to update the schema (as seen by visitSchemaDirectives) mid-traversal.
This automatically guarantees the following invariant holds for all named
types in the schema:

  schema.getType(name).name === name

This reconciliation falls into the category of "healing" because it
doesn't require any input from the implementor of the schema visitor, and
strictly improves the consistency of the final schema.
It should be possible to visit a schema without going to the trouble of
healing it as well.
@benjamn
Copy link
Contributor Author

benjamn commented Mar 14, 2018

Heads up: I'm going to force-push to this branch to resolve the merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants