Skip to content

Commit

Permalink
feat: add safe validators
Browse files Browse the repository at this point in the history
  • Loading branch information
wobsoriano committed Nov 12, 2022
1 parent d6c3515 commit 4d91b29
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 130 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ lib-cov
logs
node_modules
temp
.vscode
203 changes: 75 additions & 128 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,164 +1,111 @@
import type { EventHandler, H3Event } from 'h3'
import { createError, eventHandler, getQuery, isMethod, readBody } from 'h3'
import type { H3Event } from 'h3'
import { createError, getQuery, readBody } from 'h3'
import { z } from 'zod'

// copy of the private Zod utility type of ZodObject
type UnknownKeysParam = 'passthrough' | 'strict' | 'strip'

type Refined<T extends z.ZodType> = T extends z.ZodType<infer O>
? z.ZodEffects<T, O, O>
: never

/**
* @desc The type allowed on the top level of Middlewares and Endpoints
* @param U — only "strip" is allowed for Middlewares due to intersection issue (Zod) #600
* */
export type IOSchema<U extends UnknownKeysParam = any> =
type Schema<U extends UnknownKeysParam = any> =
| z.ZodObject<any, U>
| z.ZodUnion<[IOSchema<U>, ...IOSchema<U>[]]>
| z.ZodIntersection<IOSchema<U>, IOSchema<U>>
| z.ZodUnion<[Schema<U>, ...Schema<U>[]]>
| z.ZodIntersection<Schema<U>, Schema<U>>
| z.ZodDiscriminatedUnion<string, z.Primitive, z.ZodObject<any, U>>
| Refined<z.ZodObject<any, U>>

type ParsedData<T extends Schema | z.ZodRawShape> = T extends Schema
? z.output<T>
: T extends z.ZodRawShape
? z.output<z.ZodObject<T>>
: never

const DEFAULT_ERROR_MESSAGE = 'Bad Request'
const DEFAULT_ERROR_STATUS = 400

