Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"conventional-changelog-cli": "^2.2.2",
"conventional-github-releaser": "^3.1.5",
"ospec": "^4.1.1",
"sinon": "^13.0.1",
"sinon": "^14.0.0",
"source-map-support": "^0.5.21"
},
"scripts": {
Expand Down
5 changes: 3 additions & 2 deletions src/__test__/alb.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context } from 'aws-lambda';
import o from 'ospec';
import { LambdaAlbRequest } from '../request.alb.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample } from './examples.js';
import { LambdaAlbRequest } from '../http/request.alb.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample, UrlExample } from './examples.js';
import { fakeLog } from './log.js';

o.spec('AlbGateway', () => {
Expand All @@ -11,6 +11,7 @@ o.spec('AlbGateway', () => {
o(LambdaAlbRequest.is(ApiGatewayExample)).equals(false);
o(LambdaAlbRequest.is(CloudfrontExample)).equals(false);
o(LambdaAlbRequest.is(AlbExample)).equals(true);
o(LambdaAlbRequest.is(UrlExample)).equals(false);
});

o('should extract headers', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/api.gateway.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Context } from 'aws-lambda';
import o from 'ospec';
import { LambdaApiGatewayRequest } from '../request.api.gateway.js';
import { LambdaApiGatewayRequest } from '../http/request.api.gateway.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample } from './examples.js';
import { fakeLog } from './log.js';

Expand Down
5 changes: 3 additions & 2 deletions src/__test__/cloudfront.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { CloudFrontRequestEvent, Context } from 'aws-lambda';
import o from 'ospec';
import { LambdaCloudFrontRequest } from '../request.cloudfront.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample } from './examples.js';
import { LambdaCloudFrontRequest } from '../http/request.cloudfront.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample, UrlExample } from './examples.js';
import { fakeLog } from './log.js';

o.spec('CloudFront', () => {
Expand All @@ -10,6 +10,7 @@ o.spec('CloudFront', () => {
o(LambdaCloudFrontRequest.is(CloudfrontExample)).equals(true);
o(LambdaCloudFrontRequest.is(AlbExample)).equals(false);
o(LambdaCloudFrontRequest.is(ApiGatewayExample)).equals(false);
o(LambdaCloudFrontRequest.is(UrlExample)).equals(false);
});

o('should extract headers', () => {
Expand Down
38 changes: 38 additions & 0 deletions src/__test__/examples.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'source-map-support/register.js';
import { ALBEvent, APIGatewayProxyEvent, CloudFrontRequestEvent } from 'aws-lambda';
import { UrlEvent } from '../http/request.function';

export const ApiGatewayExample: APIGatewayProxyEvent = {
body: 'eyJ0ZXN0IjoiYm9keSJ9',
Expand Down Expand Up @@ -154,6 +155,43 @@ export const AlbExample: ALBEvent = {
isBase64Encoded: true,
};

export const UrlExample: UrlEvent = {
version: '2.0',
routeKey: '$default',
rawPath: '/v1/🦄/🌈/🦄.json',
rawQueryString: '%F0%9F%A6%84=abc123',
headers: {
'x-amzn-trace-id': 'Root=1-624e71a0-114297900a437c050c74f1fe',
'x-forwarded-proto': 'https',
host: 'fakeId.lambda-url.ap-southeast-2.on.aws',
'x-forwarded-port': '443',
'x-forwarded-for': '10.88.254.254',
'accept-encoding': 'br,gzip',
'x-amz-cf-id': '5jJe5RyAHtE6OmIFkedddTRlFpvHYZvGIwoWNEm9YJ0OUHOFVET_Pw==',
'user-agent': 'Amazon CloudFront',
via: '2.0 db2406d2a95ec212c318a2e2518f9244.cloudfront.net (CloudFront)',
},
requestContext: {
accountId: 'anonymous',
apiId: 'fakeId',
domainName: 'fakeId.lambda-url.ap-southeast-2.on.aws',
domainPrefix: 'fakeId',
http: {
method: 'GET',
path: '/v1/🦄/🌈/🦄.json',
protocol: 'HTTP/1.1',
sourceIp: '64.252.109.40',
userAgent: 'Amazon CloudFront',
},
requestId: '6ffbc360-d84e-463c-a112-dcc6279cb4bb',
routeKey: '$default',
stage: '$default',
time: '07/Apr/2022:05:07:44 +0000',
timeEpoch: 1649308064171,
},
isBase64Encoded: false,
};

export function clone<T>(c: T): T {
return JSON.parse(JSON.stringify(c));
}
2 changes: 1 addition & 1 deletion src/__test__/readme.example.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { S3Event } from 'aws-lambda';
import { lf } from '../function.js';
import { LambdaRequest } from '../request.js';
import { LambdaHttpResponse } from '../response.http.js';
import { LambdaHttpResponse } from '../http/response.http.js';

export async function main(req: LambdaRequest<S3Event>): Promise<void> {
console.log('foo', req.id);
Expand Down
2 changes: 1 addition & 1 deletion src/__test__/request.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LambdaAlbRequest } from '../request.alb.js';
import { LambdaAlbRequest } from '../http/request.alb.js';
import { AlbExample, clone } from './examples.js';
import { fakeLog } from './log.js';
import o from 'ospec';
Expand Down
50 changes: 50 additions & 0 deletions src/__test__/url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Context } from 'aws-lambda';
import o from 'ospec';
import { LambdaUrlRequest } from '../http/request.function.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample, UrlExample } from './examples.js';
import { fakeLog } from './log.js';

o.spec('FunctionUrl', () => {
const fakeContext = {} as Context;

o('should match the event', () => {
o(LambdaUrlRequest.is(ApiGatewayExample)).equals(false);
o(LambdaUrlRequest.is(CloudfrontExample)).equals(false);
o(LambdaUrlRequest.is(AlbExample)).equals(false);
o(LambdaUrlRequest.is(UrlExample)).equals(true);
});

o('should extract headers', () => {
const req = new LambdaUrlRequest(UrlExample, fakeContext, fakeLog);

o(req.header('accept-encoding')).equals('br,gzip');
o(req.header('Accept-Encoding')).equals('br,gzip');
});

o('should extract methods', () => {
const req = new LambdaUrlRequest(UrlExample, fakeContext, fakeLog);
o(req.method).equals('GET');
});

o('should upper case method', () => {
const newReq = clone(UrlExample);
newReq.requestContext.http.method = 'post';
const req = new LambdaUrlRequest(newReq, fakeContext, fakeLog);
o(req.method).equals('POST');
});

o('should extract query parameters', () => {
const newReq = clone(UrlExample);
newReq.rawQueryString = 'api=abc123';

const req = new LambdaUrlRequest(newReq, fakeContext, fakeLog);
o(req.query.get('api')).deepEquals('abc123');
o(req.query.getAll('api')).deepEquals(['abc123']);
});

o('should support utf8 paths and query', () => {
const req = new LambdaUrlRequest(UrlExample, fakeContext, fakeLog);
o(req.path).equals('/v1/🦄/🌈/🦄.json');
o(req.query.get('🦄')).equals('abc123');
});
});
6 changes: 3 additions & 3 deletions src/__test__/wrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import o from 'ospec';
import sinon from 'sinon';
import { lf } from '../function.js';
import { LambdaRequest } from '../request.js';
import { LambdaHttpRequest } from '../request.http.js';
import { LambdaHttpResponse } from '../response.http.js';
import { LambdaHttpRequest } from '../http/request.http.js';
import { LambdaHttpResponse } from '../http/response.http.js';
import { AlbExample, ApiGatewayExample, clone, CloudfrontExample } from './examples.js';
import { fakeLog } from './log.js';
import { HttpMethods } from '../router.js';
import { HttpMethods } from '../http/router.js';

function assertAlbResult(x: unknown): asserts x is ALBResult {}
function assertCloudfrontResult(x: unknown): asserts x is CloudFrontResultResponse {}
Expand Down
14 changes: 8 additions & 6 deletions src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { LambdaResponse } from './response.js';
import { ApplicationJson, HttpHeader, HttpHeaderAmazon, HttpHeaderRequestId } from './header.js';
import { LogType } from './log.js';
import { LambdaRequest } from './request.js';
import { LambdaAlbRequest } from './request.alb.js';
import { LambdaApiGatewayRequest } from './request.api.gateway.js';
import { LambdaCloudFrontRequest } from './request.cloudfront.js';
import { HttpRequestEvent, HttpResponse, LambdaHttpRequest } from './request.http.js';
import { LambdaHttpResponse } from './response.http.js';
import { Router } from './router.js';
import { LambdaAlbRequest } from './http/request.alb.js';
import { LambdaApiGatewayRequest } from './http/request.api.gateway.js';
import { LambdaCloudFrontRequest } from './http/request.cloudfront.js';
import { HttpRequestEvent, HttpResponse, LambdaHttpRequest } from './http/request.http.js';
import { LambdaHttpResponse } from './http/response.http.js';
import { Router } from './http/router.js';
import { LambdaUrlRequest } from './http/request.function.js';

export interface HttpStatus {
statusCode: string;
Expand Down Expand Up @@ -119,6 +120,7 @@ export class lf {

static request(req: HttpRequestEvent, ctx: Context, log: LogType): LambdaHttpRequest {
if (LambdaAlbRequest.is(req)) return new LambdaAlbRequest(req, ctx, log);
if (LambdaUrlRequest.is(req)) return new LambdaUrlRequest(req, ctx, log);
if (LambdaApiGatewayRequest.is(req)) return new LambdaApiGatewayRequest(req, ctx, log);
if (LambdaCloudFrontRequest.is(req)) return new LambdaCloudFrontRequest(req, ctx, log);
throw new Error('Request is not a a ALB, ApiGateway or Cloudfront event');
Expand Down
2 changes: 1 addition & 1 deletion src/request.alb.ts → src/http/request.alb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ALBEvent, ALBResult } from 'aws-lambda';
import { URLSearchParams } from 'url';
import { isRecord } from './request.js';
import { isRecord } from '../request.js';
import { LambdaHttpRequest } from './request.http.js';
import { LambdaHttpResponse } from './response.http.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { APIGatewayEvent, APIGatewayProxyResultV2 } from 'aws-lambda';
import { URLSearchParams } from 'url';
import { isRecord } from './request.js';
import { isRecord } from '../request.js';
import { LambdaHttpRequest } from './request.http.js';
import { LambdaHttpResponse } from './response.http.js';

Expand All @@ -17,12 +17,12 @@ export class LambdaApiGatewayRequest<T extends Record<string, string>> extends L
return {
statusCode: res.status,
body: res.body,
headers: this.toHeaders(res),
headers: LambdaApiGatewayRequest.toHeaders(res),
isBase64Encoded: res.isBase64Encoded,
};
}

toHeaders(res: LambdaHttpResponse): Record<string, string> | undefined {
static toHeaders(res: LambdaHttpResponse): Record<string, string> | undefined {
if (res.headers.size === 0) return undefined;

const obj: Record<string, string> = {};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CloudFrontRequestEvent, CloudFrontRequestResult } from 'aws-lambda';
import { URLSearchParams } from 'url';
import { isRecord } from './request.js';
import { isRecord } from '../request.js';
import { LambdaHttpRequest } from './request.http.js';
import { LambdaHttpResponse } from './response.http.js';

Expand Down
80 changes: 80 additions & 0 deletions src/http/request.function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { URLSearchParams } from 'url';
import { isRecord } from '../request.js';
import { LambdaApiGatewayRequest } from './request.api.gateway.js';
import { LambdaHttpRequest } from './request.http.js';
import { LambdaHttpResponse } from './response.http.js';

export interface UrlEvent {
version: '2.0';
routeKey: string;

rawPath: string;
/** Query string without '?' @example "api=abc123" */
rawQueryString: string;
headers: Record<string, string>;
requestContext: {
accountId: string;
apiId: string;
domainName: string;
domainPrefix: string;
requestId: string;
http: {
method: string;
path: string;
protocol: string;
sourceIp: string;
userAgent: string;
};
routeKey: string;
stage: string;
time: string;
timeEpoch: number;
};
isBase64Encoded: boolean;
}

export interface UrlResult {
statusCode: number;
headers?: Record<string, string>;
body: string;
isBase64Encoded: boolean;
}

export class LambdaUrlRequest<T extends Record<string, string>> extends LambdaHttpRequest<T, UrlEvent, UrlResult> {
toResponse(res: LambdaHttpResponse): UrlResult {
return {
statusCode: res.status,
body: res.body,
headers: LambdaApiGatewayRequest.toHeaders(res),
isBase64Encoded: res.isBase64Encoded,
};
}
loadHeaders(): void {
for (const [key, value] of Object.entries(this.event.headers)) {
this.headers.set(key.toLowerCase(), value);
}
}

loadQueryString(): URLSearchParams {
return new URLSearchParams(this.event.rawQueryString);
}

get method(): string {
return this.event.requestContext.http.method.toUpperCase();
}
get path(): string {
return this.event.rawPath;
}

get isBase64Encoded(): boolean {
return this.event.isBase64Encoded;
}

get body(): string | null {
return null;
}

static is(x: unknown): x is UrlEvent {
return isRecord(x) && isRecord(x['requestContext']) && isRecord(x['requestContext']['http']);
}
}
11 changes: 6 additions & 5 deletions src/request.http.ts → src/http/request.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import {
} from 'aws-lambda';
import * as ulid from 'ulid';
import { URLSearchParams } from 'url';
import { ApplicationJson, HttpHeader, HttpHeaderRequestId } from './header.js';
import { LogType } from './log.js';
import { LambdaRequest } from './request.js';
import { ApplicationJson, HttpHeader, HttpHeaderRequestId } from '../header.js';
import { LogType } from '../log.js';
import { LambdaRequest } from '../request.js';
import { UrlEvent, UrlResult } from './request.function.js';
import { LambdaHttpResponse } from './response.http.js';

export type HttpRequestEvent = ALBEvent | CloudFrontRequestEvent | APIGatewayProxyEvent;
export type HttpResponse = ALBResult | CloudFrontRequestResult | APIGatewayProxyResultV2;
export type HttpRequestEvent = ALBEvent | CloudFrontRequestEvent | APIGatewayProxyEvent | UrlEvent;
export type HttpResponse = ALBResult | CloudFrontRequestResult | APIGatewayProxyResultV2 | UrlResult;

// TODO these should ideally be validated before being given to the api, should this force a ZOD validation step
export interface RequestTypes {
Expand Down
12 changes: 9 additions & 3 deletions src/response.http.ts → src/http/response.http.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApplicationJson, HttpHeader, HttpHeaderRequestId } from './header.js';
import { ApplicationJson, HttpHeader, HttpHeaderRequestId } from '../header.js';

export class LambdaHttpResponse {
/** Http status code */
Expand All @@ -14,6 +14,10 @@ export class LambdaHttpResponse {
return x instanceof LambdaHttpResponse;
}

static ok(code = 200, status = 'Ok'): LambdaHttpResponse {
return new LambdaHttpResponse(code, status);
}

public constructor(status: number, description: string, headers?: Record<string, string>) {
this.status = status;
this.statusDescription = description;
Expand All @@ -37,14 +41,16 @@ export class LambdaHttpResponse {
}

/** Set a JSON output */
json(obj: Record<string, unknown>): void {
json(obj: Record<string, unknown>): LambdaHttpResponse {
this.buffer(JSON.stringify(obj), ApplicationJson);
return this;
}

/** Set the output type and the Content-Type header */
buffer(buf: Buffer | string, contentType = ApplicationJson): void {
buffer(buf: Buffer | string, contentType = ApplicationJson): LambdaHttpResponse {
this.header(HttpHeader.ContentType, contentType);
this._body = buf;
return this;
}

get body(): string {
Expand Down
2 changes: 1 addition & 1 deletion src/router.ts → src/http/router.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execute } from './function.js';
import { execute } from '../function.js';
import { LambdaHttpRequest, RequestTypes } from './request.http.js';
import { LambdaHttpResponse } from './response.http.js';

Expand Down
Loading