0
0
mirror of https://github.com/honojs/hono.git synced 2024-12-01 11:51:01 +01:00

feat(req): support c.req.valid('query') (#832)

This commit is contained in:
Yusuke Wada 2023-01-23 07:47:29 +09:00 committed by GitHub
parent 2de2ef3279
commit 2afa06a5ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 140 additions and 29 deletions

View File

@ -1,6 +1,5 @@
import type { Context } from '../../context.ts'
import type { Env, ValidationTypes, MiddlewareHandler } from '../../types.ts'
import { mergeObjects } from '../../utils/object.ts'
type ValidationTypeKeysWithBody = 'form' | 'json'
type ValidationTypeByMethod<M> = M extends 'get' | 'head' // GET and HEAD request must not have a body content.
@ -55,10 +54,8 @@ export const validator = <
return res
}
const target = c.req.valid()
const newObject = mergeObjects(target, res)
c.req.addValidatedData(type, res as {})
c.req.valid(newObject)
await next()
}
}

View File

@ -1,8 +1,9 @@
import type { InputToData } from './types.ts'
import type { InputToData, InputToTypeData, ValidationTypes } from './types.ts'
import { parseBody } from './utils/body.ts'
import type { BodyData } from './utils/body.ts'
import type { Cookie } from './utils/cookie.ts'
import { parse } from './utils/cookie.ts'
import { mergeObjects } from './utils/object.ts'
import type { UnionToIntersection } from './utils/types.ts'
import { getQueryStringFromURL, getQueryParam, getQueryParams } from './utils/url.ts'
@ -36,7 +37,7 @@ export class HonoRequest<Path extends string = '/', Input = {}> {
private bodyData: BodyData | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private jsonData: Promise<any> | undefined
private data: InputToData<Input>
private validatedData: { [K in keyof ValidationTypes]?: {} }
private queryIndex: number
constructor(
@ -46,8 +47,8 @@ export class HonoRequest<Path extends string = '/', Input = {}> {
) {
this.raw = request
this.paramData = paramData
this.data = {} as InputToData<Input>
this.queryIndex = queryIndex
this.validatedData = {}
}
param(key: RemoveQuestion<ParamKeys<Path>>): UndefinedIfHavingQuestion<ParamKeys<Path>>
@ -157,14 +158,25 @@ export class HonoRequest<Path extends string = '/', Input = {}> {
return this.raw.formData()
}
valid(data?: unknown): InputToData<Input> {
if (!this.data) {
this.data = {} as InputToData<Input>
addValidatedData(type: keyof ValidationTypes, data: {}) {
const storedData = this.validatedData[type] || {}
const merged = mergeObjects(storedData, data)
this.validatedData[type] = merged
}
valid(): InputToData<Input>
valid<T extends keyof ValidationTypes>(type: T): InputToTypeData<T, Input>
valid<T extends keyof ValidationTypes>(type?: T) {
if (type) {
const data = this.validatedData[type]
return data
} else {
let data: Record<string, unknown> = {}
for (const v of Object.values(this.validatedData)) {
data = mergeObjects(data, v)
}
return data
}
if (data) {
this.data = data as InputToData<Input>
}
return this.data
}
get url() {

View File

@ -269,8 +269,16 @@ type ToAppTypeInner<P extends string, M extends string, I, O> = RemoveBlank<I> e
export type InputToData<T> = ExtractData<T> extends never
? any
: UnionToIntersection<ExtractData<T>>
type ExtractData<T> = T extends { type: keyof ValidationTypes }
? T extends { type: keyof ValidationTypes; data?: infer R }
? R
: any
: T
export type InputToTypeData<K extends keyof ValidationTypes, T> = T extends {
type: K
data: infer R
}
? R
: never

View File

@ -305,6 +305,8 @@ describe('Validator middleware with Zod multiple validators', () => {
(c) => {
const id = c.get('id')
type verify = Expect<Equal<number, typeof id>>
const formValidatedData = c.req.valid('form')
type verify2 = Expect<Equal<{ title: string }, typeof formValidatedData>>
const res = c.req.valid()
return c.jsonT({
page: res.page,

View File

@ -1,6 +1,5 @@
import type { Context } from '../../context'
import type { Env, ValidationTypes, MiddlewareHandler } from '../../types'
import { mergeObjects } from '../../utils/object'
type ValidationTypeKeysWithBody = 'form' | 'json'
type ValidationTypeByMethod<M> = M extends 'get' | 'head' // GET and HEAD request must not have a body content.
@ -55,10 +54,8 @@ export const validator = <
return res
}
const target = c.req.valid()
const newObject = mergeObjects(target, res)
c.req.addValidatedData(type, res as {})
c.req.valid(newObject)
await next()
}
}

57
src/request.test.ts Normal file
View File

@ -0,0 +1,57 @@
import { HonoRequest } from './request'
const rawRequest = new Request('http://localhost')
describe('req.addValidatedData() and req.data()', () => {
const payload = {
title: 'hello',
author: {
name: 'young man',
age: 20,
},
}
test('add data - json', () => {
const req = new HonoRequest(rawRequest)
req.addValidatedData('json', payload)
const data = req.valid('json')
expect(data).toEqual(payload)
})
test('append data - json', () => {
const req = new HonoRequest(rawRequest)
req.addValidatedData('json', payload)
req.addValidatedData('json', {
tag: ['sport', 'music'],
author: {
tall: 170,
},
})
const data = req.valid('json')
expect(data).toEqual({
title: 'hello',
author: {
name: 'young man',
age: 20,
tall: 170,
},
tag: ['sport', 'music'],
})
})
test('req.data() with no argument', () => {
const req = new HonoRequest(rawRequest)
req.addValidatedData('query', { page: '123', q: 'foo' })
req.addValidatedData('json', payload)
const data = req.valid()
expect(data).toEqual({
page: '123',
q: 'foo',
title: 'hello',
author: {
name: 'young man',
age: 20,
},
})
})
})

View File

@ -1,8 +1,9 @@
import type { InputToData } from './types'
import type { InputToData, InputToTypeData, ValidationTypes } from './types'
import { parseBody } from './utils/body'
import type { BodyData } from './utils/body'
import type { Cookie } from './utils/cookie'
import { parse } from './utils/cookie'
import { mergeObjects } from './utils/object'
import type { UnionToIntersection } from './utils/types'
import { getQueryStringFromURL, getQueryParam, getQueryParams } from './utils/url'
@ -36,7 +37,7 @@ export class HonoRequest<Path extends string = '/', Input = {}> {
private bodyData: BodyData | undefined
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private jsonData: Promise<any> | undefined
private data: InputToData<Input>
private validatedData: { [K in keyof ValidationTypes]?: {} }
private queryIndex: number
constructor(
@ -46,8 +47,8 @@ export class HonoRequest<Path extends string = '/', Input = {}> {
) {
this.raw = request
this.paramData = paramData
this.data = {} as InputToData<Input>
this.queryIndex = queryIndex
this.validatedData = {}
}
param(key: RemoveQuestion<ParamKeys<Path>>): UndefinedIfHavingQuestion<ParamKeys<Path>>
@ -157,14 +158,25 @@ export class HonoRequest<Path extends string = '/', Input = {}> {
return this.raw.formData()
}
valid(data?: unknown): InputToData<Input> {
if (!this.data) {
this.data = {} as InputToData<Input>
addValidatedData(type: keyof ValidationTypes, data: {}) {
const storedData = this.validatedData[type] || {}
const merged = mergeObjects(storedData, data)
this.validatedData[type] = merged
}
valid(): InputToData<Input>
valid<T extends keyof ValidationTypes>(type: T): InputToTypeData<T, Input>
valid<T extends keyof ValidationTypes>(type?: T) {
if (type) {
const data = this.validatedData[type]
return data
} else {
let data: Record<string, unknown> = {}
for (const v of Object.values(this.validatedData)) {
data = mergeObjects(data, v)
}
return data
}
if (data) {
this.data = data as InputToData<Input>
}
return this.data
}
get url() {

View File

@ -2,7 +2,13 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Hono } from './hono'
import { poweredBy } from './middleware/powered-by'
import type { Env, CustomHandler as Handler, InputToData, ToAppType } from './types'
import type {
Env,
CustomHandler as Handler,
InputToData,
InputToTypeData,
ToAppType,
} from './types'
import type { Expect, Equal } from './utils/types'
describe('Test types of CustomHandler', () => {
@ -135,6 +141,18 @@ describe('Types used in the validator', () => {
}
type verify = Expect<Equal<Expected, Actual>>
})
test('InputToTypeData', () => {
type P =
| {}
| { type: 'query'; data: { page: number } }
| { type: 'form'; data: { title: string } }
type Actual = InputToTypeData<'query', P>
type Expected = {
page: number
}
type verify = Expect<Equal<Expected, Actual>>
})
})
describe('`jsonT()`', () => {

View File

@ -269,8 +269,16 @@ type ToAppTypeInner<P extends string, M extends string, I, O> = RemoveBlank<I> e
export type InputToData<T> = ExtractData<T> extends never
? any
: UnionToIntersection<ExtractData<T>>
type ExtractData<T> = T extends { type: keyof ValidationTypes }
? T extends { type: keyof ValidationTypes; data?: infer R }
? R
: any
: T
export type InputToTypeData<K extends keyof ValidationTypes, T> = T extends {
type: K
data: infer R
}
? R
: never