GithubHelp home page GithubHelp logo

expediagroup / graphql-component Goto Github PK

View Code? Open in Web Editor NEW
53.0 9.0 24.0 557 KB

Composeable graphql components

License: MIT License

JavaScript 100.00%
graphql schema-stitching oss-portal-featured

graphql-component's Introduction

GraphQL schema components.

This project is designed to facilitate componentized or modularized development of GraphQL schemas.

Read more about the idea here.

graphql-component lets you build a schema progressively through a tree (facilitated through imports) of GraphQLComponent instances. Each GraphQLComponent instance encapsulates an executable GraphQL schema, specifically a graphql-js GraphQLSchema object. See the API below, but the encapsulated schema is accessible through a simple schema getter on a given GraphQLComponent instance.

Generally speaking, each instance of GraphQLComponent has reference to an instance of GraphQLSchema. This instance of GraphQLSchema is built in a several ways, depending on the options passed to a given GraphQLComponent's constructor.

  • when a GraphQLComponent instance has imports (ie. other GraphQLComponent instances or component configuration objects) graphql-tools stitchSchemas() is used to create a "gateway" or aggregate schema that is the combination of the underlying imported schemas, and the typeDefs/resolvers passed to the root or importing GraphQLComponent
  • when a GraphQLComponent has no imports, graphql-tools' makeExecuteableSchema({typeDefs, resolvers}) is used to generate an executable GraphQL schema using the passed/required inputs.

It's worth noting that GraphQLComponent can also be used to construct componentized Apollo Federated schemas. That is, if you pass the federation: true flag to a GraphQLComponent constructor, @apollo/federation's buildSubgraphSchema() is used in lieu of graphql-tools makeExecutableSchema({...}) and the above still schema construction rule applies. The general use case here might be to help modularize an individual federated subschema service implementation.

Running the examples

local schema composition:

  • can be run with npm run start-composition

federation (2 subschema services implemented via GraphQLComponent and a vanilla Apollo Gateway):

  • can be run with npm run start-federation

Repository structure

  • lib - the graphql-component code.
  • examples/composition - a simple example of composition using graphql-component
  • examples/federation - a simple example of building a federated schema using graphql-component

Running examples:

  • composition: npm run start-composition
  • federation: npm run start-federation
  • go to localhost:4000/graphql
    • for composition this will bring up the GraphQL Playground for a plain old Apollo Server
    • for the federation example this will bring up the GraphQL Playground for an Apollo Federated Gateway

Debug output

GraphQLComponent uses debug for local stdout based debug logging. Enable all debug logging with the node environment variable DEBUG=graphql-component:*. Generally speaking, most debug output occurs during GraphQLComponent construction.

