diff --git a/apps/web/__tests__/unit/developer-list-params.test.ts b/apps/web/__tests__/unit/developer-list-params.test.ts new file mode 100644 index 00000000000..5ec87ff6cdb --- /dev/null +++ b/apps/web/__tests__/unit/developer-list-params.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { parseDeveloperListParams } from "@/lib/developer-list-params"; + +const parse = (limit?: string, offset?: string) => + parseDeveloperListParams(limit, offset); + +describe("parseDeveloperListParams", () => { + it("keeps the existing defaults and cap", () => { + expect(parse()).toEqual({ limit: 50, offset: 0 }); + expect(parse("0", "abc")).toEqual({ limit: 50, offset: 0 }); + expect(parse("25", "10")).toEqual({ limit: 25, offset: 10 }); + expect(parse("500")).toEqual({ limit: 100, offset: 0 }); + }); + + it("only passes whole numbers MySQL accepts in LIMIT and OFFSET", () => { + expect(parse("5.5", "2.5")).toEqual({ limit: 5, offset: 2 }); + expect(parse("-1", "-5")).toEqual({ limit: 1, offset: 0 }); + expect(parse("0.5")).toEqual({ limit: 1, offset: 0 }); + expect(parse("-0.5")).toEqual({ limit: 1, offset: 0 }); + expect(parse("Infinity", "99999999999999999999")).toEqual({ + limit: 100, + offset: Number.MAX_SAFE_INTEGER, + }); + }); +}); diff --git a/apps/web/app/api/developer/v1/[...route]/videos.ts b/apps/web/app/api/developer/v1/[...route]/videos.ts index e8cbecdb1e4..72465d050d1 100644 --- a/apps/web/app/api/developer/v1/[...route]/videos.ts +++ b/apps/web/app/api/developer/v1/[...route]/videos.ts @@ -2,6 +2,7 @@ import { db } from "@cap/database"; import { developerVideos } from "@cap/database/schema"; import { and, desc, eq, isNull } from "drizzle-orm"; import { Hono } from "hono"; +import { parseDeveloperListParams } from "@/lib/developer-list-params"; import { withDeveloperSecretAuth } from "../../../utils"; export const app = new Hono<{ @@ -14,8 +15,10 @@ export const app = new Hono<{ app.get("/", async (c) => { const appId = c.get("developerAppId"); const userId = c.req.query("userId"); - const limit = Math.min(Number(c.req.query("limit") ?? 50) || 50, 100); - const offset = Math.max(0, Number(c.req.query("offset") ?? 0) || 0); + const { limit, offset } = parseDeveloperListParams( + c.req.query("limit"), + c.req.query("offset"), + ); const conditions = [ eq(developerVideos.appId, appId), diff --git a/apps/web/lib/developer-list-params.ts b/apps/web/lib/developer-list-params.ts new file mode 100644 index 00000000000..c7d87659d8e --- /dev/null +++ b/apps/web/lib/developer-list-params.ts @@ -0,0 +1,18 @@ +const DEFAULT_LIMIT = 50; +const MAX_LIMIT = 100; + +export function parseDeveloperListParams( + limit: string | undefined, + offset: string | undefined, +) { + const requestedLimit = Number(limit); + return { + limit: requestedLimit + ? Math.min(Math.max(1, Math.trunc(requestedLimit)), MAX_LIMIT) + : DEFAULT_LIMIT, + offset: Math.min( + Math.max(0, Math.trunc(Number(offset)) || 0), + Number.MAX_SAFE_INTEGER, + ), + }; +}