Code Monkey home page Code Monkey logo

graphql-weaver's Introduction

NO LONGER MAINTAINED

We no longer use this library at AEB, so unfortunately, we are not able to maintain it properly. The repository is therefore archived.

graphql-weaver

npm version Build Status

A tool to combine, link and transform GraphQL schemas

Use graphql-weaver if you have multiple GraphQL servers and want to combine them into one API. Features like namespacing, links and custom transformation modules allow you to augment the API as you like.

Try it online in Apollo Launchpad

How to use

npm install --save graphql-weaver

Basic usage:

const schema: GraphQLSchema = await weaveSchemas({
    endpoints: [{
        namespace: 'model',
        typePrefix: 'Model',
        url: 'http://localhost:8080/graphql' // url to a GraphQL endpoint
    }, {
        namespace: 'local',
        schema: new GraphQLSchema(/* ... */) // use schema instance directly
    }]
})

A woven schema is an executable GraphQL schema built from several endpoints. For each endpoint, you can either specify a URL to a GraphQL server, pass an executable GraphQL schema instance, or implement the GraphQLClient interface yourself.

In its basic configuration, weaveSchemas merges the query, mutation and subscription fields of all endpoints. To avoid name collisions, you can specify the namespace and typePrefix properties like seen above. The typePrefix will be prepended to all types; namespace causes the fields of this endpoint to be wrapped in a field, to be queried via { model { aFieldOfModel } }.

Links

In the spirit of GraphQL, this tool allows you to create links between objects of different endpoints. Suppose you have a music recommendation service and a music library service. You can make the whole properties of a song available in the recommendation API without the recommendation service knowing all song properties.

const schema: GraphQLSchema = await weaveSchemas({
    endpoints: [{
        namespace: 'library',
        url: 'http://example.com/library/graphql'
    }, {
        namespace: 'recommendations',
        url: 'http://example.com/recommendations/graphql',
        fieldMetadata: {
            'Recommendation.song': { // Field song in type Recommendation
                link: {
                    field: 'library.Song', // field Song in namespace library
                    argument: 'id', // argument of library.Song
                    batchMode: false,
                }
            }
        }
     }]
});

This assumes the library schema has a field Song with argument id, and the recommendations schema has a type Recommendation with a field song which contains the song id. Then, you can query the recommendations with all song information like this:

query {
    recommendations {
        myRecommendations {
            recommendedAt
            song {
                id
                artist
                title
                year
            }
        }
    }
}

If there are many recommendations, this is ineficcient because all songs are queried independently. If the library schema supports querying multiple songs at once, you can set batchMode to true. If the library schema may return the songs in a different order than the ids its get, you need to set keyField too.

const schema: GraphQLSchema = await weaveSchemas({
    endpoints: [{
        namespace: 'library',
        url: 'http://example.com/library/graphql'
    }, {
        namespace: 'recommendations',
        url: 'http://example.com/recommendations/graphql',
        fieldMetadata: {
            'Recommendation.song': {
                link: {
                    field: 'library.allSongs',
                    argument: 'filter.ids', // allSongs has an argument filter with an array field ids
                    batchMode: true,
                    keyField: 'id' // the name of a field in Song type that contains the id
                }
            }
        }
     }]
});

In the case where you want to perfom a link from a single id to a list of many related items, include linkFieldName as an alias for the linked list of items, and the oneToMany flag. Since we are making a single request for a GraphQL list of items, batchMode is not required and should be set to false.

const schema: GraphQLSchema = await weaveSchemas({
    endpoints: [{
        namespace: 'library',
        url: 'http://example.com/library/graphql'
    }, {
        namespace: 'recommendations',
        url: 'http://example.com/recommendations/graphql',
        fieldMetadata: {
            'Artist.name': { // Field name in type Artist
                link: {
                    field: 'library.allSongs', // field allSongs in namespace library
                    argument: 'filter.artistName', // argument of library.Song
                    batchMode: false, // batchMode must be set to false
                    oneToMany: true,
                    linkFieldName: 'songsByThisArtist' // the "virtual" field that will be populated
                }
            }
        }
     }]
});

This assumes the library schema has a field allSongs which accepts a filter argument, where one of the filter properties is artistName. This also assumes that the recommendations schema has a type Artist with a field name which contains the artists name. This configuration will add new virtual field songsByThisArtist on the Artist type which will allow you to query an Artist along with their songs like this:

query {
    recommendations {
        myFavoriteArtists {
            name
            genre
            songsByThisArtist {
                id
                title
                year
            }
        }
    }
}

Joins

What if you want to sort the recommendations by the song age, or filter by artist? The recommendation service currently does not know about these fields, so it does not offer an API to sort or order by any of them. Using graphql-weaver, this problem is easily solved:

const schema: GraphQLSchema = await weaveSchemas({
    endpoints: [{
        namespace: 'library',
        url: 'http://example.com/library/graphql'
    }, {
        namespace: 'recommendations',
        url: 'http://example.com/recommendations/graphql',
        fieldMetadata: {
            'Recommendation.song': {
                link: {
                    field: 'library.allSongs',
                    argument: 'filter.ids',
                    batchMode: true, // is now required
                    keyField: 'id' // this one too
                }
            },
            'Query.myRecommendations': { // Field myRecommendations on type Query
                join: {
                    linkField: 'song', // The field name song in the type Recommendation
                }
            }
        }
     }]
});

