Skip to content

Commit

Permalink
Merge pull request #75 from dbartholomae/add-typebox-validator
Browse files Browse the repository at this point in the history
Add typebox validator middleware
  • Loading branch information
mergify[bot] authored Oct 16, 2024
2 parents 91a7ec5 + 1ac1d01 commit 7f256b2
Show file tree
Hide file tree
Showing 16 changed files with 453 additions and 0 deletions.
52 changes: 52 additions & 0 deletions packages/typebox-validator/README.md
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 packages/typebox-validator/examples/helloWorld.int-test.ts
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"
);
});
});
});
31 changes: 31 additions & 0 deletions packages/typebox-validator/examples/helloWorld.ts
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
);
2 changes: 2 additions & 0 deletions packages/typebox-validator/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const baseConfig = require("../../jest.unit.config");
module.exports = baseConfig;
2 changes: 2 additions & 0 deletions packages/typebox-validator/jest.integration.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const baseConfig = require("../../jest.integration.config");
module.exports = baseConfig;
83 changes: 83 additions & 0 deletions packages/typebox-validator/package.json
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 packages/typebox-validator/src/typeboxValidator.test.ts
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);
});
});
});
28 changes: 28 additions & 0 deletions packages/typebox-validator/src/typeboxValidator.ts
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;
}
};
2 changes: 2 additions & 0 deletions packages/typebox-validator/test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
lib
8 changes: 8 additions & 0 deletions packages/typebox-validator/test/handler.ts
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
};
}
24 changes: 24 additions & 0 deletions packages/typebox-validator/test/serverless.yml
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
1 change: 1 addition & 0 deletions packages/typebox-validator/test/source-map-install.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require("source-map-support").install();
Loading

0 comments on commit 7f256b2

Please sign in to comment.