Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/zod #2326

Merged
merged 5 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "iSunFA",
"version": "0.8.1+3",
"version": "0.8.1+4",
"private": false,
"scripts": {
"dev": "next dev",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/constants/api_connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions src/constants/zod_schema.ts
Original file line number Diff line number Diff line change
@@ -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, IZodValidator> = {
// [APIName.ZOD_EXAMPLE]: zodExampleValidator,
// };

export const API_ZOD_SCHEMA = {
[APIName.ZOD_EXAMPLE]: zodExampleValidator,
};
18 changes: 18 additions & 0 deletions src/interfaces/zod_validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { z } from 'zod';
// Info: (20240909 - Murky) This interface is specifically for validator of api

// export interface IZodValidator<T extends z.ZodRawShape, U extends z.ZodRawShape> {
// query: z.ZodObject<T> | z.ZodUndefined, // T 用于表示 query 的 Zod schema 类型
// body: z.ZodObject<U> | z.ZodUndefined, // U 用于表示 body 的 Zod schema 类型
// }

export interface IZodValidator<
T extends z.ZodRawShape | z.ZodOptional<z.ZodNullable<z.ZodString>>,
U extends z.ZodRawShape | z.ZodOptional<z.ZodNullable<z.ZodString>>,
> {
// Info: (20240909 - Murky) If T is undefined, query is z.ZodUndefined, otherwise it is z.ZodObject<T>
query: T extends z.ZodRawShape ? z.ZodObject<T> : z.ZodOptional<z.ZodNullable<z.ZodString>>;

// Info: (20240909 - Murky) If U is undefined, body is z.ZodUndefined, otherwise it is z.ZodObject<U>
body: U extends z.ZodRawShape ? z.ZodObject<U> : z.ZodOptional<z.ZodNullable<z.ZodString>>;
}
69 changes: 69 additions & 0 deletions src/lib/utils/request_validator.ts
Original file line number Diff line number Diff line change
@@ -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<T extends keyof API_ZodSchema> = z.infer<API_ZodSchema[T]['query']>;
type BodyType<T extends keyof API_ZodSchema> = z.infer<API_ZodSchema[T]['body']>;

export function validateRequest<T extends keyof typeof API_ZOD_SCHEMA>(
apiName: T,
req: NextApiRequest,
userId: number = -1
): { query: QueryType<T> | null; body: BodyType<T> | 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<T> | null;
body: BodyType<T> | 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;
}
49 changes: 49 additions & 0 deletions src/lib/utils/zod_schema/zod_example.ts
Original file line number Diff line number Diff line change
@@ -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,
};
15 changes: 15 additions & 0 deletions src/pages/api/v1/company/zod/index.ts
Original file line number Diff line number Diff line change
@@ -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 });
}