Skip to content

Commit 722539c

Browse files
authored
Merge pull request #40 from apollo-server-integrations/feature/accept-alb-event-type
Added support for ALB event type
2 parents f6346d1 + 84028ee commit 722539c

File tree

5 files changed

+167
-70
lines changed

5 files changed

+167
-70
lines changed

.changeset/blue-baboons-attend.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@as-integrations/aws-lambda': minor
3+
---
4+
5+
ALB Event type integration

src/__tests__/integrationALB.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ApolloServer, ApolloServerOptions, BaseContext } from '@apollo/server';
2+
import {
3+
CreateServerForIntegrationTestsOptions,
4+
defineIntegrationTestSuite,
5+
} from '@apollo/server-integration-testsuite';
6+
import type { ALBEvent, ALBResult, Handler } from 'aws-lambda';
7+
import { createServer } from 'http';
8+
import { startServerAndCreateLambdaHandler } from '..';
9+
import { createMockALBServer } from './mockALBServer';
10+
import { urlForHttpServer } from './mockServer';
11+
12+
describe('lambdaHandlerALB', () => {
13+
defineIntegrationTestSuite(
14+
async function (
15+
serverOptions: ApolloServerOptions<BaseContext>,
16+
testOptions?: CreateServerForIntegrationTestsOptions,
17+
) {
18+
const httpServer = createServer();
19+
const server = new ApolloServer({
20+
...serverOptions,
21+
});
22+
23+
const handler = testOptions
24+
? startServerAndCreateLambdaHandler(server, testOptions)
25+
: startServerAndCreateLambdaHandler(server);
26+
27+
httpServer.addListener(
28+
'request',
29+
createMockALBServer(handler as Handler<ALBEvent, ALBResult>),
30+
);
31+
32+
await new Promise<void>((resolve) => {
33+
httpServer.listen({ port: 0 }, resolve);
34+
});
35+
36+
return {
37+
server,
38+
url: urlForHttpServer(httpServer),
39+
async extraCleanup() {
40+
await new Promise<void>((resolve) => {
41+
httpServer.close(() => resolve());
42+
});
43+
},
44+
};
45+
},
46+
{
47+
serverIsStartedInBackground: true,
48+
noIncrementalDelivery: true,
49+
},
50+
);
51+
});

src/__tests__/mockALBServer.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import url from 'url';
2+
import type { IncomingMessage } from 'http';
3+
import type { ALBEvent, ALBResult, Handler } from 'aws-lambda';
4+
import { createMockServer } from './mockServer';
5+
6+
export function createMockALBServer(handler: Handler<ALBEvent, ALBResult>) {
7+
return createMockServer(handler, albEventFromRequest);
8+
}
9+
10+
function albEventFromRequest(req: IncomingMessage, body: string): ALBEvent {
11+
const urlObject = url.parse(req.url || '', false);
12+
const searchParams = new URLSearchParams(urlObject.search ?? '');
13+
14+
const multiValueQueryStringParameters: ALBEvent['multiValueQueryStringParameters'] =
15+
{};
16+
17+
for (const [key] of searchParams.entries()) {
18+
const all = searchParams.getAll(key);
19+
if (all.length > 1) {
20+
multiValueQueryStringParameters[key] = all;
21+
}
22+
}
23+
24+
return {
25+
requestContext: {
26+
elb: {
27+
targetGroupArn: '...',
28+
},
29+
},
30+
httpMethod: req.method ?? 'GET',
31+
path: urlObject.pathname ?? '/',
32+
queryStringParameters: Object.fromEntries(searchParams.entries()),
33+
headers: Object.fromEntries(
34+
Object.entries(req.headers).map(([name, value]) => {
35+
if (Array.isArray(value)) {
36+
return [name, value.join(',')];
37+
} else {
38+
return [name, value];
39+
}
40+
}),
41+
),
42+
multiValueQueryStringParameters,
43+
body,
44+
isBase64Encoded: false,
45+
};
46+
}

