首先,我实际上是刚在Stack Overflow上发帖的,但我当然会尽我所能,在此处提供所有相关信息并分享找到的解决方案,因为我可以想象会有更多的人对此感到麻烦。
因此,我们从一个以多个小型微服务为后端的系统开始,我们发现Apollo服务器能够从graphql端点检索模式并将它们缝合在一起,因此我们可以有一个不错的切入点。 我们已经可以工作了,但是apollo服务器并没有真正帮助总体架构的工具。那就是我们找到NestJS的原因,因为我们在前端使用了angular,而NestJS如此相似,因此看起来很合适。
我们遇到的问题是,以下功能似乎无法正常工作: -我想要一个包含服务的模块,该服务可以被赋予许多端点(从uri到微服务) -使用给定的端点,服务应从这些端点检索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的考虑,我只是获得了一个简单的公共graphQL API作为端点。该函数返回一个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.
我认为我与这种方法相当接近,但我不确定。我收到的错误指出所有Promise必须通过try / catch进行处理,这样我们才不会收到未处理的Promise,我相信我到处都这样做,所以我不知道此错误的来源...
如果有人有任何指示,解决方案或建议,我将非常高兴。一个多星期以来,我一直在努力地使我们想要的功能适合于nestjs,并且已经看到了许多示例,摘要和讨论,但是我找不到找到缝合远程模式并将其交还给nestjs的示例。 / p>
非常感谢您对此提出的任何评论, 特耶德
答案 0 :(得分:0)
我已经通过使用transform方法解决了模式拼接问题。
查看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;
}
}
答案 1 :(得分:0)
这是第一个答案的简化 - 无论在何处调用 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