Giter Site home page Giter Site logo

n1ru4l / graphql-public-schema-filter Goto Github PK

View Code? Open in Web Editor NEW
52.0 3.0 2.0 954 KB

Filter your GraphQL graph into a subgraph. Code-first & SDL-first!

License: MIT License

JavaScript 3.57% TypeScript 96.19% Shell 0.24%
access-control api graphql graphql-tools the-guild

graphql-public-schema-filter's Introduction

@n1ru4l/graphql-public-schema-filter

This library allows filtering an existing GraphQL schema down into a subset of the original schema. It supports both the code first development flow of building GraphQL schemas (via GraphQL extension fields) and the SDL first development flow (via schema directives).

The implementation is smart and warns the user if the processed annotations would result in an invalid schema such as:

  • Object Type without fields
  • Field whose type is not part of the schema

If such a scenario is encountered the implementation will propagate and hide all fields/types that use types that are not marked as public or that would be invalid.

Why would you need this?

As I have been building GraphQL APIs I often had the need to have both a private and public API.

The private API is used for in-house products. Breaking changes can and will occur. It also includes types and fields specific to in-house application built around the API. The public API, however, is used by individuals not part of our organization. We cannot simply roll out breaking changes on the GraphQL API for those. Furthermore, they should only have access to a subset of the whole GraphQL graph. By generating a subgraph out of the internal graph we can hide stuff, without having to maintain and build two GraphQL schema.

Install instructions

This library requires graphql as a peer dependency and has a runtime dependency on @graphql-tools/utils.

yarn add -E @n1ru4l/graphql-public-schema-filter

Usage Instructions

This library is designed to be inclusive for anyone within the GraphQL.js ecosystem. It supports both the SDL makeExecutableSchema and code-first via extension fields flow.

There is no delegation or validation overhead when executing against the newly generated schema. It is highly recommended to built the public schema during server-startup and not on the fly during incoming requests.

Code-First

Annotate types and fields that should be public with the isPublic extension.

import { GraphQLObjectType, GraphQLString } from "graphql";
import { buildPublicSchema } from "@n1ru4l/graphql-public-schema-filter";

const GraphQLQueryType = new GraphQLObjectType({
  name: "Query",
  fields: {
    hello: {
      type: GraphQLString,
      resolve: () => "hi",
      extensions: {
        isPublic: true,
      },
    },
    secret: {
      type: GraphQLString,
      resolve: () => "sup",
    },
  },
});

const privateSchema = new GraphQLSchema({
  query: GraphQLQueryType,
});
const publicSchema = buildPublicSchema({ schema: privateSchema });
// serve privateSchema or publicSchema based on the request :)

You can also find this example within examples/src/schema.ts.

SDL-First

Instead of using the extension fields, we use the @public directive.

import { makeExecutableSchema } from "@graphql-tools/schema";
import {
  publicDirectiveSDL,
  buildPublicSchema,
} from "@n1ru4l/graphql-public-schema-filter";

const source = /* GraphQL */ `
  type Query {
    hello: String @public
    secret: String
  }
`;

const privateSchema = makeExecutableSchema({
  typeDefs: [publicDirectiveSDL, source],
});
const publicSchema = buildPublicSchema({ schema: privateSchema });
// serve privateSchema or publicSchema based on the request :)

FAQ

Why isPublic and @public over isPrivate and @private?

Deny-listing is more prone to errors than allow-listing. By adding a directive/extension field we explicitly set something to public. The other way around it is easier to forgot to add a @private/ isPrivate annotation, which would automatically result in the new fields being public. Being verbose about what fields are public is the safest way.

Why is the no more granular control that allows building multiple unique public schemas?

I considered this at the beginning, but in practice we never had a use for this. Having multiple public schemas requires maintaining a lot of documentation. In our use-case we only have a public and a private schema. There is still role based access for the public schema. certain users are not allowed to select specific fields. Instead of hiding those fields for those users we instead deny operations that select fields the users are not allowed to select before even executing it with the envelop useOperationFieldPermissions plugin.

You can overwrite the isPublic function which is used to determine whether a field or type is public based on directives and extensions. This allows to fully customize the behavior based on your needs.

type SharedExtensionAndDirectiveInformation = {
  extensions?: Maybe<{
    [attributeName: string]: any;
  }>;
  astNode?: Maybe<
    Readonly<{
      directives?: ReadonlyArray<DirectiveNode>;
    }>
  >;
};

export const defaultIsPublic = (
  input: SharedExtensionAndDirectiveInformation
): boolean =>
  input.extensions?.["isPublic"] === true ||
  !!input.astNode?.directives?.find(
    (directive) => directive.name.value === "public"
  );

