-
Notifications
You must be signed in to change notification settings - Fork 189
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: class validator middleware for Hono (#788)
* setup middleware package * add implementation middleware * add documentation + updae tsconfig for decorator handling * add tests * fix format class-validator middleware * Add Readme * Update changelog & changeset * update changelog 2 * update changelog 2 * fix working directory ci * rm jest dependencies change to tsup for build fix ci name * revert changes not related to class-validator * remove the changeset since Changesets will add a changeset automatically * package description
- Loading branch information
1 parent
28ca0f3
commit a5c20b3
Showing
11 changed files
with
1,212 additions
and
1 deletion.
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,5 @@ | ||
--- | ||
'@hono/class-validator': major | ||
--- | ||
|
||
First release |
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,25 @@ | ||
name: ci-class-validator | ||
on: | ||
push: | ||
branches: [main] | ||
paths: | ||
- 'packages/class-validator/**' | ||
pull_request: | ||
branches: ['*'] | ||
paths: | ||
- 'packages/class-validator/**' | ||
|
||
jobs: | ||
ci: | ||
runs-on: ubuntu-latest | ||
defaults: | ||
run: | ||
working-directory: ./packages/class-validator | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-node@v4 | ||
with: | ||
node-version: 20.x | ||
- run: yarn install --frozen-lockfile | ||
- run: yarn build | ||
- run: yarn test |
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
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,56 @@ | ||
# Class-validator middleware for Hono | ||
|
||
The validator middleware using [class-validator](https://github.com/typestack/class-validator) for [Hono](https://github.com/honojs/hono) applications. | ||
|
||
## Usage | ||
|
||
```ts | ||
import { classValidator } from '@hono/class-validator' | ||
import { IsInt, IsString } from 'class-validator' | ||
|
||
class CreateUserDto { | ||
@IsString() | ||
name!: string; | ||
|
||
@IsInt() | ||
age!: number; | ||
} | ||
|
||
|
||
const route = app.post('/user', classValidator('json', CreateUserDto), (c) => { | ||
const user = c.req.valid('json') | ||
return c.json({ success: true, message: `${user.name} is ${user.age}` }) | ||
}) | ||
``` | ||
|
||
With hook: | ||
|
||
```ts | ||
import { classValidator } from '@hono/class-validator' | ||
import { IsInt, IsString } from 'class-validator' | ||
|
||
class CreateUserDto { | ||
@IsString() | ||
name!: string; | ||
|
||
@IsInt() | ||
age!: number; | ||
} | ||
|
||
app.post( | ||
'/user', classValidator('json', CreateUserDto, (result, c) => { | ||
if (!result.success) { | ||
return c.text('Invalid!', 400) | ||
} | ||
}) | ||
//... | ||
) | ||
``` | ||
|
||
## Author | ||
|
||
**Pr0m3ht3us** - https://github.com/pr0m3th3usex | ||
|
||
## License | ||
|
||
MIT |
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,47 @@ | ||
{ | ||
"name": "@hono/class-validator", | ||
"packageManager": "[email protected]", | ||
"description": "Validator middleware using class-validator", | ||
"version": "0.1.0", | ||
"type": "module", | ||
"main": "dist/index.js", | ||
"module": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"exports": { | ||
".": { | ||
"import": "./dist/index.js", | ||
"types": "./dist/index.d.ts" | ||
} | ||
}, | ||
"scripts": { | ||
"test": "vitest --run", | ||
"build": "rimraf dist && tsup ./src/index.ts --format esm,cjs --dts", | ||
"prerelease": "yarn build && yarn test", | ||
"release": "yarn publish" | ||
}, | ||
"license": "MIT", | ||
"publishConfig": { | ||
"registry": "https://registry.npmjs.org", | ||
"access": "public" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/honojs/middleware.git" | ||
}, | ||
"homepage": "https://github.com/honojs/middleware", | ||
"peerDependencies": { | ||
"hono": ">=3.9.0" | ||
}, | ||
"devDependencies": { | ||
"hono": "^4.0.10", | ||
"rimraf": "^5.0.5", | ||
"tsup": "^8.3.5", | ||
"typescript": "^5.3.3", | ||
"vitest": "^1.4.0" | ||
}, | ||
"dependencies": { | ||
"class-transformer": "^0.5.1", | ||
"class-validator": "^0.14.1", | ||
"reflect-metadata": "^0.2.2" | ||
} | ||
} |
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,158 @@ | ||
import 'reflect-metadata' | ||
import { validator } from 'hono/validator' | ||
import { ClassConstructor, ClassTransformOptions, plainToClass } from 'class-transformer' | ||
import { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono' | ||
import { ValidationError, validate } from 'class-validator' | ||
|
||
/** | ||
* Hono middleware that validates incoming data using class-validator(https://github.com/typestack/class-validator). | ||
* | ||
* --- | ||
* | ||
* No Hook | ||
* | ||
* ```ts | ||
* import { classValidator } from '@hono/class-validator' | ||
* import { IsInt, IsString } from 'class-validator' | ||
* | ||
* class CreateUserDto { | ||
* @IsString() | ||
* name!: string; | ||
* | ||
* @IsInt() | ||
* age!: number; | ||
* } | ||
* | ||
* | ||
* const route = app.post('/user', classValidator('json', CreateUserDto), (c) => { | ||
* const user = c.req.valid('json') | ||
* return c.json({ success: true, message: `${user.name} is ${user.age}` }) | ||
* }) | ||
* ``` | ||
* | ||
* --- | ||
* Hook | ||
* | ||
* ```ts | ||
* import { classValidator } from '@hono/class-validator' | ||
* import { IsInt, IsString } from 'class-validator' | ||
* | ||
* class CreateUserDto { | ||
* @IsString() | ||
* name!: string; | ||
* | ||
* @IsInt() | ||
* age!: number; | ||
* } | ||
* | ||
* app.post( | ||
* '/user', | ||
* classValidator('json', CreateUserDto, (result, c) => { | ||
* if (!result.success) { | ||
* return c.text('Invalid!', 400) | ||
* } | ||
* }) | ||
* //... | ||
* ) | ||
* ``` | ||
*/ | ||
|
||
type Hook< | ||
T, | ||
E extends Env, | ||
P extends string, | ||
Target extends keyof ValidationTargets = keyof ValidationTargets, | ||
O = object | ||
> = ( | ||
result: ({ success: true } | { success: false; errors: ValidationError[] }) & { | ||
data: T | ||
target: Target | ||
}, | ||
c: Context<E, P> | ||
) => Response | void | TypedResponse<O> | Promise<Response | void | TypedResponse<O>> | ||
|
||
type HasUndefined<T> = undefined extends T ? true : false | ||
|
||
type HasClassConstructor<T> = ClassConstructor<any> extends T ? true : false | ||
|
||
export type StaticObject<T extends ClassConstructor<any>> = { | ||
[K in keyof InstanceType<T>]: HasClassConstructor<InstanceType<T>[K]> extends true | ||
? StaticObject<InstanceType<T>[K]> | ||
: InstanceType<T>[K] | ||
} | ||
|
||
const parseAndValidate = async <T extends ClassConstructor<any>>( | ||
dto: T, | ||
obj: object, | ||
options: ClassTransformOptions | ||
): Promise< | ||
{ success: false; errors: ValidationError[] } | { success: true; output: InstanceType<T> } | ||
> => { | ||
// tranform the literal object to class object | ||
const objInstance = plainToClass(dto, obj, options) | ||
// validating and check the errors, throw the errors if exist | ||
|
||
const errors = await validate(objInstance) | ||
// errors is an array of validation errors | ||
if (errors.length > 0) { | ||
return { | ||
success: false, | ||
errors, | ||
} | ||
} | ||
|
||
return { success: true, output: objInstance as InstanceType<T> } | ||
} | ||
|
||
export const classValidator = < | ||
T extends ClassConstructor<any>, | ||
Output extends InstanceType<T> = InstanceType<T>, | ||
Target extends keyof ValidationTargets = keyof ValidationTargets, | ||
E extends Env = Env, | ||
P extends string = string, | ||
In = StaticObject<T>, | ||
I extends Input = { | ||
in: HasUndefined<In> extends true | ||
? { | ||
[K in Target]?: K extends 'json' | ||
? In | ||
: HasUndefined<keyof ValidationTargets[K]> extends true | ||
? { [K2 in keyof In]?: ValidationTargets[K][K2] } | ||
: { [K2 in keyof In]: ValidationTargets[K][K2] } | ||
} | ||
: { | ||
[K in Target]: K extends 'json' | ||
? In | ||
: HasUndefined<keyof ValidationTargets[K]> extends true | ||
? { [K2 in keyof In]?: ValidationTargets[K][K2] } | ||
: { [K2 in keyof In]: ValidationTargets[K][K2] } | ||
} | ||
out: { [K in Target]: Output } | ||
}, | ||
V extends I = I | ||
>( | ||
target: Target, | ||
dataType: T, | ||
hook?: Hook<Output, E, P, Target>, | ||
options: ClassTransformOptions = { enableImplicitConversion: false } | ||
): MiddlewareHandler<E, P, V> => | ||
// @ts-expect-error not typed well | ||
validator(target, async (data, c) => { | ||
const result = await parseAndValidate(dataType, data, options) | ||
|
||
if (hook) { | ||
const hookResult = hook({ ...result, data, target }, c) | ||
if (hookResult instanceof Response || hookResult instanceof Promise) { | ||
if ('response' in hookResult) { | ||
return hookResult.response | ||
} | ||
return hookResult | ||
} | ||
} | ||
|
||
if (!result.success) { | ||
return c.json({ errors: result.errors }, 400) | ||
} | ||
|
||
return result.output | ||
}) |
Oops, something went wrong.