-
-
Notifications
You must be signed in to change notification settings - Fork 818
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
Conversation
src/schemaVisitor.ts
Outdated
public visitInputObject(object: GraphQLInputObjectType) {} | ||
public visitInputFieldDefinition(field: GraphQLInputField, details: { | ||
objectType: GraphQLInputObjectType, | ||
}) {} |
There was a problem hiding this comment.
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[], |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 @directive
s 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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
src/schemaGenerator.ts
Outdated
|
||
Object.keys(directiveResolvers).forEach(directiveName => { | ||
directiveVisitors[directiveName] = class extends SchemaDirectiveVisitor { |
There was a problem hiding this comment.
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
.
src/Interfaces.ts
Outdated
@@ -78,6 +82,7 @@ export interface IExecutableSchemaDefinition { | |||
allowUndefinedInResolve?: boolean; | |||
resolverValidationOptions?: IResolverValidationOptions; | |||
directiveResolvers?: IDirectiveResolvers<any, any>; | |||
directiveVisitors?: { [name: string]: typeof SchemaDirectiveVisitor }; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]); |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 SchemaDirectiveVisitor
s don't have to commit to any specific names for the directives they're implementing.
src/schemaVisitor.ts
Outdated
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo vistor
cool! 😄 |
There is some overlapping work here I guess. |
There was a problem hiding this 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
src/Interfaces.ts
Outdated
@@ -78,6 +82,7 @@ export interface IExecutableSchemaDefinition { | |||
allowUndefinedInResolve?: boolean; | |||
resolverValidationOptions?: IResolverValidationOptions; | |||
directiveResolvers?: IDirectiveResolvers<any, any>; | |||
directiveVisitors?: { [name: string]: typeof SchemaDirectiveVisitor }; |
There was a problem hiding this comment.
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?
src/schemaGenerator.ts
Outdated
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; |
There was a problem hiding this comment.
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
src/schemaVisitor.ts
Outdated
} | ||
|
||
function visit(type: VisitableSchemaType) { | ||
if (type instanceof GraphQLSchema) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤗
b76f0a2
to
cd83f38
Compare
@@ -0,0 +1,175 @@ | |||
|
|||
## Directive example |
There was a problem hiding this comment.
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
.
There was a problem hiding this 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
src/Interfaces.ts
Outdated
@@ -78,6 +82,7 @@ export interface IExecutableSchemaDefinition { | |||
allowUndefinedInResolve?: boolean; | |||
resolverValidationOptions?: IResolverValidationOptions; | |||
directiveResolvers?: IDirectiveResolvers<any, any>; | |||
directiveVisitors?: { [name: string]: typeof SchemaDirectiveVisitor }; |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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')) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some suggestions
docs/source/schema-directives.md
Outdated
--- | ||
|
||
## Simple Directive example | ||
## Schema directives |
There was a problem hiding this comment.
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
docs/source/schema-directives.md
Outdated
|
||
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. |
There was a problem hiding this comment.
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:
docs/source/schema-directives.md
Outdated
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. |
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
|
||
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 |
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
|
||
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: |
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
const schema = makeExecutableSchema({ typeDefs }); | ||
|
||
SchemaDirectiveVisitor.visitSchemaDirectives(schema, { | ||
rest: class extends SchemaDirectiveVisitor { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
}, | ||
}, | ||
}; | ||
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
|
||
## 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this 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
docs/source/schema-directives.md
Outdated
|
||
* 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. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)` |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
const schema = makeExecutableSchema({ | ||
typeDefs, | ||
directives: { | ||
deprecated: DeprecatedDirective |
There was a problem hiding this comment.
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
docs/source/schema-directives.md
Outdated
|
||
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. |
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
|
||
### Uppercasing strings | ||
|
||
Suppose you want to ensure a string-valued field is converted to uppercase: |
There was a problem hiding this comment.
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.
docs/source/schema-directives.md
Outdated
``` | ||
|
||
> 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. |
There was a problem hiding this comment.
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"
docs/source/schema-directives.md
Outdated
this.wrapType(field); | ||
} | ||
|
||
wrapType(field) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
4329811
to
b3f11aa
Compare
src/test/testDirectives.ts
Outdated
name: { value: 'Human' } | ||
}); | ||
this.schema.getTypeMap()[object.name] = HumanType; |
There was a problem hiding this comment.
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.
Heads up: I'm going to force-push to this branch to resolve the merge conflicts. |
5028f7f
to
86666cd
Compare
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.By overriding one or more
visit{Object,Union,...}
methods, a subclass ofSchemaDirectiveVisitor
registers interest in certain schema types, such asGraphQLObjectType
,GraphQLUnionType
, etc. WhenSchemaDirectiveVisitor.visitSchemaDirectives
is called with aGraphQLSchema
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@directive
s 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, aSchemaDirectiveVisitor
subclass could provide meaning to that directive by overriding thevisitFieldDefinition
method (which receives aGraphQLField
parameter), and then the body of that visitor method could manipulate the field's resolver function to fetch data from a REST endpoint:The subclass in this example is defined as an anonymous
class
expression, for brevity. A truly reusableSchemaDirectiveVisitor
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:
directiveResolvers
).