Skip to content

Commit

Permalink
feat(apollo-server): added support for apollo server
Browse files Browse the repository at this point in the history
H4ad committed Nov 27, 2022
1 parent b047b56 commit 4d6f35b
Showing 3 changed files with 3,155 additions and 525 deletions.
3,402 changes: 2,945 additions & 457 deletions package-lock.json

Large diffs are not rendered by default.

41 changes: 20 additions & 21 deletions src/frameworks/apollo-server/apollo-server.framework.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//#region

import { IncomingMessage, ServerResponse } from 'http';
import { ApolloServer, BaseContext, ContextFunction } from '@apollo/server';
import { ApolloServer, BaseContext, HeaderMap } from '@apollo/server';
import { FrameworkContract } from '../../contracts';
import { ServerlessRequest } from '../../network';
import { getDefaultIfUndefined } from '../../core';
@@ -14,9 +14,10 @@ import { getDefaultIfUndefined } from '../../core';
* @breadcrumb Frameworks / ApolloServerFramework
* @public
*/
export type ApolloServerContextArguments = [
{ request: IncomingMessage; response: ServerResponse },
];
export type ApolloServerContextArguments = {
request: IncomingMessage;
response: ServerResponse;
};

/**
* The options to customize {@link ApolloServerFramework}
@@ -25,7 +26,12 @@ export type ApolloServerContextArguments = [
* @public
*/
export interface ApolloServerOptions<TContext extends BaseContext> {
context: ContextFunction<ApolloServerContextArguments, TContext>;
/**
* Define a function to create the context of Apollo Server
*
* @param options - Default options passed by library
*/
context: (options: ApolloServerContextArguments) => Promise<TContext>;
}

