From f66848a9e689aac673ce5867d7e2eedeada153f3 Mon Sep 17 00:00:00 2001 From: brainrepo Date: Tue, 18 Apr 2023 12:56:08 +0200 Subject: [PATCH 1/8] docs: define custom directive and docs page --- docs/custom-directive.md | 85 ++++++++++++++++++++++++++++++++++++ examples/custom-directive.js | 75 +++++++++++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 docs/custom-directive.md create mode 100644 examples/custom-directive.js diff --git a/docs/custom-directive.md b/docs/custom-directive.md new file mode 100644 index 00000000..5c81d553 --- /dev/null +++ b/docs/custom-directive.md @@ -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') + +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, +}) +``` \ No newline at end of file diff --git a/examples/custom-directive.js b/examples/custom-directive.js new file mode 100644 index 00000000..8c73b2e8 --- /dev/null +++ b/examples/custom-directive.js @@ -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 }) From e4092c9f8723f816a2655a95bee16882d1c096d6 Mon Sep 17 00:00:00 2001 From: brainrepo Date: Tue, 18 Apr 2023 15:31:36 +0200 Subject: [PATCH 2/8] docs: example change --- docs/custom-directive.md | 126 ++++++++++++++++++++++------------- examples/custom-directive.js | 36 +++++----- 2 files changed, 95 insertions(+), 67 deletions(-) diff --git a/docs/custom-directive.md b/docs/custom-directive.md index 5c81d553..8ef29e11 100644 --- a/docs/custom-directive.md +++ b/docs/custom-directive.md @@ -1,44 +1,88 @@ # 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. + +We might need to customise our schema by decorating parts of it or operations to add new reusable features to these elements. +To do that, we can use a GraphQL concept called **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 +- 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. + +**Let's explore the custom directive creation process by creating a directive to redact some fields value hiding a specific word.* + +First of all, we must define the schema + +```js +const schema = ` + # Define the directive schema + directive @redact(find: String) on FIELD_DEFINITION + + type Document { + id: String! + text: String! @redact(find: "password") + } + + type Query { + document: Document + }` +``` + +To define a custom directive, we must use the directive keyword, followed by its name prefixed by a `@`, the arguments (if any), and the locations where it can be applied. ``` -directive @censorship(find: String) on FIELD_DEFINITION +directive @redact(find: String) on FIELD_DEFINITION ``` -## Schema transformer +The directive can be applied in multiple locations. + +- **QUERY:** Location adjacent to a query operation. +- **MUTATION:** Location adjacent to a mutation operation. +- **SUBSCRIPTION:** Location adjacent to a subscription operation. +- **FIELD:** Location adjacent to a field. +- **FRAGMENT_DEFINITION:** Location adjacent to a fragment definition. +- **FRAGMENT_SPREAD:** Location adjacent to a fragment spread. +- **INLINE_FRAGMENT:** Location adjacent to an inline fragment. +- **SCALAR:** Location adjacent to a scalar definition. +- **OBJECT:** Location adjacent to an object type definition. +- **FIELD_DEFINITION:** Location adjacent to a field definition. +- **ARGUMENT_DEFINITION:** Location adjacent to an argument definition. +- **INTERFACE:** Location adjacent to an interface definition. +- **UNION:** Location adjacent to a union definition. +- **ENUM:** Location adjacent to an enum definition. +- **ENUM_VALUE:** Location adjacent to an enum value definition. +- **INPUT_OBJECT:** Location adjacent to an input object type definition. +- **INPUT_FIELD_DEFINITION:** Location adjacent to an input object field definition + +## Transformer -A schema transformer is a function that takes a GraphQL schema as input and modifies it somehow before returning the modified schema. +Every directive needs its transformer. +A transformer is a function that takes an existing schema and applies the modifications to the schema and resolvers. + +To simplify the process of creating a transformer, we use the `mapSchema` function from the `@graphql-tools` library. + +The `mapSchema` function applies each callback function to the corresponding type definition in the schema, creating a new schema with the modified type definitions. The function also provides access to the field resolvers of each object type, allowing you to alter the behaviour of the fields in the schema. ```js const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils') -const censorshipSchemaTransformer = (schema) => mapSchema(schema, { +const redactionSchemaTransformer = (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, '**********') } + const redactDirective = getDirective(schema, fieldConfig, "redact")?.[0] + if (redactDirective) { + // Extract the find attribute from te directive + const { find } = redactDirective + // Create a new resolver + fieldConfig.resolve = async (obj, _args, _ctx, info) => { + // Extract the value of the property we want redact + // getting the field name from the info parameter. + const value = obj[info.fieldName] + return value.replace(find, '**********') } } } @@ -46,40 +90,30 @@ const censorshipSchemaTransformer = (schema) => mapSchema(schema, { ``` ## Generate executable schema -All the transformations must be applied to the executable schema, which contains both the schema and the resolvers. +To make our custom directive work, we must first create an executable schema required by the `mapSchema` function to change the resolvers' behaviour. ```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") - } - `, +const executableSchema = makeExecutableSchema({ + typeDefs: schema, resolvers }) ``` ## Apply transformations to the executable schema -Now we can apply the transformations to the schema before registering the mercurius plugin +Now it is time to transform our schema. + +```js +const newSchema = redactionSchemaTransformer(executableSchema) +``` + +and to register mercurius inside fastify ```js app.register(mercurius, { // schema changed by the transformer - schema: censorshipSchemaTransformer(schema), - context: (request, reply) => { - return { - censored: false - } - }, + schema: newSchema, graphiql: true, }) -``` \ No newline at end of file +``` + diff --git a/examples/custom-directive.js b/examples/custom-directive.js index 8c73b2e8..9936a3f1 100644 --- a/examples/custom-directive.js +++ b/examples/custom-directive.js @@ -9,7 +9,7 @@ const app = Fastify() const resolvers = { Query: { - document: async (_, obj, ctx) => { + document: async (_, _obj, _ctx) => { return { id: '1', text: 'Proin password rutrum pulvinar lectus sed placerat.' @@ -22,40 +22,34 @@ const resolvers = { const schema = makeExecutableSchema({ typeDefs: ` # Define the directive schema - directive @censorship(find: String) on FIELD_DEFINITION + directive @redact(find: String) on FIELD_DEFINITION type Document { id: String! - text: String! + text: String! @redact(find: "password") } type Query { - document: Document @censorship(find: "password") + document: Document } `, resolvers }) // Define directive schema resolver -const censorshipSchemaTransformer = (schema) => mapSchema(schema, { +const redactionSchemaTransformer = (schema) => mapSchema(schema, { // When parsing the schema we find a FIELD - [MapperKind.FIELD]: fieldConfig => { + [MapperKind.OBJECT_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) { + const redactDirective = getDirective(schema, fieldConfig, 'redact')?.[0] + if (redactDirective) { + const { find } = redactDirective + fieldConfig.resolve = async (obj, _args, ctx, info) => { + const value = obj[info.fieldName] + if (!ctx.redaction) { return document } - return { ...document, text: document.text.replace(find, '**********') } + return value.replace(find, '**********') } } } @@ -63,10 +57,10 @@ const censorshipSchemaTransformer = (schema) => mapSchema(schema, { // Register mercurius and run it app.register(mercurius, { - schema: censorshipSchemaTransformer(schema), + schema: redactionSchemaTransformer(schema), context: (request, reply) => { return { - censored: false + redaction: true } }, graphiql: true From d9810b6cd73376cfbefd9b6ff337097e216202e2 Mon Sep 17 00:00:00 2001 From: brainrepo Date: Tue, 18 Apr 2023 15:42:45 +0200 Subject: [PATCH 3/8] Docs: add more info --- docs/custom-directive.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/custom-directive.md b/docs/custom-directive.md index 8ef29e11..aed3bf93 100644 --- a/docs/custom-directive.md +++ b/docs/custom-directive.md @@ -89,6 +89,15 @@ const redactionSchemaTransformer = (schema) => mapSchema(schema, { }) ``` +As you can see in the new resolver function as props, we receive the `current object`, the `arguments`, the `context` and the `info`. + +Using the field name exposed by the `info` object, we get the field value from the `obj` object, object that contains lots of helpful informations like + +- fieldNodes +- returnType +- parentType +- operation + ## Generate executable schema To make our custom directive work, we must first create an executable schema required by the `mapSchema` function to change the resolvers' behaviour. From 8b5c3566fca918da4696ffc65ee7853ddfa80514 Mon Sep 17 00:00:00 2001 From: brainrepo Date: Tue, 18 Apr 2023 15:44:41 +0200 Subject: [PATCH 4/8] docs: add example link --- docs/custom-directive.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/custom-directive.md b/docs/custom-directive.md index aed3bf93..135ac7fd 100644 --- a/docs/custom-directive.md +++ b/docs/custom-directive.md @@ -126,3 +126,6 @@ app.register(mercurius, { }) ``` +## Example + +We have a runnable example on "example/custom-directive.js" From 5a1f6f17f6e322dada687f87898841f8e3775efb Mon Sep 17 00:00:00 2001 From: brainrepo Date: Tue, 18 Apr 2023 15:47:50 +0200 Subject: [PATCH 5/8] docs: typos --- docs/custom-directive.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/custom-directive.md b/docs/custom-directive.md index 135ac7fd..ac539b07 100644 --- a/docs/custom-directive.md +++ b/docs/custom-directive.md @@ -12,7 +12,7 @@ A custom directive is composed of 2 parts: ## Schema Definition -**Let's explore the custom directive creation process by creating a directive to redact some fields value hiding a specific word.* +**Let's explore the custom directive creation process by creating a directive to redact some fields value hiding a specific word.** First of all, we must define the schema @@ -120,7 +120,6 @@ and to register mercurius inside fastify ```js app.register(mercurius, { - // schema changed by the transformer schema: newSchema, graphiql: true, }) From 9e6ba091cc6b96de0d877cca213ef5b2b0457e66 Mon Sep 17 00:00:00 2001 From: brainrepo Date: Wed, 19 Apr 2023 12:16:38 +0200 Subject: [PATCH 6/8] docs: change redaction strategy --- docs/custom-directive.md | 55 +++++++++++++++++------------------- examples/custom-directive.js | 35 ++++++++++++++--------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/docs/custom-directive.md b/docs/custom-directive.md index ac539b07..0082ea69 100644 --- a/docs/custom-directive.md +++ b/docs/custom-directive.md @@ -4,58 +4,38 @@ We might need to customise our schema by decorating parts of it or operations to To do that, we can use a GraphQL concept called **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. +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 - transformer ## Schema Definition - -**Let's explore the custom directive creation process by creating a directive to redact some fields value hiding a specific word.** +Let's explore the custom directive creation process by creating a directive to redact some fields value, hiding a phone number or an email. First of all, we must define the schema ```js const schema = ` - # Define the directive schema directive @redact(find: String) on FIELD_DEFINITION type Document { - id: String! - text: String! @redact(find: "password") + excerpt: String! @redact(find: "email") + text: String! @redact(find: "phone") } type Query { - document: Document + documents: [Document] }` ``` -To define a custom directive, we must use the directive keyword, followed by its name prefixed by a `@`, the arguments (if any), and the locations where it can be applied. +To define a custom directive, we must use the directive keyword, followed by its name prefixed by a `@`, the arguments (if any), and the locations where it can be applied. ``` directive @redact(find: String) on FIELD_DEFINITION ``` -The directive can be applied in multiple locations. - -- **QUERY:** Location adjacent to a query operation. -- **MUTATION:** Location adjacent to a mutation operation. -- **SUBSCRIPTION:** Location adjacent to a subscription operation. -- **FIELD:** Location adjacent to a field. -- **FRAGMENT_DEFINITION:** Location adjacent to a fragment definition. -- **FRAGMENT_SPREAD:** Location adjacent to a fragment spread. -- **INLINE_FRAGMENT:** Location adjacent to an inline fragment. -- **SCALAR:** Location adjacent to a scalar definition. -- **OBJECT:** Location adjacent to an object type definition. -- **FIELD_DEFINITION:** Location adjacent to a field definition. -- **ARGUMENT_DEFINITION:** Location adjacent to an argument definition. -- **INTERFACE:** Location adjacent to an interface definition. -- **UNION:** Location adjacent to a union definition. -- **ENUM:** Location adjacent to an enum definition. -- **ENUM_VALUE:** Location adjacent to an enum value definition. -- **INPUT_OBJECT:** Location adjacent to an input object type definition. -- **INPUT_FIELD_DEFINITION:** Location adjacent to an input object field definition +According to the graphql specs the directive can be applied in multiple locations. See the list on https://spec.graphql.org/October2021/#sec-Type-System.Directives. ## Transformer @@ -72,17 +52,31 @@ const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils') const redactionSchemaTransformer = (schema) => mapSchema(schema, { // When parsing the schema we find a FIELD [MapperKind.FIELD]: fieldConfig => { + // Define the regexp + const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g; + const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g + // Get the directive information const redactDirective = getDirective(schema, fieldConfig, "redact")?.[0] if (redactDirective) { - // Extract the find attribute from te directive + // Extract the find attribute from the directive, this attribute will + // be used to chose which replace strategy adopt const { find } = redactDirective // Create a new resolver fieldConfig.resolve = async (obj, _args, _ctx, info) => { // Extract the value of the property we want redact // getting the field name from the info parameter. const value = obj[info.fieldName] - return value.replace(find, '**********') + + // Apply the redaction strategy and return the result + switch (find) { + case 'email': + return value.replace(EMAIL_REGEXP, '****@*****.***') + case 'phone': + return value.replace(PHONE_REGEXP, m => '*'.repeat(m.length)) + default: + return value + } } } } @@ -125,6 +119,9 @@ app.register(mercurius, { }) ``` + + + ## Example We have a runnable example on "example/custom-directive.js" diff --git a/examples/custom-directive.js b/examples/custom-directive.js index 9936a3f1..3c28511d 100644 --- a/examples/custom-directive.js +++ b/examples/custom-directive.js @@ -9,11 +9,11 @@ const app = Fastify() const resolvers = { Query: { - document: async (_, _obj, _ctx) => { - return { - id: '1', - text: 'Proin password rutrum pulvinar lectus sed placerat.' - } + documents: async (_, _obj, _ctx) => { + return [{ + excerpt: 'Proin info@mercurius.dev rutrum pulvinar lectus sed placerat.', + text: 'Proin 33 222-33355 rutrum pulvinar lectus sed placerat.' + }] } } } @@ -25,31 +25,38 @@ const schema = makeExecutableSchema({ directive @redact(find: String) on FIELD_DEFINITION type Document { - id: String! - text: String! @redact(find: "password") + excerpt: String! @redact(find: "email") + text: String! @redact(find: "phone") } type Query { - document: Document + documents: [Document] } `, resolvers }) -// Define directive schema resolver const redactionSchemaTransformer = (schema) => mapSchema(schema, { - // When parsing the schema we find a FIELD [MapperKind.OBJECT_FIELD]: fieldConfig => { - // Get the directive information + const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g + const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g const redactDirective = getDirective(schema, fieldConfig, 'redact')?.[0] if (redactDirective) { const { find } = redactDirective fieldConfig.resolve = async (obj, _args, ctx, info) => { const value = obj[info.fieldName] - if (!ctx.redaction) { + if (!ctx.redact) { return document } - return value.replace(find, '**********') + + switch (find) { + case 'email': + return value.replace(EMAIL_REGEXP, '****@*****.***') + case 'phone': + return value.replace(PHONE_REGEXP, m => '*'.repeat(m.length)) + default: + return value + } } } } @@ -60,7 +67,7 @@ app.register(mercurius, { schema: redactionSchemaTransformer(schema), context: (request, reply) => { return { - redaction: true + redact: true } }, graphiql: true From 02159f251aaadb9f62427ec16ec2606bb5d6e47a Mon Sep 17 00:00:00 2001 From: brainrepo Date: Wed, 19 Apr 2023 14:34:01 +0200 Subject: [PATCH 7/8] docs: nits --- docs/custom-directive.md | 14 ++++++-------- examples/custom-directive.js | 8 ++++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/custom-directive.md b/docs/custom-directive.md index 0082ea69..862a068e 100644 --- a/docs/custom-directive.md +++ b/docs/custom-directive.md @@ -35,7 +35,7 @@ To define a custom directive, we must use the directive keyword, followed by its directive @redact(find: String) on FIELD_DEFINITION ``` -According to the graphql specs the directive can be applied in multiple locations. See the list on https://spec.graphql.org/October2021/#sec-Type-System.Directives. +According to the graphql specs the directive can be applied in multiple locations. See the list on [the GraphQL spec page](https://spec.graphql.org/October2021/#sec-Type-System.Directives). ## Transformer @@ -49,13 +49,14 @@ The `mapSchema` function applies each callback function to the corresponding typ ```js const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils') +// Define the regexp +const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g; +const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g + const redactionSchemaTransformer = (schema) => mapSchema(schema, { // When parsing the schema we find a FIELD [MapperKind.FIELD]: fieldConfig => { - // Define the regexp - const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g; - const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g - + // Get the directive information const redactDirective = getDirective(schema, fieldConfig, "redact")?.[0] if (redactDirective) { @@ -119,9 +120,6 @@ app.register(mercurius, { }) ``` - - - ## Example We have a runnable example on "example/custom-directive.js" diff --git a/examples/custom-directive.js b/examples/custom-directive.js index 3c28511d..95ea9858 100644 --- a/examples/custom-directive.js +++ b/examples/custom-directive.js @@ -36,15 +36,19 @@ const schema = makeExecutableSchema({ resolvers }) +const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g +const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g + const redactionSchemaTransformer = (schema) => mapSchema(schema, { [MapperKind.OBJECT_FIELD]: fieldConfig => { - const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g - const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g const redactDirective = getDirective(schema, fieldConfig, 'redact')?.[0] + if (redactDirective) { const { find } = redactDirective + fieldConfig.resolve = async (obj, _args, ctx, info) => { const value = obj[info.fieldName] + if (!ctx.redact) { return document } From f78febb557f354256bc0109dcc2ab54fbbabb2c1 Mon Sep 17 00:00:00 2001 From: brainrepo Date: Fri, 28 Apr 2023 14:13:29 +0200 Subject: [PATCH 8/8] add sidebar link and graphql tools version --- docs/custom-directive.md | 84 +++++++++++++++++++++------------------- docsify/sidebar.md | 82 +++++++++++++++++++-------------------- 2 files changed, 85 insertions(+), 81 deletions(-) diff --git a/docs/custom-directive.md b/docs/custom-directive.md index 862a068e..f021ee7b 100644 --- a/docs/custom-directive.md +++ b/docs/custom-directive.md @@ -3,14 +3,16 @@ We might need to customise our schema by decorating parts of it or operations to add new reusable features to these elements. To do that, we can use a GraphQL concept called **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. -Directives can be used to modify the behaviour of fields, arguments, or types in your schema. +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. +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 - transformer ## Schema Definition + Let's explore the custom directive creation process by creating a directive to redact some fields value, hiding a phone number or an email. First of all, we must define the schema @@ -26,7 +28,7 @@ const schema = ` type Query { documents: [Document] - }` + }`; ``` To define a custom directive, we must use the directive keyword, followed by its name prefixed by a `@`, the arguments (if any), and the locations where it can be applied. @@ -35,7 +37,7 @@ To define a custom directive, we must use the directive keyword, followed by its directive @redact(find: String) on FIELD_DEFINITION ``` -According to the graphql specs the directive can be applied in multiple locations. See the list on [the GraphQL spec page](https://spec.graphql.org/October2021/#sec-Type-System.Directives). +According to the graphql specs the directive can be applied in multiple locations. See the list on [the GraphQL spec page](https://spec.graphql.org/October2021/#sec-Type-System.Directives). ## Transformer @@ -43,45 +45,46 @@ Every directive needs its transformer. A transformer is a function that takes an existing schema and applies the modifications to the schema and resolvers. To simplify the process of creating a transformer, we use the `mapSchema` function from the `@graphql-tools` library. +In this example we are refering to [graphqltools 8.3.20](https://www.npmjs.com/package/graphql-tools/v/8.3.20) The `mapSchema` function applies each callback function to the corresponding type definition in the schema, creating a new schema with the modified type definitions. The function also provides access to the field resolvers of each object type, allowing you to alter the behaviour of the fields in the schema. ```js -const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils') +const { mapSchema, getDirective, MapperKind } = require("@graphql-tools/utils"); -// Define the regexp +// Define the regexp const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g; -const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g - -const redactionSchemaTransformer = (schema) => mapSchema(schema, { - // When parsing the schema we find a FIELD - [MapperKind.FIELD]: fieldConfig => { - - // Get the directive information - const redactDirective = getDirective(schema, fieldConfig, "redact")?.[0] - if (redactDirective) { - // Extract the find attribute from the directive, this attribute will - // be used to chose which replace strategy adopt - const { find } = redactDirective - // Create a new resolver - fieldConfig.resolve = async (obj, _args, _ctx, info) => { - // Extract the value of the property we want redact - // getting the field name from the info parameter. - const value = obj[info.fieldName] - - // Apply the redaction strategy and return the result - switch (find) { - case 'email': - return value.replace(EMAIL_REGEXP, '****@*****.***') - case 'phone': - return value.replace(PHONE_REGEXP, m => '*'.repeat(m.length)) - default: - return value - } +const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g; + +const redactionSchemaTransformer = (schema) => + mapSchema(schema, { + // When parsing the schema we find a FIELD + [MapperKind.FIELD]: (fieldConfig) => { + // Get the directive information + const redactDirective = getDirective(schema, fieldConfig, "redact")?.[0]; + if (redactDirective) { + // Extract the find attribute from the directive, this attribute will + // be used to chose which replace strategy adopt + const { find } = redactDirective; + // Create a new resolver + fieldConfig.resolve = async (obj, _args, _ctx, info) => { + // Extract the value of the property we want redact + // getting the field name from the info parameter. + const value = obj[info.fieldName]; + + // Apply the redaction strategy and return the result + switch (find) { + case "email": + return value.replace(EMAIL_REGEXP, "****@*****.***"); + case "phone": + return value.replace(PHONE_REGEXP, (m) => "*".repeat(m.length)); + default: + return value; + } + }; } - } - } -}) + }, + }); ``` As you can see in the new resolver function as props, we receive the `current object`, the `arguments`, the `context` and the `info`. @@ -94,13 +97,14 @@ Using the field name exposed by the `info` object, we get the field value from t - operation ## Generate executable schema + To make our custom directive work, we must first create an executable schema required by the `mapSchema` function to change the resolvers' behaviour. ```js const executableSchema = makeExecutableSchema({ typeDefs: schema, - resolvers -}) + resolvers, +}); ``` ## Apply transformations to the executable schema @@ -108,7 +112,7 @@ const executableSchema = makeExecutableSchema({ Now it is time to transform our schema. ```js -const newSchema = redactionSchemaTransformer(executableSchema) +const newSchema = redactionSchemaTransformer(executableSchema); ``` and to register mercurius inside fastify @@ -117,7 +121,7 @@ and to register mercurius inside fastify app.register(mercurius, { schema: newSchema, graphiql: true, -}) +}); ``` ## Example diff --git a/docsify/sidebar.md b/docsify/sidebar.md index ac50b6b7..72fc8971 100644 --- a/docsify/sidebar.md +++ b/docsify/sidebar.md @@ -1,41 +1,41 @@ - -* [**Home**](/) - * [Install](/#install) - * [Quick Start](/#quick-start) - * [Examples](/#examples) - * [Acknowledgements](/#acknowledgements) - * [License](/#license) -* [API](/docs/api/options) - * [Plugin Options](/docs/api/options#plugin-options) - * [HTTP Endpoints](/docs/api/options#http-endpoints) - * [Decorators](/docs/api/options#decorators) - * [Error Extensions](/docs/api/options#errors) -* [Context](/docs/context) -* [Loaders](/docs/loaders) -* [Hooks](/docs/hooks) -* [Lifecycle](/docs/lifecycle) -* [Federation](/docs/federation) -* [Subscriptions](/docs/subscriptions) -* [Batched Queries](/docs/batched-queries) -* [Persisted Queries](/docs/persisted-queries) -* [TypeScript Usage](/docs/typescript) -* [HTTP](/docs/http) -* [GraphQL over WebSocket](/docs/graphql-over-websocket.md) -* [Integrations](/docs/integrations/) - * [nexus](/docs/integrations/nexus) - * [TypeGraphQL](/docs/integrations/type-graphql) - * [Prisma](/docs/integrations/prisma) - * [Testing](/docs/integrations/mercurius-integration-testing) - * [Tracing - OpenTelemetry](/docs/integrations/open-telemetry) - * [NestJS](/docs/integrations/nestjs.md) -* [Related Plugins](/docs/plugins) - * [mercurius-auth](/docs/plugins#mercurius-auth) - * [mercurius-cache](/docs/plugins#mercurius-cache) - * [mercurius-validation](/docs/plugins#mercurius-validation) - * [mercurius-upload](/docs/plugins#mercurius-upload) - * [altair-fastify-plugin](/docs/plugins#altair-fastify-plugin) - * [mercurius-apollo-registry](/docs/plugins#mercurius-apollo-registry) - * [mercurius-apollo-tracing](/docs/plugins#mercurius-apollo-tracing) -* [Development](/docs/development) -* [Faq](/docs/faq) -* [Contribute](/docs/contribute) +- [**Home**](/) + - [Install](/#install) + - [Quick Start](/#quick-start) + - [Examples](/#examples) + - [Acknowledgements](/#acknowledgements) + - [License](/#license) +- [API](/docs/api/options) + - [Plugin Options](/docs/api/options#plugin-options) + - [HTTP Endpoints](/docs/api/options#http-endpoints) + - [Decorators](/docs/api/options#decorators) + - [Error Extensions](/docs/api/options#errors) +- [Context](/docs/context) +- [Loaders](/docs/loaders) +- [Hooks](/docs/hooks) +- [Lifecycle](/docs/lifecycle) +- [Federation](/docs/federation) +- [Subscriptions](/docs/subscriptions) +- [Custom directives](/docs/custom-directive.md) +- [Batched Queries](/docs/batched-queries) +- [Persisted Queries](/docs/persisted-queries) +- [TypeScript Usage](/docs/typescript) +- [HTTP](/docs/http) +- [GraphQL over WebSocket](/docs/graphql-over-websocket.md) +- [Integrations](/docs/integrations/) + - [nexus](/docs/integrations/nexus) + - [TypeGraphQL](/docs/integrations/type-graphql) + - [Prisma](/docs/integrations/prisma) + - [Testing](/docs/integrations/mercurius-integration-testing) + - [Tracing - OpenTelemetry](/docs/integrations/open-telemetry) + - [NestJS](/docs/integrations/nestjs.md) +- [Related Plugins](/docs/plugins) + - [mercurius-auth](/docs/plugins#mercurius-auth) + - [mercurius-cache](/docs/plugins#mercurius-cache) + - [mercurius-validation](/docs/plugins#mercurius-validation) + - [mercurius-upload](/docs/plugins#mercurius-upload) + - [altair-fastify-plugin](/docs/plugins#altair-fastify-plugin) + - [mercurius-apollo-registry](/docs/plugins#mercurius-apollo-registry) + - [mercurius-apollo-tracing](/docs/plugins#mercurius-apollo-tracing) +- [Development](/docs/development) +- [Faq](/docs/faq) +- [Contribute](/docs/contribute)