src/__tests__/mockServer.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
import type { IncomingMessage, Server, ServerResponse } from 'http';
22
import type {
3+
ALBResult,
34
APIGatewayProxyEvent,
5+
APIGatewayProxyEventV2,
46
APIGatewayProxyResult,
57
APIGatewayProxyStructuredResultV2,
68
Context as LambdaContext,
79
Handler,
810
} from 'aws-lambda';
911
import { format } from 'url';
1012
import type { AddressInfo } from 'net';
11-
import type { GatewayEvent } from '..';
13+
import type { IncomingEvent } from '..';
1214

13-
type LambdaHandler<T = GatewayEvent> = Handler<
15+
type LambdaHandler<T = IncomingEvent> = Handler<
1416
T,
1517
T extends APIGatewayProxyEvent
1618
? APIGatewayProxyResult
17-
: APIGatewayProxyStructuredResultV2
19+
: T extends APIGatewayProxyEventV2
20+
? APIGatewayProxyStructuredResultV2
21+
: ALBResult
1822
>;
1923

2024
// Returns a Node http handler that invokes a Lambda handler (v1 / v2)
21-
export function createMockServer<T extends GatewayEvent>(
25+
export function createMockServer<T extends IncomingEvent>(
2226
handler: LambdaHandler<T>,
2327
eventFromRequest: (req: IncomingMessage, body: string) => T,
2428
) {

src/index.ts

Lines changed: 57 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,41 @@ import type {
77
import { HeaderMap } from '@apollo/server';
88
import type { WithRequired } from '@apollo/utils.withrequired';
99
import type {
10+
ALBEvent,
11+
ALBResult,
1012
APIGatewayProxyEvent,
11-
APIGatewayProxyEventHeaders,
12-
APIGatewayProxyEventQueryStringParameters,
1313
APIGatewayProxyEventV2,
1414
APIGatewayProxyResult,
1515
APIGatewayProxyStructuredResultV2,
1616
Context,
1717
Handler,
1818
} from 'aws-lambda';
1919

20-
export type GatewayEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2;
20+
export type IncomingEvent =
21+
| APIGatewayProxyEvent
22+
| APIGatewayProxyEventV2
23+
| ALBEvent;
24+
25+
/**
26+
* @deprecated Use {IncomingEvent} instead
27+
*/
28+
export type GatewayEvent = IncomingEvent;
2129

2230
export interface LambdaContextFunctionArgument {
23-
event: GatewayEvent;
31+
event: IncomingEvent;
2432
context: Context;
2533
}
2634

2735
export interface LambdaHandlerOptions<TContext extends BaseContext> {
2836
context?: ContextFunction<[LambdaContextFunctionArgument], TContext>;
2937
}
3038

31-
type LambdaHandler = Handler<
32-
GatewayEvent,
33-
APIGatewayProxyStructuredResultV2 | APIGatewayProxyResult
34-
>;
39+
export type HandlerResult =
40+
| APIGatewayProxyStructuredResultV2
41+
| APIGatewayProxyResult
42+
| ALBResult;
43+
44+
type LambdaHandler = Handler<IncomingEvent, HandlerResult>;
3545

3646
export function startServerAndCreateLambdaHandler(
3747
server: ApolloServer<BaseContext>,
@@ -62,7 +72,7 @@ export function startServerAndCreateLambdaHandler<TContext extends BaseContext>(
6272

6373
return async function (event, context) {
6474
try {
65-
const normalizedEvent = normalizeGatewayEvent(event);
75+
const normalizedEvent = normalizeIncomingEvent(event);
6676

6777
const { body, headers, status } = await server.executeHTTPGraphQLRequest({
6878
httpGraphQLRequest: normalizedEvent,
@@ -90,63 +100,33 @@ export function startServerAndCreateLambdaHandler<TContext extends BaseContext>(
90100
};
91101
}
92102

93-
function normalizeGatewayEvent(event: GatewayEvent): HTTPGraphQLRequest {
94-
if (isV1Event(event)) {
95-
return normalizeV1Event(event);
96-
}
97-
98-
if (isV2Event(event)) {
99-
return normalizeV2Event(event);
103+
function normalizeIncomingEvent(event: IncomingEvent): HTTPGraphQLRequest {
104+
let httpMethod: string;
105+
if ('httpMethod' in event) {
106+
httpMethod = event.httpMethod;
107+
} else {
108+
httpMethod = event.requestContext.http.method;
100109
}
101-
102-
throw Error('Unknown event type');
103-
}
104-
105-
function isV1Event(event: GatewayEvent): event is APIGatewayProxyEvent {
106-
// APIGatewayProxyEvent incorrectly omits `version` even though API Gateway v1
107-
// events may include `version: "1.0"`
108-
return (
109-
!('version' in event) || ('version' in event && event.version === '1.0')
110-
);
111-
}
112-
113-
function isV2Event(event: GatewayEvent): event is APIGatewayProxyEventV2 {
114-
return 'version' in event && event.version === '2.0';
115-
}
116-
117-
function normalizeV1Event(event: APIGatewayProxyEvent): HTTPGraphQLRequest {
118110
const headers = normalizeHeaders(event.headers);
119-
const body = parseBody(event.body, headers.get('content-type'));
120-
// Single value parameters can be directly added
121-
const searchParams = new URLSearchParams(
122-
normalizeQueryStringParams(event.queryStringParameters),
123-
);
124-
// Passing a key with an array entry to the constructor yields
125-
// one value in the querystring with %2C as the array was flattened to a string
126-
// Multi values must be appended individually to get the to-spec output
127-
for (const [key, values] of Object.entries(
128-
event.multiValueQueryStringParameters ?? {},
129-
)) {
130-
for (const value of values ?? []) {
131-
searchParams.append(key, value);
132-
}
111+
let search: string;
112+
if ('rawQueryString' in event) {
113+
search = event.rawQueryString;
114+
} else if ('queryStringParameters' in event) {
115+
search = normalizeQueryStringParams(
116+
event.queryStringParameters,
117+
event.multiValueQueryStringParameters,
118+
).toString();
119+
} else {
120+
throw new Error('Search params not parsable from event');
133121
}
134122

135-
return {
136-
method: event.httpMethod,
137-
headers,
138-
search: searchParams.toString(),
139-
body,
140-
};
141-
}
123+
const body = event.body ?? '';
142124

143-
function normalizeV2Event(event: APIGatewayProxyEventV2): HTTPGraphQLRequest {
144-
const headers = normalizeHeaders(event.headers);
145125
return {
146-
method: event.requestContext.http.method,
126+
method: httpMethod,
147127
headers,
148-
search: event.rawQueryString,
149-
body: parseBody(event.body, headers.get('content-type')),
128+
search,
129+
body: parseBody(body, headers.get('content-type')),
150130
};
151131
}
152132

@@ -165,20 +145,31 @@ function parseBody(
165145
return '';
166146
}
167147

168-
function normalizeHeaders(headers: APIGatewayProxyEventHeaders): HeaderMap {
148+
function normalizeHeaders(headers: IncomingEvent['headers']): HeaderMap {
169149
const headerMap = new HeaderMap();
170-
for (const [key, value] of Object.entries(headers)) {
150+
for (const [key, value] of Object.entries(headers ?? {})) {
171151
headerMap.set(key, value ?? '');
172152
}
173153
return headerMap;
174154
}
175155

176156
function normalizeQueryStringParams(
177-
queryStringParams: APIGatewayProxyEventQueryStringParameters | null,
178-
): Record<string, string> {
179-
const queryStringRecord: Record<string, string> = {};
157+
queryStringParams: Record<string, string | undefined> | null | undefined,
158+
multiValueQueryStringParameters:
159+
| Record<string, string[] | undefined>
160+
| null
161+
| undefined,
162+
): URLSearchParams {
163+
const params = new URLSearchParams();
180164
for (const [key, value] of Object.entries(queryStringParams ?? {})) {
181-
queryStringRecord[key] = value ?? '';
165+
params.append(key, value ?? '');
166+
}
167+
for (const [key, value] of Object.entries(
168+
multiValueQueryStringParameters ?? {},
169+
)) {
170+
for (const v of value ?? []) {
171+
params.append(key, v);
172+
}
182173
}
183-
return queryStringRecord;
174+
return params;
184175
}

0 commit comments

Comments
 (0)