fix(mcp): SQL-side filtering and pagination for list_verifikat_without_documents (#872)
The tool applied min_amount in memory after the PostgREST .range() page: total_count ignored the filter, and next_offset advanced by the filtered row count while the DB page consumed 'limit' rows — consecutive pages overlapped and full backlog coverage could not be proven (reported via agent.feedback). gross_amount is an aggregate over journal_entry_lines that PostgREST cannot filter on, so filtering/counting/pagination move into a new verifikat_without_documents RPC (SECURITY DEFINER with the PR #625 tenant-guard pattern; id tiebreak for total ordering; filter-respecting total_count). Also indexes document_attachments.journal_entry_id, which the anti-join and the link tools both hit. pg-real invariants: disjoint + complete pages under min_amount, filter-respecting totals, since filter, doc/draft exclusion, tenant guard both ways. Part of dev_docs/mcp_optimization_plan.md (P0-2). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4b5fe9ee57
commit
45438d4d64
@@ -2777,62 +2777,42 @@ export const tools: McpTool[] = [
|
||||
const offset = Math.max(0, Number(args.offset) || 0)
|
||||
const since = typeof args.since === 'string' ? args.since : null
|
||||
const minAmount = typeof args.min_amount === 'number' && Number.isFinite(args.min_amount)
|
||||
? args.min_amount
|
||||
? Math.max(0, args.min_amount)
|
||||
: 0
|
||||
|
||||
// PostgREST left-join filter: journal_entries left-joined to
|
||||
// document_attachments and filtered to rows where the join produced
|
||||
// no document. The filter syntax `document_attachments.id=is.null`
|
||||
// applies the predicate post-join (Supabase: foreign-table is-null).
|
||||
let query = supabase
|
||||
.from('journal_entries')
|
||||
.select(
|
||||
'id, voucher_series, voucher_number, entry_date, description, source_type, document_attachments!left(id), journal_entry_lines(debit_amount)',
|
||||
{ count: 'exact' },
|
||||
)
|
||||
.eq('company_id', companyId)
|
||||
.eq('status', 'posted')
|
||||
.is('document_attachments.id', null)
|
||||
if (since) query = query.gte('entry_date', since)
|
||||
|
||||
const { data, error, count } = await query
|
||||
.order('entry_date', { ascending: false })
|
||||
.order('voucher_number', { ascending: false })
|
||||
.range(offset, offset + limit - 1)
|
||||
// gross_amount is an aggregate over journal_entry_lines, which PostgREST
|
||||
// cannot filter on — filtering it in memory after .range() made
|
||||
// total_count ignore min_amount and consecutive pages overlap. The RPC
|
||||
// filters, counts and paginates in SQL so the total respects the filter
|
||||
// and next_offset advances by exactly the rows consumed.
|
||||
const { data, error } = await supabase.rpc('verifikat_without_documents', {
|
||||
p_company_id: companyId,
|
||||
p_since: since,
|
||||
p_min_amount: minAmount,
|
||||
p_limit: limit,
|
||||
p_offset: offset,
|
||||
})
|
||||
if (error) throw new Error(`Database error: ${error.message}`)
|
||||
|
||||
const rows = ((data ?? []) as Array<{
|
||||
id: string
|
||||
voucher_series: string
|
||||
voucher_number: number
|
||||
entry_date: string
|
||||
description: string
|
||||
source_type: string
|
||||
journal_entry_lines: { debit_amount: number | string }[] | null
|
||||
}>).map((e) => {
|
||||
const lines = e.journal_entry_lines ?? []
|
||||
const gross = lines.reduce((sum, l) => sum + (Number(l.debit_amount) || 0), 0)
|
||||
return {
|
||||
journal_entry_id: e.id,
|
||||
voucher_series: e.voucher_series,
|
||||
voucher_number: e.voucher_number,
|
||||
entry_date: e.entry_date,
|
||||
description: e.description,
|
||||
source_type: e.source_type,
|
||||
gross_amount: Math.round(gross * 100) / 100,
|
||||
}
|
||||
})
|
||||
const result = data as {
|
||||
ok: boolean
|
||||
code?: string
|
||||
total_count?: number
|
||||
verifikat?: unknown[]
|
||||
} | null
|
||||
if (!result?.ok) {
|
||||
throw new Error(`verifikat_without_documents failed: ${result?.code ?? 'unknown error'}`)
|
||||
}
|
||||
|
||||
const filtered = minAmount > 0 ? rows.filter((r) => r.gross_amount >= minAmount) : rows
|
||||
|
||||
const total = count ?? 0
|
||||
const hasMore = total > offset + filtered.length
|
||||
const rows = result.verifikat ?? []
|
||||
const total = result.total_count ?? 0
|
||||
const hasMore = offset + rows.length < total
|
||||
return {
|
||||
verifikat: filtered,
|
||||
count: filtered.length,
|
||||
verifikat: rows,
|
||||
count: rows.length,
|
||||
total_count: total,
|
||||
has_more: hasMore,
|
||||
...(hasMore ? { next_offset: offset + filtered.length } : {}),
|
||||
...(hasMore ? { next_offset: offset + rows.length } : {}),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user