Address filtered export review comments

This commit is contained in:
karilint
2026-06-15 15:11:49 +03:00
parent 0c4e44f947
commit bf464fe579
5 changed files with 83 additions and 10 deletions
@@ -57,6 +57,39 @@ describe('DwC-A locality export (admin-only)', () => {
expect(measurementCsv).toContain('"verbatimMeasurementType"') 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 () => { it('rejects non-admin requests', async () => {
const result = await request(app).get('/locality/export/dwc-archive') const result = await request(app).get('/locality/export/dwc-archive')
expect(result.status).toEqual(403) expect(result.status).toEqual(403)
@@ -66,6 +66,39 @@ describe('DwC-A species export (admin-only)', () => {
expect(metaXml).toContain('<extension') expect(metaXml).toContain('<extension')
}) })
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('/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 () => { it('rejects non-admin requests', async () => {
const result = await request(app).get('/species/export/dwc-archive') const result = await request(app).get('/species/export/dwc-archive')
expect(result.status).toEqual(403) expect(result.status).toEqual(403)
+3 -1
View File
@@ -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) => { router.post('/export/dwc-archive', requireOneOf([Role.Admin]), async (req, res) => {
try { 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) { } catch (error) {
return res.status(400).send({ error: error instanceof Error ? error.message : 'Invalid export filters.' }) return res.status(400).send({ error: error instanceof Error ? error.message : 'Invalid export filters.' })
} }
+3 -1
View File
@@ -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) => { router.post('/export/dwc-archive', requireOneOf([Role.Admin]), async (req, res) => {
try { 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) { } catch (error) {
return res.status(400).send({ error: error instanceof Error ? error.message : 'Invalid export filters.' }) return res.status(400).send({ error: error instanceof Error ? error.message : 'Invalid export filters.' })
} }
+11 -8
View File
@@ -956,11 +956,18 @@ const fetchOccurrencesForDwcDataPackageExport = async (
} }
export const buildDwcDataPackageZipBuffer = async (occurrenceKeys?: DwcOccurrenceKey[]): Promise<Buffer> => { export const buildDwcDataPackageZipBuffer = async (occurrenceKeys?: DwcOccurrenceKey[]): Promise<Buffer> => {
const { nowDb } = await import('../utils/db')
const occurrences = await fetchOccurrencesForDwcDataPackageExport(occurrenceKeys) 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<Buffer> => {
const { nowDb } = await import('../utils/db')
const localityIds = isFilteredExport ? [...new Set(occurrences.map(occurrence => occurrence.lid))] : undefined
const localities = (await nowDb.now_loc.findMany({ const localities = (await nowDb.now_loc.findMany({
where: localityIds ? { lid: { in: localityIds } } : undefined, where: isFilteredExport ? { lid: { in: localityIds } } : undefined,
orderBy: { lid: 'asc' }, orderBy: { lid: 'asc' },
select: localitySelect, select: localitySelect,
})) as unknown as LocalityForDwcDpExport[] })) as unknown as LocalityForDwcDpExport[]
@@ -1129,13 +1136,9 @@ export const buildFullDarwinCoreExportZipBufferFromArchives = async ({
export const buildFullDarwinCoreExportZipBuffer = async (occurrenceKeys?: DwcOccurrenceKey[]): Promise<Buffer> => { export const buildFullDarwinCoreExportZipBuffer = async (occurrenceKeys?: DwcOccurrenceKey[]): Promise<Buffer> => {
const occurrences = occurrenceKeys ? await fetchOccurrencesForDwcDataPackageExport(occurrenceKeys) : undefined 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 speciesIds = occurrences ? [...new Set(occurrences.map(occurrence => occurrence.species_id))] : undefined
const [dwcDataPackageZipBuffer, dwcTaxonArchiveZipBuffer] = await Promise.all([ const [dwcDataPackageZipBuffer, dwcTaxonArchiveZipBuffer] = await Promise.all([
buildDwcDataPackageZipBuffer(filteredOccurrenceKeys), occurrences ? buildDwcDataPackageZipBufferFromOccurrences(occurrences, true) : buildDwcDataPackageZipBuffer(),
buildDwcArchiveZipBuffer(speciesIds), buildDwcArchiveZipBuffer(speciesIds),
]) ])