Skip to content

Commit

Permalink
feat(ajv-validator): Add Ajv validator middleware (#794)
Browse files Browse the repository at this point in the history
  • Loading branch information
ikhvost authored Nov 14, 2024
1 parent b0320d9 commit c9f63de
Show file tree
Hide file tree
Showing 10 changed files with 500 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/two-poets-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hono/ajv-validator': patch
---

Add Ajv validator middleware
25 changes: 25 additions & 0 deletions .github/workflows/ci-ajv-validator.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: ci-ajv-validator
on:
push:
branches: [main]
paths:
- 'packages/ajv-validator/**'
pull_request:
branches: ['*']
paths:
- 'packages/ajv-validator/**'

jobs:
ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./packages/ajv-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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"build:effect-validator": "yarn workspace @hono/effect-validator build",
"build:conform-validator": "yarn workspace @hono/conform-validator build",
"build:casbin": "yarn workspace @hono/casbin build",
"build:ajv-validator": "yarn workspace @hono/ajv-validator build",
"build": "run-p 'build:*'",
"lint": "eslint 'packages/**/*.{ts,tsx}'",
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
Expand Down
63 changes: 63 additions & 0 deletions packages/ajv-validator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Ajv validator middleware for Hono

Validator middleware using [Ajv](https://github.com/ajv-validator/ajv) for [Hono](https://honojs.dev) applications.
Define your schema with Ajv and validate incoming requests.

## Usage

No Hook:

```ts
import { type JSONSchemaType } from 'ajv';
import { ajvValidator } from '@hono/ajv-validator';

const schema: JSONSchemaType<{ name: string; age: number }> = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name', 'age'],
additionalProperties: false,
} as const;

const route = app.post('/user', ajvValidator('json', schema), (c) => {
const user = c.req.valid('json');
return c.json({ success: true, message: `${user.name} is ${user.age}` });
});
```

Hook:

```ts
import { type JSONSchemaType } from 'ajv';
import { ajvValidator } from '@hono/ajv-validator';

const schema: JSONSchemaType<{ name: string; age: number }> = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
},
required: ['name', 'age'],
additionalProperties: false,
};

app.post(
'/user',
ajvValidator('json', schema, (result, c) => {
if (!result.success) {
return c.text('Invalid!', 400);
}
})
//...
);
```

## Author

Illia Khvost <https://github.com/ikhvost>

## License

MIT
49 changes: 49 additions & 0 deletions packages/ajv-validator/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@hono/ajv-validator",
"version": "0.0.0",
"description": "Validator middleware using Ajv",
"type": "module",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "vitest --run",
"build": "tsup ./src/index.ts --format esm,cjs --dts",
"publint": "publint",
"release": "yarn build && yarn test && yarn publint && yarn publish"
},
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"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": {
"ajv": ">=8.12.0",
"hono": ">=3.9.0"
},
"devDependencies": {
"ajv": ">=8.12.0",
"hono": "^4.4.12",
"tsup": "^8.1.0",
"vitest": "^1.6.0"
}
}
106 changes: 106 additions & 0 deletions packages/ajv-validator/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type { Context, Env, MiddlewareHandler, ValidationTargets } from 'hono';
import { validator } from 'hono/validator';
import { Ajv, type JSONSchemaType, type ErrorObject } from 'ajv';

type Hook<T, E extends Env, P extends string> = (
result: { success: true; data: T } | { success: false; errors: ErrorObject[] },
c: Context<E, P>
) => Response | Promise<Response> | void;

/**
* Hono middleware that validates incoming data via an Ajv JSON schema.
*
* ---
*
* No Hook
*
* ```ts
* import { type JSONSchemaType } from 'ajv';
* import { ajvValidator } from '@hono/ajv-validator';
*
* const schema: JSONSchemaType<{ name: string; age: number }> = {
* type: 'object',
* properties: {
* name: { type: 'string' },
* age: { type: 'number' },
* },
* required: ['name', 'age'],
* additionalProperties: false,
* };
*
* const route = app.post('/user', ajvValidator('json', schema), (c) => {
* const user = c.req.valid('json');
* return c.json({ success: true, message: `${user.name} is ${user.age}` });
* });
* ```
*
* ---
* Hook
*
* ```ts
* import { type JSONSchemaType } from 'ajv';
* import { ajvValidator } from '@hono/ajv-validator';
*
* const schema: JSONSchemaType<{ name: string; age: number }> = {
* type: 'object',
* properties: {
* name: { type: 'string' },
* age: { type: 'number' },
* },
* required: ['name', 'age'],
* additionalProperties: false,
* };
*
* app.post(
* '/user',
* ajvValidator('json', schema, (result, c) => {
* if (!result.success) {
* return c.text('Invalid!', 400);
* }
* })
* //...
* );
* ```
*/
export function ajvValidator<
T,
Target extends keyof ValidationTargets,
E extends Env = Env,
P extends string = string
>(
target: Target,
schema: JSONSchemaType<T>,
hook?: Hook<T, E, P>
): MiddlewareHandler<
E,
P,
{
in: { [K in Target]: T };
out: { [K in Target]: T };
}
> {
const ajv = new Ajv();
const validate = ajv.compile(schema);

return validator(target, (data, c) => {
const valid = validate(data);
if (valid) {
if (hook) {
const hookResult = hook({ success: true, data: data as T }, c);
if (hookResult instanceof Response || hookResult instanceof Promise) {
return hookResult;
}
}
return data as T;
}

const errors = validate.errors || [];
if (hook) {
const hookResult = hook({ success: false, errors }, c);
if (hookResult instanceof Response || hookResult instanceof Promise) {
return hookResult;
}
}
return c.json({ success: false, errors }, 400);
});
}
Loading

0 comments on commit c9f63de

Please sign in to comment.