/**
*
* @param event H3 Event
* @param schema Zod Schema
* @param errorHandler Use your own error handler instead of throwing an H3Error
* Parse and validate request query from event handler. Throws an error if validation fails.
* @param event - A H3 event object.
* @param schema - A Zod object shape or object schema to validate.
*/
export function useValidatedQuery<T extends IOSchema>(
export function useValidatedQuery<T extends Schema | z.ZodRawShape>(
event: H3Event,
schema: T,
errorHandler?: (error: z.ZodIssue[]) => void,
) {
const query = getQuery(event)
const parsed = schema.safeParse(query)

if (!parsed.success) {
if (errorHandler) {
errorHandler(parsed.error.issues)
return
}

): ParsedData<T> {
try {
const query = getQuery(event)
const finalSchema = schema instanceof z.ZodType ? schema : z.object(schema)
return finalSchema.parse(query)
}
catch (error) {
throw createError({
statusCode: 400,
statusMessage: JSON.stringify({
errors: parsed.error.issues,
statusCode: DEFAULT_ERROR_STATUS,
statusText: DEFAULT_ERROR_MESSAGE,
data: JSON.stringify({
errors: error as z.ZodIssue[],
}),
})
}

return parsed.data as z.infer<T>
}

type SafeParsedData<T extends Schema | z.ZodRawShape> = T extends Schema
? z.SafeParseReturnType<T, ParsedData<T>>
: T extends z.ZodRawShape
? z.SafeParseReturnType<z.ZodObject<T>, ParsedData<T>>
: never

/**
*
* @param event H3 Event
* @param schema Zod Schema
* @param errorHandler Use your own error handler instead of throwing an H3Error
* Parse and validate query from event handler. Doesn't throw if validation fails.
* @param event - A H3 event object.
* @param schema - A Zod object shape or object schema to validate.
*/
export async function useValidatedBody<T extends IOSchema>(
export function useSafeValidatedQuery<T extends Schema | z.ZodRawShape>(
event: H3Event,
schema: T,
errorHandler?: (error: z.ZodIssue[]) => void,
) {
const body = await readBody(event)
const parsed = schema.safeParse(body)

if (!parsed.success) {
if (errorHandler) {
errorHandler(parsed.error.issues)
return
}
): SafeParsedData<T> {
const query = getQuery(event)
const finalSchema = schema instanceof z.ZodType ? schema : z.object(schema)
return finalSchema.safeParse(query) as SafeParsedData<T>
}

/**
* Parse and validate request body from event handler. Throws an error if validation fails.
* @param event - A H3 event object.
* @param schema - A Zod object shape or object schema to validate.
*/
export async function useValidatedBody<T extends Schema | z.ZodRawShape>(
event: H3Event,
schema: T,
): Promise<ParsedData<T>> {
try {
const body = await readBody(event)
const finalSchema = schema instanceof z.ZodType ? schema : z.object(schema)
return finalSchema.parse(body)
}
catch (error) {
throw createError({
statusCode: 400,
statusMessage: JSON.stringify({
errors: parsed.error.issues,
statusCode: DEFAULT_ERROR_STATUS,
statusText: DEFAULT_ERROR_MESSAGE,
data: JSON.stringify({
errors: error as z.ZodIssue[],
}),
})
}

return parsed.data as z.infer<T>
}

interface RequestSchemas<
TBody extends IOSchema,
TQuery extends IOSchema,
> {
body?: TBody
query?: TQuery
}

interface RequestIssues {
body: z.ZodIssue[] | null
query: z.ZodIssue[] | null
}

export function defineEventHandlerWithSchema<
TBody extends IOSchema,
TQuery extends IOSchema,
>({
handler,
schema,
errorHandler,
}: {
handler: EventHandler
schema: RequestSchemas<TBody, TQuery>
errorHandler?: (errors: RequestIssues, event: H3Event) => void
}) {
return eventHandler(async (event) => {
const errors: RequestIssues = {
body: null,
query: null,
}

const parsedData: {
body: z.infer<TBody> | null
query: z.infer<TQuery> | null
} = {
body: null,
query: null,
}

if (schema.query) {
const query = getQuery(event)
const parsed = schema.query.safeParse(query)

if (!parsed.success)
errors.query = parsed.error.issues
else
parsedData.query = parsed.data as z.infer<TQuery>
}

if (schema.body && isMethod(event, 'POST')) {
const body = await readBody(event)
const parsed = schema.body.safeParse(body)

if (!parsed.success)
errors.body = parsed.error.issues
else
parsedData.body = parsed.data as z.infer<TBody>
}

if (errors.body || errors.query) {
if (errorHandler) {
errorHandler(errors, event)
return
}

throw createError({
statusCode: 400,
statusMessage: JSON.stringify({
errors,
}),
})
}

event.context.parsedData = parsedData

return handler(event)
})
/**
* Parse and validate request body from event handler. Doesn't throw if validation fails.
* @param event - A H3 event object.
* @param schema - A Zod object shape or object schema to validate.
*/
export async function useSafeValidatedBody<T extends Schema | z.ZodRawShape>(
event: H3Event,
schema: T,
): Promise<SafeParsedData<T>> {
const body = await readBody(event)
const finalSchema = schema instanceof z.ZodType ? schema : z.object(schema)
return finalSchema.safeParse(body) as SafeParsedData<T>
}

export {
Expand Down
3 changes: 2 additions & 1 deletion test/__snapshots__/body.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ exports[`useValidatedBody > returns 200 OK if body matches validation schema 1`]

exports[`useValidatedBody > throws 400 Bad Request if body does not match validation schema 1`] = `
{
"data": "{\\"errors\\":{\\"issues\\":[{\\"code\\":\\"invalid_type\\",\\"expected\\":\\"boolean\\",\\"received\\":\\"undefined\\",\\"path\\":[\\"required\\"],\\"message\\":\\"Required\\"}],\\"name\\":\\"ZodError\\"}}",
"stack": [],
"statusCode": 400,
"statusMessage": "{\\"errors\\":[{\\"code\\":\\"invalid_type\\",\\"expected\\":\\"boolean\\",\\"received\\":\\"undefined\\",\\"path\\":[\\"required\\"],\\"message\\":\\"Required\\"}]}",
"statusMessage": "Bad Request",
}
`;
3 changes: 2 additions & 1 deletion test/__snapshots__/query.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ exports[`useValidatedQuery > returns 200 OK if query matches validation schema 1

exports[`useValidatedQuery > throws 400 Bad Request if query does not match validation schema 1`] = `
{
"data": "{\\"errors\\":{\\"issues\\":[{\\"code\\":\\"invalid_type\\",\\"expected\\":\\"string\\",\\"received\\":\\"undefined\\",\\"path\\":[\\"required\\"],\\"message\\":\\"Required\\"}],\\"name\\":\\"ZodError\\"}}",
"stack": [],
"statusCode": 400,
"statusMessage": "{\\"errors\\":[{\\"code\\":\\"invalid_type\\",\\"expected\\":\\"string\\",\\"received\\":\\"undefined\\",\\"path\\":[\\"required\\"],\\"message\\":\\"Required\\"}]}",
"statusMessage": "Bad Request",
}
`;

0 comments on commit 4d91b29

Please sign in to comment.