Skip to content

Commit

Permalink
added support for zod validation
Browse files Browse the repository at this point in the history
  • Loading branch information
xiduzo committed May 6, 2024
1 parent e31f2ad commit 320b860
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 29 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p align="center">
<h1 align="center">AWS Lambda Handyman</h1>
<p align="center">
AWS Lambda TypeScript validation made easy 🏄 ...️and some other things
AWS Lambda TypeScript validation made easy 🏄 ...️and some other things
</p>
</p>

Expand Down Expand Up @@ -81,6 +81,10 @@ Next we need to enable these options in our `.tsconfig` file

## Basic Usage

AWS Lambda Handyman accpest both [`class-validator`](https://www.npmjs.com/package/class-validator) classes as [`zod`](https://www.npmjs.com/package/zod) parsable classes.

### class-validator

```typescript
import 'reflect-metadata'

Expand All @@ -89,6 +93,33 @@ class CustomBodyType {
email: string
}

class AccountDelete {
@Handler()
static async handle(@Body() { email }: CustomBodyType) {
await deleteAccount(email)
return ok()
}
}
```

### Zod

```typescript
const CustomBodySchema = z.object({
email: z.string().email()
})

class CustomBodyType {
constructor(input: z.input<typeof CustomBodySchema>) {
Object.assign(this, CustomBodyType.parse(input))
}

// Requires a static parse method
static parse(input: unknown) {
return new CustomBody(input as z.input<typeof CustomBodySchema>)
}
}

class AccountDelete {
@Handler()
static async handle(@Body() { email }: CustomBodyType) {
Expand Down
20 changes: 17 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aws-lambda-handyman",
"version": "0.5.9",
"version": "0.5.10",
"description": "AWS Lambda TypeScript validation made easy 🏄",
"main": "./lib/cjs/src/index.js",
"typings": "./lib/types/src/index.d.ts",
Expand All @@ -9,7 +9,10 @@
"files": [
"lib"
],
"author": "Slaven Ivanov <[email protected]>",
"authors": [
"Slaven Ivanov <[email protected]>",
"Sander Boer <[email protected]>"
],
"license": "ISC",
"repository": {
"type": "git",
Expand All @@ -29,7 +32,8 @@
"sideEffects": false,
"dependencies": {
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0"
"class-validator": "^0.14.0",
"zod": "^3.23.6"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.101",
Expand Down
25 changes: 21 additions & 4 deletions src/Decorators.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import 'reflect-metadata'
import { APIGatewayEventDefaultAuthorizerContext, APIGatewayProxyEventBase, Context } from 'aws-lambda'
import { validateSync, ValidationError } from 'class-validator'
import { ClassConstructor, plainToInstance } from 'class-transformer'
import { badRequest, HttpError, internalServerError, response, TransformValidateOptions } from '.'
import { ValidationError, validateSync } from 'class-validator'
import 'reflect-metadata'
import { ZodSchema } from 'zod'
import { HttpError, TransformValidateOptions, badRequest, internalServerError, response } from '.'

const eventMetadataKey = Symbol('Event')
const contextMetadataKey = Symbol('Ctx')
Expand All @@ -15,7 +16,7 @@ const defaultHandlerOptions = { enableImplicitConversion: true, forbidUnknownVal

export const defaultInternalServerErrorMessage = 'Oops, something went wrong 😬'
export const bodyIsNotProperJSON = `The request's body is not proper JSON 🤔`
export const handlerNotAsyncMessage = '⚠️ Methods, decorated with @Handler, need to be async / need to return a Promise ⚠️ '
export const handlerNotAsyncMessage = '⚠️ Methods, decorated with @Handler, need to be async / need to return a Promise ⚠️'

export function Event() {
return (target: Object, propertyKey: string | symbol, parameterIndex: number) => {
Expand Down Expand Up @@ -128,6 +129,22 @@ export function Handler(options: TransformValidateOptions = defaultHandlerOption
}

function transformValidateOrReject<T extends object, V extends object>(cls: ClassConstructor<T>, plain: V, options?: TransformValidateOptions): T {
if ('parse' in cls && typeof cls['parse'] === 'function') {
return transformUsingZod(cls as any, plain)
}

return transformUsingClassTransformer(cls, plain, options)
}

function transformUsingZod<T extends object, V extends object>(schema: ZodSchema<T>, plain: V): T {
return schema.parse(plain)
}

function transformUsingClassTransformer<T extends object, V extends object>(
cls: ClassConstructor<T>,
plain: V,
options?: TransformValidateOptions
): T {
const instance = plainToInstance(cls, plain, options)
const validationErrors = validateSync(instance, options)

Expand Down
68 changes: 50 additions & 18 deletions test/Decorators.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,6 @@
import {
Body,
Ctx,
defaultInternalServerErrorMessage,
Event,
Handler,
handlerNotAsyncMessage,
Headers,
HttpError,
Paths,
Queries,
TransformBoolean,
TransformValidateOptions
} from '../src'
import 'reflect-metadata'
import * as mockData from '../test/mock/httpEventContext.json'
import { APIGatewayEventDefaultAuthorizerContext, APIGatewayProxyEventBase, Context } from 'aws-lambda'
import * as ct from 'class-transformer'
import { Type } from 'class-transformer'
import {
IsBoolean,
IsEmail,
Expand All @@ -30,8 +16,23 @@ import {
Length,
ValidateNested
} from 'class-validator'
import * as ct from 'class-transformer'
import { Type } from 'class-transformer'
import 'reflect-metadata'
import { z } from 'zod'
import {
Body,
Ctx,
Event,
Handler,
Headers,
HttpError,
Paths,
Queries,
TransformBoolean,
TransformValidateOptions,
defaultInternalServerErrorMessage,
handlerNotAsyncMessage
} from '../src'
import * as mockData from '../test/mock/httpEventContext.json'

const { event, context, eventWithNoPathParams, eventWithBrokenBody, eventWithBrokenNestedBody, eventWithNoBody, eventWithNoQueries, emptyEvent } =
mockData
Expand Down Expand Up @@ -265,6 +266,37 @@ test('Handler has Body param, and is called with expected payload', async () =>
expect(injectedBody.email).toEqual(JSON.parse(event.body).email)
})

test('Handler has Body parram, and is called with expected zod payload', async () => {
const BodySchema = z.object({
email: z.string().email()
})

class BodyType {
constructor(input: z.input<typeof BodySchema>) {
Object.assign(this, BodySchema.parse(input))
}

static parse(input: unknown) {
return new BodyType(input as z.input<typeof BodySchema>)
}
}

class HandlerTest {
@Handler()
static async handle(@Body() body: BodyType) {
return arguments
}
}

//@ts-ignore
const calledWithArgs = await HandlerTest.handle(event, context)

expect(calledWithArgs.length).toEqual(1)

const [injectedBody] = calledWithArgs
expect(injectedBody.email).toEqual(JSON.parse(event.body).email)
})

test('Handler has Body param, and is called with unexpected payload', async () => {
class BodyType {
@IsEmail()
Expand Down

0 comments on commit 320b860

Please sign in to comment.