/**
@@ -54,18 +60,19 @@ export class ApolloServerFramework<TContext extends BaseContext>
request: ServerlessRequest,
response: ServerResponse,
): void {
const headers = new Map<string, string>();
const headers = new HeaderMap();

for (const [key, value] of Object.entries(request.headers)) {
if (value === undefined) continue;

headers.set(key, Array.isArray(value) ? value.join(', ') : value);
headers.set(
key,
Array.isArray(value) ? value.join(', ') : value.toString(),
);
}

const defaultContext: ContextFunction<
ApolloServerContextArguments,
any
> = context => Promise.resolve(context);
const defaultContext: ApolloServerOptions<any>['context'] = context =>
Promise.resolve(context);

const context = () =>
getDefaultIfUndefined(
@@ -77,18 +84,13 @@ export class ApolloServerFramework<TContext extends BaseContext>
? new URL(request.url).search ?? ''
: request.url?.split('?')[1] || '';

let body: string = '{}';

if (request.body instanceof Buffer) body = request.body.toString('utf-8');
else if (request.body && typeof request.body === 'object')
body = JSON.stringify(request.body);

// we don't need to handle catch because of https://www.apollographql.com/docs/apollo-server/integrations/building-integrations/#handle-errors
app
.executeHTTPGraphQLRequest({
httpGraphQLRequest: {
method: request.method!.toUpperCase(),
headers,
body: JSON.parse(body),
body: request.body,
search,
},
context,
@@ -116,9 +118,6 @@ export class ApolloServerFramework<TContext extends BaseContext>
}

response.end();
})
.catch(err => {
response.destroy(err);
});
}
}
237 changes: 190 additions & 47 deletions test/frameworks/apollo-server.framework.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { IncomingMessage, ServerResponse } from 'http';
import { ApolloServer, BaseContext } from '@apollo/server';
import { OutgoingHttpHeaders } from 'http2';
import { ApolloServer, BaseContext, HeaderMap } from '@apollo/server';
import {
ServerlessRequest,
ServerlessResponse,
@@ -9,92 +10,234 @@ import {
import {
ApolloServerContextArguments,
ApolloServerFramework,
} from '../../src/frameworks/apollo-server/apollo-server.framework';
import { TestOptions } from './utils';
} from '../../src/frameworks/apollo-server';
import { JsonBodyParserFramework } from '../../src/frameworks/body-parser';
import { TestRouteBuilderMethods } from './utils';

export const frameworkTestOptions: TestOptions[] = [
['post', 'GetUser', 200, 'Joga10'],
['post', 'ListUser', 200, 'Unkownn'],
['post', 'UserCreated', 200, 'Created'],
export const frameworkTestOptions: [
method: TestRouteBuilderMethods,
path: string,
query: string,
statusCode: number,
expectedValue: string,
expectHeaderSet: boolean,
][] = [
['post', 'GetUser', 'message', 200, 'Joga10', true],
['post', 'ListUser', 'message', 200, 'Unkownn', true],
['post', 'UserCreated', 'message', 200, 'Created', true],
['post', 'WrongQuery', 'nonexist', 400, 'Cannot query field', false],
];

describe(ApolloServerFramework.name, () => {
for (const [
method,
query,
statusCode,
message,
expectedValue,
] of frameworkTestOptions) {
it(`${method}${query}: should forward request and receive response correctly`, async () => {
interface ApolloCustomContext extends BaseContext {
request: IncomingMessage;
response: ServerResponse;
}
describe('test requests', () => {
for (const [
method,
queryName,
query,
statusCode,
expectedValue,
expectHeaderSet,
] of frameworkTestOptions) {
it(`${method}${queryName}: should forward request and receive response correctly`, async () => {
interface ApolloCustomContext extends BaseContext {
request: IncomingMessage;
response: ServerResponse;
}

const app = new ApolloServer<ApolloCustomContext>({
typeDefs: 'type Query { message: String }',
resolvers: {
Query: {
message: (_, __, context: ApolloServerContextArguments) => {
context.response.setHeader('response-header', 'true');

return expectedValue;
},
},
},
});

app.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests();

const stringBody = JSON.stringify({
query: `
query Query {
${query}
}
`,
});
const [bufferBody, bodyLength] = stringBody
? getEventBodyAsBuffer(stringBody, false)
: [undefined, 0];

const framework = new JsonBodyParserFramework(
new ApolloServerFramework<ApolloCustomContext>(),
);

const request = new ServerlessRequest({
method: method.toUpperCase(),
url: '/',
headers: {
'content-length': String(bodyLength),
'request-header': 'true',
'content-type': 'application/json',
},
body: bufferBody,
});

const app = new ApolloServer<ApolloCustomContext>({
const response = new ServerlessResponse({
method: method.toUpperCase(),
});

framework.sendRequest(app, request, response);

await waitForStreamComplete(response);

const resultBody = ServerlessResponse.body(response);

expect(resultBody.toString('utf-8')).toContain(
expectedValue !== undefined ? expectedValue : expectedValue,
);
if (expectHeaderSet) {
expect(ServerlessResponse.headers(response)).toHaveProperty(
'response-header',
'true',
);
} else {
expect(ServerlessResponse.headers(response)).not.toHaveProperty(
'response-header',
'true',
);
}
expect(response.statusCode).toBe(statusCode);
});
}
});

describe('async iterator', () => {
it('should handle well async iterator', async () => {
const app = new ApolloServer<BaseContext>({
typeDefs: 'type Query { message: String }',
resolvers: {
Query: {
message: (_, __, context: ApolloServerContextArguments[0]) => {
message: (_, __, context: ApolloServerContextArguments) => {
context.response.setHeader('response-header', 'true');

return message;
return 'ok';
},
},
},
});

const asyncContent = ['hello', 'world', '!'];

// eslint-disable-next-line @typescript-eslint/require-await
async function* iterator(values) {
for (let i = 0; i < values.length; i++) yield values[i];
}

jest.spyOn(app, 'executeHTTPGraphQLRequest').mockImplementation(() =>
Promise.resolve({
status: 200,
headers: new HeaderMap(),
body: {
kind: 'chunked',
asyncIterator: iterator(asyncContent),
},
}),
);

app.startInBackgroundHandlingStartupErrorsByLoggingAndFailingAllRequests();

const stringBody = JSON.stringify({
query: `
query Query {
message
}
query Query {
message
}
`,
});

const [bufferBody, bodyLength] = stringBody
? getEventBodyAsBuffer(stringBody, false)
: [undefined, 0];

const framework = new ApolloServerFramework<ApolloCustomContext>({
context: context => Promise.resolve(context),
});
const framework = new JsonBodyParserFramework(
new ApolloServerFramework(),
);

const request = new ServerlessRequest({
method: method.toUpperCase(),
url: `/?query=${query}`,
method: 'POST',
url: '/',
headers: {
'content-length': String(bodyLength),
'request-header': 'true',
...(message && {
'content-type': 'application/json',
}),
'content-type': 'application/json',
},
body: bufferBody,
});

const response = new ServerlessResponse({
method: method.toUpperCase(),
});
const response: ServerlessResponse & { flush?: () => void } =
new ServerlessResponse({
method: 'POST',
});

response.flush = jest.fn(() => void 0);

framework.sendRequest(app, request, response);

await waitForStreamComplete(response);

const resultBody = ServerlessResponse.body(response);

expect(resultBody.toString('utf-8')).toEqual(
expectedValue !== undefined
? expectedValue
: JSON.stringify({ data: { message: message } }) + '\n',
);
expect(ServerlessResponse.headers(response)).toHaveProperty(
'response-header',
'true',
);
expect(response.statusCode).toBe(statusCode);
expect(resultBody.toString()).toEqual(asyncContent.join(''));
expect(response.flush).toHaveBeenNthCalledWith(asyncContent.length);
});
});

describe('possible headers', () => {
it('should handle all types of OutgoingHttpHeaders', () => {
const headers: OutgoingHttpHeaders = {
test: 'test',
foo: ['bar', 'boe'],
bar: undefined,
doe: 10,
};

const app = new ApolloServer<BaseContext>({
typeDefs: 'type Query { doe: String }',
resolvers: {},
});

jest
.spyOn(app, 'executeHTTPGraphQLRequest')
.mockImplementation(({ httpGraphQLRequest: { headers } }) => {
const objHeaders = Object.fromEntries(headers.entries());

expect(objHeaders).toHaveProperty('test', 'test');
expect(objHeaders).toHaveProperty('foo', 'bar, boe');
expect(objHeaders).toHaveProperty('doe', '10');
expect(objHeaders).not.toHaveProperty('bar');

return Promise.resolve({
status: 200,
body: { kind: 'complete', string: 'ok' },
headers: new HeaderMap(),
});
});

const request = new ServerlessRequest({
method: 'POST',
url: '/',
headers: headers as any,
});

const response: ServerlessResponse = new ServerlessResponse({
method: 'POST',
});

const framework = new ApolloServerFramework();

framework.sendRequest(app, request, response);
});
}
});
});

0 comments on commit 4d6f35b

Please sign in to comment.