mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
Compare commits
No commits in common. "master" and "opencontext-v0.1.0" have entirely different histories.
master
...
opencontex
8 changed files with 36 additions and 159 deletions
|
|
@ -102,32 +102,6 @@ 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,5 +1,4 @@
|
||||||
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";
|
||||||
|
|
||||||
|
|
@ -7,7 +6,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 = parseBoundedIntegerParam(url.searchParams.get("limit"), 100, 1, 500);
|
const limit = Math.min(Number(url.searchParams.get("limit") ?? 100), 500);
|
||||||
|
|
||||||
return handle(request, ontologyId, (engine) => {
|
return handle(request, ontologyId, (engine) => {
|
||||||
const claims = engine.store.listClaims({
|
const claims = engine.store.listClaims({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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";
|
||||||
|
|
||||||
|
|
@ -9,8 +8,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 = parseBoundedIntegerParam(url.searchParams.get("limit"), 50, 1, 200);
|
const limit = Math.min(Number(url.searchParams.get("limit") ?? 50), 200);
|
||||||
const offset = parseBoundedIntegerParam(url.searchParams.get("offset"), 0, 0, Number.MAX_SAFE_INTEGER);
|
const offset = Math.max(Number(url.searchParams.get("offset") ?? 0), 0);
|
||||||
|
|
||||||
return handle(request, ontologyId, (engine) => {
|
return handle(request, ontologyId, (engine) => {
|
||||||
if (q) {
|
if (q) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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";
|
||||||
|
|
||||||
|
|
@ -28,7 +27,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 = parseBoundedIntegerParam(url.searchParams.get("limit"), 100, 1, 500);
|
const limit = Math.min(Number(url.searchParams.get("limit") ?? 100), 500);
|
||||||
return apiJson({ events: engine.listEvents({ limit }) });
|
return apiJson({ events: engine.listEvents({ limit }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { httpAdapter } from "./http.js";
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("http adapter response limits", () => {
|
|
||||||
it("enforces the byte limit for multibyte responses without a content-length header", async () => {
|
|
||||||
const content = "é".repeat(3 * 1024 * 1024);
|
|
||||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(content)));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
httpAdapter.load("https://example.com/context.md", {
|
|
||||||
dir: process.cwd(),
|
|
||||||
offline: false,
|
|
||||||
config: {}
|
|
||||||
})
|
|
||||||
).rejects.toThrow(/response exceeds the 5242880 byte limit/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("decodes responses within the byte limit", async () => {
|
|
||||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("café")));
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
httpAdapter.load("https://example.com/context.md", {
|
|
||||||
dir: process.cwd(),
|
|
||||||
offline: false,
|
|
||||||
config: {}
|
|
||||||
})
|
|
||||||
).resolves.toMatchObject({ content: "café", trust: "untrusted" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -20,38 +20,6 @@ export class OfflineError extends Error {
|
||||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||||
const MAX_BYTES = 5 * 1024 * 1024;
|
const MAX_BYTES = 5 * 1024 * 1024;
|
||||||
|
|
||||||
async function readBody(response: Response, uri: string): Promise<string> {
|
|
||||||
if (!response.body) return "";
|
|
||||||
|
|
||||||
const reader = response.body.getReader();
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
let totalBytes = 0;
|
|
||||||
|
|
||||||
try {
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
totalBytes += value.byteLength;
|
|
||||||
if (totalBytes > MAX_BYTES) {
|
|
||||||
await reader.cancel().catch(() => undefined);
|
|
||||||
throw new Error(`Refusing to load ${uri}: response exceeds the ${MAX_BYTES} byte limit.`);
|
|
||||||
}
|
|
||||||
chunks.push(value);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
reader.releaseLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bytes = new Uint8Array(totalBytes);
|
|
||||||
let offset = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
bytes.set(chunk, offset);
|
|
||||||
offset += chunk.byteLength;
|
|
||||||
}
|
|
||||||
return new TextDecoder().decode(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const httpAdapter: Adapter = {
|
export const httpAdapter: Adapter = {
|
||||||
name: "http",
|
name: "http",
|
||||||
schemes: ["http", "https"],
|
schemes: ["http", "https"],
|
||||||
|
|
@ -71,7 +39,6 @@ export const httpAdapter: Adapter = {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
try {
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
try {
|
try {
|
||||||
response = await fetch(url, {
|
response = await fetch(url, {
|
||||||
|
|
@ -83,6 +50,8 @@ export const httpAdapter: Adapter = {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to fetch ${uri}: ${(error as Error).message}`);
|
throw new Error(`Failed to fetch ${uri}: ${(error as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
@ -94,7 +63,10 @@ export const httpAdapter: Adapter = {
|
||||||
throw new Error(`Refusing to load ${uri}: ${declaredLength} bytes exceeds the ${MAX_BYTES} byte limit.`);
|
throw new Error(`Refusing to load ${uri}: ${declaredLength} bytes exceeds the ${MAX_BYTES} byte limit.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await readBody(response, uri);
|
const content = await response.text();
|
||||||
|
if (content.length > MAX_BYTES) {
|
||||||
|
throw new Error(`Refusing to load ${uri}: response exceeds the ${MAX_BYTES} byte limit.`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content,
|
content,
|
||||||
|
|
@ -103,8 +75,5 @@ export const httpAdapter: Adapter = {
|
||||||
retrievedAt: new Date().toISOString(),
|
retrievedAt: new Date().toISOString(),
|
||||||
trust: (ctx.config.trust as AdapterResult["trust"]) ?? "untrusted"
|
trust: (ctx.config.trust as AdapterResult["trust"]) ?? "untrusted"
|
||||||
};
|
};
|
||||||
} finally {
|
|
||||||
clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue