diff --git a/package.json b/package.json index 660ec15..3144e67 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "unstorage": "^1.17.2", "vue": "^3.5.22", "vue-chartjs": "^5.3.2", - "vue-router": "^4.6.3" + "vue-router": "^4.6.3", + "zod": "^4.1.12" }, "devDependencies": { "@biomejs/biome": "^2.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fc41c5..94ffc19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: vue-router: specifier: ^4.6.3 version: 4.6.3(vue@3.5.22(typescript@5.9.2)) + zod: + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@biomejs/biome': specifier: ^2.3.2 @@ -2748,6 +2751,7 @@ packages: libsql@0.5.22: resolution: {integrity: sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA==} + cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] lightningcss-android-arm64@1.30.2: @@ -4238,6 +4242,9 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + snapshots: '@antfu/install-pkg@1.1.0': @@ -8734,3 +8741,5 @@ snapshots: archiver-utils: 5.0.2 compress-commons: 6.0.2 readable-stream: 4.7.0 + + zod@4.1.12: {} diff --git a/server/api/reverseSearch/all.ts b/server/api/reverseSearch/all.ts index 4d014b3..46c1b6b 100644 --- a/server/api/reverseSearch/all.ts +++ b/server/api/reverseSearch/all.ts @@ -1,19 +1,27 @@ +import { z } from "zod"; import type { ProjectData } from "~~/server/utils/processing/connections/types"; -type QueryData = { - id: string; - offset: string; - limit: string; -}; +const querySchema = z.object({ + id: z.string(), + offset: z.string(), + limit: z.string(), +}); export default defineCachedEventHandler( async (event): Promise => { - const query = getQuery(event); + const query = querySchema.safeParse(getQuery(event)); + if (!query.success) { + throw createError({ + statusCode: 400, + statusMessage: "Invalid query parameters", + message: query.error.message, + }); + } const projectIds = await DB.getAllForProject( - query.id, - parseInt(query.offset, 10), - parseInt(query.limit, 10), + query.data.id, + parseInt(query.data.offset, 10), + parseInt(query.data.limit, 10), ); if (projectIds instanceof Error) { diff --git a/server/api/reverseSearch/count.ts b/server/api/reverseSearch/count.ts index 5cbcd7e..d727654 100644 --- a/server/api/reverseSearch/count.ts +++ b/server/api/reverseSearch/count.ts @@ -1,8 +1,21 @@ +import { z } from "zod"; + +const querySchema = z.object({ + id: z.string(), +}); + export default defineCachedEventHandler( async (event): Promise<{ count: number }> => { - const query = getQuery(event); + const query = querySchema.safeParse(getQuery(event)); + if (!query.success) { + throw createError({ + statusCode: 400, + statusMessage: "Invalid query parameters", + message: query.error.message, + }); + } - const count = await DB.getCountForProject(query.id as string); + const count = await DB.getCountForProject(query.data.id); if (count instanceof Error) { setResponseStatus(event, 500); diff --git a/server/api/stats/projects.ts b/server/api/stats/projects.ts index 0dd4684..5add62c 100644 --- a/server/api/stats/projects.ts +++ b/server/api/stats/projects.ts @@ -1,29 +1,36 @@ +import { z } from "zod"; import { exportStats } from "~~/server/utils/api/export/projectStats"; import { statStringToExtraction } from "~~/server/utils/api/project"; -import type { VersionCategories } from "~~/server/utils/processing/gameVersions/types"; -import type { ProjectStatCategory } from "~~/server/utils/processing/projects/types"; -type QueryData = { - stat: ProjectStatCategory; - mode: string; - type: ProjectTypes; - exclusive: string; - versionFrom: string | null; - versionTo: string | null; -}; +const querySchema = z.object({ + stat: ZProjectStatCategory, + mode: ZVersionCategories, + type: ZProjectTypes, + exclusive: z.string(), + versionFrom: z.string().optional(), + versionTo: z.string().optional(), +}); export default defineCachedEventHandler( async (event): Promise => { - const query = getQuery(event); - const typeFn = statStringToExtraction(query.stat); + const query = querySchema.safeParse(getQuery(event)); + if (!query.success) { + throw createError({ + statusCode: 400, + statusMessage: "Invalid query parameters", + message: query.error.message, + }); + } + + const typeFn = statStringToExtraction(query.data.stat); return exportStats( - query.mode as VersionCategories, - query.type, - query.exclusive === "true", + query.data.mode, + query.data.type, + query.data.exclusive === "true", typeFn, - query.versionTo, - query.versionFrom, + query.data.versionTo, + query.data.versionFrom, ); }, { maxAge: 60 * 60 * 4 /* 4 hour */, swr: false }, diff --git a/server/api/stats/time/global.ts b/server/api/stats/time/global.ts index d08ebcc..6618ce2 100644 --- a/server/api/stats/time/global.ts +++ b/server/api/stats/time/global.ts @@ -1,17 +1,24 @@ import consola from "consola"; +import { z } from "zod"; import { exportGlobalStatsOverTime } from "~~/server/utils/api/export/globalStats"; import { fracture } from "~~/server/utils/processing/aggregate"; -import type { GlobalStatCategory } from "~~/server/utils/processing/global/types"; -type QueryData = { - type: GlobalStatCategory; - days: number | undefined; - aggregate: string; -}; +const querySchema = z.object({ + type: ZGlobalStatCategory, + days: z.string().optional(), + aggregate: z.string(), +}); export default defineCachedEventHandler( async (event): Promise => { - const query = getQuery(event); + const query = querySchema.safeParse(getQuery(event)); + if (!query.success) { + throw createError({ + statusCode: 400, + statusMessage: "Invalid query parameters", + message: query.error.message, + }); + } const dateKey = await KV.LatestDate.get(); @@ -31,7 +38,7 @@ export default defineCachedEventHandler( authors: number; }) => number; - switch (query.type) { + switch (query.data.type) { case "projects": { typeFn = (value) => value.projects; break; @@ -50,10 +57,12 @@ export default defineCachedEventHandler( } } - const data = await exportGlobalStatsOverTime(dateKey, typeFn, query.days); + const days = query.data.days ? parseInt(query.data.days, 10) : undefined; + + const data = await exportGlobalStatsOverTime(dateKey, typeFn, days); - const dataPoints = Math.min(64, query.days ?? 64); - if (query.aggregate === "false") { + const dataPoints = Math.min(64, days ?? 64); + if (query.data.aggregate === "false") { return summarize(fracture(data), dataPoints, false); } return summarize(data, dataPoints, true); diff --git a/server/api/stats/time/projects.ts b/server/api/stats/time/projects.ts index 6933fa6..d84465e 100644 --- a/server/api/stats/time/projects.ts +++ b/server/api/stats/time/projects.ts @@ -1,36 +1,44 @@ +import { z } from "zod"; import { exportStatsOverTime } from "~~/server/utils/api/export/projectStats"; import { statStringToExtraction } from "~~/server/utils/api/project"; -import type { VersionCategories } from "~~/server/utils/processing/gameVersions/types"; -import type { ProjectStatCategory } from "~~/server/utils/processing/projects/types"; -type QueryData = { - stat: ProjectStatCategory; - mode: string; - type: ProjectTypes; - exclusive: string; - version: string; - days: number | undefined; - aggregate: string; -}; +const querySchema = z.object({ + stat: ZProjectStatCategory, + mode: ZVersionCategories, + type: ZProjectTypes, + exclusive: z.string(), + version: z.string(), + days: z.string().optional(), + aggregate: z.string(), +}); export default defineCachedEventHandler( async (event): Promise => { - const query = getQuery(event); - const typeFn = statStringToExtraction(query.stat); + const query = querySchema.safeParse(getQuery(event)); + if (!query.success) { + throw createError({ + statusCode: 400, + statusMessage: "Invalid query parameters", + message: query.error.message, + }); + } + + const typeFn = statStringToExtraction(query.data.stat); + const days = query.data.days ? parseInt(query.data.days, 10) : undefined; const data = await exportStatsOverTime( - query.mode as VersionCategories, - query.type, - query.exclusive === "true", + query.data.mode, + query.data.type, + query.data.exclusive === "true", new Date(), - query.version, + query.data.version, typeFn, - query.days, + days, ); - const dataPoints = Math.min(64, query.days ?? 64); + const dataPoints = Math.min(64, days ?? 64); - if (query.aggregate === "false") { + if (query.data.aggregate === "false") { return summarize(fracture(data), dataPoints, false); } return summarize(data, dataPoints, true); diff --git a/server/utils/api/export/projectStats.ts b/server/utils/api/export/projectStats.ts index 022c215..d70105c 100644 --- a/server/utils/api/export/projectStats.ts +++ b/server/utils/api/export/projectStats.ts @@ -29,8 +29,8 @@ function mapStats( // extract only versions in the given version range function filterVersions( mappedStats: StatExport, - versionTo: string | null, - versionFrom: string | null, + versionTo: string | undefined, + versionFrom: string | undefined, ) { let to = mappedStats.labels.length; if (versionTo) { @@ -58,8 +58,8 @@ export async function exportStats( type: ProjectTypes, exclusive: boolean, fn: (value: ProjectStatsValue) => number, - versionTo: string | null, - versionFrom: string | null, + versionTo: string | undefined, + versionFrom: string | undefined, ): Promise { const stats = await KV.ProjectStats.getLatest( type, diff --git a/server/utils/api/project.ts b/server/utils/api/project.ts index 0a52125..516675d 100644 --- a/server/utils/api/project.ts +++ b/server/utils/api/project.ts @@ -3,7 +3,7 @@ import type { ProjectStatsValue, } from "~~/server/utils/processing/projects/types"; -export function statStringToExtraction(stat: ProjectStatCategory) { +export function statStringToExtraction(stat: ProjectStatCategory | null) { let typeFn: (value: ProjectStatsValue) => number; switch (stat) { diff --git a/server/utils/processing/gameVersions/types.ts b/server/utils/processing/gameVersions/types.ts index 70214dc..36b6534 100644 --- a/server/utils/processing/gameVersions/types.ts +++ b/server/utils/processing/gameVersions/types.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export type GameVersion = { name: string; fullVersion: boolean; @@ -5,7 +7,8 @@ export type GameVersion = { export type GameVersionData = { name: string; contains: string[] }[]; -export type VersionCategories = "all" | "major" | "minor" | "unified"; +export const ZVersionCategories = z.enum(["all", "major", "minor", "unified"]); +export type VersionCategories = z.infer; export type GameVersions = Record; export type SupportedVersions = Record; diff --git a/server/utils/processing/global/types.ts b/server/utils/processing/global/types.ts index 8a468b8..335d0dc 100644 --- a/server/utils/processing/global/types.ts +++ b/server/utils/processing/global/types.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export type GlobalStats = { projects: number; versions: number; @@ -5,4 +7,10 @@ export type GlobalStats = { authors: number; }; -export type GlobalStatCategory = "projects" | "versions" | "files" | "authors"; +export const ZGlobalStatCategory = z.enum([ + "projects", + "versions", + "files", + "authors", +]); +export type GlobalStatCategory = z.infer; diff --git a/server/utils/processing/projects/types.ts b/server/utils/processing/projects/types.ts index e1f0f8c..3a055d2 100644 --- a/server/utils/processing/projects/types.ts +++ b/server/utils/processing/projects/types.ts @@ -1,3 +1,5 @@ +import { z } from "zod"; + export type Version = { project_id: string; loaders: string[]; @@ -19,4 +21,5 @@ export type ProjectStatsValue = { count: number; }; -export type ProjectStatCategory = "versions" | "count" | "downloads"; +export const ZProjectStatCategory = z.enum(["versions", "count", "downloads"]); +export type ProjectStatCategory = z.infer; diff --git a/shared/utils/project.ts b/shared/utils/project.ts index ae0d9c9..2d53ff4 100644 --- a/shared/utils/project.ts +++ b/shared/utils/project.ts @@ -1,10 +1,15 @@ -export type ProjectTypes = - | "mod" - | "plugin" - | "datapack" - | "shader" - | "resourcepack" - | "modpack"; +import { z } from "zod"; + +export const ZProjectTypes = z.enum([ + "mod", + "plugin", + "datapack", + "shader", + "resourcepack", + "modpack", +]); +export type ProjectTypes = z.infer; + export const projectTypeList: ProjectTypes[] = [ "mod", "plugin",