This assumes that the library service offers a way to filter and sort songs via the orderBy and filter arguments. Using it is simple:

query {
    recommendations {
        myRecommendations(filter: { song: { artist: "Ed Sheeran" } }, orderBy: song_year_DESC) {
            recommendedAt
            song {
                id
                artist
                title
                year
            }
        }
    }
}

A note on efficiency: The list of recommendations should be relatively small (not more than a few hundred), as all recommendations need to be fetched so that their ids can be sent to the library for filtering and sorting.

Custom transformations

All four presented features (namespaces, type prefixes, links and joins) are implemented as independent modules. If you need something else, you can just write your own module:

class MyModule implements PipelineModule {
    transformExtendedSchema(schema: ExtendedSchema): ExtendedSchema {
        // do something with the schema
        return schema;
    }
    transformQuery(query: Query): Query {
        // do something with the query
        return query;
    }
}

const schema: GraphQLSchema = weaveSchemas({
    endpoints: [{
        namespace: 'library',
        url: 'http://example.com/library/graphql',
        
    }],
    pipelineConfig: {
        transformPreMergePipeline(modules: PipelineModule[], context: PreMergeModuleContext): PipelineModule[] {
            // These modules are executed for each endpoint
            return [
                ...modules,
                new MyModule()
            ]
        },
        transformPostMergePipeline(modules: PipelineModule[], context: PostMergeModuleContext): PipelineModule[] {
            // These modules are executed once for the merged schema
            return [
                ...modules,
                new MyModule()
            ]
        }
    }
});

For a simple module, see TypePrefixModule. The section Architecture below gives an overview over the pipeline architecture.

To simplify modifications to a schema, graphql-weaver ships graphql-transformer (and transformExtendedSchema). You can change types and fields as you like with a simple function:

const transformedSchema = transformSchema(originalSchema, {
    transformField(field: GraphQLNamedFieldConfig<any, any>, context) {
        // Rename a field in a type
        if (context.oldOuterType.name == 'MyType') {
            return {
                ...field,
                name: field.name + 'ButCooler'
            }
        }
        return field;
    },

    transformObjectType(type: GraphQLObjectTypeConfig<any, any>) {
        if (type.name == 'MyType') {
            return {
                ...type,
                name: 'MyCoolType'
            };
        }
        return type;
    },

    transformFields(fields: GraphQLFieldConfigMap<any, any>, context) {
        // You can even copy types on the fly and transform the copies
        const type2 = context.copyType(context.oldOuterType, {
            transformObjectType(typeConfig: GraphQLObjectTypeConfig<any, any>) {
                return {
                    ...typeConfig,
                    name: typeConfig.name + '2'
                };
            }
        });

        // This just adds a reflexive field "self" to all types, but its type does not have
        // the "self" field (because it is a copy from the original type, see above)
        // it also won't have the "cool" rename applied because the top-level transformers are not applied
        return {
            ...fields,
            self: {
                type: type2,
                resolve: (source: any) => source
            }
        }
    }
});

For more information, refer to the graphql-transformer project.

Schema error handling

By default, weaveSchemas throws a WeavingError if an endpoint schema could not be fetched or the link and join features are configured incorrectly. You can change this behavior with the errorHandling property:

weaveSchemas({
    endpoints: [ /* ... */ ],
    errorHandling: WeavingErrorHandlingMode.CONTINUE_AND_REPORT_IN_SCHEMA
})

There are four modes available:

  • THROW is the default behavior which throws all schema errors
  • CONTINUE ignores errors. If the endpoint schema cannot be created at all, it will be missing in the result config. If a join or link feature is malconfigured, this one configuration will be skipped. Use weaveSchemasExt to retrieve a list of errors.
  • CONTINUE_AND_REPORT_IN_SCHEMA behaves like CONTINUE, but errors are additionally displayed to the user via a special _errors field on the root query type.
  • CONTINUE_AND_ADD_PLACEHOLDERS is like CONTINUE_AND_REPORT_IN_SCHEMA, but namespaced endpoints that completely fail are also replaced by an object with a field _error.

Runtime error handling

Since version 0.11, graphql-weaver propagates runtime errors of endpoints transparently and in the most intuitive way.

  • If a user requested fields from two endpoints of which one failed and one succeeded, the result data of the successful endpoint will be returned and the error of the failing one will be propagated.
  • If the endpoint reported errors for some fields but returned data for other fields, graphql-weaver will pass this through exactly. The errors will be reported at the correct fields in the woven schema.
  • If the error reported by the endpoint included source location information, graphql-weaver will try to map this these source location to the woven schema. If the location can not be mapped, the field's location in the woven schema will be used, so if you get a location, you can be sure it is a location in your request to graphql-weaver.

For the most part, this means everything should work as expected. However, the mapping is more complex as it might seem, so if you find an error mapping that looks wrong, please open an issue.

Contributing

After cloning the repository, run

npm install
npm start

To run the test suite, run

