Skip to content

Commit

Permalink
feat(core): Allow custom ApolloServerPlugins to be specified
Browse files Browse the repository at this point in the history
For certain types of data transformation, express plugins and Nestjs interceptors are not sufficient since they occur at the wrong point in the request/response pipeline. This is the case with the built-in ID-transformation code. For these problems, ApolloServerPlugins provide very fine-grained control over the request/response, with full access to the GraphQL schema and document. This can in theory enable very advanced use-cases such as implementing custom GraphQL directives.

Closes #210
  • Loading branch information
michaelbromley committed Nov 22, 2019
1 parent 3a7de83 commit dc45c87
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 0 deletions.
106 changes: 106 additions & 0 deletions packages/core/e2e/apollo-server-plugin.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
ApolloServerPlugin,
GraphQLRequestContext,
GraphQLRequestListener,
GraphQLServiceContext,
} from 'apollo-server-plugin-base';
import gql from 'graphql-tag';
import path from 'path';

import { createTestEnvironment } from '../../testing/lib/create-test-environment';

import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
import { initialData } from './fixtures/e2e-initial-data';

class MyApolloServerPlugin implements ApolloServerPlugin {
static serverWillStartFn = jest.fn();
static requestDidStartFn = jest.fn();
static willSendResponseFn = jest.fn();

static reset() {
this.serverWillStartFn = jest.fn();
this.requestDidStartFn = jest.fn();
this.willSendResponseFn = jest.fn();
}

serverWillStart(service: GraphQLServiceContext): Promise<void> | void {
MyApolloServerPlugin.serverWillStartFn(service);
}

requestDidStart(): GraphQLRequestListener | void {
MyApolloServerPlugin.requestDidStartFn();
return {
willSendResponse(requestContext: any): Promise<void> | void {
const data = requestContext.response.data;
MyApolloServerPlugin.willSendResponseFn(data);
},
};
}
}

describe('custom apolloServerPlugins', () => {
const { server, adminClient, shopClient } = createTestEnvironment({
...testConfig,
apolloServerPlugins: [new MyApolloServerPlugin()],
});

beforeAll(async () => {
await server.init({
dataDir,
initialData,
productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'),
customerCount: 1,
});
await adminClient.asSuperAdmin();
}, TEST_SETUP_TIMEOUT_MS);

afterAll(async () => {
await server.destroy();
});

it('calls serverWillStart()', () => {
expect(MyApolloServerPlugin.serverWillStartFn).toHaveBeenCalled();
});

it('runs plugin on shop api query', async () => {
MyApolloServerPlugin.reset();
await shopClient.query(gql`
query Q1 {
product(id: "T_1") {
id
name
}
}
`);

expect(MyApolloServerPlugin.requestDidStartFn).toHaveBeenCalledTimes(1);
expect(MyApolloServerPlugin.willSendResponseFn).toHaveBeenCalledTimes(1);
expect(MyApolloServerPlugin.willSendResponseFn.mock.calls[0][0]).toEqual({
product: {
id: 'T_1',
name: 'Laptop',
},
});
});

it('runs plugin on admin api query', async () => {
MyApolloServerPlugin.reset();
await adminClient.query(gql`
query Q2 {
product(id: "T_1") {
id
name
}
}
`);

expect(MyApolloServerPlugin.requestDidStartFn).toHaveBeenCalledTimes(1);
expect(MyApolloServerPlugin.willSendResponseFn).toHaveBeenCalledTimes(1);
expect(MyApolloServerPlugin.willSendResponseFn.mock.calls[0][0]).toEqual({
product: {
id: 'T_1',
name: 'Laptop',
},
});
});
});
1 change: 1 addition & 0 deletions packages/core/src/api/config/configure-graphql-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ async function createGraphQLOptions(
new IdCodecPlugin(idCodecService),
new TranslateErrorsPlugin(i18nService),
new AssetInterceptorPlugin(configService),
...configService.apolloServerPlugins,
],
} as GqlModuleOptions;

Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/config/config.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DynamicModule, Injectable, Type } from '@nestjs/common';
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
import { LanguageCode } from '@vendure/common/lib/generated-types';
import { PluginDefinition } from 'apollo-server-core';
import { RequestHandler } from 'express';
import { ConnectionOptions } from 'typeorm';

Expand Down Expand Up @@ -110,6 +111,10 @@ export class ConfigService implements VendureConfig {
return this.activeConfig.middleware;
}

get apolloServerPlugins(): PluginDefinition[] {
return this.activeConfig.apolloServerPlugins;
}

get plugins(): Array<DynamicModule | Type<any>> {
return this.activeConfig.plugins;
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/default-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,6 @@ export const defaultConfig: RuntimeVendureConfig = {
User: [],
},
middleware: [],
apolloServerPlugins: [],
plugins: [],
};
12 changes: 12 additions & 0 deletions packages/core/src/config/vendure-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { DynamicModule, Type } from '@nestjs/common';
import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface';
import { ClientOptions, Transport } from '@nestjs/microservices';
import { LanguageCode } from '@vendure/common/lib/generated-types';
import { PluginDefinition } from 'apollo-server-core';
import { RequestHandler } from 'express';
import { Observable } from 'rxjs';
import { ConnectionOptions } from 'typeorm';
Expand Down Expand Up @@ -476,6 +477,17 @@ export interface VendureConfig {
* @default []
*/
middleware?: Array<{ handler: RequestHandler; route: string }>;
/**
* @description
* Custom [ApolloServerPlugins](https://www.apollographql.com/docs/apollo-server/integrations/plugins/) which
* allow the extension of the Apollo Server, which is the underlying GraphQL server used by Vendure.
*
* Apollo plugins can be used e.g. to perform custom data transformations on incoming operations or outgoing
* data.
*
* @default []
*/
apolloServerPlugins?: PluginDefinition[];
/**
* @description
* Configures available payment processing methods.
Expand Down

0 comments on commit dc45c87

Please sign in to comment.