Мы реализовали сшивание схем, при котором сервер GraphQL извлекает схему с двух удаленных серверов и сшивает их вместе. Все работало нормально, когда мы работали только с запросами и мутациями, но теперь у нас есть вариант использования, когда нам даже нужно сшить подписки, а удаленная схема имеет аутентификацию, реализованную поверх нее.
Нам сложно понять, как передать токен авторизации, полученный в connectionParams, от клиента к удаленному серверу через шлюз.
Вот как мы исследуем схему:
Код шлюза API:
const getLink = async(): Promise<ApolloLink> => {
const http = new HttpLink({uri: process.env.GRAPHQL_ENDPOINT, fetch:fetch})
const link = setContext((request, previousContext) => {
if (previousContext
&& previousContext.graphqlContext
&& previousContext.graphqlContext.request
&& previousContext.graphqlContext.request.headers
&& previousContext.graphqlContext.request.headers.authorization) {
const authorization = previousContext.graphqlContext.request.headers.authorization;
return {
headers: {
authorization
}
}
}
else {
return {};
}
}).concat(http);
const wsLink: any = new WebSocketLink(new SubscriptionClient(process.env.REMOTE_GRAPHQL_WS_ENDPOINT, {
reconnect: true,
// There is no way to update connectionParams dynamically without resetting connection
// connectionParams: () => {
// return { Authorization: wsAuthorization }
// }
}, ws));
// Following does not work
const wsLinkContext = setContext((request, previousContext) => {
let authToken = previousContext.graphqlContext.connection && previousContext.graphqlContext.connection.context ? previousContext.graphqlContext.connection.context.Authorization : null
return {
context: {
Authorization: authToken
}
}
}).concat(<any>wsLink);
const url = split(({query}) => {
const {kind, operation} = <any>getMainDefinition(<any>query);
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLinkContext,
link)
return url;
}
const getSchema = async (): Promise < GraphQLSchema > => {
const link = await getLink();
return makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link,
});
}
const linkSchema = `
extend type UserPayload {
user: User
}
`;
const schema: any = mergeSchemas({
schemas: [linkSchema, getSchema],
});
const server = new GraphQLServer({
schema: schema,
context: req => ({
...req,
})
});
Есть ли способ добиться этого с помощью graphql-tools? Любая помощь приветствуется.
Есть ли в этом прогресс?
@gandalfml к сожалению нет прогресса :(
Немного продвинулся :) Дело в том, что каждый экземпляр WebSocketLink - это одно соединение ws. Итак, у вас не может быть один экземпляр для сервера, а скорее один экземпляр для подключения к клиенту :) Я постараюсь предоставить пример по сути на следующей неделе.


У меня есть одно рабочее решение: идея состоит в том, чтобы не создавать один экземпляр SubscriptionClient для всего приложения. Вместо этого я создаю клиентов для каждого подключения к прокси-серверу:
server.start({
port: 4000,
subscriptions: {
onConnect: (connectionParams, websocket, context) => {
return {
subscriptionClients: {
messageService: new SubscriptionClient(process.env.MESSAGE_SERVICE_SUBSCRIPTION_URL, {
connectionParams,
reconnect: true,
}, ws)
}
};
},
onDisconnect: async (websocket, context) => {
const params = await context.initPromise;
const { subscriptionClients } = params;
for (const key in subscriptionClients) {
subscriptionClients[key].close();
}
}
}
}, (options) => console.info('Server is running on http://localhost:4000'))
если бы у вас было больше удаленных схем, вы бы просто создали больше экземпляров SubscriptionClient в карте subscriptionClients.
Чтобы использовать этих клиентов в удаленной схеме, вам нужно сделать две вещи:
выставить их в контексте:
const server = new GraphQLServer({
schema,
context: ({ connection }) => {
if (connection && connection.context) {
return connection.context;
}
}
});
использовать реализацию настраиваемой ссылки вместо WsLink
(operation, forward) => {
const context = operation.getContext();
const { graphqlContext: { subscriptionClients } } = context;
return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
};
Таким образом, все параметры подключения будут переданы на удаленный сервер.
Полный пример можно найти здесь: https://gist.github.com/josephktcheung/cd1b65b321736a520ae9d822ae5a951b
Заявление об ограничении ответственности:
Код не мой, поскольку @josephktcheung опередил меня, предоставив пример. Я просто немного помог с этим. Вот исходное обсуждение: https://github.com/apollographql/graphql-tools/issues/864
Это рабочий пример удаленной схемы с подпиской через webscoket и запросом и изменением через http. Его можно защитить с помощью настраиваемых заголовков (параметров) и показать в этом примере.
Поток
Запрос клиента
-> context создается путем чтения req или connection (jwt декодируется и создает пользовательский объект в контексте)
-> удаленная схема выполняется
-> link называется
-> link разбивается по операции (wsLink для подписки, httpLink для запросов и мутаций)
-> wsLink или httpLink доступ к context, созданному выше (= graphqlContext)
-> wsLink или httpLink используют context для создания заголовков (заголовок авторизации с подписанным jwt в этом примере) для удаленной схемы.
-> «подписка» или «запрос или изменение» пересылаются на удаленный сервер.
Примечание
concat мы должны создать необработанный ApolloLink.connection, а не только req. Первый будет доступен, если запрос является веб-сокетом и содержит метаинформацию, отправляемую пользователем, например токен аутентификации.node-fetch, спецификация которого несовместима (особенно с машинописным текстом). Вместо этого используйте cross-fetch.const wsLink = new ApolloLink(operation => {
// This is your context!
const context = operation.getContext().graphqlContext
// Create a new websocket link per request
return new WebSocketLink({
uri: "<YOUR_URI>",
options: {
reconnect: true,
connectionParams: { // give custom params to your websocket backend (e.g. to handle auth)
headers: {
authorization: jwt.sign(context.user, process.env.SUPER_SECRET),
foo: 'bar'
}
},
},
webSocketImpl: ws,
}).request(operation)
// Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
})
const httpLink = setContext((_graphqlRequest, { graphqlContext }) => {
return {
headers: {
authorization: jwt.sign(graphqlContext.user, process.env.SUPER_SECRET),
},
}
}).concat(new HttpLink({
uri,
fetch,
}))
const link = split(
operation => {
const definition = getMainDefinition(operation.query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink, // <-- Executed if above function returns true
httpLink, // <-- Executed if above function returns false
)
const schema = await introspectSchema(link)
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
})
const server = new ApolloServer({
schema: mergeSchemas([ executableSchema, /* ...anotherschemas */]),
context: ({ req, connection }) => {
let authorization;
if (req) { // when query or mutation is requested by http
authorization = req.headers.authorization
} else if (connection) { // when subscription is requested by websocket
authorization = connection.context.authorization
}
const token = authorization.replace('Bearer ', '')
return {
user: getUserFromToken(token),
}
},
})
Я думаю, у вас есть две проблемы: первая - получить схему интроспекции без какого-либо ключа авторизации (из того, что я понял, ключ авторизации получен от клиента в контексте соединения). а второй - каким-то образом при каждой операции подписки отправлять ключ аутентификации. первая проблема, вероятно, разрешима при правильной архитектуре. но вторая проблема в настоящее время не поддерживается в
subscription-transport-wsилиgraphl-toolsсо сшиванием схемы. решение для этого должно будет расширить текущий протокол, который они создали.