Skip to content

Commit

Permalink
feat: add runtime validation
Browse files Browse the repository at this point in the history
  • Loading branch information
Hebilicious committed Jul 25, 2023
1 parent 78aec63 commit 2597c82
Show file tree
Hide file tree
Showing 6 changed files with 337 additions and 3 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"test": "pnpm lint && vitest run --coverage"
},
"dependencies": {
"@decs/typeschema": "^0.5.2",
"cookie-es": "^1.0.0",
"defu": "^6.1.2",
"destr": "^2.0.0",
Expand Down Expand Up @@ -58,7 +59,8 @@
"supertest": "^6.3.3",
"typescript": "^5.1.6",
"unbuild": "^1.2.1",
"vitest": "^0.33.0"
"vitest": "^0.33.0",
"zod": "^3.21.4"
},
"packageManager": "[email protected]"
}
73 changes: 72 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 31 additions & 1 deletion src/utils/body.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import type { IncomingMessage } from "node:http";
import destr from "destr";
import destr, { safeDestr } from "destr";
import type { Encoding, HTTPMethod } from "../types";
import type { H3Event } from "../event";
import { createError } from "../error";
import { parse as parseMultipartData } from "./internal/multipart";
import { assertSchema, type Infer, type Schema } from "./internal/validation";
import { assertMethod, getRequestHeader } from "./request";

export type { MultiPartData } from "./internal/multipart";
Expand Down Expand Up @@ -183,3 +184,32 @@ function _parseURLEncodedBody(body: string) {
}
return parsedForm as unknown;
}

/**
* Accept an event and a schema, and return a typed and runtime validated object.
* Throws an error if the object doesn't match the schema.
* @param event {H3Event}
* @param schema {Schema} Any valid schema: zod, yup, joi, ajv, superstruct, io-ts, ow, typebox, typia, deepkit,
* runtypes, arktype or custom validation function.
* @param onError {Function} Optional error handler. Will receive the error thrown by the schema validation as first argument.
*/
export async function readBodySafe<TSchema extends Schema>(
event: H3Event,
schema: TSchema,
onError?: (err: any) => any
) {
if (ParsedBodySymbol in event.node.req) {
return (event.node.req as any)[ParsedBodySymbol] as Infer<typeof schema>;
}
const contentType = getRequestHeader(event, "content-type");
if (contentType?.startsWith("application/x-www-form-urlencoded")) {
const formPayload = Object.fromEntries(await readFormData(event));
const result = await assertSchema(schema, formPayload, onError);
(event.node.req as any)[ParsedBodySymbol] = result;
return result as Infer<typeof schema>;
}
const json = safeDestr(await readRawBody(event));
const result = await assertSchema(schema, json, onError);
(event.node.req as any)[ParsedBodySymbol] = result;
return result as Infer<typeof schema>;
}
19 changes: 19 additions & 0 deletions src/utils/internal/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Schema } from "@decs/typeschema";
import { assert } from "@decs/typeschema";
import { createError } from "src/error";
export type { Infer, Schema } from "@decs/typeschema";

export const assertSchema = async <T>(
schema: Schema,
payload: T,
onError?: (err: any) => any
) => {
try {
return await assert(schema, payload);
} catch (error) {
if (onError) {
return onError(error);
}
throw createError({ statusCode: 500, statusMessage: "Assertion Error." });
}
};
19 changes: 19 additions & 0 deletions src/utils/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { getQuery as _getQuery } from "ufo";
import { createError } from "../error";
import type { HTTPMethod, RequestHeaders } from "../types";
import type { H3Event } from "../event";
import { assertSchema, type Infer, type Schema } from "./internal/validation";

export function getQuery(event: H3Event) {
return _getQuery(event.path || "");
Expand Down Expand Up @@ -127,3 +128,21 @@ export function getRequestURL(
const path = event.path;
return new URL(path, `${protocol}://${host}`);
}

/**
* Accept an event and a schema, and return a typed and runtime validated object.
* Throws an error if the object doesn't match the schema.
* @param event {H3Event}
* @param schema {Schema} Any valid schema: zod, yup, joi, ajv, superstruct, io-ts, ow, typebox, typia, deepkit,
* runtypes, arktype or custom validation function.
* @param onError {Function} Optional error handler. Will receive the error thrown by the schema validation as first argument.
*/
export async function getQuerySafe<TSchema extends Schema>(
event: H3Event,
schema: TSchema,
onError?: (err: any) => any
) {
const query = getQuery(event);
const result = await assertSchema(schema, query, onError);
return result as Infer<typeof schema>;
}
Loading

0 comments on commit 2597c82

Please sign in to comment.