Skip to content

Commit 7f256b2

Browse files
authored
Merge pull request #75 from dbartholomae/add-typebox-validator
Add typebox validator middleware
2 parents 91a7ec5 + 1ac1d01 commit 7f256b2

16 files changed

+453
-0
lines changed

packages/typebox-validator/README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# @lambda-middleware/typebox-validator
2+
3+
[![npm version](https://badge.fury.io/js/%40lambda-middleware%2Ftypebox-validator.svg)](https://npmjs.org/package/@lambda-middleware/typebox-validator)
4+
[![downloads](https://img.shields.io/npm/dw/%40lambda-middleware%2Ftypebox-validator.svg)](https://npmjs.org/package/@lambda-middleware/typebox-validator)
5+
[![open issues](https://img.shields.io/github/issues-raw/dbartholomae/lambda-middleware.svg)](https://github.com/dbartholomae/lambda-middleware/issues)
6+
[![debug](https://img.shields.io/badge/debug-blue.svg)](https://github.com/visionmedia/debug#readme)
7+
[![build status](https://github.com/dbartholomae/lambda-middleware/workflows/.github/workflows/build.yml/badge.svg?branch=main)](https://github.com/dbartholomae/lambda-middleware/actions?query=workflow%3A.github%2Fworkflows%2Fbuild.yml)
8+
[![codecov](https://codecov.io/gh/dbartholomae/lambda-middleware/branch/main/graph/badge.svg)](https://codecov.io/gh/dbartholomae/lambda-middleware)
9+
[![dependency status](https://david-dm.org/dbartholomae/lambda-middleware.svg?theme=shields.io)](https://david-dm.org/dbartholomae/lambda-middleware)
10+
[![devDependency status](https://david-dm.org/dbartholomae/lambda-middleware/dev-status.svg)](https://david-dm.org/dbartholomae/lambda-middleware?type=dev)
11+
12+
A validation middleware for AWS http lambda functions based on [typebox](https://github.com/sinclairzx81/typebox).
13+
14+
## Lambda middleware
15+
16+
This middleware is part of the [lambda middleware series](https://dbartholomae.github.io/lambda-middleware/). It can be used independently.
17+
18+
## Usage
19+
20+
```typescript
21+
import { composeHandler } from "@lambda-middleware/compose";
22+
import { errorHandler } from "@lambda-middleware/http-error-handler";
23+
import { Type } from "@sinclair/typebox";
24+
import { typeboxValidator } from "@lambda-middleware/typebox-validator";
25+
import { APIGatewayProxyResult } from "aws-lambda";
26+
27+
const NameBodySchema = Type.Object({
28+
firstName: Type.String(),
29+
lastName: Type.String(),
30+
});
31+
32+
type NameBody = {
33+
firstName: string;
34+
lastName: string;
35+
};
36+
37+
async function helloWorld(event: { body: NameBody }): Promise<APIGatewayProxyResult> {
38+
return {
39+
body: `Hello ${event.body.firstName} ${event.body.lastName}`,
40+
headers: {
41+
"content-type": "text",
42+
},
43+
statusCode: 200,
44+
};
45+
}
46+
47+
export const handler = composeHandler(
48+
errorHandler(),
49+
typeboxValidator(NameBodySchema),
50+
helloWorld
51+
);
52+
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import request from "supertest";
2+
import { handler } from "./helloWorld";
3+
4+
const server = request("http://localhost:3000/dev");
5+
6+
describe("Handler with typebox validator middleware", () => {
7+
describe("with valid input", () => {
8+
it("returns 200", async () => {
9+
const response = await server
10+
.post("/hello")
11+
.send({
12+
firstName: "John",
13+
lastName: "Doe",
14+
})
15+
.expect(200);
16+
expect(response.text).toEqual("Hello John Doe");
17+
});
18+
});
19+
20+
describe("with invalid input", () => {
21+
it("returns 400 and the validation error", async () => {
22+
const response = await server
23+
.post("/hello")
24+
.send({
25+
firstName: "John",
26+
})
27+
.expect(400);
28+
expect(JSON.stringify(response.body)).toContain(
29+
"lastName must be a string"
30+
);
31+
});
32+
});
33+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { composeHandler } from "@lambda-middleware/compose";
2+
import { errorHandler } from "@lambda-middleware/http-error-handler";
3+
import { Type } from "@sinclair/typebox";
4+
import { typeboxValidator } from "../src/typeboxValidator";
5+
import { APIGatewayProxyResult } from "aws-lambda";
6+
7+
const NameBodySchema = Type.Object({
8+
firstName: Type.String(),
9+
lastName: Type.String(),
10+
});
11+
12+
type NameBody = {
13+
firstName: string;
14+
lastName: string;
15+
};
16+
17+
async function helloWorld(event: { body: NameBody }): Promise<APIGatewayProxyResult> {
18+
return {
19+
body: `Hello ${event.body.firstName} ${event.body.lastName}`,
20+
headers: {
21+
"content-type": "text",
22+
},
23+
statusCode: 200,
24+
};
25+
}
26+
27+
export const handler = composeHandler(
28+
errorHandler(),
29+
typeboxValidator(NameBodySchema),
30+
helloWorld
31+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const baseConfig = require("../../jest.unit.config");
2+
module.exports = baseConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const baseConfig = require("../../jest.integration.config");
2+
module.exports = baseConfig;
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
"name": "@lambda-middleware/typebox-validator",
3+
"version": "1.0.0",
4+
"description": "A validation middleware for AWS http lambda functions based on typebox.",
5+
"homepage": "https://dbartholomae.github.io/lambda-middleware/",
6+
"license": "MIT",
7+
"author": {
8+
"name": "Daniel Bartholomae",
9+
"email": "[email protected]",
10+
"url": ""
11+
},
12+
"files": [
13+
"lib"
14+
],
15+
"main": "lib/index.js",
16+
"keywords": [
17+
"aws",
18+
"lambda",
19+
"middleware",
20+
"validation",
21+
"validator"
22+
],
23+
"types": "lib/index.d.ts",
24+
"engines": {
25+
"npm": ">= 4.0.0"
26+
},
27+
"private": false,
28+
"dependencies": {
29+
"@lambda-middleware/utils": "^1.0.4",
30+
"debug": ">=4.1.0",
31+
"@sinclair/typebox": "^0.21.0",
32+
"tslib": "^2.0.1"
33+
},
34+
"directories": {
35+
"example": "examples"
36+
},
37+
"scripts": {
38+
"build": "rimraf ./lib && tsc --project tsconfig.build.json",
39+
"lint": "eslint src/**/*.ts examples/**/*.ts",
40+
"pretest": "npm run build",
41+
"start": "cd test && serverless offline",
42+
"test": "npm run lint && npm run test:unit && npm run test:integration && pkg-ok",
43+
"test:integration": "concurrently --timeout 600000 --kill-others --success first \"cd test && serverless offline\" \"wait-on http://localhost:3000/dev/status && jest -c jest.integration.config.js\"",
44+
"test:unit": "jest"
45+
},
46+
"devDependencies": {
47+
"@lambda-middleware/compose": "^1.2.0",
48+
"@lambda-middleware/http-error-handler": "^2.0.0",
49+
"@types/debug": "^4.1.5",
50+
"@types/jest": "^25.2.1",
51+
"@types/supertest": "^2.0.8",
52+
"@types/aws-lambda": "^8.10.47",
53+
"@typescript-eslint/parser": "^2.26.0",
54+
"@typescript-eslint/eslint-plugin": "^2.26.0",
55+
"aws-lambda": "^1.0.5",
56+
"concurrently": "^5.1.0",
57+
"eslint": "^6.8.0",
58+
"eslint-config-prettier": "^6.10.1",
59+
"eslint-plugin-prettier": "^3.1.2",
60+
"jest": "^25.2.7",
61+
"jest-junit": "^10.0.0",
62+
"pkg-ok": "^2.3.1",
63+
"prettier": "^2.0.2",
64+
"prettier-config-standard": "^1.0.1",
65+
"reflect-metadata": "^0.1.13",
66+
"rimraf": "^3.0.2",
67+
"serverless": "^2.57.0",
68+
"serverless-offline": "^8.0.0",
69+
"serverless-webpack": "^5.5.3",
70+
"source-map-support": "^0.5.16",
71+
"supertest": "^4.0.2",
72+
"ts-jest": "^25.3.1",
73+
"ts-loader": "^6.2.2",
74+
"typescript": "^4.5.4",
75+
"wait-on": "^5.2.0",
76+
"webpack": "^4.41.5"
77+
},
78+
"repository": {
79+
"type": "git",
80+
"url": "[email protected]:dbartholomae/lambda-middleware.git",
81+
"directory": "packages/typebox-validator"
82+
}
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { createContext, createEvent } from "@lambda-middleware/utils";
2+
import { APIGatewayEvent } from "aws-lambda";
3+
import { Type, Static } from "@sinclair/typebox";
4+
import { typeboxValidator } from "./typeboxValidator";
5+
6+
const NameBodySchema = Type.Object({
7+
firstName: Type.String(),
8+
lastName: Type.String(),
9+
});
10+
11+
type NameBody = Static<typeof NameBodySchema>;
12+
13+
describe("typeboxValidator", () => {
14+
describe("with valid input", () => {
15+
const body = JSON.stringify({
16+
firstName: "John",
17+
lastName: "Doe",
18+
});
19+
20+
it("sets the body to the validated value", async () => {
21+
const handler = jest.fn();
22+
await typeboxValidator(NameBodySchema)(handler)(
23+
createEvent({ body }),
24+
createContext()
25+
);
26+
expect(handler).toHaveBeenCalledWith(
27+
expect.objectContaining({
28+
body: {
29+
firstName: "John",
30+
lastName: "Doe",
31+
},
32+
}),
33+
expect.anything()
34+
);
35+
});
36+
});
37+
38+
describe("with superfluous input", () => {
39+
const body = JSON.stringify({
40+
firstName: "John",
41+
lastName: "Doe",
42+
injection: "malicious",
43+
});
44+
45+
it("omits the superfluous input", async () => {
46+
const handler = jest.fn();
47+
await typeboxValidator(NameBodySchema)(handler)(
48+
createEvent({ body }),
49+
createContext()
50+
);
51+
expect(handler).toHaveBeenCalledWith(
52+
expect.objectContaining({
53+
body: {
54+
firstName: "John",
55+
lastName: "Doe",
56+
},
57+
}),
58+
expect.anything()
59+
);
60+
});
61+
});
62+
63+
describe("with invalid input", () => {
64+
const body = JSON.stringify({
65+
firstName: "John",
66+
});
67+
68+
it("throws an error with statusCode 400", async () => {
69+
const handler = jest.fn();
70+
await expect(
71+
typeboxValidator(NameBodySchema)(handler)(
72+
createEvent({ body }),
73+
createContext()
74+
)
75+
).rejects.toMatchObject({
76+
statusCode: 400,
77+
});
78+
});
79+
});
80+
81+
describe("with null input", () => {
82+
const body = null;
83+
84+
it("throws an error with statusCode 400", async () => {
85+
const handler = jest.fn();
86+
await expect(
87+
typeboxValidator(NameBodySchema)(handler)(
88+
createEvent({ body }),
89+
createContext()
90+
)
91+
).rejects.toMatchObject({
92+
statusCode: 400,
93+
});
94+
});
95+
});
96+
97+
describe("with an empty body and optional validation", () => {
98+
const body = "";
99+
100+
const OptionalNameBodySchema = Type.Object({
101+
firstName: Type.Optional(Type.String()),
102+
lastName: Type.Optional(Type.String()),
103+
});
104+
105+
type OptionalNameBody = Static<typeof OptionalNameBodySchema>;
106+
107+
it("returns the handler's response", async () => {
108+
const expectedResponse = {
109+
statusCode: 200,
110+
body: "Done",
111+
};
112+
const handler = jest.fn().mockResolvedValue(expectedResponse);
113+
const actualResponse = await typeboxValidator(OptionalNameBodySchema)(
114+
handler
115+
)(createEvent({ body }), createContext());
116+
expect(actualResponse).toEqual(expectedResponse);
117+
});
118+
});
119+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { PromiseHandler } from "@lambda-middleware/utils";
2+
import debugFactory, { IDebugger } from "debug";
3+
import { APIGatewayEvent, Context } from "aws-lambda";
4+
import { Type, TypeCompiler } from "@sinclair/typebox";
5+
6+
const logger: IDebugger = debugFactory("@lambda-middleware/typebox-validator");
7+
8+
export type WithBody<Event, Body> = Omit<Event, "body"> & { body: Body };
9+
10+
export const typeboxValidator = <T extends object>(schema: Type<T>) => <R>(
11+
handler: PromiseHandler<WithBody<APIGatewayEvent, T>, R>
12+
) => async (event: APIGatewayEvent, context: Context): Promise<R> => {
13+
logger(`Checking input ${JSON.stringify(event.body)}`);
14+
try {
15+
const body = event.body ?? "{}";
16+
const compiledSchema = TypeCompiler.Compile(schema);
17+
const validationResult = compiledSchema.Check(JSON.parse(body));
18+
if (!validationResult) {
19+
throw new Error("Validation failed");
20+
}
21+
logger("Input is valid");
22+
return handler({ ...event, body: JSON.parse(body) }, context);
23+
} catch (error) {
24+
logger("Input is invalid");
25+
(error as { statusCode?: number }).statusCode = 400;
26+
throw error;
27+
}
28+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
lib
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { handler as fullExample } from '../examples/helloWorld';
2+
3+
export async function status() {
4+
return {
5+
body: '',
6+
statusCode: 200
7+
};
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
service: test-microservice
2+
3+
plugins:
4+
- serverless-webpack
5+
- serverless-offline
6+
7+
provider:
8+
name: aws
9+
runtime: nodejs14.x
10+
11+
functions:
12+
hello:
13+
handler: handler.fullExample
14+
events:
15+
- http:
16+
method: post
17+
path: hello
18+
19+
status:
20+
handler: handler.status
21+
events:
22+
- http:
23+
method: get
24+
path: status
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require("source-map-support").install();

0 commit comments

Comments
 (0)