npm test

To debug/run the application (or tests) in WebStorm, right-click on graphql-weaver.js (or graphql-weaver-tests.js, respectively) and choose Debug/Run.

Release workflow

  • For normal development, create a branch from master, commit and create a merge request to master.
  • To fix a bug in a previous release, find the release- branch for the corresponding version, increase the patch level in package.json and push the changes. Once the tests pass, manually trigger the deploy stage in Gitlab. You can also release a -rc.1 version before the actual release for prior testing in dependent modules.
  • To prepare a new feature release (currently, this means a new minor version), create a release-0.x branch from master. Set the version to 0.x-rc.1, push and manually trigger the deploy stage in Gitlab. Test the changes in dependent modules. Once everything is ok, change the version to 0.x and deploy again. Finally, merge the release branch into master. Do not delete the release branch as it is used for hotfixes.

Architecture

graphql-weaver takes a set of GraphQL endpoints, transforms them through pipelines, merges them, transforms the merged schema again and exposes that as its woven schema.

           +------+  +------+  +------+
Endpoints  |Schema|  |Schema|  |Schema|
           +------+  +------+  +------+

           +------+  +------+  +------+
            X    X    X    X    X    X
Pipelines    X  X      X  X      X  X
              XX        XX        XX

               +                  +
Merge           +----------------+

                     +------+
                      X    X
Pipeline               X  X
                        XX

                     +------+
Server               |Schema|
                     +------+

The merge in the middle simply merges all the fields of the Query/Mutation types. All the other features, like type prefixing, field namespacing, even resolvers, is implemented by pipeline modules.

You'll find the list of modules in src/pipeline/pipeline.ts. For a description of each module, please refer to the TypeDoc comments.

Module structure

  • graphql - general utilities for working with GraphQL schemas and queries
  • extended-schema - an implementation of storing and exposing metadata on fields, the concept being discussed on GitHub
  • graphql-client - GraphQL client library, with local and http implementations
  • pipeline - the core, being framework and modules for graphql-weaver's features
  • config - configuration parameter types for weaveSchemas
  • utils - utilities unrelated to GraphQL
  • typings - typings for thirdparty modules

graphql-weaver's People

Contributors

dependabot[bot] avatar fx-hao avatar henkesn avatar igorlesnenko avatar kroeder avatar mfusser avatar simhnna avatar yogu 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  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  avatar  avatar  avatar  avatar

graphql-weaver's Issues

Some implementations lost after weaveSchemas

