mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 22:24:16 +01:00
Fix company search cross-type pagination
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
d709f10c07
commit
c88a1e894d
2 files changed
+32
-12
No files matched your search
@@ -46,12 +46,12 @@ describe("company search query validation", () => {
|
||||
expect(parsed.scope).toBe("all");
|
||||
});
|
||||
|
||||
it("keeps the internal per-branch fetch limit independent of high offsets", () => {
|
||||
it("includes offset in the internal per-branch fetch window", () => {
|
||||
const lowOffset = companySearchQuerySchema.parse({ q: "needle", limit: "50", offset: "0" });
|
||||
const highOffset = companySearchQuerySchema.parse({ q: "needle", limit: "50", offset: "9000" });
|
||||
|
||||
expect(companySearchBranchFetchLimit(lowOffset.limit)).toBe(COMPANY_SEARCH_BRANCH_FETCH_LIMIT);
|
||||
expect(companySearchBranchFetchLimit(highOffset.limit)).toBe(COMPANY_SEARCH_BRANCH_FETCH_LIMIT);
|
||||
expect(companySearchBranchFetchLimit(lowOffset.limit, lowOffset.offset)).toBe(51);
|
||||
expect(companySearchBranchFetchLimit(highOffset.limit, highOffset.offset)).toBe(COMPANY_SEARCH_BRANCH_FETCH_LIMIT);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -370,6 +370,27 @@ describeEmbeddedPostgres("companySearchService", () => {
|
||||
]));
|
||||
});
|
||||
|
||||
it("applies offset after merging cross-type result ranking", async () => {
|
||||
const companyId = await createCompany();
|
||||
const base = new Date("2026-01-01T00:00:00.000Z").getTime();
|
||||
const agentIds = await Promise.all([
|
||||
createAgent(companyId, { name: "Needle agent 1", updatedAt: new Date(base + 6_000) }),
|
||||
createAgent(companyId, { name: "Needle agent 2", updatedAt: new Date(base + 5_000) }),
|
||||
createAgent(companyId, { name: "Needle agent 3", updatedAt: new Date(base + 4_000) }),
|
||||
]);
|
||||
const projectIds = await Promise.all([
|
||||
createProject(companyId, { name: "Needle project 1", updatedAt: new Date(base + 3_000) }),
|
||||
createProject(companyId, { name: "Needle project 2", updatedAt: new Date(base + 2_000) }),
|
||||
createProject(companyId, { name: "Needle project 3", updatedAt: new Date(base + 1_000) }),
|
||||
]);
|
||||
|
||||
const result = await svc.search(companyId, companySearchQuerySchema.parse({ q: "needle", limit: "2", offset: "2" }));
|
||||
|
||||
expect(result.results.map((row) => row.id)).toEqual([agentIds[2], projectIds[0]]);
|
||||
expect(result.countsByType).toEqual({ issue: 0, agent: 3, project: 3 });
|
||||
expect(result.hasMore).toBe(true);
|
||||
});
|
||||
|
||||
it("escapes underscore and backslash characters in issue phrase and token patterns", async () => {
|
||||
const companyId = await createCompany();
|
||||
const literalId = await createIssue(companyId, {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Db } from "@paperclipai/db";
|
||||
import { agents, companies, issues, projects } from "@paperclipai/db";
|
||||
import {
|
||||
COMPANY_SEARCH_MAX_LIMIT,
|
||||
COMPANY_SEARCH_MAX_OFFSET,
|
||||
COMPANY_SEARCH_MAX_TOKENS,
|
||||
type CompanySearchIssueSummary,
|
||||
type CompanySearchQuery,
|
||||
@@ -26,7 +27,7 @@ const FUZZY_PAIR_MEDIUM_MAX_EDITS = 1;
|
||||
const FUZZY_PAIR_SHORT_MAX_EDITS = 0;
|
||||
const FUZZY_IDENTIFIER_SIMILARITY_THRESHOLD = 0.45;
|
||||
const SNIPPET_MAX_CHARS = 240;
|
||||
export const COMPANY_SEARCH_BRANCH_FETCH_LIMIT = COMPANY_SEARCH_MAX_LIMIT + 1;
|
||||
export const COMPANY_SEARCH_BRANCH_FETCH_LIMIT = COMPANY_SEARCH_MAX_OFFSET + COMPANY_SEARCH_MAX_LIMIT + 1;
|
||||
|
||||
type IssueSearchRow = {
|
||||
id: string;
|
||||
@@ -291,9 +292,10 @@ function simpleTextCondition(fields: SQL[], containsPattern: string, tokenArray:
|
||||
return sql<boolean>`(${sql.join([...phraseConditions, ...tokenConditions], sql` OR `)})`;
|
||||
}
|
||||
|
||||
export function companySearchBranchFetchLimit(limit: number) {
|
||||
export function companySearchBranchFetchLimit(limit: number, offset = 0) {
|
||||
const normalizedLimit = Number.isFinite(limit) ? Math.max(1, Math.floor(limit)) : COMPANY_SEARCH_MAX_LIMIT;
|
||||
return Math.min(COMPANY_SEARCH_BRANCH_FETCH_LIMIT, normalizedLimit + 1);
|
||||
const normalizedOffset = Number.isFinite(offset) ? Math.max(0, Math.floor(offset)) : 0;
|
||||
return Math.min(COMPANY_SEARCH_BRANCH_FETCH_LIMIT, normalizedOffset + normalizedLimit + 1);
|
||||
}
|
||||
|
||||
export function companySearchService(db: Db) {
|
||||
@@ -324,7 +326,7 @@ export function companySearchService(db: Db) {
|
||||
.where(eq(companies.id, companyId))
|
||||
.then((rows) => rows[0] ?? null);
|
||||
const prefix = routePrefix(company?.issuePrefix);
|
||||
const fetchLimit = companySearchBranchFetchLimit(limit);
|
||||
const fetchLimit = companySearchBranchFetchLimit(limit, offset);
|
||||
const escapedTokens = tokens.map(escapeLikePattern);
|
||||
const tokenArray = sqlTextArray(escapedTokens);
|
||||
const fuzzyTokens = fuzzyEligibleTokens(tokens);
|
||||
@@ -596,7 +598,6 @@ export function companySearchService(db: Db) {
|
||||
))
|
||||
.orderBy(desc(score), desc(issues.updatedAt), desc(issues.id))
|
||||
.limit(fetchLimit)
|
||||
.offset(offset)
|
||||
: [];
|
||||
|
||||
const simpleCondition = simpleTextCondition([
|
||||
@@ -618,7 +619,6 @@ export function companySearchService(db: Db) {
|
||||
.where(and(eq(agents.companyId, companyId), simpleCondition))
|
||||
.orderBy(desc(agents.updatedAt), desc(agents.id))
|
||||
.limit(fetchLimit)
|
||||
.offset(offset)
|
||||
: [];
|
||||
|
||||
const projectCondition = simpleTextCondition([
|
||||
@@ -637,7 +637,6 @@ export function companySearchService(db: Db) {
|
||||
.where(and(eq(projects.companyId, companyId), isNull(projects.archivedAt), projectCondition))
|
||||
.orderBy(desc(projects.updatedAt), desc(projects.id))
|
||||
.limit(fetchLimit)
|
||||
.offset(offset)
|
||||
: [];
|
||||
|
||||
const results: CompanySearchResult[] = [
|
||||
@@ -681,7 +680,7 @@ export function companySearchService(db: Db) {
|
||||
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
|
||||
});
|
||||
|
||||
const paged = results.slice(0, limit);
|
||||
const paged = results.slice(offset, offset + limit);
|
||||
return {
|
||||
query: query.q,
|
||||
normalizedQuery,
|
||||
@@ -690,7 +689,7 @@ export function companySearchService(db: Db) {
|
||||
offset,
|
||||
results: paged,
|
||||
countsByType: makeCounts(results),
|
||||
hasMore: results.length > limit,
|
||||
hasMore: results.length > offset + limit,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user