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

docs: custom directive examples and docs page #982

Merged
merged 8 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions docs/custom-directive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Custom directive
A GraphQL directive is a special syntax used to provide additional information to the GraphQL execution engine about how to process a query, mutation, or schema definition. For example, directives can be used to modify the behaviour of fields, arguments, or types in your schema.

A custom directive is composed of 2 parts:
- schema definitions
- schema transformer

## Schema Definition
It is the syntax used to describe a custom directive within a GraphQL schema.
To define a custom directive, you must use the directive keyword, followed by its name, arguments (if any), and the locations where it can be applied.

```
directive @censorship(find: String) on FIELD_DEFINITION
```

## Schema transformer

A schema transformer is a function that takes a GraphQL schema as input and modifies it somehow before returning the modified schema.

```js
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils')
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should document somewhere what version of graphql-tools this guide refers to graphql-tools.

I would also prefer to not use graphql-tools here or show both examples.

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 opted for the use of graphql tools because, being an example, I wanted to keep it as lean as possible. Inserting a code snippet (generic) for creating an executable schema or for decorating resolvers, risks being misleading for the purpose of the example, making it more complex to understand and also to reuse.
Instead, if I adopt the strategy to modify the schema ad hoc for the redact directive, we might reduce the amount of code but make the example hard to reuse.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I agree with Mauro, we have discussed in detail and replicating the features coming from the graphql-tools library for the sake of this example is just not worth it, and quite frankly in general it's not worth it. Nevertheless since this issue comes up quite often, we'll come up with a couple of ideas that we'll open as issues in this repo:

  • one specific to this request, a way built-into mercurius (or a plugin) to make it easier to define and attach custom directives
  • one generic about creating a new library alternative to graphql-tools with similar features

Copy link
Contributor Author

@brainrepo brainrepo Apr 28, 2023

Choose a reason for hiding this comment

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

Yes, I have provided a more detailed argument on this issue. #989


const censorshipSchemaTransformer = (schema) => mapSchema(schema, {
// When parsing the schema we find a FIELD
[MapperKind.FIELD]: fieldConfig => {
// Get the directive information
const censorshipDirective = getDirective(schema, fieldConfig, "censorship")?.[0]
if (censorshipDirective) {
// Get the resolver of the field
const innerResolver = fieldConfig.resolve
// Extract the find property from the directive
const { find } = censorshipDirective
// Define a new resolver for the field
fieldConfig.resolve = async (_obj, args, ctx, info) => {
// Run the original resolver to get the result
const document = await innerResolver(_obj, args, ctx, info)
// Apply censorship only if context censored is true
if (!ctx.censored) {
return document
}
return { ...document, text: document.text.replace(find, '**********') }
}
}
}
})
```

## Generate executable schema
All the transformations must be applied to the executable schema, which contains both the schema and the resolvers.

```js
const schema = makeExecutableSchema({
typeDefs: `
# Define the directive schema
directive @censorship(find: String) on FIELD_DEFINITION

type Document {
id: String!
text: String!
}

type Query {
document: Document @censorship(find: "password")
}
`,
resolvers
})
```

## Apply transformations to the executable schema

Now we can apply the transformations to the schema before registering the mercurius plugin

```js
app.register(mercurius, {
// schema changed by the transformer
schema: censorshipSchemaTransformer(schema),
context: (request, reply) => {
return {
censored: false
}
},
graphiql: true,
})
```
75 changes: 75 additions & 0 deletions examples/custom-directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict'

const Fastify = require('fastify')
const mercurius = require('..')
const { makeExecutableSchema } = require('@graphql-tools/schema')
const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils')

const app = Fastify()

const resolvers = {
Query: {
document: async (_, obj, ctx) => {
return {
id: '1',
text: 'Proin password rutrum pulvinar lectus sed placerat.'
}
}
}
}

// Define the executable schema
const schema = makeExecutableSchema({
typeDefs: `
# Define the directive schema
directive @censorship(find: String) on FIELD_DEFINITION

type Document {
id: String!
text: String!
}

type Query {
document: Document @censorship(find: "password")
}
`,
resolvers
})

// Define directive schema resolver
const censorshipSchemaTransformer = (schema) => mapSchema(schema, {
// When parsing the schema we find a FIELD
[MapperKind.FIELD]: fieldConfig => {
// Get the directive information
const censorshipDirective = getDirective(schema, fieldConfig, 'censorship')?.[0]
if (censorshipDirective) {
// Get the resolver of the field
const innerResolver = fieldConfig.resolve
// Extract the find property from the directive
const { find } = censorshipDirective
// Define a new resolver for the field
fieldConfig.resolve = async (_obj, args, ctx, info) => {
// Run the original resolver to get the result
const document = await innerResolver(_obj, args, ctx, info)
// Apply censorship only if context censored is true
if (!ctx.censored) {
return document
}
return { ...document, text: document.text.replace(find, '**********') }
}
}
}
})

// Register mercurius and run it
app.register(mercurius, {
schema: censorshipSchemaTransformer(schema),
context: (request, reply) => {
return {
censored: false
}
},
graphiql: true
})

app.listen({ port: 3000 })