Before weave schema:

 _implementations: 
   { _Document: [ Navigation, Page, Page_template, Test ],
     _Linkable: 
      [ Navigation,
        Page,
        Page_template,
        Test,
        _ExternalLink,
        _FileLink,
        _ImageLink ],

Running:

 weaveSchema = await weaveSchemas({
      endpoints: [
        {
          namespace: 'cms',
          typePrefix: 'CMS',
          schema: anotherSchema,
        },
        {
          schema: localSchema,
        },
      ],
    });

Result:

_implementations: 
   { CMS_Document: [ CMSNavigation, CMSPage, CMSPage_template, CMSTest ],
     CMS_Linkable: [ CMSNavigation, CMSPage, CMSPage_template, CMSTest ],

Below fields are missing:

 _ExternalLink,
        _FileLink,
        _ImageLink

Expected:

_implementations: 
   { CMS_Document: [ CMSNavigation, CMSPage, CMSPage_template, CMSTest ],
     CMS_Linkable: [ CMSNavigation, CMSPage, CMSPage_template, CMSTest, 
CMS_ExternalLink, CMS_FileLink, CMS_ImageLink ],

Help: How to completely replace types/interfaces

My case is that I have some common types and interfaces between apis. But when trying to merge the schemas I get schema must contain unique named types but contains multiple types named "Node" for example, which makes sense. I've been trying to replace the common types with a local value, so all 'node' usage in each api uses local 'node' type.

I've tried the transformSchema method, which works to a point but it looks like you need to replace the field types also with this new type, which would then involve descending through non null lists etc to find the type to replace also.

Am I missing something, is there a simpler way?

Propagate HTTP status codes

I run into some issue on production enviroment.

  • I use nginx as reverse proxy, sometimes I got 502 error.
  • How to handle error. Such as if one schema request return 401 if token expire, another schema request is 500, in those circumstances, how to return to the proper merged shema response status. Maybe this is related to the first question.

Relay support

Hi! Thank you so very kindly for the work! This is a tremendous piece of work!

However, I'd one question regarding "production-ready" status of the project.

Does graphql-weaver support Relay?

Create a schema from already weaved schemas

In my local server I want to create a schema that is a merge of:

  • local schemas (with namspaces and prefixes)
  • a schema from a remote server (without namespace/prefix), that was itself produced by graphql-weaver

See https://github.com/AEB-labs/graphql-weaver/blob/weave-a-weaved-schema/spec/regression/data/merge_weaved_schema.config.ts

This runs into an error:

Failed: Schema must contain unique named types but contains multiple types named "_FieldJoin".

(on my local server the error was on _ExtendedIntrospection, but probably the same problem?)

Woven Schemas lose their astNodes

When building a schema from SDL or any other source, the astNodes are present. The schema that is returned does not seem to have this field specified. Is there any way to preserve this info if it is present already?

Cannot read property 'map' of undefined (link issue with undefined variableDefinitions)

I'm not able to call link without getting the error "Cannot read property 'map' of undefined" in graphql-tools AddArgumentsAsVariables (in addVariablesToRootFields, which calls operation.variableDefinitions.map()).

The issue seems to originate from here -- https://github.com/AEB-labs/graphql-weaver/blob/master/src/pipeline/links.ts#L301 -- if I change undefined to [] that solves the error.

I've tried it with graphql-tools 4.0.0 (from your lock file) and 4.0.4 (the latest in 4.0) and get the same error in both. Quite honestly I don't know nearly enough about GraphQL query evaluation to understand what's causing this.

  const schema = await weaveSchemas({
    endpoints: [
      {
        namespace: 'account',
        typePrefix: 'Account',
        schema: auth0,
      },

      /* Wordpress post schemas */
      {
        namespace: 'blog',
        typePrefix: 'Blog',
        schema: createWordpressSchema('https://wp-int.codeday.org/graphql'),
        fieldMetadata: {
          'User.slug': {
            link: {
              field: 'account.user',
              argument: 'where.username',
              batchMode: false,
              linkFieldName: 'account',
            },
          },
        },
      },
    ]
  });

(auth0 is using makeExecutableSchema({ typeDefs, resolvers }); -- is there something special I'd need to do since it's not a remote schema?)

[FEATURE REQUEST]: Ability to include HTTP headers

I think it would be super useful if one could include headers when defining the endpoints.

Something like:

endpoints: [{
  namespace: 'model',
  typePrefix: 'Model',
  url: 'http://localhost:8080/graphql' // url to a GraphQL endpoint,
  headers: {
    'Authorization': 'Bearer **********************',
    'content-type': 'application/json'
  }
}

#6 explains how to implement this by extending the HttpGraphQLClient, but I think it'll be a great addition to the core lib.

Deduplicate types

is there a way to merge all duplicate types into 1?

e.g. 2 query types into 1, and any other type?

Linked field returns null when using linkFieldName

Summary

For some reason some virtual fields are returning null for me, while others are working. When I specify the linkFieldName, the endpoint for lc doesn't even get hit to resolve the query. However, if I don't use a linkFieldName, it does hit the lc endpoint, and returns the correct data.

This library is amazing, and I love it, so I'm hoping there is a way to get this to work.

I also have another unrelated question: Is it possible to create more than one link for a field, to support polymorphic relations?

Example of the problem

Schema

const schema = await weaveSchemas({
      endpoints: [{
        namespace: 'cw',
        typePrefix: 'Courseware',
        url: 'http://localhost:2000',
        fieldMetadata: {
          'Assignment.id': {
            link: {
              field: 'rls.rls_resource', // field location in namespace go
              argument: 'external_id', // argument of geo.location,
              linkFieldName: 'resource',
            }
          },
        }
      }, {
        namespace: 'lc',
        typePrefix: 'LearningCurve',
        url: 'http://localhost:2001'
      }, {
        namespace: 'rls',
        typePrefix: 'RLS',
        url: 'http://localhost:2003',
        fieldMetadata: {
          'rlsResource.id': {
            link: {
              field: 'lc.assignment',
              argument: 'assignment_id',
              linkFieldName: 'activity',
              batchMode: false,
            }
          },
        }
      }]
    });

Query

query {
  cw {
    course(id:"15968801-643e-48df-83ca-da275bf4acdd") {
      id
      name
      assignments {
        id
        name
        resource {
          id
          external_id
          activity {
            activity_title
          }
        }
      }
    }
  }
}

Response

{
  "data": {
    "cw": {
      "course": {
        "id": "15968801-643e-48df-83ca-da275bf4acdd",
        "name": "LC Test",
        "assignments": [
          {
            "id": "2d5d1eda-9322-46fa-ad58-9b34558e18fe",
            "name": " Appendix B: Work/Life Satisfaction; I-O Psychology; Motivating Achievement; Leadership",
            "resource": {
              "id": "4340af8d-77bf-40c4-b7a5-83113a576034",
              "external_id": "2d5d1eda-9322-46fa-ad58-9b34558e18fe",
              "activity": null
            }
          },
          {
            "id": "8af965b3-bfbc-4c69-9d93-3fd0b61ea0fe",
            "name": " Appendix B: Career Fields in Psychology",
            "resource": null
          },
          {
            "id": "a783ff43-86c7-4a5e-b863-9c991244cd92",
            "name": " Appendix A: Psychology at Work",
            "resource": null
          },
          {
            "id": "c1f6cdc1-1b71-4113-95ee-aa614a7d075c",
            "name": " Appendix A: Psychology at Work",
            "resource": null
          },
          {
            "id": "5905dd3b-70b8-4b6b-b98f-236656c8f026",
            "name": " 4a. An Introduction to Consciousness and Sleep",
            "resource": {
              "id": "c50f80d4-cfce-47c9-a328-a613e9036aad",
              "external_id": "5905dd3b-70b8-4b6b-b98f-236656c8f026",
              "activity": null
            }
          }
        ]
      }
    }
  }
}

Notice how all of the activity fields are null

Working (without linkFieldName)

Schema

const schema = await weaveSchemas({
      endpoints: [{
        namespace: 'cw',
        typePrefix: 'Courseware',
        url: 'http://localhost:2000',
        fieldMetadata: {
          'Assignment.id': {
            link: {
              field: 'rls.rls_resource', 
              argument: 'external_id', 
              linkFieldName: 'resource',
            }
          },
        }
      }, {
        namespace: 'lc',
        typePrefix: 'LearningCurve',
        url: 'http://localhost:2001'
      }, {
        namespace: 'rls',
        typePrefix: 'RLS',
        url: 'http://localhost:2003',
        fieldMetadata: {
          'rlsResource.id': {
            link: {
              field: 'lc.assignment',
              argument: 'assignment_id',
              batchMode: false,
              // linkFieldName removed
            }
          },
        }
      }]
    });

Query

query {
  cw {
    course(id:"15968801-643e-48df-83ca-da275bf4acdd") {
      id
      name
      assignments {
        id
        name
        resource {
          id {
            activity_title
          }
          external_id
        }
      }
    }
  }
}

Response

{
  "data": {
    "cw": {
      "course": {
        "id": "15968801-643e-48df-83ca-da275bf4acdd",
        "name": "LC Test",
        "assignments": [
          {
            "id": "2d5d1eda-9322-46fa-ad58-9b34558e18fe",
            "name": " Appendix B: Work/Life Satisfaction; I-O Psychology; Motivating Achievement; Leadership",
            "resource": {
              "id": {
                "activity_title": "Appendix B: Work/Life Satisfaction; I-O Psychology; Motivating Achievement; Leadership"
              },
              "external_id": "2d5d1eda-9322-46fa-ad58-9b34558e18fe"
            }
          },
          {
            "id": "8af965b3-bfbc-4c69-9d93-3fd0b61ea0fe",
            "name": " Appendix B: Career Fields in Psychology",
            "resource": null
          },
          {
            "id": "a783ff43-86c7-4a5e-b863-9c991244cd92",
            "name": " Appendix A: Psychology at Work",
            "resource": null
          },
          {
            "id": "c1f6cdc1-1b71-4113-95ee-aa614a7d075c",
            "name": " Appendix A: Psychology at Work",
            "resource": null
          },
          {
            "id": "5905dd3b-70b8-4b6b-b98f-236656c8f026",
            "name": " 4a. An Introduction to Consciousness and Sleep",
            "resource": {
              "id": {
                "activity_title": "4a. An Introduction to Consciousness and Sleep"
              },
              "external_id": "5905dd3b-70b8-4b6b-b98f-236656c8f026"
            }
          }
        ]
      }
    }
  }
}

Add a type name for namespaces

The optimistic-response mechanism of apollo-client wants a __type for every queried type
This might be useful in general, e.g. for things like introspection queries

myNamespace:  {
    __type: 'Namespace',
    myData: [...]
},
someOtherNamespace: {
    __type: 'Namespace',
    ...
}

Typename 'Namespace' could also be 'WeaverNamespace', I'm not sure what would fit best.

Crash when load schema

Error: Unexpected reference to type Ride
    at Transformer.findType (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:249:19)
    at Transformer.mapType (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:269:24)
    at /Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:487:59
    at Array.map (<anonymous>)
    at Transformer.transformUnionType (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:487:36)
    at Transformer.copyType (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:315:25)
    at Transformer.transformType (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:297:21)
    at Transformer.processType (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:290:40)
    at Transformer.transform (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:210:18)
    at Object.transformSchema (/Users/gengjiawen/Documents/schema-stitching-demo/node_modules/[email protected]@graphql-weaver/src/graphql/schema-transformer.ts:185:24)

Documentation on links, pipelines, fieldMetadata and more insufficient

I really wanted to use this because the plumbing seems extensive enough and because you've solved several features like custom scalars; that being said, I have spent too many days both converting TypeScript to JavaScript in my head (not a super big deal, but causes some issues), reading through code that has far too few comments, and all this because the documentation is lacking.

Please provide better docs. For now we are using Apollo's schema stitching until this becomes a bit more fleshed out.

Wish list

  • better docs on pipelines
  • better docs on links
    • better docs on fieldMetadata
  • full SDL of the example linked stitched schemas in the README.md examples
  • ways to have a type in schema B implement an interface defined in schema A

Authenticating User at API Gateway and then redirecting them to microservices

I am trying to build an API Gateway with GraphQL for my microservices architecture. Here's how I am trying to implement this:

  • Suppose we have 4 microservices named as M1, M2, M3, and M4
  • The M1 microservice is the one which is responsible for logging in the user and generating JWT token
  • Whenever user makes a request for other microservices like M2, M3, or M4, he must be logged in to the system

Following are the expected API Gateway features:

  • Wherever a request comes, if it's for microservice M1 the API Gateway should just redirect the user to that gateway
  • If user requests for M2, M3 or M4 microservices the API Gateway should check for JWT token, decode it, and append the data into the header.

Following are the things is not expected from the API Gateway:

  • Writing all schemas from all microservices in the API Gateway. Since it is not practical, if I change the schema in Microservice M3 I have to change it at the API gateway also which I don't want to do

This framework is all cool, but I don't find any examples or methods through which I can achieve this. Maybe someone can guide me through.

Perhaps @Yogu you can help me.

Thanks

Weaved schema doesn't include type descriptions

I just spent a little time updating the type descriptions on one of by back-end graphql services and I noticed that these descriptions don't appear to be displayed in the weaved schema on my gateway server.

I'm looking to provide a self serve graphql API to my consumers so these type descriptions would be pretty valuable.

I'm going to see if I can get some time to work on this myself if you're open to pull requests.

One-to-many links

I'm currently putting together an example but I'm unsure how graphql-weaver works.

  const schema = await weaveSchemas({
    endpoints: [
      {
        namespace: 'properties',
        typePrefix: 'Property',
        url: 'https://v7l45qkw3.lp.gql.zone/graphql',
        fieldMetadata: {
          'PropertyProperty.booking': {
            link: {
              field: 'bookings.bookingsByPropertyId', // field Song in namespace library
              argument: 'propertyId', // argument of library.Song
              batchMode: true,
              keyField: 'propertyId'
            }
          },
          'Query.properties.properties': {
            join: {
              linkField: 'bookings'
            }
          }
        }
      },
      {
        namespace: 'bookings',
        typePrefix: 'Booking',
        url: 'https://41p4j4309.lp.gql.zone/graphql'
      },
      {
        namespace: 'weather',
        typePrefix: 'Weather',
        url: 'https://5rrx10z19.lp.gql.zone/graphql'
      }
    ]
  });

would love some help fleshing this out.

Duplicate filter type generation on joins

Let there be two schemas:

A
->A1: TypeA
->A2: TypeA

B
->B1: TypeB

When joining B1 on A1 AND A2, the filter type TypeAWithTypeBFilter will be generated two times and not re-used.

I'm planning to have a look at it next week.

GraphQL 15 compatibility

Should this package work fine with GraphQL 15? I'm encountering some issues since upgrading but haven't managed to figure out precisely what the issues are yet.

Are there fundamental changes in GraphQL 15 that make weaver unworkable? We've been using weaver for a couple of years and have recently investigated moving to graphql-tool's WrapType but that only seems to support queries and not mutations.

Thanks in advance.

Directives, especially and primarily, default ones, get duplicated for each schema

Is there a particular reason why directives are duplicated. Given the Facebook Reference implementation, there is no way to specify a resolve function for a directive. Furthermore all attempts to do so, do so by wrapping all field resolvers with additional logic that checks the astNodes for attached directives.

Can we dedupe the directives?

Query root type duplicate

Root types (query, mutation, etc) should be merged, yes?
But I am trying to do the following: combining apollo server with schema stiching and weaver like this (a demo):

const link = new HttpLink({uri: 'https://graphql-demo.azurewebsites.net/graphql', fetch});

  const remoteSchema = await introspectSchema(link);

  const executableSchema = makeRemoteExecutableSchema({
    schema: remoteSchema,
    link,
  });

  const transformedSchema = await weaveSchemas({
    endpoints: [{
      // typePrefix: 'Demo__',
      namespace: 'demo',
      schema: executableSchema
    }]
  });

And I get:

(node:5997) UnhandledPromiseRejectionWarning: Error: Schema must contain unique named types but contains multiple types named "Query".

Even without apollo's tooling, like this:

const transformedSchema = await weaveSchemas({
      endpoints: [{
        namespace: 'demo',
        url:  'https://graphql-demo.azurewebsites.net/graphql'
      }]
    });

I get the same.

If I add typePrefix, schema becomes strange:

Query → demo → Demo__Query → ...  stuff

While queries perform correctly, I don't understand, why this conflict in root types appears (especially if to consider that I try it with one endpoint)

How to rename fields properly?

I want to camelize field and argument names, I thought it was simple, just renaming the snake-style fields to camel-style. So I wrote a custom module as the following:

export class SnakeToCamelFieldModule implements PipelineModule {
    private snakeToCamelFieldSchema = new SnakeToCamelFieldSchema();

    transformSchema(schema: GraphQLSchema): GraphQLSchema {
        return transformSchema(schema, this.snakeToCamelFieldSchema);
    }
}

function isSpecificCase(s: string, converter: (value: string, locale?: string) => string ) {
    return s === converter(s);
}

class SnakeToCamelFieldSchema implements SchemaTransformer {
    private convertMap: { [key: string]: string; } = {};
    transformField(field: GraphQLNamedFieldConfig<any, any>, context: any) {
        // Rename a field in a type
        if (isSpecificCase(field.name, snakeCase)) {
            const camelName = camelCase(field.name);
            this.convertMap[camelName] = field.name;
            return {
                ...field,
                name: camelName
            };
        }
        return field;
    }
    renameField(name: string): string {
        return this.convertMap[name] || name;
    }
}

The schema was exactly what I want, but I also need to rename the query back. Then I updated FieldNode's name, but it failed and I got Failed to determine type of SelectionSet node. It seems like some validations called before executing the query.
Then I thought maybe graphql aliases could help, so I updated the code as the following:

export class SnakeToCamelFieldModule implements PipelineModule {
    private snakeToCamelFieldSchema = new SnakeToCamelFieldSchema();

    transformSchema(schema: GraphQLSchema): GraphQLSchema {
        return transformSchema(schema, this.snakeToCamelFieldSchema);
    }

    transformQuery(query: Query): Query {
        const definitions: Array<OperationDefinitionNode> = query.document.definitions.filter(
            (e: DefinitionNode): e is OperationDefinitionNode => e.kind === "OperationDefinition");

        const fieldNodes = flatten(definitions.map(def => collectFieldNodes(def)));
        fieldNodes.map((field: any) => {
            const originalName = this.snakeToCamelFieldSchema.renameField(field.name.value);
            if (originalName !== field.name.value) {
                field.alias = {
                    kind: "Name",
                    value: field.name.value
                };
                field.name.value = originalName;
            }
        });

        return query;
    }
}

It only partly succeeded, no errors occured this time, some queries like { getUsername } succeeded, it returned { "data": { "getUsername": "haofuxin" } }. But some queries like

{
  relay {
    routes {
      edges {
        node {
          isActive
          id
        }
      }
    }
}

returned

{
  "data": {
    "relay": {
      "routes": {
        "edges": [
          {
            "node": {
              "id": "Um91dGUtYXNjZW5kXzE2NA=="
            }
          }
        ]
      }
    }
  }
}

isActive was missing. After diving in for a while, I found that isActive was filtered out in https://github.com/graphql/graphql-js/blob/ed74228dc8540e3d2681cb6da473e7ce5edb7850/src/execution/execute.js#L649-L655 . fieldName is is_active rather than isActive.

Apollo's schema stiching

Are you guys in touch with the Apollo community on their schema stiching initiative? Both projects seem to have much to benefit from each other.

woking with uploads via apollo-upload-server

Hello.

Did you try/had success to use weaver together with uploads?

I have such gateway (it fetches backends per request — this is needed by design):

import {HttpLink} from 'apollo-link-http';
import fetch from 'node-fetch';

import express from 'express'
import * as bodyParser from 'body-parser-graphql'
import { apolloUploadExpress, GraphQLUpload } from 'apollo-upload-server'
import {graphiqlExpress, graphqlExpress} from 'apollo-server-express'
import {
  introspectSchema,
  makeRemoteExecutableSchema,
} from 'graphql-tools'
import {weaveSchemas} from "graphql-weaver";
import jwt from 'jsonwebtoken';
import createLocaleMiddleware from 'express-locale';
import fetchServices from "./utils/fetchServices";

require('dotenv').config();

const cors = require('cors');

const start = async function () {

  const app = express();

  const PORT = process.env.PORT || 3000;

  app.use(cors());
  app.post('/graphql', bodyParser.graphql());
  app.post('/graphql', apolloUploadExpress());

  app.use('/graphql', async function (req, res, next) {
    // should to fetch this dynamically
    const services = await fetchServices();

    const serviceContainers = await Promise.all(services.map(async (service) => {
      const dosvitMeta = req._dosvit;
      const headers = {};
      // ... some magic with headers
      const link = new HttpLink({uri: service.url, fetch, headers});

      const remoteSchema = makeRemoteExecutableSchema({
        schema: await introspectSchema(link),
        link
      });
      return {service, remoteSchema};
    }));

    const transformedSchema = await weaveSchemas({
      endpoints: serviceContainers.map(s => {
        return {
          typePrefix: s.service.prefix,
          namespace: s.service.namespace,
          schema: s.remoteSchema
        }
      })
    });

    return graphqlExpress({schema: transformedSchema})(req, res, next);
  });

  app.use('/graphiql',
      graphiqlExpress({
        endpointURL: '/graphql',
      }),
  );

  app.listen(PORT);
};

start();

When I try to upload a file server fails with error:

FileStreamDisconnectUploadError: Request disconnected during file upload stream parsing.
    at IncomingMessage.request.on (.../local-gateway/node_modules/apollo-upload-server/lib/middleware.js:151:15)
    at IncomingMessage.emit (events.js:182:13)
    at resOnFinish (_http_server.js:564:7)
    at ServerResponse.emit (events.js:182:13)
    at onFinish (_http_outgoing.js:683:10)
    at onCorkedFinish (_stream_writable.js:666:5)
    at afterWrite (_stream_writable.js:480:3)
    at process._tickCallback (internal/process/next_tick.js:63:19)
Emitted 'error' event at:
    at IncomingMessage.request.on (.../local-gateway/node_modules/apollo-upload-server/lib/middleware.js:149:32)
    at IncomingMessage.emit (events.js:182:13)
    [... lines matching original stack trace ...]
    at process._tickCallback (internal/process/next_tick.js:63:19)

Absolutely undebugable :(

First thought is that apollo-upload-server needs an Upload scalar type, and after weaveSchemas — it is prefixed (to NewsUpload for example).

To begin with: Is there a way to not prefix scalars or certain types?

Or in general did you use this with uploads?

Launchpad example broken

Says SyntaxError: Unexpected token I in JSON at position 0 at https://launchpad.graphql.com/static/js/main.8ecc894c.js:1:741833 when using in Launchpad, and cannot be connected to by GraphiQL, returns the following error:

Invalid options provided to ApolloServer: Cannot use GraphQLSchema "[object Object]" from another module or realm.

Ensure that there is only one instance of "graphql" in the node_modules
directory. If different versions of "graphql" are the dependencies of other
relied on modules, use "resolutions" to ensure only one version is installed.

https://yarnpkg.com/en/docs/selective-version-resolutions

Duplicate "graphql" modules cannot be used at the same time since different
versions may have different capabilities and behavior. The data from one
version used in the function from another could produce confusing and
spurious results.

Update README

  • weaveSchemas is exposed as createProxySchema
  • createProxySchema return promise, should use await to get schema.

Access to rootValue

Hi! I've tried weaving two schemas, one of which was queried before using graphql-js' rootValue argument. The resulting schema however does not seem to be getting the rootValue that is set.

Steps to reproduce:
Run one of the following examples:

const {
  graphql,
  GraphQLObjectType,
  GraphQLSchema,
  GraphQLString,
} = require('graphql');
const { weaveSchemas } = require('graphql-weaver');

const beverageSchema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'beverage',
    fields: {
      beverage: {
        resolve: (root, args, ctx, ast) => {
          if (
            (root && root.bar) ||
            (ast && ast.rootValue && ast.rootValue.bar)
          ) {
            return '🍺';
          }
          return '💧';
        },
        type: GraphQLString
      }
    },
  })
});

const foodSchema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'food',
    fields: {
      food: {
        resolve: () => '🍔',
        type: GraphQLString
      }
    },
  })
});

