feat(standard-validator): Add standard schema validation (#887)
* feat(standard-validator): Add standard schema validation * feat(standard-validator): add changeset * feat(standard-validator): reintroduce type tests * feat(standard-validator): simplif tests * build(standard-validator): add gitlab pipeline * chore(standard-validator): remove redundant files * feat(standard-validator): cleanup tests, adjust comments * fix(standard-validator): adjust versions, fix doc * build: fix lockfile * feat(standard-validator): drop headers lower-casing, update readme * check types in test dir and add `tsc` to the test commandpull/961/head
parent
4e4e40cdbf
commit
f77d7ba2e2
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'@hono/standard-validator': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Initial implementation for Standard Schema support
|
|
@ -0,0 +1,25 @@
|
||||||
|
name: ci-standard-validator
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'packages/standard-validator/**'
|
||||||
|
pull_request:
|
||||||
|
branches: ['*']
|
||||||
|
paths:
|
||||||
|
- 'packages/standard-validator/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ci:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./packages/standard-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
|
|
@ -41,6 +41,7 @@
|
||||||
"build:ajv-validator": "yarn workspace @hono/ajv-validator build",
|
"build:ajv-validator": "yarn workspace @hono/ajv-validator build",
|
||||||
"build:tsyringe": "yarn workspace @hono/tsyringe build",
|
"build:tsyringe": "yarn workspace @hono/tsyringe build",
|
||||||
"build:cloudflare-access": "yarn workspace @hono/cloudflare-access build",
|
"build:cloudflare-access": "yarn workspace @hono/cloudflare-access build",
|
||||||
|
"build:standard-validator": "yarn workspace @hono/standard-validator build",
|
||||||
"build": "run-p 'build:*'",
|
"build": "run-p 'build:*'",
|
||||||
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
||||||
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Standard Schema validator middleware for Hono
|
||||||
|
|
||||||
|
The validator middleware using [Standard Schema Spec](https://github.com/standard-schema/standard-schema) for [Hono](https://honojs.dev) applications.
|
||||||
|
You can write a schema with any validation library supporting Standard Schema and validate the incoming values.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
|
||||||
|
### Basic:
|
||||||
|
```ts
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { sValidator } from '@hono/standard-validator'
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
age: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/author', sValidator('json', schema), (c) => {
|
||||||
|
const data = c.req.valid('json')
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `${data.name} is ${data.age}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hook:
|
||||||
|
```ts
|
||||||
|
app.post(
|
||||||
|
'/post',
|
||||||
|
sValidator('json', schema, (result, c) => {
|
||||||
|
if (!result.success) {
|
||||||
|
return c.text('Invalid!', 400)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Headers:
|
||||||
|
Headers are internally transformed to lower-case in Hono. Hence, you will have to make them lower-cased in validation object.
|
||||||
|
```ts
|
||||||
|
import { object, string } from 'valibot'
|
||||||
|
import { sValidator } from '@hono/standard-validator'
|
||||||
|
|
||||||
|
const schema = object({
|
||||||
|
'content-type': string(),
|
||||||
|
'user-agent': string()
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/author', sValidator('header', schema), (c) => {
|
||||||
|
const headers = c.req.valid('header')
|
||||||
|
// do something with headers
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Rokas Muningis <https://github.com/muningis>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "@hono/standard-validator",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Validator middleware using Standard Schema",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.cjs",
|
||||||
|
"module": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "tsc --noEmit && vitest --run",
|
||||||
|
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
||||||
|
"publint": "publint",
|
||||||
|
"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": {
|
||||||
|
"@standard-schema/spec": "1.0.0",
|
||||||
|
"hono": ">=3.9.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@standard-schema/spec": "1.0.0",
|
||||||
|
"arktype": "^2.0.0-rc.26",
|
||||||
|
"hono": "^4.0.10",
|
||||||
|
"publint": "^0.2.7",
|
||||||
|
"tsup": "^8.1.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"valibot": "^1.0.0-beta.9",
|
||||||
|
"vitest": "^1.4.0",
|
||||||
|
"zod": "^3.24.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
import type { Context, Env, Input, MiddlewareHandler, TypedResponse, ValidationTargets } from 'hono'
|
||||||
|
import { validator } from 'hono/validator'
|
||||||
|
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
||||||
|
|
||||||
|
type HasUndefined<T> = undefined extends T ? true : false
|
||||||
|
type TOrPromiseOfT<T> = T | Promise<T>
|
||||||
|
|
||||||
|
type Hook<
|
||||||
|
T,
|
||||||
|
E extends Env,
|
||||||
|
P extends string,
|
||||||
|
Target extends keyof ValidationTargets = keyof ValidationTargets,
|
||||||
|
O = {}
|
||||||
|
> = (
|
||||||
|
result: (
|
||||||
|
| { success: boolean; data: T }
|
||||||
|
| { success: boolean; error: ReadonlyArray<StandardSchemaV1.Issue>; data: T }
|
||||||
|
) & {
|
||||||
|
target: Target
|
||||||
|
},
|
||||||
|
c: Context<E, P>
|
||||||
|
) => TOrPromiseOfT<Response | void | TypedResponse<O>>
|
||||||
|
|
||||||
|
const isStandardSchemaValidator = (validator: unknown): validator is StandardSchemaV1 =>
|
||||||
|
!!validator && typeof validator === 'object' && '~standard' in validator
|
||||||
|
|
||||||
|
const sValidator = <
|
||||||
|
Schema extends StandardSchemaV1,
|
||||||
|
Target extends keyof ValidationTargets,
|
||||||
|
E extends Env,
|
||||||
|
P extends string,
|
||||||
|
In = StandardSchemaV1.InferInput<Schema>,
|
||||||
|
Out = StandardSchemaV1.InferOutput<Schema>,
|
||||||
|
I extends Input = {
|
||||||
|
in: HasUndefined<In> extends true
|
||||||
|
? {
|
||||||
|
[K in Target]?: In extends ValidationTargets[K]
|
||||||
|
? In
|
||||||
|
: { [K2 in keyof In]?: ValidationTargets[K][K2] }
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
[K in Target]: In extends ValidationTargets[K]
|
||||||
|
? In
|
||||||
|
: { [K2 in keyof In]: ValidationTargets[K][K2] }
|
||||||
|
}
|
||||||
|
out: { [K in Target]: Out }
|
||||||
|
},
|
||||||
|
V extends I = I
|
||||||
|
>(
|
||||||
|
target: Target,
|
||||||
|
schema: Schema,
|
||||||
|
hook?: Hook<StandardSchemaV1.InferOutput<Schema>, E, P, Target>
|
||||||
|
): MiddlewareHandler<E, P, V> =>
|
||||||
|
// @ts-expect-error not typed well
|
||||||
|
validator(target, async (value, c) => {
|
||||||
|
const result = await schema['~standard'].validate(value)
|
||||||
|
|
||||||
|
if (hook) {
|
||||||
|
const hookResult = await hook(
|
||||||
|
!!result.issues
|
||||||
|
? { data: value, error: result.issues, success: false, target }
|
||||||
|
: { data: value, success: true, target },
|
||||||
|
c
|
||||||
|
)
|
||||||
|
if (hookResult) {
|
||||||
|
if (hookResult instanceof Response) {
|
||||||
|
return hookResult
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('response' in hookResult) {
|
||||||
|
return hookResult.response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.issues) {
|
||||||
|
return c.json({ data: value, error: result.issues, success: false }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.value as StandardSchemaV1.InferOutput<Schema>
|
||||||
|
})
|
||||||
|
|
||||||
|
export type { Hook }
|
||||||
|
export { sValidator }
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { type } from 'arktype'
|
||||||
|
|
||||||
|
const personJSONSchema = type({
|
||||||
|
name: 'string',
|
||||||
|
age: 'number',
|
||||||
|
})
|
||||||
|
|
||||||
|
const postJSONSchema = type({
|
||||||
|
id: 'number',
|
||||||
|
title: 'string',
|
||||||
|
})
|
||||||
|
|
||||||
|
const idJSONSchema = type({
|
||||||
|
id: 'string',
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryNameSchema = type({
|
||||||
|
'name?': 'string',
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryPaginationSchema = type({
|
||||||
|
page: type('unknown').pipe((p) => Number(p)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const querySortSchema = type({
|
||||||
|
order: "'asc'|'desc'",
|
||||||
|
})
|
||||||
|
|
||||||
|
export {
|
||||||
|
idJSONSchema,
|
||||||
|
personJSONSchema,
|
||||||
|
postJSONSchema,
|
||||||
|
queryNameSchema,
|
||||||
|
queryPaginationSchema,
|
||||||
|
querySortSchema,
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { object, string, number, optional, pipe, unknown, transform, picklist } from 'valibot'
|
||||||
|
|
||||||
|
const personJSONSchema = object({
|
||||||
|
name: string(),
|
||||||
|
age: number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const postJSONSchema = object({
|
||||||
|
id: number(),
|
||||||
|
title: string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const idJSONSchema = object({
|
||||||
|
id: string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryNameSchema = optional(
|
||||||
|
object({
|
||||||
|
name: optional(string()),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const queryPaginationSchema = object({
|
||||||
|
page: pipe(unknown(), transform(Number)),
|
||||||
|
})
|
||||||
|
|
||||||
|
const querySortSchema = object({
|
||||||
|
order: picklist(['asc', 'desc']),
|
||||||
|
})
|
||||||
|
|
||||||
|
export {
|
||||||
|
idJSONSchema,
|
||||||
|
personJSONSchema,
|
||||||
|
postJSONSchema,
|
||||||
|
queryNameSchema,
|
||||||
|
queryPaginationSchema,
|
||||||
|
querySortSchema,
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const personJSONSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
age: z.number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const postJSONSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
title: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const idJSONSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryNameSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
|
||||||
|
const queryPaginationSchema = z.object({
|
||||||
|
page: z.coerce.number(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const querySortSchema = z.object({
|
||||||
|
order: z.enum(['asc', 'desc']),
|
||||||
|
})
|
||||||
|
|
||||||
|
export {
|
||||||
|
idJSONSchema,
|
||||||
|
personJSONSchema,
|
||||||
|
postJSONSchema,
|
||||||
|
queryNameSchema,
|
||||||
|
queryPaginationSchema,
|
||||||
|
querySortSchema,
|
||||||
|
}
|
|
@ -0,0 +1,356 @@
|
||||||
|
import { Hono } from 'hono'
|
||||||
|
import type { Equal, Expect, UnionToIntersection } from 'hono/utils/types'
|
||||||
|
import { sValidator } from '../src'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
import * as valibotSchemas from './__schemas__/valibot'
|
||||||
|
import * as zodSchemas from './__schemas__/zod'
|
||||||
|
import * as arktypeSchemas from './__schemas__/arktype'
|
||||||
|
|
||||||
|
type ExtractSchema<T> = T extends Hono<infer _, infer S> ? S : never
|
||||||
|
type MergeDiscriminatedUnion<U> = UnionToIntersection<U> extends infer O
|
||||||
|
? { [K in keyof O]: O[K] }
|
||||||
|
: never
|
||||||
|
|
||||||
|
const libs = ['valibot', 'zod', 'arktype'] as const
|
||||||
|
const schemasByLibrary = {
|
||||||
|
valibot: valibotSchemas,
|
||||||
|
zod: zodSchemas,
|
||||||
|
arktype: arktypeSchemas,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Standard Schema Validation', () => {
|
||||||
|
libs.forEach((lib) => {
|
||||||
|
const schemas = schemasByLibrary[lib]
|
||||||
|
describe(`Using ${lib} schemas for validation`, () => {
|
||||||
|
describe('Basic', () => {
|
||||||
|
const app = new Hono()
|
||||||
|
const route = app.post(
|
||||||
|
'/author',
|
||||||
|
sValidator('json', schemas.personJSONSchema),
|
||||||
|
sValidator('query', schemas.queryNameSchema),
|
||||||
|
(c) => {
|
||||||
|
const data = c.req.valid('json')
|
||||||
|
const query = c.req.valid('query')
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: `${data.name} is ${data.age}`,
|
||||||
|
queryName: query?.name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Actual = ExtractSchema<typeof route>
|
||||||
|
type verifyOutput = Expect<
|
||||||
|
Equal<
|
||||||
|
{
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
queryName: string | undefined
|
||||||
|
},
|
||||||
|
MergeDiscriminatedUnion<Actual['/author']['$post']['output']>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
type verifyJSONInput = Expect<
|
||||||
|
Equal<
|
||||||
|
{
|
||||||
|
name: string
|
||||||
|
age: number
|
||||||
|
},
|
||||||
|
MergeDiscriminatedUnion<Actual['/author']['$post']['input']['json']>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
type verifyQueryInput = Expect<
|
||||||
|
Equal<
|
||||||
|
| {
|
||||||
|
name?: string | undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name?: string | undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
name?: string | undefined
|
||||||
|
}
|
||||||
|
| undefined,
|
||||||
|
Actual['/author']['$post']['input']['query']
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
it('Should return 200 response', async () => {
|
||||||
|
const req = new Request('http://localhost/author?name=Metallo', {
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Superman',
|
||||||
|
age: 20,
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await app.request(req)
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({
|
||||||
|
success: true,
|
||||||
|
message: 'Superman is 20',
|
||||||
|
queryName: 'Metallo',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should return 400 response', async () => {
|
||||||
|
const req = new Request('http://localhost/author', {
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: 'Superman',
|
||||||
|
age: '20',
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await app.request(req)
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
const data = (await res.json()) as { success: boolean }
|
||||||
|
expect(data['success']).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('coerce', () => {
|
||||||
|
const app = new Hono()
|
||||||
|
const schema = schemas.queryPaginationSchema
|
||||||
|
|
||||||
|
const route = app.get('/page', sValidator('query', schema), (c) => {
|
||||||
|
const { page } = c.req.valid('query')
|
||||||
|
return c.json({ page })
|
||||||
|
})
|
||||||
|
|
||||||
|
type Actual = ExtractSchema<typeof route>
|
||||||
|
type Expected = {
|
||||||
|
'/page': {
|
||||||
|
$get: {
|
||||||
|
input: {
|
||||||
|
query:
|
||||||
|
| {
|
||||||
|
page: string | string[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
page: string | string[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
page: string | string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output: {
|
||||||
|
page: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type verifyInput = Expect<
|
||||||
|
Equal<
|
||||||
|
{ page: string | string[] },
|
||||||
|
MergeDiscriminatedUnion<Actual['/page']['$get']['input']['query']>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
type verifyOutput = Expect<
|
||||||
|
Equal<
|
||||||
|
{
|
||||||
|
page: number
|
||||||
|
},
|
||||||
|
MergeDiscriminatedUnion<Actual['/page']['$get']['output']>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
|
it('Should return 200 response', async () => {
|
||||||
|
const res = await app.request('/page?page=123')
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.json()).toEqual({
|
||||||
|
page: 123,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With Hook', () => {
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
const schema = schemas.postJSONSchema
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/post',
|
||||||
|
sValidator('json', schema, (result, c) => {
|
||||||
|
if (!result.success) {
|
||||||
|
return c.text(`${result.data.id} is invalid!`, 400)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(c) => {
|
||||||
|
const data = c.req.valid('json')
|
||||||
|
return c.text(`${data.id} is valid!`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it('Should return 200 response', async () => {
|
||||||
|
const req = new Request('http://localhost/post', {
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: 123,
|
||||||
|
title: 'Hello',
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await app.request(req)
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.text()).toBe('123 is valid!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should return 400 response', async () => {
|
||||||
|
const req = new Request('http://localhost/post', {
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: '123',
|
||||||
|
title: 'Hello',
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await app.request(req)
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
expect(await res.text()).toBe('123 is invalid!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With Async Hook', () => {
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
const schema = schemas.postJSONSchema
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
'/post',
|
||||||
|
sValidator('json', schema, async (result, c) => {
|
||||||
|
if (!result.success) {
|
||||||
|
return c.text(`${result.data.id} is invalid!`, 400)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
(c) => {
|
||||||
|
const data = c.req.valid('json')
|
||||||
|
return c.text(`${data.id} is valid!`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it('Should return 200 response', async () => {
|
||||||
|
const req = new Request('http://localhost/post', {
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: 123,
|
||||||
|
title: 'Hello',
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await app.request(req)
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.text()).toBe('123 is valid!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should return 400 response', async () => {
|
||||||
|
const req = new Request('http://localhost/post', {
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: '123',
|
||||||
|
title: 'Hello',
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const res = await app.request(req)
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(400)
|
||||||
|
expect(await res.text()).toBe('123 is invalid!')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('With target', () => {
|
||||||
|
it('should call hook for correctly validated target', async () => {
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
const schema = schemas.idJSONSchema
|
||||||
|
|
||||||
|
const jsonHook = vi.fn()
|
||||||
|
const paramHook = vi.fn()
|
||||||
|
const queryHook = vi.fn()
|
||||||
|
app.post(
|
||||||
|
'/:id/post',
|
||||||
|
sValidator('json', schema, jsonHook),
|
||||||
|
sValidator('param', schema, paramHook),
|
||||||
|
sValidator('query', schema, queryHook),
|
||||||
|
(c) => {
|
||||||
|
return c.text('ok')
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const req = new Request('http://localhost/1/post?id=2', {
|
||||||
|
body: JSON.stringify({
|
||||||
|
id: '3',
|
||||||
|
}),
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await app.request(req)
|
||||||
|
expect(res).not.toBeNull()
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
expect(await res.text()).toBe('ok')
|
||||||
|
expect(paramHook).toHaveBeenCalledWith(
|
||||||
|
{ data: { id: '1' }, success: true, target: 'param' },
|
||||||
|
expect.anything()
|
||||||
|
)
|
||||||
|
expect(queryHook).toHaveBeenCalledWith(
|
||||||
|
{ data: { id: '2' }, success: true, target: 'query' },
|
||||||
|
expect.anything()
|
||||||
|
)
|
||||||
|
expect(jsonHook).toHaveBeenCalledWith(
|
||||||
|
{ data: { id: '3' }, success: true, target: 'json' },
|
||||||
|
expect.anything()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Only Types', () => {
|
||||||
|
it('Should return correct enum types for query', () => {
|
||||||
|
const app = new Hono()
|
||||||
|
|
||||||
|
const schema = schemas.querySortSchema
|
||||||
|
|
||||||
|
const route = app.get('/', sValidator('query', schema), (c) => {
|
||||||
|
const data = c.req.valid('query')
|
||||||
|
return c.json(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
type Actual = ExtractSchema<typeof route>
|
||||||
|
type verifyInput = Expect<
|
||||||
|
Equal<
|
||||||
|
{ order: 'asc' | 'desc' },
|
||||||
|
MergeDiscriminatedUnion<Actual['/']['$get']['input']['query']>
|
||||||
|
>
|
||||||
|
>
|
||||||
|
type verifyOutput = Expect<
|
||||||
|
Equal<{ order: 'asc' | 'desc' }, MergeDiscriminatedUnion<Actual['/']['$get']['output']>>
|
||||||
|
>
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "./",
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"test/**/*.ts"
|
||||||
|
],
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
/// <reference types="vitest" />
|
||||||
|
import { defineConfig } from 'vitest/config'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
})
|
91
yarn.lock
91
yarn.lock
|
@ -44,6 +44,22 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@ark/schema@npm:0.26.0":
|
||||||
|
version: 0.26.0
|
||||||
|
resolution: "@ark/schema@npm:0.26.0"
|
||||||
|
dependencies:
|
||||||
|
"@ark/util": "npm:0.26.0"
|
||||||
|
checksum: e038b73bd0d1a7556d5d7ab70382ddcc1ba35cafdddf81ea961a676d10bd2d642514c08b7a7cd9f4d99fb8e5ec3ae4e2ea8d3bbdf1d8b19d94149379f1c739f3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@ark/util@npm:0.26.0":
|
||||||
|
version: 0.26.0
|
||||||
|
resolution: "@ark/util@npm:0.26.0"
|
||||||
|
checksum: 60c54dca4556b1ccb6f4a3dc1e28beb09219ee3f01124916550c993f0508ac2142c9c967992ee16a71334e849c214234e6c3e4c2271dc104a893068a9cc33afc
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@arktype/schema@npm:0.1.4-cjs":
|
"@arktype/schema@npm:0.1.4-cjs":
|
||||||
version: 0.1.4-cjs
|
version: 0.1.4-cjs
|
||||||
resolution: "@arktype/schema@npm:0.1.4-cjs"
|
resolution: "@arktype/schema@npm:0.1.4-cjs"
|
||||||
|
@ -2916,6 +2932,25 @@ __metadata:
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@hono/standard-validator@workspace:packages/standard-validator":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@hono/standard-validator@workspace:packages/standard-validator"
|
||||||
|
dependencies:
|
||||||
|
"@standard-schema/spec": "npm:1.0.0"
|
||||||
|
arktype: "npm:^2.0.0-rc.26"
|
||||||
|
hono: "npm:^4.0.10"
|
||||||
|
publint: "npm:^0.2.7"
|
||||||
|
tsup: "npm:^8.1.0"
|
||||||
|
typescript: "npm:^5.7.3"
|
||||||
|
valibot: "npm:^1.0.0-beta.9"
|
||||||
|
vitest: "npm:^1.4.0"
|
||||||
|
zod: "npm:^3.24.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@standard-schema/spec": 1.0.0
|
||||||
|
hono: ">=3.9.0"
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@hono/swagger-editor@workspace:packages/swagger-editor":
|
"@hono/swagger-editor@workspace:packages/swagger-editor":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@hono/swagger-editor@workspace:packages/swagger-editor"
|
resolution: "@hono/swagger-editor@workspace:packages/swagger-editor"
|
||||||
|
@ -4846,6 +4881,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@standard-schema/spec@npm:1.0.0":
|
||||||
|
version: 1.0.0
|
||||||
|
resolution: "@standard-schema/spec@npm:1.0.0"
|
||||||
|
checksum: a1ab9a8bdc09b5b47aa8365d0e0ec40cc2df6437be02853696a0e377321653b0d3ac6f079a8c67d5ddbe9821025584b1fb71d9cc041a6666a96f1fadf2ece15f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@szmarczak/http-timer@npm:^1.1.2":
|
"@szmarczak/http-timer@npm:^1.1.2":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "@szmarczak/http-timer@npm:1.1.2"
|
resolution: "@szmarczak/http-timer@npm:1.1.2"
|
||||||
|
@ -6522,6 +6564,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"arktype@npm:^2.0.0-rc.26":
|
||||||
|
version: 2.0.0-rc.26
|
||||||
|
resolution: "arktype@npm:2.0.0-rc.26"
|
||||||
|
dependencies:
|
||||||
|
"@ark/schema": "npm:0.26.0"
|
||||||
|
"@ark/util": "npm:0.26.0"
|
||||||
|
checksum: 190c4a82baec546bca704b26bffa73209a756ea4a0a2ade080a6d640a807e7eb1990a69e6507112f103151a37592cca84591569c90e607fb2865bd6ea2532b9a
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"array-buffer-byte-length@npm:^1.0.0":
|
"array-buffer-byte-length@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "array-buffer-byte-length@npm:1.0.0"
|
resolution: "array-buffer-byte-length@npm:1.0.0"
|
||||||
|
@ -20770,6 +20822,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"typescript@npm:^5.7.3":
|
||||||
|
version: 5.7.3
|
||||||
|
resolution: "typescript@npm:5.7.3"
|
||||||
|
bin:
|
||||||
|
tsc: bin/tsc
|
||||||
|
tsserver: bin/tsserver
|
||||||
|
checksum: b7580d716cf1824736cc6e628ab4cd8b51877408ba2be0869d2866da35ef8366dd6ae9eb9d0851470a39be17cbd61df1126f9e211d8799d764ea7431d5435afa
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"typescript@patch:typescript@npm%3A^4.7.4#optional!builtin<compat/typescript>":
|
"typescript@patch:typescript@npm%3A^4.7.4#optional!builtin<compat/typescript>":
|
||||||
version: 4.9.5
|
version: 4.9.5
|
||||||
resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin<compat/typescript>::version=4.9.5&hash=289587"
|
resolution: "typescript@patch:typescript@npm%3A4.9.5#optional!builtin<compat/typescript>::version=4.9.5&hash=289587"
|
||||||
|
@ -20820,6 +20882,16 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"typescript@patch:typescript@npm%3A^5.7.3#optional!builtin<compat/typescript>":
|
||||||
|
version: 5.7.3
|
||||||
|
resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin<compat/typescript>::version=5.7.3&hash=e012d7"
|
||||||
|
bin:
|
||||||
|
tsc: bin/tsc
|
||||||
|
tsserver: bin/tsserver
|
||||||
|
checksum: 3b56d6afa03d9f6172d0b9cdb10e6b1efc9abc1608efd7a3d2f38773d5d8cfb9bbc68dfb72f0a7de5e8db04fc847f4e4baeddcd5ad9c9feda072234f0d788896
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"typia@npm:^7.3.0":
|
"typia@npm:^7.3.0":
|
||||||
version: 7.3.0
|
version: 7.3.0
|
||||||
resolution: "typia@npm:7.3.0"
|
resolution: "typia@npm:7.3.0"
|
||||||
|
@ -21246,6 +21318,18 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"valibot@npm:^1.0.0-beta.9":
|
||||||
|
version: 1.0.0-beta.9
|
||||||
|
resolution: "valibot@npm:1.0.0-beta.9"
|
||||||
|
peerDependencies:
|
||||||
|
typescript: ">=5"
|
||||||
|
peerDependenciesMeta:
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
checksum: ecd20ec024f5f05985002b385f624d9218c839a54c23f3dbf3e193161207c049859d99069b257334756b3a07e2734e93456061600dd1101aec121828df3ab286
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"valid-url@npm:^1":
|
"valid-url@npm:^1":
|
||||||
version: 1.0.9
|
version: 1.0.9
|
||||||
resolution: "valid-url@npm:1.0.9"
|
resolution: "valid-url@npm:1.0.9"
|
||||||
|
@ -22571,6 +22655,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"zod@npm:^3.24.0":
|
||||||
|
version: 3.24.1
|
||||||
|
resolution: "zod@npm:3.24.1"
|
||||||
|
checksum: 0223d21dbaa15d8928fe0da3b54696391d8e3e1e2d0283a1a070b5980a1dbba945ce631c2d1eccc088fdbad0f2dfa40155590bf83732d3ac4fcca2cc9237591b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"zwitch@npm:^2.0.0":
|
"zwitch@npm:^2.0.0":
|
||||||
version: 2.0.4
|
version: 2.0.4
|
||||||
resolution: "zwitch@npm:2.0.4"
|
resolution: "zwitch@npm:2.0.4"
|
||||||
|
|
Loading…
Reference in New Issue