FEAT(Serverless AWS Lambda): Post-CR fixes
MODIFY
packages/core/src/server/specs/server.factory.spec.ts
packages/core/src/server/specs/server.factory.spec.ts
@@ -48,12 +48,14 @@ describe('#createServer', () => {
hostname,
httpListener: app,
}).run();
-
+ server.on('error', error => {
+ console.error(error);
+ });
server.on('listening', () => {
expect(server.listening).toBe(true);
done();
});
- }, 10000);
+ });
test('creates http server and starts listening without specified port and hostname', done => {
// given

MODIFY
packages/serverless/README.md
packages/serverless/README.md
</a>
</p>
-# @marblejs/serverless-aws-lambda
+# @marblejs/serverless
AWS Lambda serverless proxy for [Marble.js](https://github.com/marblejs/marble). Based on work of [@mflorence99](https://github.com/mflorence99/aws-serverless-marblejs).
@@ -26,7 +26,7 @@ import { createLambda, ProxyType } from '@marblejs/severless';
import httpListener from './httpListener';
export = createLambda({
- app: httpListener, // Your app goes here
+ httpListener, // Your app goes here
type: ProxyType.AWS, // Currently we support only AWS Lambda with API Gateway
proxyOptions: { // Optional configuration for specified ProxyType
binaryMimeTypes: ['application/octet-stream'],

MODIFY
packages/serverless/src/+awsLambda/awsLambda.middleware.ts
packages/serverless/src/+awsLambda/awsLambda.middleware.ts
import { map } from 'rxjs/operators';
import { HttpMiddlewareEffect } from '@marblejs/core';
import { getHeaderContent } from '../serverProxy.helpers';
+import { AwsLambdaHeaders } from './awsLambda.types';
export const awsApiGatewayMiddleware$ = (): HttpMiddlewareEffect => (req$) => req$.pipe(
map(req => {
- req.apiGatewayEvent = getHeaderContent(req.headers['x-apigateway-event']);
- req.apiGatewayContext = getHeaderContent(req.headers['x-apigateway-context']);
+ req.apiGatewayEvent = getHeaderContent(req.headers[AwsLambdaHeaders.APIGATEWAY_EVENT]);
+ req.apiGatewayContext = getHeaderContent(req.headers[AwsLambdaHeaders.AWSLAMBDA_CONTEXT]);
return req;
}),
);

MODIFY
packages/serverless/src/+awsLambda/awsLambda.types.ts
packages/serverless/src/+awsLambda/awsLambda.types.ts
@@ -2,3 +2,7 @@ export interface AwsLambdaProxyOptions {
binaryMimeTypes: string[];
}
+export enum AwsLambdaHeaders {
+ APIGATEWAY_EVENT = 'x-apigateway-event',
+ AWSLAMBDA_CONTEXT = 'x-awslambda-context',
+}

ADD
packages/serverless/src/+awsLambda/awsLambdaHandler.ts
+import { APIGatewayProxyEvent, Context } from 'aws-lambda';
+import { ServerApp } from '../serverProxy';
+import { AwsLambdaProxy } from './awsLambdaProxy';
+import { AwsLambdaProxyOptions } from './awsLambda.types';
+
+export const createAwsLambdaHandler = (app: ServerApp, options: AwsLambdaProxyOptions) => {
+ const proxy = new AwsLambdaProxy(app, options);
+
+ const handler = (event: APIGatewayProxyEvent, context: Context) =>
+ proxy.handle({ event, context });
+ handler.close = () => proxy.close();
+ return handler;
+};

MODIFY
packages/serverless/src/+awsLambda/awsLambdaProxy.ts
packages/serverless/src/+awsLambda/awsLambdaProxy.ts
@@ -3,7 +3,8 @@ import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda
import { HttpStatus } from '@marblejs/core';
import { ServerApp, ServerProxy, ServerProxyRequest, ServerProxyResponse } from '../serverProxy';
import { defaultAwsLambdaProxyOptions } from './awsLambdaProxy.options';
-import { AwsLambdaProxyOptions } from './awsLambda.types';
+import { AwsLambdaHeaders, AwsLambdaProxyOptions } from './awsLambda.types';
+import { HttpMethod } from '@marblejs/core';
interface ApiGatewayRequest {
event: APIGatewayProxyEvent;
@@ -18,35 +19,36 @@ export class AwsLambdaProxy extends ServerProxy<ApiGatewayRequest, APIGatewayPro
this.options = Object.assign({}, defaultAwsLambdaProxyOptions, options);
}
- normalizeError(error: Error): Promise<APIGatewayProxyResult> | APIGatewayProxyResult {
+ normalizeError(error: Error): APIGatewayProxyResult {
console.log(error);
return { body: error.message, headers: {}, statusCode: HttpStatus.BAD_GATEWAY };
}
- normalizeRequest(proxyRequest: ApiGatewayRequest): ServerProxyRequest | Promise<ServerProxyRequest> {
+ normalizeRequest(proxyRequest: ApiGatewayRequest): ServerProxyRequest {
const { event, context } = proxyRequest;
const { body, headers, ...eventRest } = event;
return {
path: url.format({ pathname: event.path, query: event.queryStringParameters }),
headers: {
...headers,
- 'x-apigateway-event': encodeURIComponent(JSON.stringify(eventRest)),
- 'x-apigateway-context': encodeURIComponent(JSON.stringify(context)),
+ [AwsLambdaHeaders.APIGATEWAY_EVENT]: encodeURIComponent(JSON.stringify(eventRest)),
+ [AwsLambdaHeaders.AWSLAMBDA_CONTEXT]: encodeURIComponent(JSON.stringify(context)),
},
- method: event.httpMethod,
+ method: event.httpMethod as HttpMethod,
body: body ? Buffer.from(body, event.isBase64Encoded ? 'base64' : 'utf8') : undefined,
};
}
- normalizeResponse(serverProxyResponse: ServerProxyResponse): Promise<APIGatewayProxyResult> | APIGatewayProxyResult {
+ normalizeResponse(serverProxyResponse: ServerProxyResponse): APIGatewayProxyResult {
const { headers, body, statusCode } = serverProxyResponse;
const { binaryMimeTypes } = this.options;
if (headers['transfer-encoding'] && headers['transfer-encoding'].includes('chunked')) {
delete headers['transfer-encoding'];
}
- if (body && !headers['content-length']) {
- headers['content-length'] = [String(Buffer.byteLength(body))];
+ // TODO Use getHeaderByKey from proxy utils that will be available in upcoming PR
+ if (body && !headers['Content-Length']) {
+ headers['Content-Length'] = [String(Buffer.byteLength(body))];
}
const contentType = headers['content-type'] && headers['content-type'][0]
@@ -55,7 +57,7 @@ export class AwsLambdaProxy extends ServerProxy<ApiGatewayRequest, APIGatewayPro
const isBase64Encoded = binaryMimeTypes.includes(contentType);
return {
body: body.toString(isBase64Encoded ? 'base64' : 'utf8'),
- statusCode: statusCode || 200,
+ statusCode: statusCode || HttpStatus.OK,
multiValueHeaders: headers,
isBase64Encoded,
};

MODIFY
packages/serverless/src/+awsLambda/index.ts
packages/serverless/src/+awsLambda/index.ts
-import { APIGatewayProxyEvent, Context } from 'aws-lambda';
-import { ServerApp } from '../serverProxy';
-import { AwsLambdaProxy } from './awsLambdaProxy';
-import { AwsLambdaProxyOptions } from './awsLambda.types';
-
-export const createAwsLambdaHandler = (app: ServerApp, options: AwsLambdaProxyOptions) => {
- const proxy = new AwsLambdaProxy(app, options);
-
- const handler = (event: APIGatewayProxyEvent, context: Context) =>
- proxy.handle({ event, context });
- handler.close = () => proxy.close();
- return handler;
-};
+export * from './awsLambdaHandler';
+export * from './awsLambda.middleware';
+export * from './awsLambda.types';

MODIFY
packages/serverless/src/+awsLambda/spec/awsLambda.middleware.spec.ts
packages/serverless/src/+awsLambda/spec/awsLambda.middleware.spec.ts
import { of } from 'rxjs';
import { awsApiGatewayMiddleware$ } from '../awsLambda.middleware';
-import { HttpRequest } from '@marblejs/core';
+import { AwsLambdaHeaders } from '../awsLambda.types';
+import { createHttpRequest } from '@marblejs/core/dist/+internal/testing/http.helper';
describe('@marblejs/serverless - AWS API Gateway Middleware', () => {
test('extracts headers from request', async () => {
const request = await awsApiGatewayMiddleware$()(
- of({
+ of(createHttpRequest({
+ url: 'ignored',
headers: {
- 'x-apigateway-context': ['%7B%22context%22%3A0%7D'],
- 'x-apigateway-event': '%7B%22event%22%3A1%7D',
+ [AwsLambdaHeaders.AWSLAMBDA_CONTEXT]: ['%7B%22context%22%3A0%7D'],
+ [AwsLambdaHeaders.APIGATEWAY_EVENT]: '%7B%22event%22%3A1%7D',
}
- } as any as HttpRequest),
+ })),
null as any,
null as any
).toPromise();

MODIFY
packages/serverless/src/+awsLambda/spec/awsLambda.setup.ts
packages/serverless/src/+awsLambda/spec/awsLambda.setup.ts
-import { EffectFactory, HttpEffectResponse, httpListener } from '@marblejs/core';
-import { map } from 'rxjs/operators';
+import { HttpEffectResponse, httpListener, r } from '@marblejs/core';
+import { map, mapTo } from 'rxjs/operators';
import { createLambda, ProxyType } from '../../';
import { awsApiGatewayMiddleware$ } from '../awsLambda.middleware';
import { bodyParser$ } from '@marblejs/middleware-body';
import { Readable } from 'stream';
-const effect$ = EffectFactory
- .matchPath('/')
- .matchType('POST')
- .use(req$ => req$.pipe(
+const effect$ = r.pipe(
+ r.matchPath('/'),
+ r.matchType('POST'),
+ r.useEffect(req$ => req$.pipe(
map((req): HttpEffectResponse => ({
status: 200,
body: {
@@ -18,13 +18,14 @@ const effect$ = EffectFactory
context: req.apiGatewayContext,
},
}))
- ));
+ )),
+);
-const chunkedTransferEncodingEffect$ = EffectFactory
- .matchPath('/chunked-transfer')
- .matchType('GET')
- .use(req$ => req$.pipe(
- map((): HttpEffectResponse => ({
+const chunkedTransferEncodingEffect$ = r.pipe(
+ r.matchPath('/chunked-transfer'),
+ r.matchType('GET'),
+ r.useEffect(req$ => req$.pipe(
+ mapTo(({
status: 200,
headers: {
'transfer-encoding': 'chunked',
@@ -36,7 +37,8 @@ const chunkedTransferEncodingEffect$ = EffectFactory
}
}),
}))
- ));
+ ))
+);
const app = httpListener({
middlewares: [

MODIFY
packages/serverless/src/+awsLambda/spec/awsLambda.spec.ts
packages/serverless/src/+awsLambda/spec/awsLambda.spec.ts
-import { createHandler } from './awsLambda.setup';
+import { httpListener } from '@marblejs/core';
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
+import { createHandler } from './awsLambda.setup';
import { createLambda } from '../../handler';
-import { httpListener } from '@marblejs/core';
describe('@marblejs/serverless - AWS Proxy integration', () => {
let handler;
@@ -43,14 +43,16 @@ describe('@marblejs/serverless - AWS Proxy integration', () => {
httpMethod: 'GET',
path: '/chunked-transfer',
}, {});
+
expect(response.statusCode).toBe(200);
expect(response.multiValueHeaders).toBeDefined();
+ // TODO Extract it to a separate method in proxy helpers (is already done in upcoming PR)
const getHeader = (key: string) =>
response.multiValueHeaders &&
response.multiValueHeaders[key] &&
response.multiValueHeaders[key][0];
expect(getHeader('content-type')).toBe('application/json');
- expect(getHeader('content-length')).toBe('15');
+ expect(getHeader('Content-Length')).toBe('15');
expect(JSON.parse(response.body)).toEqual({ key: 'value' });
});
});

MODIFY
packages/serverless/src/+awsLambda/spec/awsLambdaProxy.spec.ts
packages/serverless/src/+awsLambda/spec/awsLambdaProxy.spec.ts
import { AwsLambdaProxy } from '../awsLambdaProxy';
+import { AwsLambdaHeaders } from '../awsLambda.types';
describe('@marblejs/serverless - AWS Proxy', () => {
let proxy: AwsLambdaProxy;
@@ -6,6 +7,7 @@ describe('@marblejs/serverless - AWS Proxy', () => {
beforeEach(() => {
proxy = new AwsLambdaProxy(() => void 0);
});
+
afterEach(() => {
proxy.close();
});
@@ -34,8 +36,8 @@ describe('@marblejs/serverless - AWS Proxy', () => {
path: '/',
body: undefined,
headers: {
- 'x-apigateway-context': '%7B%7D',
- 'x-apigateway-event': '%7B%22httpMethod%22%3A%22GET%22%2C%22path%22%3A%22%2F%22%7D',
+ [AwsLambdaHeaders.AWSLAMBDA_CONTEXT]: '%7B%7D',
+ [AwsLambdaHeaders.APIGATEWAY_EVENT]: '%7B%22httpMethod%22%3A%22GET%22%2C%22path%22%3A%22%2F%22%7D',
},
});
});
@@ -56,13 +58,30 @@ describe('@marblejs/serverless - AWS Proxy', () => {
path: '/',
body: Buffer.from('test'),
headers: {
- 'x-apigateway-context': '%7B%7D',
- 'x-apigateway-event': '%7B%22httpMethod%22%3A%22GET%22%2C%22path%22%3A%22%2F%22%2C%22isBase64Encoded%22%3Atrue%7D',
+ [AwsLambdaHeaders.AWSLAMBDA_CONTEXT]: '%7B%7D',
+ [AwsLambdaHeaders.APIGATEWAY_EVENT]: '%7B%22httpMethod%22%3A%22GET%22%2C%22path%22%3A%22%2F%22%2C%22isBase64Encoded%22%3Atrue%7D',
},
});
});
test('#normalizeResponse() ', async () => {
-
+ const body = JSON.stringify({ test: 'json body' });
+ const normalizedResponse = await Promise.resolve(proxy.normalizeResponse({
+ statusMessage: 'OK',
+ statusCode: 200,
+ body: Buffer.from(body),
+ headers: {
+ 'Content-Type': ['application/json'],
+ },
+ }));
+ expect(normalizedResponse).toEqual({
+ multiValueHeaders: {
+ 'Content-Type': ['application/json'],
+ 'Content-Length': ['20'],
+ },
+ statusCode: 200,
+ body,
+ isBase64Encoded: false,
+ });
});
});

MODIFY
packages/serverless/src/serverProxy.ts
packages/serverless/src/serverProxy.ts
-import { createServer, IncomingMessage, OutgoingMessage, request, Server } from 'http';
+import { createServer, IncomingMessage, OutgoingHttpHeaders, OutgoingMessage, request, Server } from 'http';
import { makeSocketPath, normalizeHeaders } from './serverProxy.helpers';
+import { HttpMethod } from '@marblejs/core';
type Resolver<T> = (result: T | Promise<T>) => void;
export type ServerApp = (req: IncomingMessage, res: OutgoingMessage) => void;
export interface ServerProxyRequest {
- method: string;
+ method: HttpMethod;
path: string;
host?: string;
protocol?: string;
- headers?: Record<string, string>;
+ headers?: OutgoingHttpHeaders;
body?: Buffer;
}
@@ -23,7 +24,7 @@ export interface ServerProxyResponse {
export abstract class ServerProxy<ProxyRequest, ProxyResponse> {
private readonly server: Server;
- private listening: boolean = false;
+ private listening = false;
private socketPath: string;
constructor(
@@ -95,7 +96,7 @@ export abstract class ServerProxy<ProxyRequest, ProxyResponse> {
response
.on('data', chunk => bufferChunks.push(chunk))
.on('end', () => {
- let { statusCode, statusMessage, headers } = response;
+ const { statusCode, statusMessage, headers } = response;
resolve(this.normalizeResponse({
statusCode,

FEAT(Serverless AWS Lambda): Post-CR fixes