Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to stitch schemas with Nest@5? #76

Closed
merongmerongmerong opened this issue Sep 25, 2018 · 13 comments
Closed

How to stitch schemas with Nest@5? #76

merongmerongmerong opened this issue Sep 25, 2018 · 13 comments
Labels

Comments

@merongmerongmerong
Copy link

I'm submitting a...


[ ] Regression 
[ ] Bug report
[ ] Feature request
[x] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

configure(consumer) {
  const typeDefs = this.graphQLFactory.mergeTypesByPaths('./**/*.graphql');
  const localSchema = this.graphQLFactory.createSchema({ typeDefs });
  const delegates = this.graphQLFactory.createDelegates();
  const schema = mergeSchemas({
    schemas: [localSchema, chirpSchema, linkTypeDefs],
    resolvers: delegates,
  });
  consumer
    .apply(graphqlExpress(req => ({ schema, rootValue: req })))
    .forRoutes('/graphql');
}

:shipit: old doc

Expected behavior

async useFactory(graphqlFactory: GraphQLFactory) {
    const link = new HttpLink({ uri: '^_^', fetch });
    const anotherSchema = await introspectSchema(link);
    const remoteSchema = makeRemoteExecutableSchema({ schema: anotherSchema, link });

    return {
      typePaths: ['src/**/*.graphql'],
      schema,
      mergeSchemas(localSchema => ({ 
        schemas: [localSchema, remoteSchema, ...],
        resolvers: graphQLFactory.createDelegates()
      }),
      ...
    };
}

injecting graphql factory
❌ accessing local schema

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?

schema stitching with nest@5

Environment


Nest version: 5.3.6

 
For Tooling issues:
- Node version: 10  
- Platform: 
  - OS: Manjaro Linux x86_64 
  - Kernel: 4.14.67-1-MANJARO 

Others:

@patpaquette
Copy link

Hi, is there an undocumented way of doing this currently? Wondering if I should dig deeper or wait. Thanks!

@idhard
Copy link

idhard commented Jun 5, 2019

what about apollo federation schema ? (https://blog.apollographql.com/apollo-federation-f260cf525d21) , is there some documentation on how to implement it with Nestjs ? thanks!

@yassernasc
Copy link

an example of apollo federation and nest microservices would be fucking awesome.

@juicycleff
Copy link

Hi, I do have apollo federation setup, but the revolvers don't work anymore. That seems to be the issue. How to make the resolvers work.

@OLDIN
Copy link

OLDIN commented Jun 22, 2019

I have same problem. It would be nice to get sample code for stitching the schema.

@marcus-sa
Copy link
Contributor

marcus-sa commented Jun 23, 2019

I did a PR guys
#301

I'm currently putting together an example repository with kubernetes and prisma 😁

@OLDIN
Copy link

OLDIN commented Jul 4, 2019

I had solved schema stitching problem by using transform method.
Look src/graphql.config/graphql.config.service.ts

here my code
link for the test

import { Injectable } from '@nestjs/common';
import { GqlOptionsFactory, GqlModuleOptions } from '@nestjs/graphql';
import * as ws from 'ws';
import {
  makeRemoteExecutableSchema,
  mergeSchemas,
  introspectSchema
} from 'graphql-tools';
import { HttpLink } from 'apollo-link-http';
import nodeFetch from 'node-fetch';
import { split, from, NextLink, Observable, FetchResult, Operation } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
import { OperationTypeNode, buildSchema as buildSchemaGraphql, GraphQLSchema, printSchema } from 'graphql';
import { setContext } from 'apollo-link-context';
import { SubscriptionClient, ConnectionContext } from 'subscriptions-transport-ws';
import * as moment from 'moment';
import { extend } from 'lodash';

import { ConfigService } from '../config';

declare const module: any;
interface IDefinitionsParams {
  operation?: OperationTypeNode;
  kind: 'OperationDefinition' | 'FragmentDefinition';
}
interface IContext {
  graphqlContext: {
    subscriptionClient: SubscriptionClient,
  };
}

@Injectable()
export class GqlConfigService implements GqlOptionsFactory {

  private remoteLink: string = 'https://countries.trevorblades.com';

  constructor(
    private readonly config: ConfigService
  ) {}

  async createGqlOptions(): Promise<GqlModuleOptions> {
    const remoteExecutableSchema = await this.createRemoteSchema();

    return {
      autoSchemaFile: 'schema.gql',
      transformSchema: async (schema: GraphQLSchema) => {
        return mergeSchemas({
          schemas: [
            schema,
            remoteExecutableSchema
          ]
        });
      },
      debug: true,
      playground: {
        env: this.config.environment,
        endpoint: '/graphql',
        subscriptionEndpoint: '/subscriptions',
        settings: {
          'general.betaUpdates': false,
          'editor.theme': 'dark' as any,
          'editor.reuseHeaders': true,
          'tracing.hideTracingResponse': true,
          'editor.fontSize': 14,
          // tslint:disable-next-line:quotemark
          'editor.fontFamily': "'Source Code Pro', 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace",
          'request.credentials': 'include',
        },
      },
      tracing: true,
      installSubscriptionHandlers: true,
      introspection: true,
      subscriptions: {
        path: '/subscriptions',
        keepAlive: 10000,
        onConnect: async (connectionParams, webSocket: any, context) => {
          const subscriptionClient = new SubscriptionClient(this.config.get('HASURA_WS_URI'), {
            connectionParams: {
              ...connectionParams,
              ...context.request.headers
            },
            reconnect: true,
            lazy: true,
          }, ws);

          return {
            subscriptionClient,
          };
        },
        async onDisconnect(webSocket, context: ConnectionContext) {
          const { subscriptionClient } = await context.initPromise;

          if (subscriptionClient) {
            subscriptionClient.close();
          }
        },
      },
      context(context) {
        const contextModified: any = {
          userRole: 'anonymous',
          currentUTCTime: moment().utc().format()
        };

        if (context && context.connection && context.connection.context) {
          contextModified.subscriptionClient = context.connection.context.subscriptionClient;
        }

        return contextModified;
      },
    };
  }

  private wsLink(operation: Operation, forward?: NextLink): Observable<FetchResult> | null {
    const context = operation.getContext();
    const { graphqlContext: { subscriptionClient } }: any = context;
    return subscriptionClient.request(operation);
  }

  private async createRemoteSchema(): Promise<GraphQLSchema> {

    const httpLink = new HttpLink({
      uri: this.remoteLink,
      fetch: nodeFetch as any,
    });

    const remoteIntrospectedSchema = await introspectSchema(httpLink);
    const remoteSchema = printSchema(remoteIntrospectedSchema);
    const link = split(
      ({ query }) => {
        const { kind, operation }: IDefinitionsParams = getMainDefinition(query);
        return kind === 'OperationDefinition' && operation === 'subscription';
      },
      this.wsLink,
      httpLink,
    );

    const contextLink = setContext((request, prevContext) => {
      extend(prevContext.headers, {
        'X-hasura-Role': prevContext.graphqlContext.userRole,
        'X-Hasura-Utc-Time': prevContext.graphqlContext.currentUTCTime,
      });
      return prevContext;
    });

    const buildedHasuraSchema = buildSchemaGraphql(remoteSchema);
    const remoteExecutableSchema = makeRemoteExecutableSchema({
      link: from([contextLink, link]),
      schema: buildedHasuraSchema,
    });

    return remoteExecutableSchema;
  }

}

@kamilmysliwiec
Copy link
Member

Schema stitching is deprecated

@ghost
Copy link

ghost commented May 5, 2020

@kamilmysliwiec Can I ask why schema stitching is deprecated? Trying to implement something and so I want to make sure I understand why it was deprecated and what should be its replacement.

@sgarner
Copy link

sgarner commented Jun 12, 2020

Schema stitching is deprecated

I don't think it's accurate to say schema stitching is deprecated.

The graphql-tools package removed their own notice about stitching being deprecated in their 5.x release. Related issue: ardatan/graphql-tools#1286

Although there is a lot of crossover, Schema Stitching and Apollo Federation serve different use cases. The key difference is that Apollo Federation requires modifications to the underlying schema(s), whereas Stitching does not. This allows Stitching to be used to wrap together remote schemas that you don't have the ability to modify, such as APIs operated by third parties.

I am pleased to see that transformSchema() can be successfully used with NestJS to enable stitching - please ensure this remains an option! 👍

It would be even better if link-level resolvers (i.e. resolvers for extended type fields that operate on the stitched schema gateway and pass down delegated queries to subschemas) could be written in the canonical NestJS style using @Resolver() decorators etc. I haven't found any way to do this, but writing them as an IResolvers map in graphql-tools style works okay.

@ghost
Copy link

ghost commented Jun 12, 2020

@sgarner Bless you... This inform is super useful to keep building our apis. Thank you so much for the response!

@brianschardt
Copy link

Does this work in Nestjs 7??
transformSchema is not running?

@sg-gdarnell
Copy link

sg-gdarnell commented Aug 11, 2021

For anyone finding this issue in 2021, stitching does work in NestJS 7. It took me a couple days to wrap my head around it, but in general you can follow the graphql-tools instructions for stitching remote schemas.

Here's a simplified example of how to stitch remote schemas into a gateway:

import { AsyncExecutor, ExecutionRequest, observableToAsyncIterable, Executor } from '@graphql-tools/utils';
import { GqlModuleOptions, GqlOptionsFactory } from '@nestjs/graphql';
import { Injectable } from '@nestjs/common';
import { introspectSchema } from '@graphql-tools/wrap';
import { stitchSchemas } from '@graphql-tools/stitch';
import { SubschemaConfig } from '@graphql-tools/delegate';
import { GraphQLError, print } from 'graphql';
import nodeFetch from 'node-fetch';
import * as ws from 'ws';

// This example uses subscriptions-transport-ws. If your subschema APIs support graphql-ws, use that instead.
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { WebSocketLink } from 'apollo-link-ws';

function buildExecutor(url: string) {
  return async (request: ExecutionRequest) => {
    // Feel free to use your favorite alternative to node-fetch here instead
    const fetchResult = await nodeFetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        query: print(request.document),
        variables: request.variables,
      })
    });
    return await fetchResult.json();
  };
}

