feat: Conform Validator Middleware (#666)
* feat: add `@hono/conform-validator` to packages * docs: Add the conform-validtor middleware usage to README.md * docs: fix README * refactor: Fix tests to use HTTPException * fix: update devDependencies in conform-validator * chore: add github workflows for conform-validator * feat: add changesets * fix: Init conform-validator version to 0.0.0 for changesets * feat: Add a hook option to `conformValidator()` * feat: Fixed the conformValidator to return an error response when a validation error occurs * fix: Fixed node version used in CI from 18.x to 20.x * fix: Fix to use tsup in build command * chore: delete `.skip` from `it` in test files. * chore: fix title in test files. * fix: Fixed to return 400 response when the request body is not FormData * chore: fixed to change patch to major in changeset. * chore: Removed unused librariespull/675/head
parent
e2ede3bdfd
commit
d4a69131e1
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@hono/conform-validator': major
|
||||
---
|
||||
|
||||
Create Conform validator middleware
|
|
@ -0,0 +1,25 @@
|
|||
name: ci-conform-validator
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'packages/conform-validator/**'
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
paths:
|
||||
- 'packages/conform-validator/**'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/conform-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
|
|
@ -34,6 +34,7 @@
|
|||
"build:node-ws": "yarn workspace @hono/node-ws build",
|
||||
"build:react-compat": "yarn workspace @hono/react-compat build",
|
||||
"build:effect-validator": "yarn workspace @hono/effect-validator build",
|
||||
"build:conform-validator": "yarn workspace @hono/conform-validator build",
|
||||
"build": "run-p 'build:*'",
|
||||
"lint": "eslint 'packages/**/*.{ts,tsx}'",
|
||||
"lint:fix": "eslint --fix 'packages/**/*.{ts,tsx}'",
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
# Conform validator middleware for Hono
|
||||
|
||||
The validator middleware using [conform](https://conform.guide) for [Hono](https://honojs.dev) applications. This middleware allows you to validate submitted FormValue and making better use of [Hono RPC](https://hono.dev/docs/guides/rpc).
|
||||
|
||||
## Usage
|
||||
|
||||
Zod:
|
||||
|
||||
```ts
|
||||
import { z } from 'zod'
|
||||
import { parseWithZod } from '@conform-to/zod'
|
||||
import { conformValidator } from '@hono/conform-validator'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
age: z.string(),
|
||||
})
|
||||
|
||||
app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithZod(formData, { schema })),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const data = submission.value
|
||||
|
||||
return c.json({ success: true, message: `${data.name} is ${data.age}` })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Yup:
|
||||
|
||||
```ts
|
||||
import { object, string } from 'yup'
|
||||
import { parseWithYup } from '@conform-to/yup'
|
||||
import { conformValidator } from '@hono/conform-validator'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
const schema = object({
|
||||
name: string(),
|
||||
age: string(),
|
||||
})
|
||||
|
||||
app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithYup(formData, { schema })),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const data = submission.value
|
||||
return c.json({ success: true, message: `${data.name} is ${data.age}` })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
Valibot:
|
||||
|
||||
```ts
|
||||
import { object, string } from 'valibot'
|
||||
import { parseWithValibot } from 'conform-to-valibot'
|
||||
import { conformValidator } from '@hono/conform-validator'
|
||||
import { HTTPException } from 'hono/http-exception'
|
||||
|
||||
const schema = object({
|
||||
name: string(),
|
||||
age: string(),
|
||||
})
|
||||
|
||||
app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithYup(formData, { schema })),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const data = submission.value
|
||||
return c.json({ success: true, message: `${data.name} is ${data.age}` })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Custom Hook Option
|
||||
|
||||
By default, `conformValidator()` returns a [`SubmissionResult`](https://github.com/edmundhung/conform/blob/6b98c077d757edd4846321678dfb6de283c177b1/packages/conform-dom/submission.ts#L40-L47) when a validation error occurs. If you wish to change this behavior, or if you wish to perform common processing, you can modify the response by passing a function as the second argument.
|
||||
|
||||
```ts
|
||||
app.post(
|
||||
'/author',
|
||||
conformValidator(
|
||||
(formData) => parseWithYup(formData, { schema })
|
||||
(submission, c) => {
|
||||
if(submission.status !== 'success') {
|
||||
return c.json({ success: false, message: 'Bad Request' }, 400)
|
||||
}
|
||||
}
|
||||
),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const data = submission.value
|
||||
return c.json({ success: true, message: `${data.name} is ${data.age}` })
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> if a response is returned by the Hook function, subsequent middleware or handler functions will not be executed. [see more](https://hono.dev/docs/concepts/middleware).
|
||||
|
||||
## Author
|
||||
|
||||
uttk <https://github.com/uttk>
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"name": "@hono/conform-validator",
|
||||
"version": "0.0.0",
|
||||
"description": "Validator middleware using Conform",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "vitest --run",
|
||||
"build": "tsup ./src/index.ts --format esm,cjs --dts",
|
||||
"prerelease": "yarn build && yarn test",
|
||||
"release": "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": {
|
||||
"@conform-to/dom": ">=1.1.5",
|
||||
"hono": ">=4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@conform-to/dom": "^1.1.5",
|
||||
"@conform-to/yup": "^1.1.5",
|
||||
"@conform-to/zod": "^1.1.5",
|
||||
"conform-to-valibot": "^1.10.0",
|
||||
"hono": "^4.5.1",
|
||||
"tsup": "^8.2.3",
|
||||
"valibot": "^0.36.0",
|
||||
"vitest": "^2.0.4",
|
||||
"yup": "^1.4.0",
|
||||
"zod": "^3.23.8"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import type { Context, Env, Input as HonoInput, MiddlewareHandler, ValidationTargets } from 'hono'
|
||||
import type { Submission } from '@conform-to/dom'
|
||||
import { getFormDataFromContext } from './utils'
|
||||
|
||||
type FormTargetValue = ValidationTargets['form']['string']
|
||||
|
||||
type GetInput<T extends ParseFn> = T extends (_: any) => infer S
|
||||
? Awaited<S> extends Submission<any, any, infer V>
|
||||
? V
|
||||
: never
|
||||
: never
|
||||
|
||||
type GetSuccessSubmission<S> = S extends { status: 'success' } ? S : never
|
||||
|
||||
type ParseFn = (formData: FormData) => Submission<unknown> | Promise<Submission<unknown>>
|
||||
|
||||
type Hook<F extends ParseFn, E extends Env, P extends string> = (
|
||||
submission: Awaited<ReturnType<F>>,
|
||||
c: Context<E, P>
|
||||
) => Response | Promise<Response> | void | Promise<Response | void>
|
||||
|
||||
export const conformValidator = <
|
||||
F extends ParseFn,
|
||||
E extends Env,
|
||||
P extends string,
|
||||
In = GetInput<F>,
|
||||
Out = Awaited<ReturnType<F>>,
|
||||
I extends HonoInput = {
|
||||
in: {
|
||||
form: { [K in keyof In]: FormTargetValue }
|
||||
}
|
||||
out: { form: GetSuccessSubmission<Out> }
|
||||
}
|
||||
>(
|
||||
parse: F,
|
||||
hook?: Hook<F, E, P>
|
||||
): MiddlewareHandler<E, P, I> => {
|
||||
return async (c, next) => {
|
||||
const formData = await getFormDataFromContext(c)
|
||||
const submission = await parse(formData)
|
||||
|
||||
if (hook) {
|
||||
const hookResult = hook(submission as any, c)
|
||||
if (hookResult instanceof Response || hookResult instanceof Promise) {
|
||||
return hookResult
|
||||
}
|
||||
}
|
||||
|
||||
if (submission.status !== 'success') {
|
||||
return c.json(submission.reply(), 400)
|
||||
}
|
||||
|
||||
c.req.addValidatedData('form', submission)
|
||||
|
||||
await next()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import type { Context } from 'hono'
|
||||
import { bufferToFormData } from 'hono/utils/buffer'
|
||||
|
||||
// ref: https://github.com/honojs/hono/blob/a63bcfd6fba66297d8234c21aed8a42ac00711fe/src/validator/validator.ts#L27-L28
|
||||
const multipartRegex = /^multipart\/form-data(; boundary=[A-Za-z0-9'()+_,\-./:=?]+)?$/
|
||||
const urlencodedRegex = /^application\/x-www-form-urlencoded$/
|
||||
|
||||
export const getFormDataFromContext = async (ctx: Context): Promise<FormData> => {
|
||||
const contentType = ctx.req.header('Content-Type')
|
||||
if (!contentType || !(multipartRegex.test(contentType) || urlencodedRegex.test(contentType))) {
|
||||
return new FormData()
|
||||
}
|
||||
|
||||
const cache = ctx.req.bodyCache.formData
|
||||
if (cache) {
|
||||
return cache
|
||||
}
|
||||
|
||||
const arrayBuffer = await ctx.req.arrayBuffer()
|
||||
const formData = await bufferToFormData(arrayBuffer, contentType)
|
||||
|
||||
ctx.req.bodyCache.formData = formData
|
||||
|
||||
return formData
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { Hono } from 'hono'
|
||||
import { z } from 'zod'
|
||||
import { parseWithZod } from '@conform-to/zod'
|
||||
import { conformValidator } from '../src'
|
||||
|
||||
describe('Validate common processing', () => {
|
||||
const app = new Hono()
|
||||
const schema = z.object({ name: z.string() })
|
||||
const route = app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithZod(formData, { schema })),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const value = submission.value
|
||||
return c.json({ success: true, message: `my name is ${value.name}` })
|
||||
}
|
||||
)
|
||||
|
||||
describe('When the request body is empty', () => {
|
||||
it('Should return 400 response', async () => {
|
||||
const res = await route.request('/author', { method: 'POST' })
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
|
||||
describe('When the request body is not FormData', () => {
|
||||
it('Should return 400 response', async () => {
|
||||
const res = await route.request('/author', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name: 'Space Cat!' }),
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,62 @@
|
|||
import * as z from 'zod'
|
||||
import { Hono } from 'hono'
|
||||
import { hc } from 'hono/client'
|
||||
import { parseWithZod } from '@conform-to/zod'
|
||||
import { conformValidator } from '../src'
|
||||
import { vi } from 'vitest'
|
||||
|
||||
describe('Validate the hook option processing', () => {
|
||||
const app = new Hono()
|
||||
const schema = z.object({ name: z.string() })
|
||||
const hookMockFn = vi.fn((submission, c) => {
|
||||
if (submission.status !== 'success') {
|
||||
return c.json({ success: false, message: 'Bad Request' }, 400)
|
||||
}
|
||||
})
|
||||
const handlerMockFn = vi.fn((c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const value = submission.value
|
||||
return c.json({ success: true, message: `name is ${value.name}` })
|
||||
})
|
||||
const route = app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithZod(formData, { schema }), hookMockFn),
|
||||
handlerMockFn
|
||||
)
|
||||
const client = hc<typeof route>('http://localhost', {
|
||||
fetch: (req, init) => {
|
||||
return app.request(req, init)
|
||||
},
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
hookMockFn.mockClear()
|
||||
handlerMockFn.mockClear()
|
||||
})
|
||||
|
||||
it('Should called hook function', async () => {
|
||||
await client.author.$post({ form: { name: 'Space Cat' } })
|
||||
expect(hookMockFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('When the hook return Response', () => {
|
||||
it('Should return response that the hook returned', async () => {
|
||||
const req = new Request('http://localhost/author', { body: new FormData(), method: 'POST' })
|
||||
const res = (await app.request(req)).clone()
|
||||
const hookRes = hookMockFn.mock.results[0].value.clone()
|
||||
expect(hookMockFn).toHaveReturnedWith(expect.any(Response))
|
||||
expect(res.status).toBe(hookRes.status)
|
||||
expect(await res.json()).toStrictEqual(await hookRes.json())
|
||||
})
|
||||
})
|
||||
|
||||
describe('When the hook not return Response', () => {
|
||||
it('Should return response that the handler function returned', async () => {
|
||||
const res = (await client.author.$post({ form: { name: 'Space Cat' } })).clone()
|
||||
const handlerRes = handlerMockFn.mock.results[0].value.clone()
|
||||
expect(hookMockFn).not.toHaveReturnedWith(expect.any(Response))
|
||||
expect(res.status).toBe(handlerRes.status)
|
||||
expect(await res.json()).toStrictEqual(await handlerRes.json())
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,111 @@
|
|||
import type { ExtractSchema, ParsedFormValue } from 'hono/types'
|
||||
import type { Equal, Expect } from 'hono/utils/types'
|
||||
import type { StatusCode } from 'hono/utils/http-status'
|
||||
import * as v from 'valibot'
|
||||
import { Hono } from 'hono'
|
||||
import { hc } from 'hono/client'
|
||||
import { parseWithValibot } from 'conform-to-valibot'
|
||||
import { conformValidator } from '../src'
|
||||
|
||||
describe('Validate requests using a Valibot schema', () => {
|
||||
const app = new Hono()
|
||||
|
||||
const schema = v.object({
|
||||
name: v.string(),
|
||||
age: v.pipe(
|
||||
v.string(),
|
||||
v.transform((v) => Number(v)),
|
||||
v.integer()
|
||||
),
|
||||
nickname: v.optional(v.string()),
|
||||
})
|
||||
|
||||
const route = app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithValibot(formData, { schema })),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const value = submission.value
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: `${value.name} is ${value.age}, nickname is ${
|
||||
value?.nickname || 'nothing yet :3'
|
||||
}`,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('check the route object types', () => {
|
||||
type Actual = ExtractSchema<typeof route>
|
||||
type Expected = {
|
||||
'/author': {
|
||||
$post: {
|
||||
input: {
|
||||
form: {
|
||||
name: ParsedFormValue | ParsedFormValue[]
|
||||
age: ParsedFormValue | ParsedFormValue[]
|
||||
nickname?: ParsedFormValue | ParsedFormValue[] | undefined
|
||||
}
|
||||
}
|
||||
output: {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
outputFormat: 'json'
|
||||
status: StatusCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type verify = Expect<Equal<Expected, Actual>>
|
||||
})
|
||||
|
||||
it('Should return 200 response', async () => {
|
||||
const client = hc<typeof route>('http://localhost', {
|
||||
fetch: (req, init) => {
|
||||
return app.request(req, init)
|
||||
},
|
||||
})
|
||||
|
||||
const res = await client.author.$post({
|
||||
form: {
|
||||
name: 'Space Cat',
|
||||
age: '20',
|
||||
nickname: 'meow',
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const json = await res.json()
|
||||
expect(json).toEqual({
|
||||
success: true,
|
||||
message: 'Space Cat is 20, nickname is meow',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response', async () => {
|
||||
const formData = new FormData()
|
||||
|
||||
const req = new Request('http://localhost/author', {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
|
||||
const json = await res.json()
|
||||
expect(json).toMatchObject({
|
||||
status: 'error',
|
||||
error: {
|
||||
name: ['Invalid type: Expected string but received undefined'],
|
||||
age: ['Invalid type: Expected string but received undefined'],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,107 @@
|
|||
import type { ExtractSchema, ParsedFormValue } from 'hono/types'
|
||||
import type { Equal, Expect } from 'hono/utils/types'
|
||||
import type { StatusCode } from 'hono/utils/http-status'
|
||||
import * as y from 'yup'
|
||||
import { Hono } from 'hono'
|
||||
import { hc } from 'hono/client'
|
||||
import { parseWithYup } from '@conform-to/yup'
|
||||
import { conformValidator } from '../src'
|
||||
|
||||
describe('Validate requests using a Yup schema', () => {
|
||||
const app = new Hono()
|
||||
|
||||
const schema = y.object({
|
||||
name: y.string().required(),
|
||||
age: y.number().required(),
|
||||
nickname: y.string().optional(),
|
||||
})
|
||||
|
||||
const route = app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithYup(formData, { schema })),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const value = submission.value
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: `${value.name} is ${value.age}, nickname is ${
|
||||
value?.nickname || 'nothing yet :3'
|
||||
}`,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('check the route object types', () => {
|
||||
type Actual = ExtractSchema<typeof route>
|
||||
type Expected = {
|
||||
'/author': {
|
||||
$post: {
|
||||
input: {
|
||||
form: {
|
||||
name: ParsedFormValue | ParsedFormValue[]
|
||||
age: ParsedFormValue | ParsedFormValue[]
|
||||
nickname?: ParsedFormValue | ParsedFormValue[] | undefined
|
||||
}
|
||||
}
|
||||
output: {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
outputFormat: 'json'
|
||||
status: StatusCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type verify = Expect<Equal<Expected, Actual>>
|
||||
})
|
||||
|
||||
it('Should return 200 response', async () => {
|
||||
const client = hc<typeof route>('http://localhost', {
|
||||
fetch: (req, init) => {
|
||||
return app.request(req, init)
|
||||
},
|
||||
})
|
||||
|
||||
const res = await client.author.$post({
|
||||
form: {
|
||||
name: 'Space Cat',
|
||||
age: '20',
|
||||
nickname: 'meow',
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const json = await res.json()
|
||||
expect(json).toEqual({
|
||||
success: true,
|
||||
message: 'Space Cat is 20, nickname is meow',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response', async () => {
|
||||
const formData = new FormData()
|
||||
|
||||
const req = new Request('http://localhost/author', {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
|
||||
const json = await res.json()
|
||||
expect(json).toMatchObject({
|
||||
status: 'error',
|
||||
error: {
|
||||
name: ['name is a required field'],
|
||||
age: ['age is a required field'],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,107 @@
|
|||
import type { ExtractSchema, ParsedFormValue } from 'hono/types'
|
||||
import type { Equal, Expect } from 'hono/utils/types'
|
||||
import type { StatusCode } from 'hono/utils/http-status'
|
||||
import * as z from 'zod'
|
||||
import { Hono } from 'hono'
|
||||
import { hc } from 'hono/client'
|
||||
import { parseWithZod } from '@conform-to/zod'
|
||||
import { conformValidator } from '../src'
|
||||
|
||||
describe('Validate requests using a Zod schema', () => {
|
||||
const app = new Hono()
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
age: z.string().transform((str) => Number(str)),
|
||||
nickname: z.string().optional(),
|
||||
})
|
||||
|
||||
const route = app.post(
|
||||
'/author',
|
||||
conformValidator((formData) => parseWithZod(formData, { schema })),
|
||||
(c) => {
|
||||
const submission = c.req.valid('form')
|
||||
const value = submission.value
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: `${value.name} is ${value.age}, nickname is ${
|
||||
value?.nickname || 'nothing yet :3'
|
||||
}`,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('check the route object types', () => {
|
||||
type Actual = ExtractSchema<typeof route>
|
||||
type Expected = {
|
||||
'/author': {
|
||||
$post: {
|
||||
input: {
|
||||
form: {
|
||||
name: ParsedFormValue | ParsedFormValue[]
|
||||
age: ParsedFormValue | ParsedFormValue[]
|
||||
nickname?: ParsedFormValue | ParsedFormValue[] | undefined
|
||||
}
|
||||
}
|
||||
output: {
|
||||
success: boolean
|
||||
message: string
|
||||
}
|
||||
outputFormat: 'json'
|
||||
status: StatusCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
type verify = Expect<Equal<Expected, Actual>>
|
||||
})
|
||||
|
||||
it('Should return 200 response', async () => {
|
||||
const client = hc<typeof route>('http://localhost', {
|
||||
fetch: (req, init) => {
|
||||
return app.request(req, init)
|
||||
},
|
||||
})
|
||||
|
||||
const res = await client.author.$post({
|
||||
form: {
|
||||
name: 'Space Cat',
|
||||
age: '20',
|
||||
nickname: 'meow',
|
||||
},
|
||||
})
|
||||
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
|
||||
const json = await res.json()
|
||||
expect(json).toEqual({
|
||||
success: true,
|
||||
message: 'Space Cat is 20, nickname is meow',
|
||||
})
|
||||
})
|
||||
|
||||
it('Should return 400 response', async () => {
|
||||
const formData = new FormData()
|
||||
|
||||
const req = new Request('http://localhost/author', {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
})
|
||||
|
||||
const res = await app.request(req)
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(400)
|
||||
|
||||
const json = await res.json()
|
||||
expect(json).toMatchObject({
|
||||
status: 'error',
|
||||
error: {
|
||||
name: ['Required'],
|
||||
age: ['Required'],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"declaration": false,
|
||||
"outDir": "./dist/cjs"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"declaration": true,
|
||||
"outDir": "./dist/esm"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue