Skip to content

Commit

Permalink
feat(client) add cookie type validate (#1476)
Browse files Browse the repository at this point in the history
* feat: add hc header type check

* fix: revert comment

* denoify

* feat(client): add cookie type valid

* denoify

* feat: remove function from cookie helper

* refactor: modify client type
  • Loading branch information
hagishi authored Sep 18, 2023
1 parent 5970f64 commit cdb36b8
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 34 deletions.
15 changes: 14 additions & 1 deletion deno_dist/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Hono } from '../hono.ts'
import type { ValidationTargets } from '../types.ts'
import { serialize } from '../utils/cookie.ts'
import type { UnionToIntersection } from '../utils/types.ts'
import type { Callback, Client, ClientRequestOptions } from './types.ts'
import { deepMerge, mergePath, removeIndexString, replaceUrlParam } from './utils.ts'
Expand Down Expand Up @@ -86,7 +87,19 @@ class ClientRequestImpl {
let methodUpperCase = this.method.toUpperCase()
let setBody = !(methodUpperCase === 'GET' || methodUpperCase === 'HEAD')

const headerValues: Record<string, string> = opt?.headers ? opt.headers : {}
const headerValues: Record<string, string> = {
...(args?.header ?? {}),
...(opt?.headers ? opt.headers : {}),
}

if (args?.cookie) {
const cookies: string[] = []
for (const [key, value] of Object.entries(args.cookie)) {
cookies.push(serialize(key, value, { path: '/' }))
}
headerValues['Cookie'] = cookies.join(',')
}

if (this.cType) headerValues['Content-Type'] = this.cType

const headers = new Headers(headerValues ?? undefined)
Expand Down
13 changes: 1 addition & 12 deletions deno_dist/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type { RemoveBlankRecord } from '../utils/types.ts'

type HonoRequest = typeof Hono.prototype['request']

type HasHeaderOption<T> = T extends { header: unknown } ? T['header'] : never

export type ClientRequestOptions<T = unknown> = keyof T extends never
? {
headers?: Record<string, string>
Expand All @@ -20,16 +18,7 @@ type ClientRequest<S extends Schema> = {
[M in keyof S]: S[M] extends { input: infer R; output: infer O }
? RemoveBlankRecord<R> extends never
? (args?: {}, options?: ClientRequestOptions) => Promise<ClientResponse<O>>
: HasHeaderOption<R> extends never
? (
// Client does not support `cookie`
args?: Omit<R, 'header' | 'cookie'>,
options?: ClientRequestOptions
) => Promise<ClientResponse<O>>
: (
args: Omit<R, 'header' | 'cookie'> | undefined,
options: ClientRequestOptions<HasHeaderOption<R>>
) => Promise<ClientResponse<O>>
: (args: R, options?: ClientRequestOptions) => Promise<ClientResponse<O>>
: never
} & {
$url: () => URL
Expand Down
93 changes: 85 additions & 8 deletions src/client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { setupServer } from 'msw/node'
import _fetch, { Request as NodeFetchRequest } from 'node-fetch'
import { vi } from 'vitest'
import { Hono } from '../hono'
import { parse, serialize } from '../utils/cookie'
import type { Equal, Expect } from '../utils/types'
import { validator } from '../validator'
import { hc } from './client'
Expand All @@ -25,7 +26,6 @@ describe('Basic - JSON', () => {
const route = app
.post(
'/posts',
// Client does not support `cookie`
validator('cookie', () => {
return {} as {
debug: string
Expand Down Expand Up @@ -96,12 +96,14 @@ describe('Basic - JSON', () => {
const res = await client.posts.$post(
{
json: payload,
},
{
headers: {
header: {
'x-message': 'foobar',
},
}
cookie: {
debug: 'true',
},
},
{}
)

expect(res.ok).toBe(true)
Expand All @@ -120,7 +122,7 @@ describe('Basic - JSON', () => {
})
})

describe('Basic - query, queries, form, and path params', () => {
describe('Basic - query, queries, form, path params, header and cookie', () => {
const app = new Hono()

const route = app
Expand Down Expand Up @@ -161,6 +163,30 @@ describe('Basic - query, queries, form, and path params', () => {
return c.jsonT(data)
}
)
.get(
'/header',
validator('header', () => {
return {
'x-message-id': 'Hello',
}
}),
(c) => {
const data = c.req.valid('header')
return c.jsonT(data)
}
)
.get(
'/cookie',
validator('cookie', () => {
return {
hello: 'world',
}
}),
(c) => {
const data = c.req.valid('cookie')
return c.jsonT(data)
}
)

const server = setupServer(
rest.get('http://localhost/api/search', (req, res, ctx) => {
Expand Down Expand Up @@ -192,6 +218,16 @@ describe('Basic - query, queries, form, and path params', () => {
// @ts-ignore
const string = String.fromCharCode.apply('', new Uint8Array(buffer))
return res(ctx.status(200), ctx.text(string))
}),
rest.get('http://localhost/api/header', async (req, res, ctx) => {
const message = await req.headers.get('x-message-id')
return res(ctx.status(200), ctx.json({ 'x-message-id': message }))
}),

rest.get('http://localhost/api/cookie', async (req, res, ctx) => {
const obj = parse(req.headers.get('cookie') || '')
const value = obj['hello']
return res(ctx.status(200), ctx.json({ hello: value }))
})
)

Expand Down Expand Up @@ -247,6 +283,30 @@ describe('Basic - query, queries, form, and path params', () => {
expect(res.status).toBe(200)
expect(await res.text()).toMatch('Good Night')
})

it('Should get 200 response - header', async () => {
const header = {
'x-message-id': 'Hello',
}
const res = await client.header.$get({
header,
})

expect(res.status).toBe(200)
expect(await res.json()).toEqual(header)
})

it('Should get 200 response - cookie', async () => {
const cookie = {
hello: 'world',
}
const res = await client.cookie.$get({
cookie,
})

expect(res.status).toBe(200)
expect(await res.json()).toEqual(cookie)
})
})

describe('Infer the response/request type', () => {
Expand All @@ -264,6 +324,11 @@ describe('Infer the response/request type', () => {
'x-request-id': 'dummy',
}
}),
validator('cookie', () => {
return {
name: 'dummy',
}
}),
(c) =>
c.jsonT({
id: 123,
Expand Down Expand Up @@ -302,11 +367,23 @@ describe('Infer the response/request type', () => {
const req = client.index.$get
type c = typeof req

type Actual = InferRequestOptionsType<c>
type Actual = InferRequestType<c>
type Expected = {
'x-request-id': string
}
type verify = Expect<Equal<Expected, Actual['headers']>>
type verify = Expect<Equal<Expected, Actual['header']>>
})

it('Should infer request cookie type the type correctly', () => {
const client = hc<AppType>('/')
const req = client.index.$get
type c = typeof req

type Actual = InferRequestType<c>
type Expected = {
name: string
}
type verify = Expect<Equal<Expected, Actual['cookie']>>
})

describe('Without input', () => {
Expand Down
15 changes: 14 additions & 1 deletion src/client/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Hono } from '../hono'
import type { ValidationTargets } from '../types'
import { serialize } from '../utils/cookie'
import type { UnionToIntersection } from '../utils/types'
import type { Callback, Client, ClientRequestOptions } from './types'
import { deepMerge, mergePath, removeIndexString, replaceUrlParam } from './utils'
Expand Down Expand Up @@ -86,7 +87,19 @@ class ClientRequestImpl {
let methodUpperCase = this.method.toUpperCase()
let setBody = !(methodUpperCase === 'GET' || methodUpperCase === 'HEAD')

const headerValues: Record<string, string> = opt?.headers ? opt.headers : {}
const headerValues: Record<string, string> = {
...(args?.header ?? {}),
...(opt?.headers ? opt.headers : {}),
}

if (args?.cookie) {
const cookies: string[] = []
for (const [key, value] of Object.entries(args.cookie)) {
cookies.push(serialize(key, value, { path: '/' }))
}
headerValues['Cookie'] = cookies.join(',')
}

if (this.cType) headerValues['Content-Type'] = this.cType

const headers = new Headers(headerValues ?? undefined)
Expand Down
13 changes: 1 addition & 12 deletions src/client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type { RemoveBlankRecord } from '../utils/types'

type HonoRequest = typeof Hono.prototype['request']

type HasHeaderOption<T> = T extends { header: unknown } ? T['header'] : never

export type ClientRequestOptions<T = unknown> = keyof T extends never
? {
headers?: Record<string, string>
Expand All @@ -20,16 +18,7 @@ type ClientRequest<S extends Schema> = {
[M in keyof S]: S[M] extends { input: infer R; output: infer O }
? RemoveBlankRecord<R> extends never
? (args?: {}, options?: ClientRequestOptions) => Promise<ClientResponse<O>>
: HasHeaderOption<R> extends never
? (
// Client does not support `cookie`
args?: Omit<R, 'header' | 'cookie'>,
options?: ClientRequestOptions
) => Promise<ClientResponse<O>>
: (
args: Omit<R, 'header' | 'cookie'> | undefined,
options: ClientRequestOptions<HasHeaderOption<R>>
) => Promise<ClientResponse<O>>
: (args: R, options?: ClientRequestOptions) => Promise<ClientResponse<O>>
: never
} & {
$url: () => URL
Expand Down

0 comments on commit cdb36b8

Please sign in to comment.