function buildSubscriber(url: string): Subscriber {
  const subscriptionClient = new SubscriptionClient(
    url,
    { reconnect: true, lazy: true },
    ws
  );

  return (request: ExecutionRequest) => {
    const wsLink = new WebSocketLink(subscriptionClient);

    const result = wsLink.request({
      query: request.document,
      variables: request.variables || {},
      operationName: request.operationName,
      extensions: {},
      getContext: () => ({}),
      setContext: (c) => c,
      toKey: () => 'someKey',
    });

    return observableToAsyncIterable(result);
  };
}

function buildRemoteExecutor(
  httpUrl: string,
  wsUrl: string
): Executor {
  const subscribe = buildSubscriber(wsUrl);
  const execute = buildExecutor(httpUrl);

  return async (request: ExecutionRequest) => {
    if (request.operationType === 'subscription') {
      return subscribe(request);
    }
    return execute(httpUrl, request, headers);
  };
}

@Injectable()
export class GqlConfigService implements GqlOptionsFactory {
  async createGqlOptions(): Promise<GqlModuleOptions> {
    const executors = [
      this.buildRemoteExecutor('https://remote-server-a.com/graphql', 'wss://remote-server-a.com/graphql'),
      this.buildRemoteExecutor('https://remote-server-b.com/graphql', 'wss://remote-server-b.com/graphql')
    ];
    const subschemas = await Promise.all(
      executors.map(async (executor): Promise<SubschemaConfig> => {
        return {
          schema: await introspectSchema(remoteExecutor),
          executor: remoteExecutor,
        };
      })
    );

    const schema = stitchSchemas({ subschemas });

    return {
      schema,
      debug: true,
      playground: {
        env: this.env,
        endpoint: '/graphql',
        subscriptionEndpoint: '/subscriptions',
      },
      tracing: true,
      installSubscriptionHandlers: true,
      introspection: true,
      subscriptions: {
        path: '/subscriptions',
        keepAlive: 10000,
      }
    };
  }
}

Some of the servers I'm working with are still using the old subscriptions-transport-ws protocol for subscriptions, which is shown in the example above. If you're consuming APIs that support the newer graphql-ws protocol then you can swap the implementation of the buildSubscriber function in the example above for something like what's shown in the graphql-tools docs here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests