Filter graphql schema introspection result to hide restricted fields and types.
It allows using extended SchemaDirectiveVisitor
s or filter functions to decide which
schema nodes will be returned with introspection result
NOTE: For successful introspection all dependent types must be returned. If any of dependent types is missing it won't be possible to rebuild graph on client side i.e. graphql playground is unable to build interactive documentation.
yarn add graphql-introspection-filtering
Most important thing is to wrap executable graphql schema with makeFilteredSchema
too add filters
const schema = makeFilteredSchema(executableSchema, filters);
executableSchema
- Executable schema created withmakeExecutableSchema
filters
- Filters definition
Object that holds a set of schema node filters
const filters = {
field: [],
type: [],
directive: []
};
field
- (optional) Array of filter functions to filter fieldstype
- (optional) Array of filter functions to filter typesdirective
- (optional) Array of filter functions to filter directives
Node filtering function, when result of this function is true
the node will be returned
(field, root, args, context, info) => boolean;
field
- Schema node to be returned, the node we decide whether we want to show it or notroot
- Root node for this introspection request as for regular queryargs
- Arguments for this introspection request as for regular querycontext
- Query context for this introspection request as for regular queryinfo
- Query info for this introspection request as for regular query
This function creates filters definition from schemaDirectives
based on static methods in
SchemaDirectiveVisitor
s.
const filters = schemaDirectivesToFilters(schemaDirectives);
schemaDirectives
- Set of schemaDirectives provided tomakeExecutableSchema
Example filtering schema introspection and checking permissions on fields, objects and enums with directives
enum Role @auth(requires: ADMIN) {
ADMIN
REVIEWER
USER
UNKNOWN
}
directive @auth(
requires: Role = ADMIN,
) on OBJECT | FIELD_DEFINITION | ENUM
type Book @auth(requires: ADMIN) {
title: String
author: String
}
type Query {
me: User
books: [Book] @auth(requires: ADMIN)
}
import makeFilteredSchema, { schemaDirectivesToFilters } from 'graphql-introspection-filtering';
const schema = makeFilteredSchema(
makeExecutableSchema({
typeDefs,
resolvers,
schemaDirectives
}),
schemaDirectivesToFilters(schemaDirectives)
);
class AuthenticationDirective extends SchemaDirectiveVisitor {
static RequiredRole = Symbol('RequiredRole');
_wrappedSymbol = Symbol('wrapped');
// filter introspection types
static visitTypeIntrospection(field, _, __, context) {
return AuthenticationDirective.isAccessible(field, context);
}
// filter introspection fields
static visitFieldIntrospection(field, _, __, context) {
return AuthenticationDirective.isAccessible(field, context);
}
// filter introspection directives
static visitDirectiveIntrospection({ name }) {
return name !== 'auth';
}
// decide if user has access to the node
static isAccessible(field, context) {
const requiredAuthRole = field[AuthenticationDirective.RequiredRole];
if (requiredAuthRole) {
if (!context || !context.user || !context.user.roles.includes(requiredAuthRole)) {
return false;
}
}
return true;
}
constructor(...args) {
super(...args);
this.ensureFieldWrapped = this.ensureFieldWrapped.bind(this);
}
ensureFieldWrapped(field) {
if (field[this._wrappedSymbol]) return;
field[this._wrappedSymbol] = true;
const { resolve = defaultFieldResolver } = field;
field.resolve = this.wrapField.call(this, resolve, field);
}
visitObject(obj) {
this.ensureFieldWrapped(obj);
obj[AuthenticationDirective.RequiredRole] = this.args.requires;
}
visitEnum(en) {
this.ensureFieldWrapped(en);
en[AuthenticationDirective.RequiredRole] = this.args.requires;
}
visitFieldDefinition(field) {
this.ensureFieldWrapped(field);
field[AuthenticationDirective.RequiredRole] = this.args.requires;
}
wrapField(resolve, field) {
return async (root, args, context, info) => {
if (!AuthenticationDirective.isAccessible(field, context)) {
throw new Error('Not authorized!');
}
return resolve.call(this, root, args, context, info);
};
}
}