// http://graphql.org/graphql-js/graphql/#graphql
graphql(
  beverageSchema,
  '{ beverage }'
).then(result => {
  console.assert(
    result.data.beverage == '💧',
    'beverage schema without root did not return 💧'
  );
}).catch(e => {
  console.log(e);
});
graphql(
  beverageSchema,
  '{ beverage }',
  { bar: true }
).then(result => {
  console.assert(
    result.data.beverage == '🍺',
    'beverage schema with root did not return 🍺'
  );
}).catch(e => {
  console.log(e);
});
graphql(
  foodSchema,
  '{ food }'
).then(result => {
  console.assert(
    result.data.food == '🍔',
    'food schema did not return the 🍔'
  );
}).catch(e => {
  console.log(e);
});

weaveSchemas({endpoints: [
  { schema: beverageSchema },
  { schema: foodSchema },
]}).then(theGrandSchema => {
  graphql(
    theGrandSchema,
    '{ beverage }'
  ).then(result => {
    console.assert(
      result.data.beverage == '💧',
      'the grand schema without root did not return 💧'
    );
  });
  graphql(
    theGrandSchema,
    '{ beverage }',
    { bar: true }
  ).then(result => {
    console.assert(
      result.data.beverage == '🍺',
      'the grand schema with root did not return 🍺'
    );
  }).catch(e => {
    console.log(e);
  });
  graphql(
    theGrandSchema,
    '{ food }'
  ).then(result => {
    console.assert(
      result.data.food == '🍔',
      'the grand schema did not return the 🍔'
    );
  }).catch(e => {
    console.log(e);
  });
});

