Skip to content
Permalink
Browse files

ApolloGateway: Construct and use RemoteGraphQLDataSource to issue int…

…rospection query to Federated Services (#3120)

* Construct and use a GraphQLDataSource when performing the enhanced introspection request.

* Preserve backwards compatibility of protected methods. The consequence here is that we end up creating a RemoteGraphQLDataSource especially for the introspection request.

* Add test to demonstrate that different headers can be passed based on the service.
  • Loading branch information...
mcohen75 authored and trevor-scheer committed Aug 22, 2019
1 parent 2235467 commit a1ddc9518f9978b01a377728474476f5d7df936b
@@ -15,8 +15,8 @@ import {
} from '../packages/apollo-server-env';

interface FetchMock extends jest.Mock<typeof fetch> {
mockResponseOnce(data?: any, headers?: HeadersInit, status?: number): void;
mockJSONResponseOnce(data?: object, headers?: HeadersInit): void;
mockResponseOnce(data?: any, headers?: HeadersInit, status?: number): this;
mockJSONResponseOnce(data?: object, headers?: HeadersInit): this;
}

const mockFetch = jest.fn<typeof fetch>(fetch) as FetchMock;
@@ -3,6 +3,7 @@
### vNEXT

* Optimize buildQueryPlan when two FetchGroups are on the same service [#3135](https://github.com/apollographql/apollo-server/pull/3135)
* Construct and use RemoteGraphQLDataSource to issue introspection query to Federated Services [#3120](https://github.com/apollographql/apollo-server/pull/3120)

# v0.9.0

@@ -78,3 +78,116 @@ it('correctly passes the context from ApolloServer to datasources', async () =>
},
});
});

function createSdlData(sdl: string): object {
return {
data: {
_service: {
sdl: sdl,
},
},
};
}

it('makes enhanced introspection request using datasource', async () => {
fetch.mockJSONResponseOnce(
createSdlData('extend type Query { one: String }'),
);

const gateway = new ApolloGateway({
serviceList: [
{
name: 'one',
url: 'https://api.example.com/one',
},
],
buildService: service => {
return new RemoteGraphQLDataSource({
url: 'https://api.example.com/override',
willSendRequest: ({ request }) => {
request.http.headers.set('custom-header', 'some-custom-value');
},
});
},
});

await gateway.load();

expect(fetch).toBeCalledTimes(1);

expect(fetch).toHaveFetched({
url: 'https://api.example.com/override',
body: {
query: `query GetServiceDefinition { _service { sdl } }`,
},
headers: {
'custom-header': 'some-custom-value',
},
});
});

it('customizes request on a per-service basis', async () => {
fetch
.mockJSONResponseOnce(createSdlData('extend type Query { one: String }'))
.mockJSONResponseOnce(createSdlData('extend type Query { two: String }'))
.mockJSONResponseOnce(createSdlData('extend type Query { three: String }'));

const gateway = new ApolloGateway({
serviceList: [
{
name: 'one',
url: 'https://api.example.com/one',
},
{
name: 'two',
url: 'https://api.example.com/two',
},
{
name: 'three',
url: 'https://api.example.com/three',
},
],
buildService: service => {
return new RemoteGraphQLDataSource({
url: service.url,
willSendRequest: ({ request }) => {
request.http.headers.set('service-name', service.name);
},
});
},
});

await gateway.load();

expect(fetch).toBeCalledTimes(3);

expect(fetch).toHaveFetched({
url: 'https://api.example.com/one',
body: {
query: `query GetServiceDefinition { _service { sdl } }`,
},
headers: {
'service-name': 'one',
},
});

expect(fetch).toHaveFetched({
url: 'https://api.example.com/two',
body: {
query: `query GetServiceDefinition { _service { sdl } }`,
},
headers: {
'service-name': 'two',
},
});

expect(fetch).toHaveFetched({
url: 'https://api.example.com/three',
body: {
query: `query GetServiceDefinition { _service { sdl } }`,
},
headers: {
'service-name': 'three',
},
});
});
@@ -377,18 +377,24 @@ export class ApolloGateway implements GraphQLService {
this.pollingTimer.unref();
}

