graphql-introspection-filtering
v3.0.0
Published
Filter graphql schema introspection result to hide restricted fields and types
Downloads
3,596
Maintainers
Readme
graphql-introspection-filtering
Extends schema mapper abilities and allows filtering/modifying introspection query results.
NOTE: For successful introspection all dependent types must be returned. If any of dependent types is missing, it's not possible to rebuild graph on client side, for example graphql playground is unable to build an interactive documentation.
NOTE:
Query
type definition is required
NOTE: Object types must contain at least one visible field
NOTE: GraphQL now does not validate data against full schema, remember to implement logic in base mappers
**Tested with GraphQL 16.6.0 & @graphql-tools 8.0.0 - 10.0.0 (for legacy graphql-tools support check release 2.1.0) **
Installation
npm install --save graphql-introspection-filtering
or
yarn add graphql-introspection-filtering
Usage
Create schema
Filtering is possible on schemas created with makeExecutableSchema
, provided by graphql-introspection-filtering
.
To enable filtering, a mapper has to be applied to schema.
import makeExecutableSchema, { mapSchema } from 'graphql-introspection-filtering';
export default mapSchema(makeExecutableSchema(schemaConfig[, builder]), mapper);
schemaConfig
- schema configuration, extended originalmakeExecutableSchema
's config objectAdditional options
| Option | Type | Default | Description | |---------------------|------------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | shouldSkipQuery |
null
,number
,(context) => boolean
|null
| When positive number provided, this number of introspection queries will be unfiltered. Alternatively callback can be provided, it takescontext
as an argument, and should return boolean. | | hookDirectives |boolean
,string[]
|true
| Whether to hook directives, array of specific directive names can be provided |builder
- builder function (default: original graphqlmakeExecutableSchema
)mapper
- introspection schema mapper
Create introspection schema mapper
Every object and field is visited by a directive visitor, where corresponding directive is applied on it in a schema definition (directives configured separately) AND applied mapper contains a corresponding introspection visitor method.
When falsy value is returned by a mapped resolver, the field / object is excluded from introspection result.
Example introspection mapper can be found below.
export const mapper = {
// (optional) If defined instance can visit `scalar` definitions
[IntrospectionMapperKind.SCALAR_TYPE](result: GraphQLScalarType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit `enum` definitions
[IntrospectionMapperKind.ENUM_TYPE](result: GraphQLEnumType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit object `type` definitions
[IntrospectionMapperKind.OBJECT_TYPE](result: GraphQLObjectType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit `input` object definitions
[IntrospectionMapperKind.INPUT_OBJECT_TYPE](result: GraphQLInputObjectType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit `union` definitions
[IntrospectionMapperKind.UNION_TYPE](result: GraphQLUnionType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit `interface` definitions
[IntrospectionMapperKind.INTERFACE_TYPE](result: GraphQLInterfaceType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit enum value definitions
[IntrospectionMapperKind.ENUM_VALUE](result: GraphQLEnumValue, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit object field definitions
[IntrospectionMapperKind.OBJECT_FIELD](result: GraphQLField<any, any>, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit field argument definitions
[IntrospectionMapperKind.ARGUMENT](result: GraphQLArgument, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit input field definitions
[IntrospectionMapperKind.INPUT_OBJECT_FIELD](result: GraphQLInputField, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
// (optional) If defined instance can visit `directive` definitions
[IntrospectionMapperKind.DIRECTIVE](result: GraphQLDirective) {
return wrap(result, schema, resolver);
}
} satisfies IntrospectionSchemaMapper & SchemaMapper;
Examples
Integration tests
There are working examples available, to start local server
use npm run example
or yarn example
.
Those examples use schema mocks from tests/integration/__mocks__
.
Authentication example
This example provides simple authentication based on roles provided in context.
Schema
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)
}
Authentication mapper
type WrappedResolverType<TSource, TContext, TArgs = any, TResult = unknown> = (
directive: Record<string, any>, orig: GraphQLFieldResolver<TSource, TContext, TArgs, TResult>,
...passtrough: Parameters<GraphQLFieldResolver<TSource, TContext, TArgs>>
) => TResult;
const check = (context: ContextType, requiredPermission?: string) => {
// permission check
};
const introspectionResolver = (
directive: Record<string, any>, orig: GraphQLFieldResolver<any, any>,
parent: unknown, args: Record<string, unknown>, context: ContextType, info: GraphQLResolveInfo
) => {
if (check(context, directive.requires)) {
return orig(parent, args, context, info);
}
return null;
};
const resolver = (
directive: Record<string, any>, orig: GraphQLFieldResolver<any, any>,
parent: unknown, args: Record<string, unknown>, context: ContextType, info: GraphQLResolveInfo
) => {
if (!check(context, directive.requires)) {
throw new ValidationError(`Cannot query field "${info.fieldName}" on type "${info.parentType.name}".`);
}
return orig(parent, args, context, info);
};
type WrappableIntrospectionType = VisitableIntrospectionType & {
resolve: GraphQLFieldResolver<any, any> | null;
subscribe: GraphQLFieldResolver<any, any> | null; // not precise
}
const isWrappable = (val: any): val is WrappableIntrospectionType => !!val;
const wrap = <T extends VisitableIntrospectionType>(result: T, schema: GraphQLSchema, resolver: WrappedResolverType<any, any>) => {
const directive = getDirective(schema, result as any, 'auth')?.[0];
if (directive && isWrappable(result)) {
const key = result.subscribe ? 'subscribe' : 'resolve';
const current = result[key];
result[key] = resolver.bind(undefined, directive, current || defaultFieldResolver);
}
return result;
};
export const authMapper = {
[IntrospectionMapperKind.SCALAR_TYPE](result: GraphQLScalarType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.ENUM_TYPE](result: GraphQLEnumType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.OBJECT_TYPE](result: GraphQLObjectType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.INPUT_OBJECT_TYPE](result: GraphQLInputObjectType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.UNION_TYPE](result: GraphQLUnionType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.INTERFACE_TYPE](result: GraphQLInterfaceType, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.ENUM_VALUE](result: GraphQLEnumValue, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.OBJECT_FIELD](result: GraphQLField<any, any>, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.ARGUMENT](result: GraphQLArgument, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.INPUT_OBJECT_FIELD](result: GraphQLInputField, parent: any, schema: GraphQLSchema) {
return wrap(result, schema, introspectionResolver);
},
[IntrospectionMapperKind.DIRECTIVE](result: GraphQLDirective) {
if (result.name === 'auth' && isWrappable(result)) {
result.resolve = () => null;
}
return result;
},
[MapperKind.ENUM_TYPE](result, schema) {
return wrap(result, schema, resolver);
},
[MapperKind.OBJECT_FIELD](result, _, __, schema) {
return wrap(result as any, schema, resolver);
},
[MapperKind.INPUT_OBJECT_FIELD](result, _, __, schema) {
return wrap(result as any, schema, resolver);
}
} satisfies IntrospectionSchemaMapper & SchemaMapper;
Make it executable
import makeExecutableSchema, { mapSchema } from 'graphql-introspection-filtering';
export default mapSchema(makeExecutableSchema({
typeDefs: ...schema...,
...,
}), authMapper);