Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
DEFAULT_SEARCH_CONFIG,
type SearchServiceConfig,
} from "../service/service.js";
import type { SearchIndex } from "../types/types.js";
import { createSearchRoutes, type SearchRouteEvent } from "./build-routes.js";
interface EmittedEvent {
type: SearchRouteEvent["type"];
payload: SearchRouteEvent["payload"];
}
function makeDeps(): {
indexes: Map<string, SearchIndex>;
emitted: EmittedEvent[];
app: ReturnType<typeof createSearchRoutes>;
serviceConfig: SearchServiceConfig;
} {
const indexes = new Map<string, SearchIndex>();
const emitted: EmittedEvent[] = [];
const serviceConfig = { ...DEFAULT_SEARCH_CONFIG };
const app = createSearchRoutes({
indexes,
serviceConfig,
emitEvent: (event) => {
emitted.push({ type: event.type, payload: event.payload });
},
});
return { indexes, emitted, app, serviceConfig };
}
async function jsonRequest(
app: ReturnType<typeof createSearchRoutes>,
method: string,
path: string,
body?: unknown
): Promise<{ status: number; body: unknown }> {
const init: RequestInit = {
method,
headers: { "content-type": "application/json" },
};
if (body !== undefined) {
init.body = JSON.stringify(body);
}
const res = await app.request(path, init);
const text = await res.text();
return {
status: res.status,
body: text.length > 0 ? JSON.parse(text) : undefined,
};
}
async function createPostsIndex(
app: ReturnType<typeof createSearchRoutes>
): Promise<void> {
const r = await jsonRequest(app, "POST", "/indexes", {
name: "posts",
fields: [
{ name: "title", weight: 2, facet: false },
{ name: "body", weight: 1, facet: false },
{ name: "tag", weight: 1, facet: true },
],
analyzer: "standard",
});
if (r.status !== 201) {
throw new Error(`Failed to create posts index: ${JSON.stringify(r)}`);
}
}
describe("POST /query", () => {
it("POST /query returns 200 and emits search:query:executed", {
timeout: 5000,
}, async () => {
const { app, emitted } = makeDeps();
await createPostsIndex(app);
await jsonRequest(app, "POST", "/indexes/posts/documents", {
id: "1",
fields: { title: "Cat" },
});
const r = await jsonRequest(app, "POST", "/query", {
query: "cat",
index: "posts",
});
expect(r.status).toBe(200);
expect(emitted.some((e) => e.type === "search:query:executed")).toBe(
true
);
});
it("POST /query returns 404 when the index does not exist", {
timeout: 5000,
}, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "POST", "/query", {
query: "x",
index: "missing",
});
expect(r.status).toBe(404);
});
it("POST /query returns 400 on schema rejection", {
timeout: 5000,
}, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "POST", "/query", {
query: "",
index: "x",
});
expect(r.status).toBe(400);
});
});
describe("POST /suggest", () => {
it("POST /suggest returns 200 with suggestions", {
timeout: 5000,
}, async () => {
const { app } = makeDeps();
await createPostsIndex(app);
await jsonRequest(app, "POST", "/indexes/posts/documents", {
id: "1",
fields: { title: "running runner" },
});
const r = await jsonRequest(app, "POST", "/suggest", {
query: "run",
index: "posts",
limit: 10,
});
expect(r.status).toBe(200);
});
it("POST /suggest returns 404 for unknown index", {
timeout: 5000,
}, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "POST", "/suggest", {
query: "run",
index: "nope",
limit: 10,
});
expect(r.status).toBe(404);
});
});
describe("GET /indexes", () => {
it("returns the index list", { timeout: 5000 }, async () => {
const { app } = makeDeps();
await createPostsIndex(app);
const r = await jsonRequest(app, "GET", "/indexes");
expect(r.status).toBe(200);
const body = r.body as { items: unknown[]; total: number };
expect(body.total).toBe(1);
expect(body.items.length).toBe(1);
});
});
describe("POST /indexes", () => {
it("POST /indexes creates an index and returns 201", {
timeout: 5000,
}, async () => {
const { app, emitted } = makeDeps();
await createPostsIndex(app);
expect(emitted.some((e) => e.type === "search:index:created")).toBe(
true
);
});
it("returns 400 on duplicate", { timeout: 5000 }, async () => {
const { app } = makeDeps();
await createPostsIndex(app);
const r = await jsonRequest(app, "POST", "/indexes", {
name: "posts",
fields: [{ name: "title" }],
analyzer: "standard",
});
expect(r.status).toBe(400);
});
});
describe("GET /indexes/:name", () => {
it("returns 200 with details", { timeout: 5000 }, async () => {
const { app } = makeDeps();
await createPostsIndex(app);
const r = await jsonRequest(app, "GET", "/indexes/posts");
expect(r.status).toBe(200);
});
it("returns 404 for missing", { timeout: 5000 }, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "GET", "/indexes/missing");
expect(r.status).toBe(404);
});
});
describe("DELETE /indexes/:name", () => {
it("returns 200 on hit", { timeout: 5000 }, async () => {
const { app, emitted } = makeDeps();
await createPostsIndex(app);
const r = await jsonRequest(app, "DELETE", "/indexes/posts");
expect(r.status).toBe(200);
expect(emitted.some((e) => e.type === "search:index:deleted")).toBe(
true
);
});
it("DELETE /indexes/:name returns 404 for an unknown index", {
timeout: 5000,
}, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "DELETE", "/indexes/missing");
expect(r.status).toBe(404);
});
});
describe("POST /indexes/:name/documents", () => {
it("returns 201 on success", { timeout: 5000 }, async () => {
const { app, emitted } = makeDeps();
await createPostsIndex(app);
const r = await jsonRequest(app, "POST", "/indexes/posts/documents", {
id: "1",
fields: { title: "hello" },
});
expect(r.status).toBe(201);
expect(emitted.some((e) => e.type === "search:index:updated")).toBe(
true
);
});
it("returns 404 when the index is missing", { timeout: 5000 }, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "POST", "/indexes/nope/documents", {
id: "1",
fields: {},
});
expect(r.status).toBe(404);
});
});
describe("POST /indexes/:name/bulk", () => {
it("returns 200 with indexed count", { timeout: 5000 }, async () => {
const { app, emitted } = makeDeps();
await createPostsIndex(app);
const r = await jsonRequest(app, "POST", "/indexes/posts/bulk", {
index: "posts",
documents: [
{ id: "1", fields: { title: "a" } },
{ id: "2", fields: { title: "b" } },
],
});
expect(r.status).toBe(200);
const body = r.body as { indexed: number };
expect(body.indexed).toBe(2);
expect(
emitted.some(
(e) =>
e.type === "search:index:updated" &&
(e.payload as { operation?: string }).operation ===
"bulk_indexed"
)
).toBe(true);
});
it("returns 400 for missing index", { timeout: 5000 }, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "POST", "/indexes/nope/bulk", {
index: "nope",
documents: [{ id: "1", fields: {} }],
});
expect(r.status).toBe(400);
});
});
describe("DELETE /indexes/:name/documents/:docId", () => {
it("DELETE /indexes/:name/documents/:docId returns 200 and emits search:index:updated", {
timeout: 5000,
}, async () => {
const { app, emitted } = makeDeps();
await createPostsIndex(app);
await jsonRequest(app, "POST", "/indexes/posts/documents", {
id: "1",
fields: { title: "a" },
});
const r = await jsonRequest(
app,
"DELETE",
"/indexes/posts/documents/1"
);
expect(r.status).toBe(200);
expect(
emitted.some(
(e) =>
e.type === "search:index:updated" &&
(e.payload as { operation?: string }).operation ===
"document_removed"
)
).toBe(true);
});
it("returns 404 when doc id absent", { timeout: 5000 }, async () => {
const { app } = makeDeps();
await createPostsIndex(app);
const r = await jsonRequest(
app,
"DELETE",
"/indexes/posts/documents/missing"
);
expect(r.status).toBe(404);
});
it("returns 404 when target index does not exist", {
timeout: 5000,
}, async () => {
const { app } = makeDeps();
const r = await jsonRequest(app, "DELETE", "/indexes/nope/documents/1");
expect(r.status).toBe(404);
});
});