private createDataSource(
serviceDef: ServiceEndpointDefinition,
): GraphQLDataSource {
if (!serviceDef.url && !isLocalConfig(this.config)) {
throw new Error(
`Service definition for service ${serviceDef.name} is missing a url`,
);
}
return this.config.buildService
? this.config.buildService(serviceDef)
: new RemoteGraphQLDataSource({
url: serviceDef.url,
});
}

protected createServices(services: ServiceEndpointDefinition[]) {
for (const serviceDef of services) {
if (!serviceDef.url && !isLocalConfig(this.config)) {
throw new Error(
`Service definition for service ${serviceDef.name} is missing a url`,
);
}
this.serviceMap[serviceDef.name] = this.config.buildService
? this.config.buildService(serviceDef)
: new RemoteGraphQLDataSource({
url: serviceDef.url,
});
this.serviceMap[serviceDef.name] = this.createDataSource(serviceDef);
}
}

@@ -400,8 +406,13 @@ export class ApolloGateway implements GraphQLService {
}

if (isRemoteConfig(config)) {
const serviceList = config.serviceList.map(serviceDefinition => ({
...serviceDefinition,
dataSource: this.createDataSource(serviceDefinition),
}));

return getServiceDefinitionsFromRemoteEndpoint({
serviceList: config.serviceList,
serviceList,
...(config.introspectionHeaders
? { headers: config.introspectionHeaders }
: {}),
@@ -1,16 +1,21 @@
import { ServiceDefinition } from '@apollo/federation';
import { GraphQLExecutionResult } from 'apollo-server-types';
import { GraphQLRequest } from 'apollo-server-types';
import { parse } from 'graphql';
import fetch, { HeadersInit } from 'node-fetch';
import { ServiceEndpointDefinition, UpdateServiceDefinitions } from './';
import { Headers, HeadersInit } from 'node-fetch';
import { GraphQLDataSource } from './datasources/types';
import { UpdateServiceDefinitions } from './';
import { ServiceDefinition } from '@apollo/federation';

let serviceDefinitionMap: Map<string, string> = new Map();

export async function getServiceDefinitionsFromRemoteEndpoint({
serviceList,
headers = {},
}: {
serviceList: ServiceEndpointDefinition[];
serviceList: {
name: string;
url?: string;
dataSource: GraphQLDataSource;
}[];
headers?: HeadersInit;
}): ReturnType<UpdateServiceDefinitions> {
if (!serviceList || !serviceList.length) {
@@ -22,31 +27,37 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
let isNewSchema = false;
// for each service, fetch its introspection schema
const serviceDefinitions: ServiceDefinition[] = (await Promise.all(
serviceList.map(service => {
if (!service.url) {
throw new Error(
`Tried to load schema from ${service.name} but no url found`,
);
serviceList.map(({ name, url, dataSource }) => {
if (!url) {
throw new Error(`Tried to load schema from ${name} but no url found`);
}
return fetch(service.url, {
method: 'POST',
body: JSON.stringify({
query: 'query GetServiceDefinition { _service { sdl } }',
}),
headers: { 'Content-Type': 'application/json', ...headers },
})
.then(res => res.json())
.then(({ data, errors }: GraphQLExecutionResult) => {

const request: GraphQLRequest = {
query: 'query GetServiceDefinition { _service { sdl } }',
http: {
url,
method: 'POST',
headers: new Headers(headers),
},
};

return dataSource
.process({ request, context: {} })
.then(({ data, errors }) => {
if (data && !errors) {
const typeDefs = data._service.sdl as string;
const previousDefinition = serviceDefinitionMap.get(service.name);
const previousDefinition = serviceDefinitionMap.get(name);
// this lets us know if any downstream service has changed
// and we need to recalculate the schema
if (previousDefinition !== typeDefs) {
isNewSchema = true;
}
serviceDefinitionMap.set(service.name, typeDefs);
return { ...service, typeDefs: parse(typeDefs) };
serviceDefinitionMap.set(name, typeDefs);
return {
name,
url,
typeDefs: parse(typeDefs),
};
}

// XXX handle local errors better for local development
@@ -58,7 +69,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
})
.catch(error => {
console.warn(
`Encountered error when loading ${service.name} at ${service.url}: ${error.message}`,
`Encountered error when loading ${name} at ${url}: ${error.message}`,
);
return false;
});

0 comments on commit a1ddc95

Please sign in to comment.
You can’t perform that action at this time.