API

  • GraphQLComponent(options) - the component class, which may also be extended. Its options include:

    • types - a string or array of strings of GraphQL SDL defining the type definitions for this component
    • resolvers - a resolver map (ie. a two level map whose first level keys are types from the SDL, mapped to objects, whose keys are fields on those types and values are resolver functions)
    • imports - an optional array of imported components for the schema to be merged with.
    • context - an optional object { namespace, factory } for contributing to context.
    • directives - an optional object containing custom schema directives.
    • mocks - a boolean (to enable default mocks) or an object to pass in custom mocks
    • dataSources - an array of data sources instances to make available on context.dataSources .
    • dataSourceOverrides - overrides for data sources in the component tree.
    • federation - make this component's schema an Apollo Federated schema (default: false).
    • pruneSchema - (optional) prune the schema according to pruneSchema in graphql-tools (default: false)
    • pruneSchemaOptions - (optional) schema options as per PruneSchemaOptions in graphql-tools
  • static GraphQLComponent.delegateToComponent(component, options) - a wrapper function that utilizes graphql-tools delegateToSchema() to delegate the calling resolver's selection set to a root type field (Query, Mutuation) of another GraphQLComponent's schema

    • component (instance of GraphQLComponent) - the component's whose schema will be the target of the delegated operation

    • options (object)

      • operation (optional, can be inferred from info): query or mutation
      • fieldName (optional, can be inferred if target field has same name as calling resolver's field): the target root type (Query, Mutation) field in the target GraphQLComponent's schema
      • context (required) - the context object from resolver that calls delegateToComponent
      • info (required) - the info object from the resolver that calls delegateToComponent
      • args (object, optional) - an object literal whose keys/values are passed as args to the delegatee's target field resolver. By default, the resolver's args from which delegateToComponent is called will be passed if the target field has an argument of the same name. Otherwise, arguments passed via the args object will override the calling resolver's args of the same name.
      • transforms (optional Array<Transform>): Transform being a valid graphql-tools transform
    • please see graphql-tools delegateToSchema documentation for more details on available options since the delegateToComponent functions is simply an adapter for the GraphQLComponent API.

A GraphQLComponent instance (ie, new GraphQLComponent({...})) has the following API:

  • schema - getter that this component's GraphQLSchema object (ie. the "executable" schema that is constructed as described above)
  • context - context function that builds context for all components in the tree.
  • types - this component's types.
  • resolvers - this component's resolvers.
  • imports - this component's imported components in the form of import configuration objects
  • mocks - custom mocks for this component.
  • directives - this component's directives.
  • dataSources - this component's data source(s), if any.

General usage

Creating a component using the GraphQLComponent class:

const GraphQLComponent = require('graphql-component');

const { schema, context } = new GraphQLComponent({ types, resolvers });

Encapsulating state

Typically the best way to make a re-useable component with instance data will be to extend GraphQLComponent.

const GraphQLComponent = require('graphql-component');
const resolvers = require('./resolvers');
const types = require('./types');
const mocks = require('./mocks');

class PropertyComponent extends GraphQLComponent {
  constructor({ types, resolvers }) {
    super({ types, resolvers });
  }
}

module.exports = PropertyComponent;

Aggregation

Example to merge multiple components:

const { schema, context } = new GraphQLComponent({
  imports: [
    new Property(),
    new Reviews()
  ]
});

const server = new ApolloServer({
  schema,
  context
});

Import configuration

Imports can be a configuration object supplying the following properties:

  • component - the component instance to import.
  • exclude - fields on types to exclude from the component being imported, if any.

Exclude

You can exclude whole types or individual fields on types.

const { schema, context } = new GraphQLComponent({
  imports: [
    {
      component: new Property(),
      exclude: ['Mutation.*']
    },
    {
      component: new Reviews(),
      exclude: ['Mutation.*']
    }
  ]
});

The excluded types will not appear in the aggregate or gateway schema exposed by the root component, but are still present in the schema encapsulated by the underlying component. This can keep from leaking unintended API surface area, if desired. You can still delegate calls to imported component's schema to utilize the excluded field under the covers.

Data Source support

Data sources in graphql-component do not extend apollo-datasource's DataSource class.

Instead, data sources in components will be injected into the context, but wrapped in a proxy such that the global context will be injected as the first argument of any function implemented in a data source class.

This allows there to exist one instance of a data source for caching or other statefulness (like circuit breakers), while still ensuring that a data source will have the current context.

For example, a data source should be implemented like:

class PropertyDataSource {
  async getPropertyById(context, id) {
    //do some work...
  }
}

This data source would be executed without passing the context manually:

const resolvers = {
  Query: {
    property(_, { id }, { dataSources }) {
      return dataSources.PropertyDataSource.getPropertyById(id);
    }
  }
}

Setting up a component to use a data source might look like:

new GraphQLComponent({
  //...
  dataSources: [new PropertyDataSource()]
})

Override data sources

Since data sources are added to the context based on the constructor name, it is possible to simply override data sources by passing the same class name or overriding the constructor name:

const { schema, context } = new GraphQLComponent({
  imports: [
    {
      component: new Property(),
      exclude: ['Mutation.*']
    },
    {
      component: new Reviews(),
      exclude: ['Mutation.*']
    }
  ],
  dataSourceOverrides: [
    new class PropertyMock {
      static get name() {
        return 'PropertyDataSource';
      }
      //...etc
    }
  ]
});

Decorating the global context

Example context argument:

const context = {
  namespace: 'myNamespace',
  factory: function ({ req }) {
    return 'my value';
  }
};

After this, resolver context will contain { ..., myNamespace: 'my value' }.

Context middleware

It may be necessary to transform the context before invoking component context.

const { schema, context } = new GraphQLComponent({types, resolvers, context});

context.use('transformRawRequest', ({ request }) => {
  return { req: request.raw.req };
});

Using context now in apollo-server-hapi for example, will transform the context to one similar to default apollo-server.

graphql-component's People

Contributors

brianjquinn avatar dmicoud avatar expeylazzari avatar fuali avatar kaiyueguan avatar mdoleh avatar prabhat1001 avatar roymanas avatar tlivings 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  avatar  avatar  avatar  avatar  avatar  avatar

graphql-component's Issues

Bug: resolver wrapping breaks trivial resolvers when type is different than expected in the schema

Hello team, found an issue with trivial resolvers and the resolver wrapping to prevent double executions in delegateToComponent situations. I don't fully understand this double execution issue that relevant code tries to prevent to propose a solution, so I thought I'd create an issue first to discuss.

Current behavior

The resolution for a graph breaks when on a root object a field is present but with a different type than expected in the schema. Example:

given a type that has a field name of type String, a root object:

{
    ...
    name: {
        value: "name"
    }
    ...
}

and field resolver:

name(obj) {
    return obj.value
}

These lines will find that there's already the field name present in the root and return that, breaking the resolution because we'd be returning an object for a String type.

Expected behavior

Instead, we'd want the resolution of the type to be passed through to the trivial resolver and the resolution to not break.

Maybe solutions

A naive first approach to a solution would be to do a type check (this field expects a string... is the field already a string? return, else pass through to resolver) but the issue remains: even if the expected type matches the type we find, there could be some transformation applied in a trivial resolver (upper case a string, + 1 a zero-based index, I don't know). So maybe there's a better way for a component to know that it is in a delegateToComponent context or to provide an option to explicitly request a resolver NOT to be executed? (maybe an option to pass a list of "noDoubleExecuteResolvers" to compare against)? thoughts?

what is exposed should be declarative, not exclusionary.

It would be cleaner if the component explicitly declared which queries and mutations to include, rather than what to exclude. that is a cleaner mental model for the component since you are stating what you want rather than filtering out what you dont. an empty config should just include all

Mocks broken

Mocks aren't propegating down the component tree

Break up imports

Instead of imports being one list, differentiate between imports used for typeDefs and imports you want to generate delegates to rootTypes for.

First class support for DataSource

Providing better support for the data source abstraction (or another similar abstraction) can keep components more agnostic to how data is resolved.

wrapResolvers prevent internal remapping of enum types

Given a schema that contains an enum type such as:

enum Status {
  APPROVED
  PENDING
  REJECTED
}

With no internal remapping declared, the values of the enum type in resolvers (and returned to clients) are the strings 'APPROVED', 'PENDING', 'REJECTED'.

But Apollo allows the implementer to remap enum values to a different value for use within resolvers (ultimately as a way to easier interface with another system or library who may expect different values than the default stringified version of the enum field). The external API remains unchanged in terms of the value received for that enum type. Remapping the value of enums in a resolver via the resolver map which gets passed to makeExecutableSchema({resolvers}) is accomplished like this:

const resolvers = {
  ...,
  Status: {
    APPROVED: 1,
    PENDING: 2,
    REJECTED: 3
  },
  ...
};

The problem is the wrapResolvers() function is expecting the field of a type in the resolver map to map to a function in order to bind this from the GraphQLComponent class: https://github.com/tlivings/graphql-component/blob/master/lib/resolvers.js#L76 .

In the case of an enum being internally remapped to a new value, it wont and can't be a function to work properly, thus a stack trace during GraphQLComponent bootstrapping:

UnhandledPromiseRejectionWarning: TypeError: func.bind is not a function at wrapResolvers
...

The proposed solution is simple. A basic check if the func is an instance of a function. A PR linked to this issue is coming shortly.

Require sub-classing and constructing

Instead of exporting an instance, require extending GraphQLComponent such that construction is forced (which allows the implementor to have instance data / state).

Flatten context?

Instead of namespacing context additions, should it just be flat?

Federation support

Enable federation behind a flag and buildFederatedSchema instead of makeExecutableSchema.

Using scalar in typdef and resolvers errors on component creation.

In a component that is using the custom scalar (Like graphql-iso-date) in the following way:

const GraphQLComponent = require('graphql-component')
const { GraphQLDate } = require('graphql-iso-date')

class SomeComponent extends GraphQLComponent {
  constructor () {
    const types = `
    scalar Date
  `
    const resolvers = {
      Date: GraphQLDate,
    }
    super({ types, resolvers })
  }
}

module.exports = SomeComponent

...the following error occurs:

graphql-component:schema creating component +0ms
  graphql-component:resolver memoized DateTime.name +0ms
/Users/cholmok/work/holmok.com/node_modules/graphql-component/lib/resolvers.js:68
      wrapped[name][resolverName] = memoize(name, resolverName, func.bind(bind));
                                                                     ^

TypeError: func.bind is not a function
    at wrapResolvers (/Users/cholmok/work/holmok.com/node_modules/graphql-component/lib/resolvers.js:68:70)
    at new GraphQLComponent (/Users/cholmok/work/holmok.com/node_modules/graphql-component/lib/index.js:32:23)
    at new BlogComponent (/Users/cholmok/work/holmok.com/server/components/blog-component.js:68:5)
    at Object.<anonymous> (/Users/cholmok/work/holmok.com/server/components/index.js:20:14)
    at Module._compile (internal/modules/cjs/loader.js:689:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
    at Module.load (internal/modules/cjs/loader.js:599:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
    at Function.Module._load (internal/modules/cjs/loader.js:530:3)
    at Module.require (internal/modules/cjs/loader.js:637:17)

It appears this breaks the memoize which is assuming all the values in the resolvers object are functions and breaks when it is not.

index.d.ts

Adding this file at the root under index.d.ts will make typescript users happy ;-)

Tks

export declare class GraphQLComponent {
_schema: any;
_types: any[];
_resolvers: any;
_imports: any[];
_directives: any;
_context: any;
_importedTypes: any[];
_importedResolvers: any;
_useMocks: boolean;
_importedMocks: any;
_mocks: any;
_preserveTypeResolvers: boolean;
_fragments: any;
_mergedTypes: any;
_mergedResolvers: any;
constructor({ types, resolvers, imports, mocks, directives, context, useMocks, preserveTypeResolvers }?: {
types?: any[];
resolvers?: {};
imports?: any[];
mocks?: any;
directives?: any;
context?: any;
useMocks?: boolean;
preserveTypeResolvers?: boolean;
});
static isComponent(check: any): any;
execute(input: any, { root, context, variables }?: {
root?: any;
context?: {};
variables?: {};
}): Promise;
readonly schema: any;
readonly context: {
(arg: any): Promise;
use(name: any, fn: any): void;
};
readonly types: any[];
readonly importedTypes: any[];
readonly resolvers: any;
readonly importedResolvers: any;
readonly imports: any[];
readonly mocks: any;
}

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.