Как сшить несколько удаленных схем с помощью Nestjs и сервера apollo

Во-первых, я новичок в том, чтобы публиковать сообщения на Stack Overflow, но я, конечно, изо всех сил постараюсь иметь здесь всю необходимую информацию и поделиться найденным решением, потому что я могу представить, что у большего количества людей могут быть проблемы с этим.

Итак, мы начали с системы, которая имеет несколько небольших микросервисов в качестве бэкэнда, и мы нашли сервер Apollo, который может извлекать схемы из конечных точек graphql и сшивать их вместе, чтобы у нас была одна хорошая точка входа. У нас это работает, но серверу apollo нечем помочь с общей архитектурой. Именно тогда мы нашли NestJS и потому, что мы используем angular во внешнем интерфейсе, а NestJS настолько похож на него, что мне показалось, что он идеально подходит.

Проблема, с которой мы сталкиваемся, заключается в том, что мы, похоже, не можем заставить работать следующие функции: - Я хотел бы иметь модуль, содержащий службу, которой может быть предоставлено несколько конечных точек (uri для микросервисов) - с enpoints учитывая, что служба должна извлекать схемы graphQL из этих конечных точек и преобразовывать их в RemoteExecutableSchemas, а затем объединять их. - После их объединения и создания 1 большой схемы с (удаленной) информацией о ссылке, чтобы graphQL знал, откуда брать данные. - После того, как это произошло, мы хотели бы добавить некоторую сшивку, чтобы присутствовали все отношения (но моя проблема не в этом)

Я просматривал официальные документы (https://docs.nestjs.com/graphql/quick-start) через их примеры (https://github.com/nestjs/nest/tree/master/sample/12-graphql-apollo) и, конечно, проверил проект github (https://github.com/nestjs/graphql) и заглядывал в это репо, чтобы увидеть, что код делает в фоновом режиме.

Мы пробовали несколько способов получить их на лету, но не смогли получить схемы в GraphQLModule до его создания. Затем мы подумали, что было бы приемлемо, чтобы служба извлекала graphqlSchema из конечной точки и записывала ее в файл с помощью printSchema (schema), которая действительно работает, но затем я теряю информацию о ссылке, эффективно делая ее локальной схемой вместо удаленной схемы. . Теперь мы придумали следующее, но снова застряли.

Давайте начнем с небольшого фрагмента из моего package.json, чтобы люди знали версии :)

"dependencies": {
    "@nestjs/common": "^5.4.0",
    "@nestjs/core": "^5.4.0",
    "@nestjs/graphql": "^5.5.1",
    "apollo-link-http": "^1.5.9",
    "apollo-server-express": "^2.3.2",
    "graphql": "^14.1.1",
    "reflect-metadata": "^0.1.12",
    "rimraf": "^2.6.2",
    "rxjs": "^6.2.2",
    "typescript": "^3.0.1"
  },
  "devDependencies": {
    "@nestjs/testing": "^5.1.0",
    "@types/express": "^4.16.0",
    "@types/jest": "^23.3.1",
    "@types/node": "^10.7.1",
    "@types/supertest": "^2.0.5",
    "jest": "^23.5.0",
    "nodemon": "^1.18.3",
    "prettier": "^1.14.2",
    "supertest": "^3.1.0",
    "ts-jest": "^23.1.3",
    "ts-loader": "^4.4.2",
    "ts-node": "^7.0.1",
    "tsconfig-paths": "^3.5.0",
    "tslint": "5.11.0"
  },

Итак, на данный момент у меня есть модуль обработчика схемы, который выглядит так:

@Module({
  imports: [GraphQLModule.forRootAsync({
    useClass: GqlConfigService
  })],
  controllers: [SchemaHandlerController],
  providers: [SchemaFetcherService, SchemaSticherService, GqlConfigService]
})
export class SchemaHandlerModule {
}

Итак, здесь мы импортируем GraphQLModule и позволяем ему использовать службу gql-config для обработки предоставления ему GraphQLModuleOptions.

Сервис gql-config выглядит так:

    @Injectable()
