Skip to content

Commit

Permalink
Implement serviceList shim
Browse files Browse the repository at this point in the history
  • Loading branch information
trevor-scheer committed Dec 2, 2021
1 parent 7714c80 commit 903bb5f
Show file tree
Hide file tree
Showing 11 changed files with 133 additions and 59 deletions.
18 changes: 17 additions & 1 deletion federation-integration-testsuite-js/src/fixtures/index.ts
Expand Up @@ -7,8 +7,24 @@ import * as reviews from './reviews';
import * as reviewsWithUpdate from './special-cases/reviewsWithUpdate';
import * as accountsWithoutTag from './special-cases/accountsWithoutTag';
import * as reviewsWithoutTag from './special-cases/reviewsWithoutTag';
import { DocumentNode } from 'graphql';
import { GraphQLResolverMap } from 'apollo-graphql';

const fixtures = [accounts, books, documents, inventory, product, reviews];
export interface Fixture {
name: string;
url: string;
typeDefs: DocumentNode;
resolvers?: GraphQLResolverMap<any>
}

const fixtures: Fixture[] = [
accounts,
books,
documents,
inventory,
product,
reviews,
];

const fixturesWithUpdate = [
accounts,
Expand Down
2 changes: 1 addition & 1 deletion federation-js/src/__tests__/joinSpec.test.ts
@@ -1,7 +1,7 @@
import { fixtures } from 'apollo-federation-integration-testsuite';
import { getJoinDefinitions } from "../joinSpec";

const questionableNamesRemap = {
const questionableNamesRemap: Record<string, string> = {
accounts: 'ServiceA',
books: 'serviceA',
documents: 'servicea_2',
Expand Down
1 change: 1 addition & 0 deletions gateway-js/package.json
Expand Up @@ -37,6 +37,7 @@
"apollo-server-env": "^3.0.0 || ^4.0.0",
"apollo-server-errors": "^2.5.0 || ^3.0.0",
"apollo-server-types": "^0.9.0 || ^3.0.0",
"callable-instance": "^2.0.0",
"loglevel": "^1.6.1",
"make-fetch-happen": "^8.0.0",
"pretty-format": "^27.3.1"
Expand Down
45 changes: 31 additions & 14 deletions gateway-js/src/__tests__/gateway/supergraphSdl.test.ts
Expand Up @@ -57,18 +57,40 @@ describe('Using supergraphSdl static configuration', () => {
});

describe('Using supergraphSdl dynamic configuration', () => {
it('starts and remains in `initialized` state until user Promise resolves', async () => {
const [forGatewayToHaveCalledSupergraphSdl, resolve] = waitUntil();
it(`calls the user provided function after gateway.load() is called`, async () => {
const spy = jest.fn(async () => ({
supergraphSdl: getTestingSupergraphSdl(),
}));

const gateway = new ApolloGateway({
supergraphSdl: spy,
});

expect(spy).not.toHaveBeenCalled();
await gateway.load();
expect(spy).toHaveBeenCalled();
});

it('starts and remains in `initialized` state until user Promise resolves', async () => {
const [promise, resolve] = waitUntil();
const gateway = new ApolloGateway({
async supergraphSdl() {
resolve();
return new Promise(() => {});
await promise;
return {
supergraphSdl: getTestingSupergraphSdl(),
};
},
});

await forGatewayToHaveCalledSupergraphSdl;
expect(gateway.__testing().state.phase).toEqual('initialized');

// If we await here, we'll get stuck.
const gatewayLoaded = gateway.load();
expect(gateway.__testing().state.phase).toEqual('initialized');

resolve();
await gatewayLoaded;
expect(gateway.__testing().state.phase).toEqual('loaded');
});

it('starts and waits in `initialized` state after calling load but before user Promise resolves', async () => {
Expand Down Expand Up @@ -158,23 +180,18 @@ describe('Using supergraphSdl dynamic configuration', () => {

describe('errors', () => {
it('fails to load if user-provided `supergraphSdl` function throws', async () => {
const failureMessage = 'Error from supergraphSdl function';
const gateway = new ApolloGateway({
async supergraphSdl() {
throw new Error('supergraphSdl failed');
throw new Error(failureMessage);
},
logger,
});

await expect(() =>
gateway.load(),
).rejects.toThrowErrorMatchingInlineSnapshot(
`"User provided \`supergraphSdl\` function did not return an object containing a \`supergraphSdl\` property"`,
);
await expect(() => gateway.load()).rejects.toThrowError(failureMessage);

expect(gateway.__testing().state.phase).toEqual('failed to load');
expect(logger.error).toHaveBeenCalledWith(
'User-defined `supergraphSdl` function threw error: supergraphSdl failed',
);
expect(logger.error).toHaveBeenCalledWith(failureMessage);
});

it('gracefully handles Promise rejections from user `cleanup` function', async () => {
Expand Down
20 changes: 17 additions & 3 deletions gateway-js/src/__tests__/integration/configuration.test.ts
Expand Up @@ -2,20 +2,20 @@ import gql from 'graphql-tag';
import http from 'http';
import mockedEnv from 'mocked-env';
import { Logger } from 'apollo-server-types';
import { ApolloGateway } from '../..';
import { ApolloGateway, RemoteGraphQLDataSource } from '../..';
import {
mockSdlQuerySuccess,
mockSupergraphSdlRequestSuccess,
mockApolloConfig,
mockCloudConfigUrl,
} from './nockMocks';
import { getTestingSupergraphSdl } from '../execution-utils';
import { MockService } from './networkRequests.test';
import { Fixture } from './networkRequests.test';
import { fixtures } from 'apollo-federation-integration-testsuite';

let logger: Logger;

const service: MockService = {
const service: Fixture = {
name: 'accounts',
url: 'http://localhost:4001',
typeDefs: gql`
Expand Down Expand Up @@ -371,4 +371,18 @@ describe('deprecation warnings', () => {
'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.',
);
});

it('warns with `buildService` option set', async () => {
new ApolloGateway({
serviceList: [{ name: 'accounts', url: 'http://localhost:4001' }],
buildService(definition) {
return new RemoteGraphQLDataSource({url: definition.url});
},
logger,
});

expect(logger.warn).toHaveBeenCalledWith(
'The `buildService` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.',
);
});
});
4 changes: 2 additions & 2 deletions gateway-js/src/__tests__/integration/networkRequests.test.ts
Expand Up @@ -27,13 +27,13 @@ import {
} from 'apollo-federation-integration-testsuite';
import { getTestingSupergraphSdl } from '../execution-utils';

export interface MockService {
export interface Fixture {
name: string;
url: string;
typeDefs: DocumentNode;
}

const simpleService: MockService = {
const simpleService: Fixture = {
name: 'accounts',
url: 'http://localhost:4001',
typeDefs: gql`
Expand Down
25 changes: 18 additions & 7 deletions gateway-js/src/__tests__/integration/nockMocks.ts
@@ -1,10 +1,9 @@
import nock from 'nock';
import { MockService } from './networkRequests.test';
import { HEALTH_CHECK_QUERY, SERVICE_DEFINITION_QUERY } from '../..';
import { SUPERGRAPH_SDL_QUERY } from '../../loadSupergraphSdlFromStorage';
import { getTestingSupergraphSdl } from '../../__tests__/execution-utils';
import { print } from 'graphql';
import { fixtures } from 'apollo-federation-integration-testsuite';
import { Fixture, fixtures as testingFixtures } from 'apollo-federation-integration-testsuite';

export const graphRef = 'federated-service@current';
export const apiKey = 'service:federated-service:DD71EBbGmsuh-6suUVDwnA';
Expand All @@ -19,31 +18,43 @@ export const mockApolloConfig = {
};

// Service mocks
function mockSdlQuery({ url }: MockService) {
function mockSdlQuery({ url }: Fixture) {
return nock(url).post('/', {
query: SERVICE_DEFINITION_QUERY,
});
}

export function mockSdlQuerySuccess(service: MockService) {
export function mockSdlQuerySuccess(service: Fixture) {
return mockSdlQuery(service).reply(200, {
data: { _service: { sdl: print(service.typeDefs) } },
});
}

export function mockServiceHealthCheck({ url }: MockService) {
export function mockAllServicesSdlQuerySuccess(
fixtures: Fixture[] = testingFixtures,
) {
return fixtures.map((fixture) =>
mockSdlQuery(fixture).reply(200, {
data: { _service: { sdl: print(fixture.typeDefs) } },
}),
);
}

export function mockServiceHealthCheck({ url }: Fixture) {
return nock(url).post('/', {
query: HEALTH_CHECK_QUERY,
});
}

export function mockServiceHealthCheckSuccess(service: MockService) {
export function mockServiceHealthCheckSuccess(service: Fixture) {
return mockServiceHealthCheck(service).reply(200, {
data: { __typename: 'Query' },
});
}

export function mockAllServicesHealthCheckSuccess() {
export function mockAllServicesHealthCheckSuccess(
fixtures: Fixture[] = testingFixtures,
) {
return fixtures.map((fixture) =>
mockServiceHealthCheck(fixture).reply(200, {
data: { __typename: 'Query' },
Expand Down
17 changes: 12 additions & 5 deletions gateway-js/src/config.ts
Expand Up @@ -177,13 +177,20 @@ export function isManuallyManagedSupergraphSdlGatewayConfig(
export interface SupergraphSdlUpdateFunction {
(updatedSupergraphSdl: string): Promise<void>
}
export interface SupergraphSdlUpdateOptions {
export interface SupergraphSdlHookOptions {
update: SupergraphSdlUpdateFunction;
}
interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase {
supergraphSdl: (
opts: SupergraphSdlUpdateOptions,
) => Promise<{ supergraphSdl: string; cleanup?: () => Promise<void> }>;

export type SupergraphSdlHookReturn = Promise<{
supergraphSdl: string;
cleanup?: () => Promise<void>;
}>;

export interface SupergraphSdlHook {
(options: SupergraphSdlHookOptions): SupergraphSdlHookReturn;
}
export interface ManuallyManagedSupergraphSdlGatewayConfig extends GatewayConfigBase {
supergraphSdl: SupergraphSdlHook;
}

type ManuallyManagedGatewayConfig =
Expand Down
42 changes: 19 additions & 23 deletions gateway-js/src/index.ts
Expand Up @@ -70,6 +70,7 @@ import {
SupergraphSdlUpdate,
CompositionUpdate,
isManuallyManagedSupergraphSdlGatewayConfig,
ManuallyManagedSupergraphSdlGatewayConfig,
} from './config';
import { loadSupergraphSdlFromStorage } from './loadSupergraphSdlFromStorage';
import { buildComposedSchema } from '@apollo/query-planner';
Expand Down Expand Up @@ -207,12 +208,7 @@ export class ApolloGateway implements GraphQLService {
// * `string` means use that endpoint
// * `undefined` means the gateway is not using managed federation
private schemaConfigDeliveryEndpoint?: string;
// The Promise<void> case is strictly for handling the case where the user-provided
// function throws an error.
private manualConfigPromise?: Promise<{
supergraphSdl: string;
cleanup?: () => Promise<void>;
} | void>;
// Functions to call during gateway cleanup (when stop() is called)
private toDispose: (() => Promise<void>)[] = [];

constructor(config?: GatewayConfig) {
Expand Down Expand Up @@ -260,15 +256,7 @@ export class ApolloGateway implements GraphQLService {
this.updateServiceDefinitions =
this.config.experimental_updateServiceDefinitions;
} else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) {
this.manualConfigPromise = this.config
.supergraphSdl({ update: this.updateWithSupergraphSdl.bind(this) })
.catch((e) => {
// Not swallowing the error here results in an uncaught rejection.
// An error will be thrown when this promise resolves to nothing.
this.logger.error(
'User-defined `supergraphSdl` function threw error: ' + e.message,
);
});
// TODO: do nothing maybe?
} else {
throw Error(
'Programming error: unexpected manual configuration provided',
Expand Down Expand Up @@ -386,6 +374,13 @@ export class ApolloGateway implements GraphQLService {
'The `localServiceList` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.',
);
}

// TODO(trevor:removeServiceList)
if ('buildService' in this.config) {
this.logger.warn(
'The `buildService` option is deprecated and will be removed in a future version of `@apollo/gateway`. Please migrate to the function form of the `supergraphSdl` configuration option.',
);
}
}

public async load(options?: {
Expand Down Expand Up @@ -451,7 +446,7 @@ export class ApolloGateway implements GraphQLService {
if (isStaticConfig(this.config)) {
this.loadStatic(this.config);
} else if (isManuallyManagedSupergraphSdlGatewayConfig(this.config)) {
await this.loadManuallyManaged();
await this.loadManuallyManaged(this.config);
} else {
await this.loadDynamic(unrefTimer);
}
Expand Down Expand Up @@ -507,18 +502,22 @@ export class ApolloGateway implements GraphQLService {
}
}

private async loadManuallyManaged() {
private async loadManuallyManaged(config: ManuallyManagedSupergraphSdlGatewayConfig) {
try {
const result = await this.manualConfigPromise;
if (!result?.supergraphSdl)
const result = await config.supergraphSdl({
update: this.updateWithSupergraphSdl.bind(this),
});
if (!result?.supergraphSdl) {
throw new Error(
'User provided `supergraphSdl` function did not return an object containing a `supergraphSdl` property',
);
}
if (result?.cleanup) {
this.toDispose.push(result.cleanup);
}
await this.updateWithSupergraphSdl(result.supergraphSdl);
} catch (e) {
this.logger.error(e.message ?? e);
this.state = { phase: 'failed to load' };
throw e;
}
Expand Down Expand Up @@ -1413,7 +1412,4 @@ export {

export * from './datasources';

export {
SupergraphSdlUpdateOptions,
SupergraphSdlUpdateFunction,
} from './config';
export { SupergraphSdlUpdateFunction } from './config';
6 changes: 3 additions & 3 deletions gateway-js/src/loadServicesFromRemoteEndpoint.ts
Expand Up @@ -3,10 +3,10 @@ import { parse } from 'graphql';
import { Headers, HeadersInit } from 'node-fetch';
import { GraphQLDataSource, GraphQLDataSourceRequestKind } from './datasources/types';
import { SERVICE_DEFINITION_QUERY } from './';
import { CompositionUpdate, ServiceEndpointDefinition } from './config';
import { ServiceDefinitionUpdate, ServiceEndpointDefinition } from './config';
import { ServiceDefinition } from '@apollo/federation';

type Service = ServiceEndpointDefinition & {
export type Service = ServiceEndpointDefinition & {
dataSource: GraphQLDataSource;
};

Expand All @@ -20,7 +20,7 @@ export async function getServiceDefinitionsFromRemoteEndpoint({
service: ServiceEndpointDefinition,
) => Promise<HeadersInit | undefined>;
serviceSdlCache: Map<string, string>;
}): Promise<CompositionUpdate> {
}): Promise<ServiceDefinitionUpdate> {
if (!serviceList || !serviceList.length) {
throw new Error(
'Tried to load services from remote endpoints but none provided',
Expand Down

0 comments on commit 903bb5f

Please sign in to comment.