Upgrade dependencies and migrate from Joi to Zod for validation
This commit is contained in:
parent
4fa0190e4b
commit
a26d0c77a1
12 changed files with 498 additions and 287 deletions
|
@ -1,18 +1,13 @@
|
|||
import joi from "joi";
|
||||
import { z } from "zod";
|
||||
import { Config } from "../../types/config";
|
||||
import { server } from "./server.config";
|
||||
|
||||
const envSchema = joi
|
||||
.object({
|
||||
NODE_ENV: joi
|
||||
.string()
|
||||
.allow("development", "production", "test")
|
||||
.required(),
|
||||
})
|
||||
.unknown()
|
||||
.required();
|
||||
const envSchema = z.object({
|
||||
NODE_ENV: z.enum(["development", "production", "test"]),
|
||||
});
|
||||
|
||||
const { error, data: envVars } = envSchema.safeParse(process.env);
|
||||
|
||||
const { error, value: envVars } = envSchema.validate(process.env);
|
||||
if (error) {
|
||||
throw new Error(`Config validation error: ${error.message}`);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,14 @@
|
|||
import joi from "joi";
|
||||
import { z } from "zod";
|
||||
import { ServerConfig } from "../../types/config";
|
||||
|
||||
const envSchema = joi
|
||||
.object({
|
||||
HOST: joi.string().default("localhost"),
|
||||
PORT: joi.number().default(3000),
|
||||
API_VERSION: joi.number().default(1),
|
||||
ALLOWED_ORIGINS: joi.string().default("http://localhost:3000"),
|
||||
})
|
||||
.unknown()
|
||||
.required();
|
||||
const envSchema = z.object({
|
||||
HOST: z.string().default("localhost"),
|
||||
PORT: z.coerce.number().default(3000),
|
||||
API_VERSION: z.coerce.number().default(1),
|
||||
ALLOWED_ORIGINS: z.string().default("http://localhost:3000"),
|
||||
});
|
||||
|
||||
const { error, value: envVars } = envSchema.validate(process.env);
|
||||
const { error, data: envVars } = envSchema.safeParse(process.env);
|
||||
if (error) {
|
||||
throw new Error(`Config validation error: ${error.message}`);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import logger from "koa-logger";
|
|||
import config from "./config";
|
||||
import corsMiddleware from "./middlewares/cors.middleware";
|
||||
import errorMiddleware from "./middlewares/error.middleware";
|
||||
import avatarRoute from "./routes/avatar.route";
|
||||
import routes, { healthRoute } from "./routes";
|
||||
import { startServerLog } from "./utils/server";
|
||||
|
||||
const { server } = config;
|
||||
|
@ -15,6 +15,7 @@ app.use(corsMiddleware());
|
|||
app.use(logger());
|
||||
app.use(errorMiddleware());
|
||||
|
||||
app.use(avatarRoute.routes()).use(avatarRoute.allowedMethods());
|
||||
app.use(healthRoute.routes()).use(healthRoute.allowedMethods());
|
||||
app.use(routes.routes()).use(routes.allowedMethods());
|
||||
|
||||
app.listen(server.port, server.host, startServerLog(server.port));
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Router from "koa-router";
|
||||
import { isValidationError } from "../utils/type-guards";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
const errorMiddleware: () => Router.IMiddleware<any, {}> =
|
||||
() => async (ctx, next) => {
|
||||
|
@ -8,7 +8,7 @@ const errorMiddleware: () => Router.IMiddleware<any, {}> =
|
|||
} catch (err) {
|
||||
console.log("error middleware", err);
|
||||
if (err && (err as any).status >= 500) console.log("Error handler:", err);
|
||||
if (isValidationError(err)) {
|
||||
if (err instanceof ZodError) {
|
||||
ctx.status = 400;
|
||||
} else {
|
||||
ctx.status = (err as any).status || 500;
|
||||
|
|
|
@ -7,8 +7,7 @@ import * as openPeeps from "@dicebear/open-peeps";
|
|||
import * as pixelArt from "@dicebear/pixel-art";
|
||||
import * as thumbs from "@dicebear/thumbs";
|
||||
import * as botttsNeutral from "@dicebear/bottts-neutral";
|
||||
|
||||
import { isValue, isSize, isStyle } from "../utils/type-guards";
|
||||
import { z } from "zod";
|
||||
|
||||
const DENSITY = 72;
|
||||
const avatars: Record<string, Style<any> & { defaultSize: number }> = {
|
||||
|
@ -18,13 +17,19 @@ const avatars: Record<string, Style<any> & { defaultSize: number }> = {
|
|||
thumbs: { ...thumbs, defaultSize: 100 },
|
||||
};
|
||||
|
||||
const validStyles = Object.keys(avatars);
|
||||
const isValidStyle = isStyle(validStyles);
|
||||
const schema = z.object({
|
||||
style: z.enum(Object.keys(avatars) as [string, ...string[]]).default("thumbs"),
|
||||
size: z.number().min(16).max(512).default(128),
|
||||
seed: z.string().min(1).max(256).default(uuidv4()),
|
||||
});
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get("/list", async (ctx) => {
|
||||
const styles = validStyles.map((key) => ({ ...avatars[key].meta, key }));
|
||||
const styles = Object.keys(avatars).map((key) => ({
|
||||
...avatars[key].meta,
|
||||
key,
|
||||
}));
|
||||
ctx.body = { styles };
|
||||
});
|
||||
|
||||
|
@ -37,12 +42,8 @@ router.use(async (ctx, next) => {
|
|||
});
|
||||
|
||||
router.get("/random", async (ctx) => {
|
||||
const { style = "thumbs", size = "100" } = ctx.query;
|
||||
if (!isValidStyle(style) || !isSize(size)) {
|
||||
throw Error("Invalid query");
|
||||
}
|
||||
const seed = uuidv4();
|
||||
const density = (DENSITY * parseInt(size)) / avatars[style].defaultSize;
|
||||
const { style, size, seed } = schema.parse(ctx.query);
|
||||
const density = (DENSITY * size) / avatars[style].defaultSize;
|
||||
ctx.body = sharp(
|
||||
Buffer.from(createAvatar(avatars[style], { seed }).toString()),
|
||||
{ density }
|
||||
|
@ -50,12 +51,11 @@ router.get("/random", async (ctx) => {
|
|||
});
|
||||
|
||||
router.get("/:seed", async (ctx) => {
|
||||
const { seed } = ctx.params;
|
||||
const { style = "thumbs", size = "100" } = ctx.query;
|
||||
if (!isValue(seed) || !isValidStyle(style) || !isSize(size)) {
|
||||
throw Error("Invalid query");
|
||||
}
|
||||
const density = (DENSITY * parseInt(size)) / avatars[style].defaultSize;
|
||||
const { style, size, seed } = schema.parse({
|
||||
...ctx.query,
|
||||
...ctx.params,
|
||||
});
|
||||
const density = (DENSITY * size) / avatars[style].defaultSize;
|
||||
ctx.body = sharp(
|
||||
Buffer.from(createAvatar(avatars[style], { seed }).toString()),
|
||||
{
|
||||
|
|
10
src/routes/health.route.ts
Normal file
10
src/routes/health.route.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import Router from 'koa-router';
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get("/health", async (ctx) => {
|
||||
ctx.status = 200;
|
||||
ctx.body = "healthy";
|
||||
});
|
||||
|
||||
export default router;
|
15
src/routes/index.ts
Normal file
15
src/routes/index.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
|
||||
import Router from 'koa-router';
|
||||
import { readdirSync } from 'fs';
|
||||
|
||||
const router = new Router<any, any>();
|
||||
|
||||
readdirSync(__dirname)
|
||||
.filter((file) => file.includes('.route.') && !file.startsWith('health.'))
|
||||
.forEach((file) => {
|
||||
const routesFile = require(`${__dirname}/${file}`).default;
|
||||
router.use(routesFile.routes(), routesFile.allowedMethods());
|
||||
});
|
||||
|
||||
export default router;
|
||||
export { default as healthRoute } from './health.route';
|
|
@ -1,19 +0,0 @@
|
|||
import Joi from "joi";
|
||||
|
||||
export const isValidationError = (
|
||||
error: unknown
|
||||
): error is Joi.ValidationError => error instanceof Joi.ValidationError;
|
||||
|
||||
export const isValue = <T>(value: T | Array<T> | undefined): value is T =>
|
||||
!!value && !Array.isArray(value);
|
||||
|
||||
export const isSize = (value: unknown): value is string =>
|
||||
!Array.isArray(value) &&
|
||||
!isNaN(parseInt(value as string)) &&
|
||||
parseInt(value as string) <= 512 &&
|
||||
parseInt(value as string) >= 16;
|
||||
|
||||
export const isStyle =
|
||||
(validStyles: string[]) =>
|
||||
<T>(value: T | Array<T> | undefined): value is T =>
|
||||
!!value && !Array.isArray(value) && validStyles.includes(value as string);
|
Loading…
Add table
Add a link
Reference in a new issue