License

MIT

graphql-public-schema-filter's People

Contributors

dependabot[bot] avatar github-actions[bot] avatar joseph-neeraj avatar n1ru4l avatar renovate[bot] avatar saihaj avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

graphql-public-schema-filter's Issues

support nested input types

const typeDefs = /* GraphQL */ `
  input A {
    foo: String!
  }
  input B @public {
    lel: String!
    a: A!
  }

  type Query @public {
    asas(lol: A!): String!
    lelb(b: B!): String!
    a: String!
  }

  ${createPublicDirectiveTypeDefs()}
`;

support for federated graphs?

Not sure if this is really an issue as you might define it, but I can't figure out how to use this great package with a federated graph and Apollo server/gateway. Is it possible? Feel free to close if my problem is irrelevant.

future

Extracts from a conversation with @sibelius

Currently, I do this at runtime, but I am not happy with it as you can still send queries (only introspection for some of these fields is disabled). My goal is to have normal web client queries persisted and then use the filtered (public) schema in case the query is not persisted.
Also I dont't like that graphql-introspection-filtering is monkey patching graphql-js.
Maybe I will omit it at all and drop support for doing it at runtime and make it only a cli that generates filtered SDL files

I am currently convinced that the "cleanest approch" is take directive annotated schema -> parse AST -> generate new AST for each role -> print each file -> construct multiple schemas on app start -> select schema based on http headers/payload

Next Steps:

  • Remove graphql-introspection-filtering as a dependency.
  • Convert this library into a cli tool that generates schema files (#1)
  • Provider examples on how to load different schema based on request (express/graphql/express/apollo etc)

Issue with export

When using the example SDL-first form the readMe, i got this error

SyntaxError: The requested module '@n1ru4l/graphql-public-schema-filter' does not provide an export named 'buildPublicSchema'

any clue?

Customizing `isPublic` bahaviour doesn't always take effect.

I'm trying to reverse the bahaviour of the library. Instead of allow-listing, I want to implement deny-listing. I've read the README. I know it's not the ideal solution. But there's a restriction in our current setup that is forcing us to do deny-listing. At least for now.

So I started out by creating the type definitons for my new direction @undocumented:

directive @undocumented on OBJECT | FIELD_DEFINITION | ENUM | UNION | INTERFACE | INPUT_OBJECT | SCALAR | INPUT_FIELD_DEFINITION | ARGUMENT_DEFINITION

Then I customized the isPublic method to look for the existance of @undocumented instead of the default @public directive:

  return buildPublicSchema({
    schema: baseSchema,
    isPublic: (input: SharedExtensionAndDirectiveInformation): boolean => {
      const isPublic = !input.astNode?.directives
        ?.map((directive) => directive.name.value)
        .includes('undocumented');

      return isPublic;
    }
  });

And then I tried to annotate a type and a field defenition with @undocumented:

  type Lolo @undocumented {
    name: String
    dodo: Boolean
  }

  type Query {
    _empty: String @undocumented
  }

Expected behaviour: neither Query._empty nor Lolo should be returned in the introspection schema.

Actual behaviour: Both fields are returned in the introspection schema.

If I remove my customization and use @public, the library is working as expected.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Edited/Blocked

These updates have been manually edited so Renovate will no longer make changes. To discard all commits and start over, click on a checkbox.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Ignored or Blocked

These are blocked by an existing closed PR and will not be recreated unless you click a checkbox below.

Detected dependencies

github-actions
.github/workflows/ci.yml
.github/workflows/pr.yml
.github/workflows/release.yml
npm
example/package.json
  • graphql-yoga 3.9.1
  • @types/koa 2.13.6
  • @types/koa__router 8.0.11
  • @types/node 18.16.5
  • ts-node 10.9.1
  • graphql 16.x.x
package.json
  • @graphql-tools/utils ^9.0.0
  • @changesets/cli 2.26.1
  • @changesets/changelog-github 0.4.8
  • @graphql-codegen/testing 2.0.0
  • @graphql-codegen/typescript 3.0.4
  • @types/jest 29.5.1
  • @typescript-eslint/eslint-plugin 5.59.2
  • @typescript-eslint/parser 5.59.2
  • bob-the-bundler 6.0.0
  • @graphql-tools/schema 9.0.19
  • graphql 16.8.1
  • husky 8.0.3
  • jest 29.5.0
  • lint-staged 13.3.0
  • patch-package 7.0.0
  • prettier 2.8.8
  • ts-jest 29.1.0
  • typescript 5.0.4
  • graphql 16.x.x

  • Check this box to trigger a request for Renovate to run again on this repository

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.