mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 10:50:38 +09:00
Speed up issue search
This commit is contained in:
parent
0edac73a68
commit
5136381d8f
15 changed files with 13127 additions and 27 deletions
|
|
@ -249,6 +249,55 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
|
|||
expect(result.map((issue) => issue.id)).toEqual([matchedIssueId]);
|
||||
});
|
||||
|
||||
it("applies result limits to issue search", async () => {
|
||||
const companyId = randomUUID();
|
||||
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: "Paperclip",
|
||||
issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
|
||||
const exactIdentifierId = randomUUID();
|
||||
const titleMatchId = randomUUID();
|
||||
const descriptionMatchId = randomUUID();
|
||||
|
||||
await db.insert(issues).values([
|
||||
{
|
||||
id: exactIdentifierId,
|
||||
companyId,
|
||||
issueNumber: 42,
|
||||
identifier: "PAP-42",
|
||||
title: "Completely unrelated",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
},
|
||||
{
|
||||
id: titleMatchId,
|
||||
companyId,
|
||||
title: "Search ranking issue",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
},
|
||||
{
|
||||
id: descriptionMatchId,
|
||||
companyId,
|
||||
title: "Another item",
|
||||
description: "Contains the search keyword",
|
||||
status: "todo",
|
||||
priority: "medium",
|
||||
},
|
||||
]);
|
||||
|
||||
const result = await svc.list(companyId, {
|
||||
q: "search",
|
||||
limit: 2,
|
||||
});
|
||||
|
||||
expect(result.map((issue) => issue.id)).toEqual([titleMatchId, descriptionMatchId]);
|
||||
});
|
||||
|
||||
it("accepts issue identifiers through getById", async () => {
|
||||
const companyId = randomUUID();
|
||||
const issueId = randomUUID();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import express from "express";
|
|||
import request from "supertest";
|
||||
import { privateHostnameGuard } from "../middleware/private-hostname-guard.js";
|
||||
|
||||
const unknownHostname = "blocked-host.invalid";
|
||||
|
||||
function createApp(opts: { enabled: boolean; allowedHostnames?: string[]; bindHost?: string }) {
|
||||
const app = express();
|
||||
app.use(
|
||||
|
|
@ -42,15 +44,15 @@ describe("privateHostnameGuard", () => {
|
|||
|
||||
it("blocks unknown hostnames with remediation command", async () => {
|
||||
const app = createApp({ enabled: true, allowedHostnames: ["some-other-host"] });
|
||||
const res = await request(app).get("/api/health").set("Host", "dotta-macbook-pro:3100");
|
||||
const res = await request(app).get("/api/health").set("Host", `${unknownHostname}:3100`);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body?.error).toContain("please run pnpm paperclipai allowed-hostname dotta-macbook-pro");
|
||||
expect(res.body?.error).toContain(`please run pnpm paperclipai allowed-hostname ${unknownHostname}`);
|
||||
});
|
||||
|
||||
it("blocks unknown hostnames on page routes with plain-text remediation command", async () => {
|
||||
const app = createApp({ enabled: true, allowedHostnames: ["some-other-host"] });
|
||||
const res = await request(app).get("/dashboard").set("Host", "dotta-macbook-pro:3100");
|
||||
const res = await request(app).get("/dashboard").set("Host", `${unknownHostname}:3100`);
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.text).toContain("please run pnpm paperclipai allowed-hostname dotta-macbook-pro");
|
||||
expect(res.text).toContain(`please run pnpm paperclipai allowed-hostname ${unknownHostname}`);
|
||||
}, 20_000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -346,6 +346,9 @@ export function issueRoutes(
|
|||
unreadForUserFilterRaw === "me" && req.actor.type === "board"
|
||||
? req.actor.userId
|
||||
: unreadForUserFilterRaw;
|
||||
const rawLimit = req.query.limit as string | undefined;
|
||||
const parsedLimit = rawLimit ? Number.parseInt(rawLimit, 10) : null;
|
||||
const limit = parsedLimit ?? undefined;
|
||||
|
||||
if (assigneeUserFilterRaw === "me" && (!assigneeUserId || req.actor.type !== "board")) {
|
||||
res.status(403).json({ error: "assigneeUserId=me requires board authentication" });
|
||||
|
|
@ -363,6 +366,10 @@ export function issueRoutes(
|
|||
res.status(403).json({ error: "unreadForUserId=me requires board authentication" });
|
||||
return;
|
||||
}
|
||||
if (rawLimit !== undefined && (parsedLimit === null || !Number.isInteger(parsedLimit) || parsedLimit <= 0)) {
|
||||
res.status(400).json({ error: "limit must be a positive integer" });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await svc.list(companyId, {
|
||||
status: req.query.status as string | undefined,
|
||||
|
|
@ -381,6 +388,7 @@ export function issueRoutes(
|
|||
includeRoutineExecutions:
|
||||
req.query.includeRoutineExecutions === "true" || req.query.includeRoutineExecutions === "1",
|
||||
q: req.query.q as string | undefined,
|
||||
limit,
|
||||
});
|
||||
res.json(result);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export interface IssueFilters {
|
|||
originId?: string;
|
||||
includeRoutineExecutions?: boolean;
|
||||
q?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
type IssueRow = typeof issues.$inferSelect;
|
||||
|
|
@ -911,6 +912,9 @@ export function issueService(db: Db) {
|
|||
return {
|
||||
list: async (companyId: string, filters?: IssueFilters) => {
|
||||
const conditions = [eq(issues.companyId, companyId)];
|
||||
const limit = typeof filters?.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.floor(filters.limit))
|
||||
: undefined;
|
||||
const touchedByUserId = filters?.touchedByUserId?.trim() || undefined;
|
||||
const inboxArchivedByUserId = filters?.inboxArchivedByUserId?.trim() || undefined;
|
||||
const unreadForUserId = filters?.unreadForUserId?.trim() || undefined;
|
||||
|
|
@ -999,7 +1003,7 @@ export function issueService(db: Db) {
|
|||
END
|
||||
`;
|
||||
const canonicalLastActivityAt = issueCanonicalLastActivityAtExpr(companyId);
|
||||
const rows = await db
|
||||
const baseQuery = db
|
||||
.select()
|
||||
.from(issues)
|
||||
.where(and(...conditions))
|
||||
|
|
@ -1009,6 +1013,7 @@ export function issueService(db: Db) {
|
|||
desc(canonicalLastActivityAt),
|
||||
desc(issues.updatedAt),
|
||||
);
|
||||
const rows = limit === undefined ? await baseQuery : await baseQuery.limit(limit);
|
||||
const withLabels = await withIssueLabels(db, rows);
|
||||
const runMap = await activeRunMapForIssues(db, withLabels);
|
||||
const withRuns = withActiveRuns(withLabels, runMap);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue