Skip to content

Commit 4b26714

Browse files
committed
finish adding generic types to wrappers and signatures
1 parent 91ac316 commit 4b26714

File tree

9 files changed

+120
-41
lines changed

9 files changed

+120
-41
lines changed

README.md

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<p align="center">
2-
<img height="140" src="https://avatars0.githubusercontent.com/u/36457275?s=400&u=16d355f384ed7f8e0655b7ed1d70ff2e411690d8&v=4e">
3-
<img height="140" src="https://user-images.githubusercontent.com/2955468/44874383-0168f780-ac69-11e8-8e51-774678cbd966.png">
2+
<img height="150" src="https://d1wzvcwrgjaybe.cloudfront.net/repos/manwaring/lambda-wrapper/readme-category-icon.png">
3+
<img height="150" src="https://d1wzvcwrgjaybe.cloudfront.net/repos/manwaring/lambda-wrapper/readme-repo-icon.png">
44
</p>
55

66
<p align="center">
@@ -86,8 +86,10 @@ All of the events bellow have a corresponding wrapper which provides a deconstru
8686

8787
```ts
8888
import { api } from '@manwaring/lambda-wrapper';
89+
import { CustomInterface } from './custom-interface';
8990

90-
export const handler = api(async ({ body, path, success, error }) => {
91+
// By passing in CustomInterface as a generic the async method signature will correctly identify newVersions as an array of CustomInterface, making TypeScript development easier (note that the generic is not required in JavaScript projects)
92+
export const handler = api<CustomInterface>(async ({ body, path, success, error }) => {
9193
try {
9294
const { pathParam1, pathParam2 } = path;
9395
const results = await doSomething(body, pathParam1, pathParam2);
@@ -101,12 +103,12 @@ export const handler = api(async ({ body, path, success, error }) => {
101103
### Properties and methods available on wrapper signature
102104

103105
```ts
104-
export interface ApiSignature {
106+
export interface ApiSignature<T> {
105107
event: APIGatewayEvent; // original event
106-
body: any; // JSON parsed body payload if exists (otherwise null)
107-
path: { [name: string]: string }; // path param payload as key-value pairs if exists (otherwise null)
108-
query: { [name: string]: string }; // query param payload as key-value pairs if exists (otherwise null)
109-
headers: { [name: string]: string }; // header payload as key-value pairs if exists (otherwise null)
108+
body: T; // JSON parsed body payload if exists (otherwise undefined)
109+
path: { [name: string]: string }; // path param payload as key-value pairs if exists (otherwise undefined)
110+
query: { [name: string]: string }; // query param payload as key-value pairs if exists (otherwise undefined)
111+
headers: { [name: string]: string }; // header payload as key-value pairs if exists (otherwise undefined)
110112
testRequest: boolean; // indicates if this is a test request - looks for a header matching process.env.TEST_REQUEST_HEADER (dynamic from application) or 'Test-Request' (default)
111113
auth: any; // auth context from custom authorizer if exists (otherwise null)
112114
success(payload?: any, replacer?: (this: any, key: string, value: any) => any): ApiResponse; // returns 200 status code with optional payload as body
@@ -161,6 +163,7 @@ interface CloudFormationSignature {
161163

162164
```ts
163165
import { dynamodbStream } from '@manwaring/lambda-wrapper';
166+
import { CustomInterface } from './custom-interface';
164167

165168
// By passing in CustomInterface as a generic the async method signature will correctly identify newVersions as an array of CustomInterface, making TypeScript development easier (note that the generic is not required in JavaScript projects)
166169
export const handler = dynamodbStream<CustomInterface>(async ({ newVersions, success, error }) => {
@@ -251,8 +254,10 @@ interface Policy {
251254

252255
```ts
253256
import { sns } from '@manwaring/lambda-wrapper';
257+
import { CustomInterface } from './custom-interface';
254258

255-
export const handler = sns(async ({ message, success, error }) => {
259+
// By passing in CustomInterface as a generic the async method signature will correctly identify newVersions as an array of CustomInterface, making TypeScript development easier (note that the generic is not required in JavaScript projects)
260+
export const handler = sns<CustomInterface>(async ({ message, success, error }) => {
256261
try {
257262
console.log(message);
258263
return success();
@@ -279,8 +284,10 @@ interface SnsSignature {
279284

280285
```ts
281286
import { wrapper } from '@manwaring/lambda-wrapper';
287+
import { CustomInterface } from './custom-interface';
282288

283-
export const handler = wrapper(async ({ event, success, error }) => {
289+
// By passing in CustomInterface as a generic the async method signature will correctly identify newVersions as an array of CustomInterface, making TypeScript development easier (note that the generic is not required in JavaScript projects)
290+
export const handler = wrapper<CustomInterface>(async ({ event, success, error }) => {
284291
try {
285292
const { value1, value2 } = event;
286293
const results = await doSomething(value1, value2);
@@ -294,8 +301,8 @@ export const handler = wrapper(async ({ event, success, error }) => {
294301
### Properties and methods available on wrapper signature
295302

296303
```ts
297-
interface WrapperSignature {
298-
event: any; // original event
304+
interface WrapperSignature<T> {
305+
event: T; // original event
299306
success(message?: any): any; // logs and returns the message
300307
error(error?: any): void; // logs the error and throws
301308
}

src/api/wrapper.test.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('API wrapper', () => {
66
body: JSON.stringify({ hello: 'world' }),
77
pathParameters: { proxy: 'not today' },
88
queryStringParameters: { name: 'a test' },
9-
headers: { 'content-type': 'application/json', 'Test-Request': 'true' }
9+
headers: { 'content-type': 'application/json', 'Test-Request': 'true' },
1010
});
1111
const context = {
1212
callbackWaitsForEmptyEventLoop: false,
@@ -20,11 +20,12 @@ describe('API wrapper', () => {
2020
getRemainingTimeInMillis: () => 2,
2121
done: () => {},
2222
fail: () => {},
23-
succeed: () => {}
23+
succeed: () => {},
2424
};
2525
const callback = jest.fn((err, result) => (err ? new Error(err) : result));
2626

2727
it('Has expected properties and response functions', () => {
28+
// @ts-ignore
2829
function custom({
2930
event,
3031
body,
@@ -38,7 +39,7 @@ describe('API wrapper', () => {
3839
notAuthorized,
3940
invalid,
4041
redirect,
41-
error
42+
error,
4243
}: ApiSignature) {
4344
expect(event).toEqual(requestEvent);
4445
expect(body).toEqual({ hello: 'world' });
@@ -57,4 +58,42 @@ describe('API wrapper', () => {
5758
}
5859
api(custom)(requestEvent, context, callback);
5960
});
61+
62+
it('Has expected properties and response functions with optional generic type', () => {
63+
interface CustomType {
64+
Message: string;
65+
Id: number;
66+
}
67+
function custom<CustomType>({
68+
event,
69+
body,
70+
path,
71+
query,
72+
headers,
73+
testRequest,
74+
auth,
75+
success,
76+
notFound,
77+
notAuthorized,
78+
invalid,
79+
redirect,
80+
error,
81+
}: ApiSignature<CustomType>) {
82+
expect(event).toEqual(requestEvent);
83+
expect(body).toEqual({ hello: 'world' });
84+
expect(path).toEqual({ proxy: 'not today' });
85+
expect(query).toEqual({ name: 'a test' });
86+
expect(headers['content-type']).toEqual('application/json');
87+
expect(testRequest).toEqual(true);
88+
expect(auth).toBeFalsy();
89+
expect(success).toBeInstanceOf(Function);
90+
expect(notFound).toBeInstanceOf(Function);
91+
expect(notAuthorized).toBeInstanceOf(Function);
92+
expect(invalid).toBeInstanceOf(Function);
93+
expect(redirect).toBeInstanceOf(Function);
94+
expect(error).toBeInstanceOf(Function);
95+
success('success');
96+
}
97+
api(custom)(requestEvent, context, callback);
98+
});
6099
});

src/api/wrapper.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { APIGatewayEvent, Context, Callback } from 'aws-lambda';
22
import { Request } from './parser';
33
import { success, invalid, notFound, notAuthorized, error, redirect, ApiResponse } from './responses';
44

5-
export function api(
6-
custom: (props: ApiSignature) => any
5+
export function api<T>(
6+
custom: (props: ApiSignature<T>) => any
77
): (event: APIGatewayEvent, context: Context, callback: Callback) => any {
88
return function handler(event: APIGatewayEvent) {
99
const { body, path, query, auth, headers, testRequest } = new Request(event).getProperties();
10-
const signature: ApiSignature = {
10+
const signature: ApiSignature<T> = {
1111
event,
1212
body,
1313
path,
@@ -20,20 +20,20 @@ export function api(
2020
notFound,
2121
notAuthorized,
2222
error,
23-
redirect
23+
redirect,
2424
};
2525
return custom(signature);
2626
};
2727
}
2828

29-
export interface ApiSignature {
29+
export interface ApiSignature<T> {
3030
event: APIGatewayEvent; // original event
31-
body: any; // JSON parsed body payload if exists (otherwise null)
32-
path: { [name: string]: string }; // path param payload as key-value pairs if exists (otherwise null)
33-
query: { [name: string]: string }; // query param payload as key-value pairs if exists (otherwise null)
34-
headers: { [name: string]: string }; // header payload as key-value pairs if exists (otherwise null)
31+
body: T; // JSON parsed body payload if exists (otherwise undefined)
32+
path: { [name: string]: string }; // path param payload as key-value pairs if exists (otherwise undefined)
33+
query: { [name: string]: string }; // query param payload as key-value pairs if exists (otherwise undefined)
34+
headers: { [name: string]: string }; // header payload as key-value pairs if exists (otherwise undefined)
3535
testRequest: boolean; // indicates if this is a test request - looks for a header matching process.env.TEST_REQUEST_HEADER (dynamic from application) or 'Test-Request' (default)
36-
auth: any; // auth context from custom authorizer if exists (otherwise null)
36+
auth: any; // auth context from custom authorizer if exists (otherwise undefined)
3737
success(payload?: any, replacer?: (this: any, key: string, value: any) => any): ApiResponse; // returns 200 status code with optional payload as body
3838
invalid(errors?: string[]): ApiResponse; // returns 400 status code with optional errors as body
3939
notFound(message?: string): ApiResponse; // returns 404 status code with optional message as body

src/dynamodb-stream/wrapper.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ describe('DynamoDB Stream wrapper', () => {
55
const requestEvent = dynamoDBStreamEvent();
66
const callback = jest.fn((err, result) => (err ? new Error(err) : result));
77

8-
it('Has expected properties and response function without optional types', () => {
8+
it('Has expected properties and response function', () => {
9+
// @ts-ignore
910
function custom({ event, newVersions, oldVersions, versions, success, error }: DynamoDBStreamSignature) {
1011
expect(event).toEqual(requestEvent);
1112
expect(newVersions).toEqual([{ Message: 'This item has changed', Id: 101 }]);
@@ -27,7 +28,7 @@ describe('DynamoDB Stream wrapper', () => {
2728
dynamodbStream(custom)(requestEvent, context, callback);
2829
});
2930

30-
it('Has expected properties and response function with optional types', () => {
31+
it('Has expected properties and response function with optional type generics', () => {
3132
interface CustomType {
3233
Message: string;
3334
Id: number;

src/generic/wrapper.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ describe('Stream wrapper', () => {
1414
getRemainingTimeInMillis: () => 2,
1515
done: () => {},
1616
fail: () => {},
17-
succeed: () => {}
17+
succeed: () => {},
1818
};
1919
const callback = jest.fn((err, result) => (err ? new Error(err) : result));
2020

2121
it('Has expected properties and response funtions', () => {
22+
// @ts-ignore
2223
function custom({ event, success, error }: WrapperSignature) {
2324
expect(event).toEqual(requestEvent);
2425
expect(success).toBeInstanceOf(Function);
@@ -28,4 +29,19 @@ describe('Stream wrapper', () => {
2829

2930
wrapper(custom)(requestEvent, context, callback);
3031
});
32+
33+
it('Has expected properties and response funtions with generic types', () => {
34+
interface CustomType {
35+
Message: string;
36+
Id: Number;
37+
}
38+
function custom<CustomType>({ event, success, error }: WrapperSignature<CustomType>) {
39+
expect(event).toEqual(requestEvent);
40+
expect(success).toBeInstanceOf(Function);
41+
expect(error).toBeInstanceOf(Function);
42+
success('success');
43+
}
44+
45+
wrapper(custom)(requestEvent, context, callback);
46+
});
3147
});

src/generic/wrapper.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ import { success, error } from './responses';
44

55
const metrics = new Metrics('Generic');
66

7-
export function wrapper(
8-
custom: (props: WrapperSignature) => any
7+
export function wrapper<T>(
8+
custom: (props: WrapperSignature<T>) => any
99
): (event: any, context: Context, callback: Callback) => any {
1010
return function handler(event: any, context: Context, callback: Callback) {
1111
metrics.common(event);
1212
return custom({ event, success, error });
1313
};
1414
}
1515

16-
export interface WrapperSignature {
17-
event: any; // original event
16+
export interface WrapperSignature<T> {
17+
event: T; // original event
1818
success(message?: any): any; // logs and returns the message
1919
error(error?: any): void; // logs the error and throws
2020
}

src/sns/parser.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { SNSEvent } from 'aws-lambda';
22

3-
export class SnsParser {
3+
export class SnsParser<T> {
44
constructor(private event: SNSEvent) {}
55

6-
getMessage() {
6+
getMessage(): T {
77
let message = this.event.Records[0].Sns.Message;
88
try {
99
message = JSON.parse(message);
1010
} catch (err) {}
11-
return message;
11+
return (<unknown>message) as T;
1212
}
1313
}

src/sns/wrapper.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ describe('Stream wrapper', () => {
1616
getRemainingTimeInMillis: () => 2,
1717
done: () => {},
1818
fail: () => {},
19-
succeed: () => {}
19+
succeed: () => {},
2020
};
2121
const callback = jest.fn((err, result) => (err ? new Error(err) : result));
2222

2323
it('Has expected properties and response funtions', () => {
24+
// @ts-ignore
2425
function custom({ event, message, success, error }: SnsSignature) {
2526
expect(event).toEqual(requestEvent);
2627
expect(message).toEqual('hello world');
@@ -30,4 +31,19 @@ describe('Stream wrapper', () => {
3031
}
3132
sns(custom)(requestEvent, context, callback);
3233
});
34+
35+
it('Has expected properties and response funtions with optional type generics', () => {
36+
interface CustomType {
37+
Message: string;
38+
Id: number;
39+
}
40+
function custom({ event, message, success, error }: SnsSignature<CustomType>) {
41+
expect(event).toEqual(requestEvent);
42+
expect(message).toEqual('hello world');
43+
expect(success).toBeInstanceOf(Function);
44+
expect(error).toBeInstanceOf(Function);
45+
success('success');
46+
}
47+
sns<CustomType>(custom)(requestEvent, context, callback);
48+
});
3349
});

src/sns/wrapper.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@ import { SnsParser } from './parser';
55

66
const metrics = new Metrics('Sns');
77

8-
export function sns(
9-
custom: (props: SnsSignature) => any
8+
export function sns<T>(
9+
custom: (props: SnsSignature<T>) => any
1010
): (event: SNSEvent, context: Context, callback: Callback) => any {
1111
return function handler(event: SNSEvent, context: Context, callback: Callback) {
12-
const message = new SnsParser(event).getMessage();
12+
const message = new SnsParser<T>(event).getMessage();
1313
metrics.common(message);
1414
return custom({ event, message, success, error });
1515
};
1616
}
1717

18-
export interface SnsSignature {
18+
export interface SnsSignature<T> {
1919
event: SNSEvent; // original event
20-
message: any; // JSON-parsed message from event
20+
message: T; // JSON-parsed message from event
2121
success(message?: any): any; // logs and returns the message
2222
error(error?: any): void; // logs the error and throws
2323
}

0 commit comments

Comments
 (0)