feat(search): search public news events users and open rooms
This commit is contained in:
1 parent
506e14783c
commit
ba9c61d808
5 files changed
+424
-184
No files matched your search
+211
-183
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,9 @@ import { db, WebsiteArticles } from "@/lib/db";
|
|||||||
export async function loadNewsArchive(
|
export async function loadNewsArchive(
|
||||||
options: { search?: string; page?: number; order?: string } = {},
|
options: { search?: string; page?: number; order?: string } = {},
|
||||||
) {
|
) {
|
||||||
const search = (options.search ?? "").trim().slice(0, 191);
|
const search = (typeof options.search === "string" ? options.search : "")
|
||||||
|
.trim()
|
||||||
|
.slice(0, 191);
|
||||||
const order = options.order === "oldest" ? "oldest" : "newest";
|
const order = options.order === "oldest" ? "oldest" : "newest";
|
||||||
const publishedAt =
|
const publishedAt =
|
||||||
sql<Date | null>`COALESCE(${WebsiteArticles.publishAt}, ${WebsiteArticles.createdAt})`.mapWith(
|
sql<Date | null>`COALESCE(${WebsiteArticles.publishAt}, ${WebsiteArticles.createdAt})`.mapWith(
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import "server-only";
|
||||||
|
import { logger } from "@/lib/logger";
|
||||||
|
|
||||||
|
/** Keep failed public reads distinct from valid empty collections and zero counts. */
|
||||||
|
export function publicReadFailure(section: string) {
|
||||||
|
return (error: unknown): null => {
|
||||||
|
logger.error("Public section query failed", {
|
||||||
|
module: "public-data",
|
||||||
|
section,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const state = vi.hoisted(() => ({
|
||||||
|
queries: [] as { sql: string; params: unknown[] }[],
|
||||||
|
failUsers: false,
|
||||||
|
}));
|
||||||
|
vi.mock("@/lib/db", async () => {
|
||||||
|
const schema = await import("@/db/schema");
|
||||||
|
const { drizzle } = await import("drizzle-orm/mysql-proxy");
|
||||||
|
return {
|
||||||
|
...schema,
|
||||||
|
db: drizzle(async (sql, params) => {
|
||||||
|
state.queries.push({ sql, params });
|
||||||
|
if (state.failUsers && sql.includes("from `users`"))
|
||||||
|
throw Error("users unavailable");
|
||||||
|
return { rows: sql.includes("count(*)") ? [[50]] : [] };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
import { loadPublicSearch } from "./public-search";
|
||||||
|
|
||||||
|
describe("public search", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
state.queries = [];
|
||||||
|
state.failUsers = false;
|
||||||
|
});
|
||||||
|
it("does not query for a blank term", async () => {
|
||||||
|
expect(await loadPublicSearch({ query: " " })).toBeNull();
|
||||||
|
expect(state.queries).toEqual([]);
|
||||||
|
});
|
||||||
|
it("searches all four categories with public visibility predicates", async () => {
|
||||||
|
const result = await loadPublicSearch({ query: "needle'", roomsPage: 2 });
|
||||||
|
expect(result?.rooms.status).toBe("fulfilled");
|
||||||
|
const rooms = state.queries.filter((q) => q.sql.includes("from `rooms`"));
|
||||||
|
expect(rooms[0].params).toEqual(["%needle'%", "open"]);
|
||||||
|
expect(rooms[1].params.slice(-2)).toEqual([12, 12]);
|
||||||
|
const news = state.queries.find((q) =>
|
||||||
|
q.sql.includes("from `website_articles`"),
|
||||||
|
);
|
||||||
|
expect(news?.sql).toContain("<= NOW()");
|
||||||
|
expect(news?.params[0]).toBe("published");
|
||||||
|
const events = state.queries.find((q) =>
|
||||||
|
q.sql.includes("from `website_events`"),
|
||||||
|
);
|
||||||
|
expect(events?.params.slice(0, 2)).toEqual(["published", "completed"]);
|
||||||
|
});
|
||||||
|
it("preserves successful categories when one query fails", async () => {
|
||||||
|
state.failUsers = true;
|
||||||
|
const result = await loadPublicSearch({ query: "test" });
|
||||||
|
expect(result?.users.status).toBe("rejected");
|
||||||
|
expect(result?.rooms.status).toBe("fulfilled");
|
||||||
|
expect(result?.news.status).toBe("fulfilled");
|
||||||
|
expect(result?.events.status).toBe("fulfilled");
|
||||||
|
});
|
||||||
|
it("clamps each independent page and limits query length", async () => {
|
||||||
|
const result = await loadPublicSearch({
|
||||||
|
query: "x".repeat(500),
|
||||||
|
usersPage: 999,
|
||||||
|
roomsPage: -1,
|
||||||
|
});
|
||||||
|
expect(result?.query).toHaveLength(191);
|
||||||
|
expect(result?.users).toMatchObject({
|
||||||
|
status: "fulfilled",
|
||||||
|
value: { page: 5 },
|
||||||
|
});
|
||||||
|
expect(result?.rooms).toMatchObject({
|
||||||
|
status: "fulfilled",
|
||||||
|
value: { page: 1 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user