mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
fix(web): validate ontology pagination (#138)
This commit is contained in:
parent
edf9a1b063
commit
1cfec322ac
6 changed files with 63 additions and 4 deletions
|
|
@ -102,6 +102,32 @@ describe("reads", () => {
|
||||||
expect(payload.total).toBeGreaterThan(3);
|
expect(payload.total).toBeGreaterThan(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("falls back safely for negative and malformed pagination values", async () => {
|
||||||
|
const entityResponse = await listEntities(
|
||||||
|
request(`/api/ontologies/${ONTOLOGY}/entities?limit=-1&offset=invalid`),
|
||||||
|
params({ ontologyId: ONTOLOGY })
|
||||||
|
);
|
||||||
|
const entityPayload = await body<{ limit: number; offset: number }>(entityResponse);
|
||||||
|
expect(entityPayload.limit).toBe(50);
|
||||||
|
expect(entityPayload.offset).toBe(0);
|
||||||
|
|
||||||
|
const claimResponse = await listClaims(
|
||||||
|
request(`/api/ontologies/${ONTOLOGY}/claims?limit=1.5`),
|
||||||
|
params({ ontologyId: ONTOLOGY })
|
||||||
|
);
|
||||||
|
expect((await body<{ limit: number }>(claimResponse)).limit).toBe(100);
|
||||||
|
|
||||||
|
const defaultEvents = await events(
|
||||||
|
request(`/api/ontologies/${ONTOLOGY}/events`),
|
||||||
|
params({ ontologyId: ONTOLOGY })
|
||||||
|
);
|
||||||
|
const invalidEvents = await events(
|
||||||
|
request(`/api/ontologies/${ONTOLOGY}/events?limit=-1`),
|
||||||
|
params({ ontologyId: ONTOLOGY })
|
||||||
|
);
|
||||||
|
expect(await body(invalidEvents)).toEqual(await body(defaultEvents));
|
||||||
|
});
|
||||||
|
|
||||||
it("returns ranked matches with evidence when searching", async () => {
|
it("returns ranked matches with evidence when searching", async () => {
|
||||||
const response = await listEntities(
|
const response = await listEntities(
|
||||||
request(`/api/ontologies/${ONTOLOGY}/entities?q=Avery`),
|
request(`/api/ontologies/${ONTOLOGY}/entities?q=Avery`),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { handle, apiJson } from "@/lib/ontology-service";
|
import { handle, apiJson } from "@/lib/ontology-service";
|
||||||
|
import { parseBoundedIntegerParam } from "@/lib/pagination";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
|
@ -6,7 +7,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ onto
|
||||||
const { ontologyId } = await params;
|
const { ontologyId } = await params;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const statusParam = url.searchParams.get("status") ?? "asserted";
|
const statusParam = url.searchParams.get("status") ?? "asserted";
|
||||||
const limit = Math.min(Number(url.searchParams.get("limit") ?? 100), 500);
|
const limit = parseBoundedIntegerParam(url.searchParams.get("limit"), 100, 1, 500);
|
||||||
|
|
||||||
return handle(request, ontologyId, (engine) => {
|
return handle(request, ontologyId, (engine) => {
|
||||||
const claims = engine.store.listClaims({
|
const claims = engine.store.listClaims({
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { handle, apiJson } from "@/lib/ontology-service";
|
import { handle, apiJson } from "@/lib/ontology-service";
|
||||||
|
import { parseBoundedIntegerParam } from "@/lib/pagination";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
|
@ -8,8 +9,8 @@ export async function GET(request: Request, { params }: { params: Promise<{ onto
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const type = url.searchParams.get("type") ?? undefined;
|
const type = url.searchParams.get("type") ?? undefined;
|
||||||
const q = url.searchParams.get("q") ?? undefined;
|
const q = url.searchParams.get("q") ?? undefined;
|
||||||
const limit = Math.min(Number(url.searchParams.get("limit") ?? 50), 200);
|
const limit = parseBoundedIntegerParam(url.searchParams.get("limit"), 50, 1, 200);
|
||||||
const offset = Math.max(Number(url.searchParams.get("offset") ?? 0), 0);
|
const offset = parseBoundedIntegerParam(url.searchParams.get("offset"), 0, 0, Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
return handle(request, ontologyId, (engine) => {
|
return handle(request, ontologyId, (engine) => {
|
||||||
if (q) {
|
if (q) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { getService, apiError, apiJson, engineFor } from "@/lib/ontology-service";
|
import { getService, apiError, apiJson, engineFor } from "@/lib/ontology-service";
|
||||||
|
import { parseBoundedIntegerParam } from "@/lib/pagination";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
|
@ -27,7 +28,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ onto
|
||||||
const engine = bound.engine;
|
const engine = bound.engine;
|
||||||
|
|
||||||
if (!wantsStream) {
|
if (!wantsStream) {
|
||||||
const limit = Math.min(Number(url.searchParams.get("limit") ?? 100), 500);
|
const limit = parseBoundedIntegerParam(url.searchParams.get("limit"), 100, 1, 500);
|
||||||
return apiJson({ events: engine.listEvents({ limit }) });
|
return apiJson({ events: engine.listEvents({ limit }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
apps/logicsrc-web/src/lib/pagination.test.ts
Normal file
17
apps/logicsrc-web/src/lib/pagination.test.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { describe, test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { parseBoundedIntegerParam } from "./pagination.ts";
|
||||||
|
|
||||||
|
describe("pagination parameters", () => {
|
||||||
|
test("uses the fallback for negative, fractional, and malformed values", () => {
|
||||||
|
for (const value of [null, "-1", "1.5", "invalid", "9007199254740992"]) {
|
||||||
|
assert.equal(parseBoundedIntegerParam(value, 50, 1, 200), 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("accepts valid values and caps the upper bound", () => {
|
||||||
|
assert.equal(parseBoundedIntegerParam(" 1 ", 50, 1, 200), 1);
|
||||||
|
assert.equal(parseBoundedIntegerParam("500", 50, 1, 200), 200);
|
||||||
|
assert.equal(parseBoundedIntegerParam("0", 0, 0, Number.MAX_SAFE_INTEGER), 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
13
apps/logicsrc-web/src/lib/pagination.ts
Normal file
13
apps/logicsrc-web/src/lib/pagination.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function parseBoundedIntegerParam(
|
||||||
|
value: string | null,
|
||||||
|
fallback: number,
|
||||||
|
minimum: number,
|
||||||
|
maximum: number
|
||||||
|
): number {
|
||||||
|
const text = value?.trim() ?? "";
|
||||||
|
if (!/^\d+$/.test(text)) return fallback;
|
||||||
|
|
||||||
|
const parsed = Number(text);
|
||||||
|
if (!Number.isSafeInteger(parsed) || parsed < minimum) return fallback;
|
||||||
|
return Math.min(parsed, maximum);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue