fix(export): paginate the archive size estimate and explain scope counts (#1635)
The period branch of estimateArchiveSize ran a single unpaginated document read with one flat IN() over every posted entry id in the year: past the PostgREST row cap it silently undercounts, and past a few hundred entry ids the URL itself blows up. Chunk the id filter (CHILD_FK_CHUNK) and paginate every read with fetchAllRows, mirroring what writeDocuments already did (the ZIP content was never affected). The dialog now says per scope which documents are counted: full history includes unlinked inbox/receipt documents, a single year only those linked to posted vouchers. Without that line, a company with many unlinked receipts reads the count gap as a pagination bug. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1037,4 +1037,34 @@ describe('estimateArchiveSize', () => {
|
||||
expect(result.document_count).toBe(0)
|
||||
expect(result.total_bytes).toBe(8 * 1024 * 1024)
|
||||
})
|
||||
|
||||
it('paginates the all-mode document read past the page cap', async () => {
|
||||
const firstPage = Array.from({ length: 1000 }, () => ({ file_size_bytes: 1_000 }))
|
||||
enqueueMany([
|
||||
{ data: firstPage }, // full first page forces a second fetch
|
||||
{ data: [{ file_size_bytes: 1_000 }] },
|
||||
])
|
||||
|
||||
const result = await estimateArchiveSize(supabase as any, 'company-1', 'all')
|
||||
|
||||
expect(result.document_count).toBe(1001)
|
||||
expect(result.document_bytes).toBe(1_001_000)
|
||||
})
|
||||
|
||||
it('chunks the period-mode entry-id filter and sums across chunks', async () => {
|
||||
// 250 posted entries -> three IN() chunks of max 100 ids. An unchunked
|
||||
// implementation consumes a single document response and undercounts.
|
||||
const entryIds = Array.from({ length: 250 }, (_, i) => ({ id: `e${i}` }))
|
||||
enqueueMany([
|
||||
{ data: entryIds }, // journal_entries for periodEntryIds
|
||||
{ data: [{ file_size_bytes: 100 }] }, // chunk 1
|
||||
{ data: [{ file_size_bytes: 200 }] }, // chunk 2
|
||||
{ data: [{ file_size_bytes: 300 }] }, // chunk 3
|
||||
])
|
||||
|
||||
const result = await estimateArchiveSize(supabase as any, 'company-1', 'period', 'p-1')
|
||||
|
||||
expect(result.document_count).toBe(3)
|
||||
expect(result.document_bytes).toBe(600)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -262,10 +262,7 @@ export async function estimateArchiveSize(
|
||||
periodId?: string
|
||||
): Promise<{ total_bytes: number; document_bytes: number; document_count: number }> {
|
||||
// Scope=all counts every document (linked or not), mirroring writeDocuments.
|
||||
let query = supabase
|
||||
.from('document_attachments')
|
||||
.select('file_size_bytes, journal_entry_id', { count: 'exact' })
|
||||
.eq('company_id', companyId)
|
||||
let rows: { file_size_bytes: number | null }[]
|
||||
|
||||
if (scope === 'period') {
|
||||
if (!periodId) {
|
||||
@@ -286,15 +283,35 @@ export async function estimateArchiveSize(
|
||||
if (ids.length === 0) {
|
||||
return { total_bytes: ARCHIVE_OVERHEAD_BYTES, document_bytes: 0, document_count: 0 }
|
||||
}
|
||||
query = query.in('journal_entry_id', ids)
|
||||
// A busy year holds thousands of entries and can hold more than a page of
|
||||
// documents: chunk the IN() list (PostgREST URL limit) and paginate every
|
||||
// chunk (PostgREST row cap). One flat IN() + single read undercounts as
|
||||
// soon as either limit is hit.
|
||||
rows = []
|
||||
for (let i = 0; i < ids.length; i += CHILD_FK_CHUNK) {
|
||||
const chunk = ids.slice(i, i + CHILD_FK_CHUNK)
|
||||
const chunkRows = await fetchAllRows<{ file_size_bytes: number | null }>(({ from, to }) =>
|
||||
supabase
|
||||
.from('document_attachments')
|
||||
.select('id, file_size_bytes')
|
||||
.eq('company_id', companyId)
|
||||
.in('journal_entry_id', chunk)
|
||||
.order('id', { ascending: true })
|
||||
.range(from, to)
|
||||
)
|
||||
rows.push(...chunkRows)
|
||||
}
|
||||
} else {
|
||||
rows = await fetchAllRows<{ file_size_bytes: number | null }>(({ from, to }) =>
|
||||
supabase
|
||||
.from('document_attachments')
|
||||
.select('id, file_size_bytes')
|
||||
.eq('company_id', companyId)
|
||||
.order('id', { ascending: true })
|
||||
.range(from, to)
|
||||
)
|
||||
}
|
||||
|
||||
const { data, error } = await query
|
||||
if (error) {
|
||||
throw new Error(`Failed to estimate archive size: ${error.message}`)
|
||||
}
|
||||
|
||||
const rows = (data as { file_size_bytes: number | null }[]) || []
|
||||
const documentBytes = rows.reduce((sum, r) => sum + (Number(r.file_size_bytes) || 0), 0)
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user