From 04c5e636efc2bc73f2eab71f37ee15594a3a12d8 Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Mon, 9 Sep 2024 16:58:26 +0800 Subject: [PATCH 1/3] add zod validator --- package.json | 5 +- src/constants/api_connection.ts | 2 + src/constants/zod_schema.ts | 17 ++++++ src/interfaces/zod_validator.ts | 18 +++++++ src/lib/utils/request_validator.ts | 69 +++++++++++++++++++++++++ src/lib/utils/zod_schema/zod_example.ts | 49 ++++++++++++++++++ src/pages/api/v1/company/zod/index.ts | 15 ++++++ 7 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 src/constants/zod_schema.ts create mode 100644 src/interfaces/zod_validator.ts create mode 100644 src/lib/utils/request_validator.ts create mode 100644 src/lib/utils/zod_schema/zod_example.ts create mode 100644 src/pages/api/v1/company/zod/index.ts diff --git a/package.json b/package.json index e45f4dcba..c103d4d42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.1", + "version": "0.8.1+1", "private": false, "scripts": { "dev": "next dev", @@ -59,7 +59,8 @@ "tailwind-merge": "^2.2.2", "ts-node": "^10.9.2", "uuid": "^10.0.0", - "winston": "^3.14.2" + "winston": "^3.14.2", + "zod": "^3.23.8" }, "devDependencies": { "@babel/eslint-plugin": "^7.25.1", diff --git a/src/constants/api_connection.ts b/src/constants/api_connection.ts index 8426a592c..601d4a6f8 100644 --- a/src/constants/api_connection.ts +++ b/src/constants/api_connection.ts @@ -81,6 +81,7 @@ export enum APIName { GET_PROJECT_BY_ID = 'GET_PROJECT_BY_ID', UPDATE_PROJECT_BY_ID = 'UPDATE_PROJECT_BY_ID', PUBLIC_KEY_GET = 'PUBLIC_KEY_GET', + ZOD_EXAMPLE = 'ZOD_EXAMPLE', // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data. } export enum APIPath { @@ -145,6 +146,7 @@ export enum APIPath { GET_PROJECT_BY_ID = `${apiPrefix}/company/:companyId/project/:projectId`, UPDATE_PROJECT_BY_ID = `${apiPrefix}/company/:companyId/project/:projectId`, PUBLIC_KEY_GET = `${apiPrefix}/company/:companyId/public_key`, + ZOD_EXAMPLE = `${apiPrefix}/company/zod`, // Info: (20240909 - Murky) This is a Zod example, to demonstrate how to use Zod schema to validate data. } const createConfig = ({ name, diff --git a/src/constants/zod_schema.ts b/src/constants/zod_schema.ts new file mode 100644 index 000000000..11055231d --- /dev/null +++ b/src/constants/zod_schema.ts @@ -0,0 +1,17 @@ +import { APIName } from '@/constants/api_connection'; +import { zodExampleValidator } from '@/lib/utils/zod_schema/zod_example'; + +/* + * Info: (20240909 - Murky) Record need to implement all the keys of the enum, + * it will cause error when not implement all the keys + * use code below after all the keys are implemented + */ + +// import { IZodValidator } from "@/interfaces/zod_validator"; +// export const API_ZOD_SCHEMA: Record = { +// [APIName.ZOD_EXAMPLE]: zodExampleValidator, +// }; + +export const API_ZOD_SCHEMA = { + [APIName.ZOD_EXAMPLE]: zodExampleValidator, +}; diff --git a/src/interfaces/zod_validator.ts b/src/interfaces/zod_validator.ts new file mode 100644 index 000000000..41f223d77 --- /dev/null +++ b/src/interfaces/zod_validator.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +// Info: (20240909 - Murky) This interface is specifically for validator of api + +// export interface IZodValidator { +// query: z.ZodObject | z.ZodUndefined, // T 用于表示 query 的 Zod schema 类型 +// body: z.ZodObject | z.ZodUndefined, // U 用于表示 body 的 Zod schema 类型 +// } + +export interface IZodValidator< + T extends z.ZodRawShape | z.ZodOptional>, + U extends z.ZodRawShape | z.ZodOptional>, +> { + // Info: (20240909 - Murky) If T is undefined, query is z.ZodUndefined, otherwise it is z.ZodObject + query: T extends z.ZodRawShape ? z.ZodObject : z.ZodOptional>; + + // Info: (20240909 - Murky) If U is undefined, body is z.ZodUndefined, otherwise it is z.ZodObject + body: U extends z.ZodRawShape ? z.ZodObject : z.ZodOptional>; +} diff --git a/src/lib/utils/request_validator.ts b/src/lib/utils/request_validator.ts new file mode 100644 index 000000000..07dd0de25 --- /dev/null +++ b/src/lib/utils/request_validator.ts @@ -0,0 +1,69 @@ +import { API_ZOD_SCHEMA } from '@/constants/zod_schema'; +import { NextApiRequest } from 'next'; +import { z } from 'zod'; +import { loggerRequest } from '@/lib/utils/logger_back'; +import { APIPath } from '@/constants/api_connection'; +/* + * Info: (20240909 - Murky) Record need to implement all the keys of the enum, + * it will cause error when not implement all the keys + * use code below after all the keys are implemented + */ +// import { APIName } from "@/constants/api_connection"; +// export function validateRequest( +// apiName: APIName, +// req: NextApiRequest, +// res: NextApiResponse) { +type API_ZodSchema = typeof API_ZOD_SCHEMA; +type QueryType = z.infer; +type BodyType = z.infer; + +export function validateRequest( + apiName: T, + req: NextApiRequest, + userId: number = -1 +): { query: QueryType | null; body: BodyType | null } { + const { query: queryValidator, body: bodyValidator } = API_ZOD_SCHEMA[apiName]; + + const { query, body } = req; + + // Info: (20240909 - Murky) Validate query and body + const queryResult = queryValidator.safeParse(query); + const bodyResult = bodyValidator.safeParse(body); + + // Info: (20240909 - Murky) If validation failed, it will return null, go to logger to check why it failed + let payload: { + query: QueryType | null; + body: BodyType | null; + } = { + query: null, + body: null, + }; + + if (!queryResult.success || !bodyResult.success) { + // Info: (20240909 - Murky) It will return why error is caused, or what is the data if success + const errorFormat = { + query: queryResult.error ? queryResult.error.format() : query.data, + body: bodyResult.error ? bodyResult.error.format() : body.data, + }; + + const logger = loggerRequest( + userId, + APIPath[apiName], + req.method || 'unknown', + 400, + errorFormat, + req.headers['user-agent'] || 'unknown user-agent', + req.socket.remoteAddress || 'unknown ip' + ); + + logger.error('Request validation failed'); + } else { + // Info: (20240909 - Murky) if validator is z.ZodOptional (which used when query or body is not needed), it will return null + payload = { + query: queryValidator instanceof z.ZodOptional ? null : queryResult.data, + body: bodyValidator instanceof z.ZodOptional ? null : bodyResult.data, + }; + } + + return payload; +} diff --git a/src/lib/utils/zod_schema/zod_example.ts b/src/lib/utils/zod_schema/zod_example.ts new file mode 100644 index 000000000..b260249f5 --- /dev/null +++ b/src/lib/utils/zod_schema/zod_example.ts @@ -0,0 +1,49 @@ +/* + * Info: (20240909 - Murky) This file is to demonstrate how to use Zod schema to validate data. + * check more validator function in:https://www.npmjs.com/package/zod#primitives + */ +import { IZodValidator } from '@/interfaces/zod_validator'; +import { z } from 'zod'; + +enum testEnum { + A = 'A', + B = 'B', + C = 'C', +} + +const queryValidator = z.object({ + name: z.string().min(3), + /** + * Info: (20240909 - Murky) + * There is plenty of ways to validate numeric string, + * Check: https://github.com/colinhacks/zod/discussions/330 + */ + age: z.string().regex(/^\d+$/).transform(Number), + email: z.string().email(), + password: z.string().min(6), + testEnum: z.nativeEnum(testEnum), +}); + +// Info: (20240909 - Murky) If you want to validate body, you can add bodyValidator +// const bodyValidator = z.object({ +// bodyName: z.string().min(3), +// }); + +// export const zodExampleValidator: IZodValidator< +// typeof queryValidator['shape'], +// typeof bodyValidator['shape'] +// > = { +// query: queryValidator, +// body: bodyValidator, +// }; + +// Info: (20240909 - Murky) If you don't want to validate body, you can use z.string().nullish() +const bodyValidator = z.string().nullish(); + +export const zodExampleValidator: IZodValidator< + (typeof queryValidator)['shape'], + typeof bodyValidator +> = { + query: queryValidator, + body: bodyValidator, +}; diff --git a/src/pages/api/v1/company/zod/index.ts b/src/pages/api/v1/company/zod/index.ts new file mode 100644 index 000000000..6456dbeb1 --- /dev/null +++ b/src/pages/api/v1/company/zod/index.ts @@ -0,0 +1,15 @@ +/* + * Info: (20240909 - Murky) This api is to demonstrate how to use Zod schema to validate data. + * Delete this file after all the keys are implemented + */ + +import { NextApiRequest, NextApiResponse } from 'next'; +import { APIName } from '@/constants/api_connection'; +import { validateRequest } from '@/lib/utils/request_validator'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const validatedQuery = validateRequest(APIName.ZOD_EXAMPLE, req); + + const { query, body } = validatedQuery; + res.status(200).json({ query, body }); +} From f31f84f6059d454b8e1618db7f55becc5de9dca9 Mon Sep 17 00:00:00 2001 From: Tiny_Murky Date: Mon, 9 Sep 2024 17:00:53 +0800 Subject: [PATCH 2/3] change package json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c103d4d42..bd2f1e0e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.1+1", + "version": "0.8.1+3", "private": false, "scripts": { "dev": "next dev", From ca9ea8b516453781511f1ab518dc1fd5b1d8c821 Mon Sep 17 00:00:00 2001 From: Luphia Chang Date: Mon, 9 Sep 2024 19:05:23 +0800 Subject: [PATCH 3/3] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bd2f1e0e1..f34a8c5c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.1+3", + "version": "0.8.1+4", "private": false, "scripts": { "dev": "next dev",