-
-
Notifications
You must be signed in to change notification settings - Fork 18
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #75 from dbartholomae/add-typebox-validator
Add typebox validator middleware
- Loading branch information
Showing
16 changed files
with
453 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# @lambda-middleware/typebox-validator | ||
|
||
[![npm version](https://badge.fury.io/js/%40lambda-middleware%2Ftypebox-validator.svg)](https://npmjs.org/package/@lambda-middleware/typebox-validator) | ||
[![downloads](https://img.shields.io/npm/dw/%40lambda-middleware%2Ftypebox-validator.svg)](https://npmjs.org/package/@lambda-middleware/typebox-validator) | ||
[![open issues](https://img.shields.io/github/issues-raw/dbartholomae/lambda-middleware.svg)](https://github.com/dbartholomae/lambda-middleware/issues) | ||
[![debug](https://img.shields.io/badge/debug-blue.svg)](https://github.com/visionmedia/debug#readme) | ||
[![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) | ||
[![codecov](https://codecov.io/gh/dbartholomae/lambda-middleware/branch/main/graph/badge.svg)](https://codecov.io/gh/dbartholomae/lambda-middleware) | ||
[![dependency status](https://david-dm.org/dbartholomae/lambda-middleware.svg?theme=shields.io)](https://david-dm.org/dbartholomae/lambda-middleware) | ||
[![devDependency status](https://david-dm.org/dbartholomae/lambda-middleware/dev-status.svg)](https://david-dm.org/dbartholomae/lambda-middleware?type=dev) | ||
|
||
A validation middleware for AWS http lambda functions based on [typebox](https://github.com/sinclairzx81/typebox). | ||
|
||
## Lambda middleware | ||
|
||
This middleware is part of the [lambda middleware series](https://dbartholomae.github.io/lambda-middleware/). It can be used independently. | ||
|
||
## Usage | ||
|
||
```typescript | ||
import { composeHandler } from "@lambda-middleware/compose"; | ||
import { errorHandler } from "@lambda-middleware/http-error-handler"; | ||
import { Type } from "@sinclair/typebox"; | ||
import { typeboxValidator } from "@lambda-middleware/typebox-validator"; | ||
import { APIGatewayProxyResult } from "aws-lambda"; | ||
|
||
const NameBodySchema = Type.Object({ | ||
firstName: Type.String(), | ||
lastName: Type.String(), | ||
}); | ||
|
||
type NameBody = { | ||
firstName: string; | ||
lastName: string; | ||
}; | ||
|
||
async function helloWorld(event: { body: NameBody }): Promise<APIGatewayProxyResult> { | ||
return { | ||
body: `Hello ${event.body.firstName} ${event.body.lastName}`, | ||
headers: { | ||
"content-type": "text", | ||
}, | ||
statusCode: 200, | ||
}; | ||
} | ||
|
||
export const handler = composeHandler( | ||
errorHandler(), | ||
typeboxValidator(NameBodySchema), | ||
helloWorld | ||
); | ||
``` |
33 changes: 33 additions & 0 deletions
33
packages/typebox-validator/examples/helloWorld.int-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import request from "supertest"; | ||
import { handler } from "./helloWorld"; | ||
|
||
const server = request("http://localhost:3000/dev"); | ||
|
||
describe("Handler with typebox validator middleware", () => { | ||
describe("with valid input", () => { | ||
it("returns 200", async () => { | ||
const response = await server | ||
.post("/hello") | ||
.send({ | ||
firstName: "John", | ||
lastName: "Doe", | ||
}) | ||
.expect(200); | ||
expect(response.text).toEqual("Hello John Doe"); | ||
}); | ||
}); | ||
|
||
describe("with invalid input", () => { | ||
it("returns 400 and the validation error", async () => { | ||
const response = await server | ||
.post("/hello") | ||
.send({ | ||
firstName: "John", | ||
}) | ||
.expect(400); | ||
expect(JSON.stringify(response.body)).toContain( | ||
"lastName must be a string" | ||
); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { composeHandler } from "@lambda-middleware/compose"; | ||
import { errorHandler } from "@lambda-middleware/http-error-handler"; | ||
import { Type } from "@sinclair/typebox"; | ||
import { typeboxValidator } from "../src/typeboxValidator"; | ||
import { APIGatewayProxyResult } from "aws-lambda"; | ||
|
||
const NameBodySchema = Type.Object({ | ||
firstName: Type.String(), | ||
lastName: Type.String(), | ||
}); | ||
|
||
type NameBody = { | ||
firstName: string; | ||
lastName: string; | ||
}; | ||
|
||
async function helloWorld(event: { body: NameBody }): Promise<APIGatewayProxyResult> { | ||
return { | ||
body: `Hello ${event.body.firstName} ${event.body.lastName}`, | ||
headers: { | ||
"content-type": "text", | ||
}, | ||
statusCode: 200, | ||
}; | ||
} | ||
|
||
export const handler = composeHandler( | ||
errorHandler(), | ||
typeboxValidator(NameBodySchema), | ||
helloWorld | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
const baseConfig = require("../../jest.unit.config"); | ||
module.exports = baseConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
const baseConfig = require("../../jest.integration.config"); | ||
module.exports = baseConfig; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
{ | ||
"name": "@lambda-middleware/typebox-validator", | ||
"version": "1.0.0", | ||
"description": "A validation middleware for AWS http lambda functions based on typebox.", | ||
"homepage": "https://dbartholomae.github.io/lambda-middleware/", | ||
"license": "MIT", | ||
"author": { | ||
"name": "Daniel Bartholomae", | ||
"email": "[email protected]", | ||
"url": "" | ||
}, | ||
"files": [ | ||
"lib" | ||
], | ||
"main": "lib/index.js", | ||
"keywords": [ | ||
"aws", | ||
"lambda", | ||
"middleware", | ||
"validation", | ||
"validator" | ||
], | ||
"types": "lib/index.d.ts", | ||
"engines": { | ||
"npm": ">= 4.0.0" | ||
}, | ||
"private": false, | ||
"dependencies": { | ||
"@lambda-middleware/utils": "^1.0.4", | ||
"debug": ">=4.1.0", | ||
"@sinclair/typebox": "^0.21.0", | ||
"tslib": "^2.0.1" | ||
}, | ||
"directories": { | ||
"example": "examples" | ||
}, | ||
"scripts": { | ||
"build": "rimraf ./lib && tsc --project tsconfig.build.json", | ||
"lint": "eslint src/**/*.ts examples/**/*.ts", | ||
"pretest": "npm run build", | ||
"start": "cd test && serverless offline", | ||
"test": "npm run lint && npm run test:unit && npm run test:integration && pkg-ok", | ||
"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\"", | ||
"test:unit": "jest" | ||
}, | ||
"devDependencies": { | ||
"@lambda-middleware/compose": "^1.2.0", | ||
"@lambda-middleware/http-error-handler": "^2.0.0", | ||
"@types/debug": "^4.1.5", | ||
"@types/jest": "^25.2.1", | ||
"@types/supertest": "^2.0.8", | ||
"@types/aws-lambda": "^8.10.47", | ||
"@typescript-eslint/parser": "^2.26.0", | ||
"@typescript-eslint/eslint-plugin": "^2.26.0", | ||
"aws-lambda": "^1.0.5", | ||
"concurrently": "^5.1.0", | ||
"eslint": "^6.8.0", | ||
"eslint-config-prettier": "^6.10.1", | ||
"eslint-plugin-prettier": "^3.1.2", | ||
"jest": "^25.2.7", | ||
"jest-junit": "^10.0.0", | ||
"pkg-ok": "^2.3.1", | ||
"prettier": "^2.0.2", | ||
"prettier-config-standard": "^1.0.1", | ||
"reflect-metadata": "^0.1.13", | ||
"rimraf": "^3.0.2", | ||
"serverless": "^2.57.0", | ||
"serverless-offline": "^8.0.0", | ||
"serverless-webpack": "^5.5.3", | ||
"source-map-support": "^0.5.16", | ||
"supertest": "^4.0.2", | ||
"ts-jest": "^25.3.1", | ||
"ts-loader": "^6.2.2", | ||
"typescript": "^4.5.4", | ||
"wait-on": "^5.2.0", | ||
"webpack": "^4.41.5" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "[email protected]:dbartholomae/lambda-middleware.git", | ||
"directory": "packages/typebox-validator" | ||
} | ||
} |
119 changes: 119 additions & 0 deletions
119
packages/typebox-validator/src/typeboxValidator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { createContext, createEvent } from "@lambda-middleware/utils"; | ||
import { APIGatewayEvent } from "aws-lambda"; | ||
import { Type, Static } from "@sinclair/typebox"; | ||
import { typeboxValidator } from "./typeboxValidator"; | ||
|
||
const NameBodySchema = Type.Object({ | ||
firstName: Type.String(), | ||
lastName: Type.String(), | ||
}); | ||
|
||
type NameBody = Static<typeof NameBodySchema>; | ||
|
||
describe("typeboxValidator", () => { | ||
describe("with valid input", () => { | ||
const body = JSON.stringify({ | ||
firstName: "John", | ||
lastName: "Doe", | ||
}); | ||
|
||
it("sets the body to the validated value", async () => { | ||
const handler = jest.fn(); | ||
await typeboxValidator(NameBodySchema)(handler)( | ||
createEvent({ body }), | ||
createContext() | ||
); | ||
expect(handler).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
body: { | ||
firstName: "John", | ||
lastName: "Doe", | ||
}, | ||
}), | ||
expect.anything() | ||
); | ||
}); | ||
}); | ||
|
||
describe("with superfluous input", () => { | ||
const body = JSON.stringify({ | ||
firstName: "John", | ||
lastName: "Doe", | ||
injection: "malicious", | ||
}); | ||
|
||
it("omits the superfluous input", async () => { | ||
const handler = jest.fn(); | ||
await typeboxValidator(NameBodySchema)(handler)( | ||
createEvent({ body }), | ||
createContext() | ||
); | ||
expect(handler).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
body: { | ||
firstName: "John", | ||
lastName: "Doe", | ||
}, | ||
}), | ||
expect.anything() | ||
); | ||
}); | ||
}); | ||
|
||
describe("with invalid input", () => { | ||
const body = JSON.stringify({ | ||
firstName: "John", | ||
}); | ||
|
||
it("throws an error with statusCode 400", async () => { | ||
const handler = jest.fn(); | ||
await expect( | ||
typeboxValidator(NameBodySchema)(handler)( | ||
createEvent({ body }), | ||
createContext() | ||
) | ||
).rejects.toMatchObject({ | ||
statusCode: 400, | ||
}); | ||
}); | ||
}); | ||
|
||
describe("with null input", () => { | ||
const body = null; | ||
|
||
it("throws an error with statusCode 400", async () => { | ||
const handler = jest.fn(); | ||
await expect( | ||
typeboxValidator(NameBodySchema)(handler)( | ||
createEvent({ body }), | ||
createContext() | ||
) | ||
).rejects.toMatchObject({ | ||
statusCode: 400, | ||
}); | ||
}); | ||
}); | ||
|
||
describe("with an empty body and optional validation", () => { | ||
const body = ""; | ||
|
||
const OptionalNameBodySchema = Type.Object({ | ||
firstName: Type.Optional(Type.String()), | ||
lastName: Type.Optional(Type.String()), | ||
}); | ||
|
||
type OptionalNameBody = Static<typeof OptionalNameBodySchema>; | ||
|
||
it("returns the handler's response", async () => { | ||
const expectedResponse = { | ||
statusCode: 200, | ||
body: "Done", | ||
}; | ||
const handler = jest.fn().mockResolvedValue(expectedResponse); | ||
const actualResponse = await typeboxValidator(OptionalNameBodySchema)( | ||
handler | ||
)(createEvent({ body }), createContext()); | ||
expect(actualResponse).toEqual(expectedResponse); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { PromiseHandler } from "@lambda-middleware/utils"; | ||
import debugFactory, { IDebugger } from "debug"; | ||
import { APIGatewayEvent, Context } from "aws-lambda"; | ||
import { Type, TypeCompiler } from "@sinclair/typebox"; | ||
|
||
const logger: IDebugger = debugFactory("@lambda-middleware/typebox-validator"); | ||
|
||
export type WithBody<Event, Body> = Omit<Event, "body"> & { body: Body }; | ||
|
||
export const typeboxValidator = <T extends object>(schema: Type<T>) => <R>( | ||
handler: PromiseHandler<WithBody<APIGatewayEvent, T>, R> | ||
) => async (event: APIGatewayEvent, context: Context): Promise<R> => { | ||
logger(`Checking input ${JSON.stringify(event.body)}`); | ||
try { | ||
const body = event.body ?? "{}"; | ||
const compiledSchema = TypeCompiler.Compile(schema); | ||
const validationResult = compiledSchema.Check(JSON.parse(body)); | ||
if (!validationResult) { | ||
throw new Error("Validation failed"); | ||
} | ||
logger("Input is valid"); | ||
return handler({ ...event, body: JSON.parse(body) }, context); | ||
} catch (error) { | ||
logger("Input is invalid"); | ||
(error as { statusCode?: number }).statusCode = 400; | ||
throw error; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
lib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import { handler as fullExample } from '../examples/helloWorld'; | ||
|
||
export async function status() { | ||
return { | ||
body: '', | ||
statusCode: 200 | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
service: test-microservice | ||
|
||
plugins: | ||
- serverless-webpack | ||
- serverless-offline | ||
|
||
provider: | ||
name: aws | ||
runtime: nodejs14.x | ||
|
||
functions: | ||
hello: | ||
handler: handler.fullExample | ||
events: | ||
- http: | ||
method: post | ||
path: hello | ||
|
||
status: | ||
handler: handler.status | ||
events: | ||
- http: | ||
method: get | ||
path: status |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
require("source-map-support").install(); |
Oops, something went wrong.