npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

graphql-introspection-filtering

v3.0.0

Published

Filter graphql schema introspection result to hide restricted fields and types

Downloads

3,596

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 original makeExecutableSchema's config object

    Additional 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 takes context 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 graphql makeExecutableSchema)

  • 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);