export class GqlConfigService implements GqlOptionsFactory {
  async createGqlOptions(): Promise<GqlModuleOptions> {
    try{
      const countrySchema = this.createCountrySchema();
      return {
        typeDefs: [countrySchema]
      };
    } catch(err) {
      console.log(err);
      return {};
    }
  }

Итак, я асинхронно создаю GqlModuleOptions и жду результата. Функции createCountrySchema выглядят так:

public async createCountrySchema() : GraphQLSchema{
    const uri = 'https://countries.trevorblades.com/Graphql';
    try {
      const link = new HttpLink({
        uri: uri,
        fetch
      });
      const remoteSchema = await introspectSchema(link);

      return makeRemoteExecutableSchema({
        schema: remoteSchema,
        link
      });
    } catch (err) {
      console.log('ERROR: exception when trying to connect to ' + uri + ' Error Message: ' + err);
    }
  };

Ради POC я получил в качестве конечной точки простой общедоступный API-интерфейс graphQL. Эта функция возвращает объект GraphQLSchema, который затем я хотел бы добавить (каким-то образом) в GqlOptions и сделать его видимым на игровой площадке. Мы также пытались заставить createCountrySchema возвращать Promise и ждать его при вызове функции в createGqlOptions, но это, похоже, не имеет значения.

Фактическая ошибка, которую мы получаем, выглядит так:

[Nest] 83   - 2/1/2019, 2:10:57 PM   [RoutesResolver] SchemaHandlerController {/schema-handler}: +1ms
apollo_1  | (node:83) UnhandledPromiseRejectionWarning: Syntax Error: Unexpected [
apollo_1  |
apollo_1  | GraphQL request (2:9)
apollo_1  | 1:
apollo_1  | 2:         [object Promise]
apollo_1  |            ^
apollo_1  | 3:
apollo_1  |
apollo_1  |     at syntaxError (/opt/node_modules/graphql/error/syntaxError.js:24:10)
apollo_1  |     at unexpected (/opt/node_modules/graphql/language/parser.js:1483:33)
apollo_1  |     at parseDefinition (/opt/node_modules/graphql/language/parser.js:155:9)
apollo_1  |     at many (/opt/node_modules/graphql/language/parser.js:1513:16)
apollo_1  |     at parseDocument (/opt/node_modules/graphql/language/parser.js:115:18)
apollo_1  |     at parse (/opt/node_modules/graphql/language/parser.js:50:10)
apollo_1  |     at parseDocument (/opt/node_modules/graphql-tag/src/index.js:129:16)
apollo_1  |     at Object.gql (/opt/node_modules/graphql-tag/src/index.js:170:10)
apollo_1  |     at GraphQLFactory.<anonymous> (/opt/node_modules/@nestjs/graphql/dist/graphql.factory.js:48:55)
apollo_1  |     at Generator.next (<anonymous>)
apollo_1  | (node:83) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 2)
apollo_1  | (node:83) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Я думаю, что я довольно близок к этому подходу, но я не совсем уверен. Ошибка, которую я получаю, гласит, что все обещания должны обрабатываться с помощью try / catch, чтобы мы не получали необработанное обещание, и я считаю, что делаю это везде, поэтому я не понимаю, откуда эта ошибка ...

Если у кого-то есть какие-либо указатели, решения или советы, я был бы очень очень рад. Я изо всех сил пытался получить нужную нам функциональность в nestjs уже более недели и видел множество примеров, фрагментов и обсуждений по этому поводу, но я не могу найти пример, который сшивает удаленные схемы и передает их обратно в nestjs.

Буду очень признателен за любые комментарии по этому поводу, с уважением, Тьерд


person Tjeerd    schedule 01.02.2019    source источник
comment
как вы вызывали функцию createGqlOptions?   -  person Nguyen Phong Thien    schedule 01.02.2019
comment
Это будет в SqlConfigService. Служба внедряется, когда GraphQLModule импортируется в модуль обработчика схемы.   -  person Tjeerd    schedule 01.02.2019


Ответы (2)


Я решил проблему сшивания схемы с помощью метода преобразования.
Посмотрите src / graphql.config / graphql.config.service.ts

здесь мой код
ссылка для теста

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;
  }

}
person Владислав Сироштан    schedule 04.07.2019

Это упрощение первого ответа - везде, где вызывается GraphQLModule.forRoot (async) (кодируется внутри файла appModule или экспортируется отдельно), приведенный ниже фрагмент кода должен помочь

import { GraphQLModule } from "@nestjs/graphql";
import { CommonModule } from "@Common";
import { GraphQLSchema } from 'graphql';
import { ConfigInterface } from "@Common/config/ConfigService";
import {
 stitchSchemas
} from '@graphql-tools/stitch';
import { introspectSchema } from '@graphql-tools/wrap';
import { print } from 'graphql';
import { fetch } from 'cross-fetch';

export default GraphQLModule.forRootAsync({
  imports: [CommonModule],
  useFactory: async (configService: ConfigInterface) => {
    const remoteSchema = await createRemoteSchema('https://countries.trevorblades.com/graphql');
    return {
      playground: process.env.NODE_ENV !== "production",
      context: ({ req, res }) => ({ req, res }),
      installSubscriptionHandlers: true,
      autoSchemaFile: "schema.gql",
      transformSchema : async (schema: GraphQLSchema) => {
        return stitchSchemas({
          subschemas: [
            schema,
            remoteSchema
          ]
        });
      },
    };
  },
});

const createRemoteSchema = async (url : string) =>{
  const executor = async ({ document, variables }) => {
    const query = print(document);
    const fetchResult = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ query, variables })
    });
    return fetchResult.json();
  };
  return {
    schema: await introspectSchema(executor),
    executor: executor
  };
}

Ссылка: https://www.graphql-tools.com/docs/stitch-combining-schemas/#stitching-remote-schemas.

person Rohith Rajesh    schedule 22.04.2021