Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow async initialization of data sources #3639

Merged
merged 10 commits into from Jan 22, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ The version headers in this history reflect the versions of Apollo Server itself

> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the the appropriate changes within that release will be moved into the new section.

- `apollo-server-core`: Allow asynchronous initialization of datasources: the `initialize` method on datasources may now return a Promise, which will be settled before any resolvers are called. [#3639](https://github.com/apollographql/apollo-server/pull/3639)
- `apollo-server-core`: Update apollo-tooling dependencies, resolve TS build error (missing types for node-fetch) [#3662](https://github.com/apollographql/apollo-server/pull/3662)

### v2.9.15
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-datasource/src/index.ts
Expand Up @@ -6,5 +6,5 @@ export interface DataSourceConfig<TContext> {
}

export abstract class DataSource<TContext = any> {
initialize?(config: DataSourceConfig<TContext>): void;
initialize?(config: DataSourceConfig<TContext>): void | Promise<void>;
}
105 changes: 105 additions & 0 deletions packages/apollo-server-core/src/__tests__/dataSources.test.ts
@@ -0,0 +1,105 @@
import { ApolloServerBase } from '../ApolloServer';
import gql from 'graphql-tag';

const typeDefs = gql`
type Query {
hello: String
}
`;

describe('ApolloServerBase dataSources', () => {
it('initializes synchronous datasources from a datasource creator function', async () => {
const initialize = jest.fn();

const server = new ApolloServerBase({
typeDefs,
resolvers: {
Query: {
hello() {
return 'world';
}
}
},
dataSources: () => ({ x: { initialize }, y: { initialize } })
});

await server.executeOperation({ query: "query { hello }"});

expect(initialize).toHaveBeenCalledTimes(2);
});

it('initializes all async and sync datasources before calling resolvers', async () => {
const INITIALIZE = "datasource initializer call";
const METHOD_CALL = "datasource method call";

const expectedCallOrder = [
INITIALIZE,
INITIALIZE,
INITIALIZE,
METHOD_CALL
];

const actualCallOrder: string[] = [];

const server = new ApolloServerBase({
typeDefs,
resolvers: {
Query: {
hello(_, __, context) {
context.dataSources.x.getData();
return "world";
}
},
},
dataSources: () => ({
x: {
initialize() {
return Promise.resolve().then(
() => { actualCallOrder.push(INITIALIZE); }
);
},
getData() { actualCallOrder.push(METHOD_CALL); }
},
y: {
initialize() {
return new Promise(res => {
setTimeout(() => {
actualCallOrder.push(INITIALIZE);
res();
}, 0);
});
},
},
z: {
initialize() { actualCallOrder.push(INITIALIZE); }
}
})
});

lostfictions marked this conversation as resolved.
Show resolved Hide resolved
await server.executeOperation({ query: "query { hello }"});

expect(actualCallOrder).toEqual(expectedCallOrder);
});

it('makes datasources available on resolver contexts', async () => {
const message = 'hi from dataSource';
const getData = jest.fn(() => message);

const server = new ApolloServerBase({
typeDefs,
resolvers: {
Query: {
hello(_, __, context) {
return context.dataSources.x.getData();
}
},
},
dataSources: () => ({ x: { initialize() {}, getData } })
});

const res = await server.executeOperation({ query: "query { hello }"});

expect(getData).toHaveBeenCalled();
expect(res.data?.hello).toBe(message);
});
});
17 changes: 11 additions & 6 deletions packages/apollo-server-core/src/requestPipeline.ts
Expand Up @@ -114,7 +114,7 @@ export async function processGraphQLRequest<TContext>(

const dispatcher = initializeRequestListenerDispatcher();

initializeDataSources();
await initializeDataSources();

const metrics = requestContext.metrics || Object.create(null);
if (!requestContext.metrics) {
Expand Down Expand Up @@ -611,21 +611,26 @@ export async function processGraphQLRequest<TContext>(
return new GraphQLExtensionStack(extensions);
}

function initializeDataSources() {
async function initializeDataSources() {
if (config.dataSources) {
const context = requestContext.context;

const dataSources = config.dataSources();

const initializers: any[] = [];
for (const dataSource of Object.values(dataSources)) {
if (dataSource.initialize) {
dataSource.initialize({
context,
cache: requestContext.cache,
});
initializers.push(
dataSource.initialize({
context,
cache: requestContext.cache,
})
);
}
}

await Promise.all(initializers);

if ('dataSources' in context) {
throw new Error(
'Please use the dataSources config option instead of putting dataSources on the context yourself.',
Expand Down