Skip to content

Commit

Permalink
Merge pull request #2326 from CAFECA-IO/feature/zod
Browse files Browse the repository at this point in the history
Feature/zod
  • Loading branch information
Luphia authored Sep 9, 2024
2 parents 254c76c + 8f51c48 commit 0bd1ab4
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 2 deletions.
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 });
}

0 comments on commit 0bd1ab4

Please sign in to comment.