Expected behaviour:
Queries on the beverage root with a { bar: true } set as root should return "🍺 ".

Actual behaviour
Queries on the beverage root with a { bar: true } set return "💧 ".

Links return an error when recommendation service sets typePrefix

const schema: GraphQLSchema = await weaveSchemas({
    endpoints: [{
        namespace: 'library',
        typePrefix: "Library",
        url: 'http://example.com/library/graphql'
    }, {
        namespace: 'recommendations',
        typePrefix: "Recommendations",
        url: 'http://example.com/recommendations/graphql',
        fieldMetadata: {
            'Recommendation.song': { // Field song in type Recommendation
                link: {
                    field: 'library.Song', // field Song in namespace library
                    argument: 'id', // argument of library.Song
                    batchMode: false,
                }
            }
        }
     }]
});

When I tried to use this schema, I got an error that includes Unexpected reference to type LibrarySong.

Traceback:

Error: Unexpected reference to type LibrarySong
    at Transformer.findType (/Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-transformer/src/schema-transformer.ts:247:19)
    at TypeResolversTransformer.<anonymous> (/Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-weaver/src/pipeline/abstract-types.ts:115:34)
    at step (/Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-weaver/dist/src/pipeline/abstract-types.js:40:23)
    at Object.next (/Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-weaver/dist/src/pipeline/abstract-types.js:21:53)
    at /Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-weaver/dist/src/pipeline/abstract-types.js:15:71
    at new Promise (<anonymous>)
    at /Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-weaver/dist/src/pipeline/abstract-types.js:11:12
    at /Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-weaver/src/pipeline/abstract-types.ts:108:25
    at /Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-transformer/src/schema-transformer.ts:524:28
    at /Users/haofuxin/shundaojia/graphql-gateway2/node_modules/graphql-transformer/src/schema-transformer.ts:524:28

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.