From bf464fe579ce6e56ead5a58532aa19031268ce50 Mon Sep 17 00:00:00 2001 From: karilint Date: Mon, 15 Jun 2026 15:11:49 +0300 Subject: [PATCH] Address filtered export review comments --- .../dwcArchiveExportLocalities.test.ts | 33 +++++++++++++++++++ .../species/dwcArchiveExport.test.ts | 33 +++++++++++++++++++ backend/src/routes/locality.ts | 4 ++- backend/src/routes/species.ts | 4 ++- backend/src/services/dwcDataPackageExport.ts | 19 ++++++----- 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/backend/src/api-tests/locality/dwcArchiveExportLocalities.test.ts b/backend/src/api-tests/locality/dwcArchiveExportLocalities.test.ts index ba77ea4f..ea60894b 100644 --- a/backend/src/api-tests/locality/dwcArchiveExportLocalities.test.ts +++ b/backend/src/api-tests/locality/dwcArchiveExportLocalities.test.ts @@ -57,6 +57,39 @@ describe('DwC-A locality export (admin-only)', () => { expect(measurementCsv).toContain('"verbatimMeasurementType"') }) + it('returns a filtered ZIP archive for POST requests', async () => { + const loginResult = await send<{ token: string }>('user/login', 'POST', { username: 'testSu', password: 'test' }) + expect(loginResult.status).toEqual(200) + + const result = await request(app) + .post('/locality/export/dwc-archive') + .set('authorization', `bearer ${loginResult.body.token}`) + .send({ ids: [21050] }) + .buffer(true) + .parse(parseBinary) + + expect(result.status).toEqual(200) + expect(result.headers['content-type']).toMatch(/application\/zip/i) + + const zip = await JSZip.loadAsync(result.body as unknown as Buffer) + const locationCsv = await zip.file('location.csv')!.async('string') + expect(locationCsv).toContain('NOW:LOC:21050') + expect(locationCsv).not.toContain('NOW:LOC:24750') + }) + + it('returns 400 for invalid POST id payloads', async () => { + const loginResult = await send<{ token: string }>('user/login', 'POST', { username: 'testSu', password: 'test' }) + expect(loginResult.status).toEqual(200) + + const result = await request(app) + .post('/locality/export/dwc-archive') + .set('authorization', `bearer ${loginResult.body.token}`) + .send({ ids: ['21050.5'] }) + + expect(result.status).toEqual(400) + expect(result.body).toEqual({ error: 'ids must contain only integers.' }) + }) + it('rejects non-admin requests', async () => { const result = await request(app).get('/locality/export/dwc-archive') expect(result.status).toEqual(403) diff --git a/backend/src/api-tests/species/dwcArchiveExport.test.ts b/backend/src/api-tests/species/dwcArchiveExport.test.ts index e832ca26..22dfda6d 100644 --- a/backend/src/api-tests/species/dwcArchiveExport.test.ts +++ b/backend/src/api-tests/species/dwcArchiveExport.test.ts @@ -66,6 +66,39 @@ describe('DwC-A species export (admin-only)', () => { expect(metaXml).toContain(' { + const loginResult = await send<{ token: string }>('user/login', 'POST', { username: 'testSu', password: 'test' }) + expect(loginResult.status).toEqual(200) + + const result = await request(app) + .post('/species/export/dwc-archive') + .set('authorization', `bearer ${loginResult.body.token}`) + .send({ ids: [85729] }) + .buffer(true) + .parse(parseBinary) + + expect(result.status).toEqual(200) + expect(result.headers['content-type']).toMatch(/application\/zip/i) + + const zip = await JSZip.loadAsync(result.body as unknown as Buffer) + const taxonCsv = await zip.file('taxon.csv')!.async('string') + expect(taxonCsv).toContain('NOW:85729') + expect(taxonCsv).not.toContain('NOW:85730') + }) + + it('returns 400 for invalid POST id payloads', async () => { + const loginResult = await send<{ token: string }>('user/login', 'POST', { username: 'testSu', password: 'test' }) + expect(loginResult.status).toEqual(200) + + const result = await request(app) + .post('/species/export/dwc-archive') + .set('authorization', `bearer ${loginResult.body.token}`) + .send({ ids: ['85729abc'] }) + + expect(result.status).toEqual(400) + expect(result.body).toEqual({ error: 'ids must contain only integers.' }) + }) + it('rejects non-admin requests', async () => { const result = await request(app).get('/species/export/dwc-archive') expect(result.status).toEqual(403) diff --git a/backend/src/routes/locality.ts b/backend/src/routes/locality.ts index 4c18c65e..ad6612ef 100644 --- a/backend/src/routes/locality.ts +++ b/backend/src/routes/locality.ts @@ -34,7 +34,9 @@ router.get('/export/dwc-archive', requireOneOf([Role.Admin]), async (_req, res) router.post('/export/dwc-archive', requireOneOf([Role.Admin]), async (req, res) => { try { - return await sendDwcArchive(parseNumericIds((req.body as { ids?: unknown }).ids), res) + const body = req.body as { ids?: unknown } | undefined + if (!body || !('ids' in body)) throw new Error('ids must be an array.') + return await sendDwcArchive(parseNumericIds(body.ids), res) } catch (error) { return res.status(400).send({ error: error instanceof Error ? error.message : 'Invalid export filters.' }) } diff --git a/backend/src/routes/species.ts b/backend/src/routes/species.ts index 369967ac..0fa0a485 100644 --- a/backend/src/routes/species.ts +++ b/backend/src/routes/species.ts @@ -33,7 +33,9 @@ router.get('/export/dwc-archive', requireOneOf([Role.Admin]), async (_req, res) router.post('/export/dwc-archive', requireOneOf([Role.Admin]), async (req, res) => { try { - return await sendDwcArchive(parseNumericIds((req.body as { ids?: unknown }).ids), res) + const body = req.body as { ids?: unknown } | undefined + if (!body || !('ids' in body)) throw new Error('ids must be an array.') + return await sendDwcArchive(parseNumericIds(body.ids), res) } catch (error) { return res.status(400).send({ error: error instanceof Error ? error.message : 'Invalid export filters.' }) } diff --git a/backend/src/services/dwcDataPackageExport.ts b/backend/src/services/dwcDataPackageExport.ts index ff6228f0..a4551d68 100644 --- a/backend/src/services/dwcDataPackageExport.ts +++ b/backend/src/services/dwcDataPackageExport.ts @@ -956,11 +956,18 @@ const fetchOccurrencesForDwcDataPackageExport = async ( } export const buildDwcDataPackageZipBuffer = async (occurrenceKeys?: DwcOccurrenceKey[]): Promise => { - const { nowDb } = await import('../utils/db') const occurrences = await fetchOccurrencesForDwcDataPackageExport(occurrenceKeys) - const localityIds = occurrenceKeys ? [...new Set(occurrences.map(occurrence => occurrence.lid))] : undefined + return await buildDwcDataPackageZipBufferFromOccurrences(occurrences, Boolean(occurrenceKeys)) +} + +const buildDwcDataPackageZipBufferFromOccurrences = async ( + occurrences: OccurrenceForDwcDpExport[], + isFilteredExport: boolean +): Promise => { + const { nowDb } = await import('../utils/db') + const localityIds = isFilteredExport ? [...new Set(occurrences.map(occurrence => occurrence.lid))] : undefined const localities = (await nowDb.now_loc.findMany({ - where: localityIds ? { lid: { in: localityIds } } : undefined, + where: isFilteredExport ? { lid: { in: localityIds } } : undefined, orderBy: { lid: 'asc' }, select: localitySelect, })) as unknown as LocalityForDwcDpExport[] @@ -1129,13 +1136,9 @@ export const buildFullDarwinCoreExportZipBufferFromArchives = async ({ export const buildFullDarwinCoreExportZipBuffer = async (occurrenceKeys?: DwcOccurrenceKey[]): Promise => { const occurrences = occurrenceKeys ? await fetchOccurrencesForDwcDataPackageExport(occurrenceKeys) : undefined - const filteredOccurrenceKeys = occurrences?.map(occurrence => ({ - lid: occurrence.lid, - speciesId: occurrence.species_id, - })) const speciesIds = occurrences ? [...new Set(occurrences.map(occurrence => occurrence.species_id))] : undefined const [dwcDataPackageZipBuffer, dwcTaxonArchiveZipBuffer] = await Promise.all([ - buildDwcDataPackageZipBuffer(filteredOccurrenceKeys), + occurrences ? buildDwcDataPackageZipBufferFromOccurrences(occurrences, true) : buildDwcDataPackageZipBuffer(), buildDwcArchiveZipBuffer(speciesIds), ])