Use POST bodies for cross-search filters

This commit is contained in:
karilint
2026-05-21 12:22:16 +03:00
parent 9e9673bb6b
commit 72b1d879b5
6 changed files with 158 additions and 75 deletions
@@ -16,6 +16,12 @@ describe('Getting cross-search export', () => {
expect(response.body).toEqual({ error: 'ColumnFilters is not an array.' })
})
it('with invalid POST column filters type does not work', async () => {
const response = await send(`crosssearch/export`, 'POST', { columnFilters: '"not an array"', sorting: [] })
expect(response.status).toEqual(403)
expect(response.body).toEqual({ error: 'ColumnFilters is not an array.' })
})
it('with invalid column filters content does not work', async () => {
const { body: responseBody1, status: responseStatus1 } = await send(
`crosssearch/export/[{"id": "not_a_column", "value": "Dmanisi"}]/[]`,
@@ -62,6 +62,19 @@ describe('Getting cross-search data', () => {
expect(response2.body.length).toEqual(10)
})
it('Requesting get all with POST body has correct data', async () => {
const { body, status } = await send<CrossSearch[]>(`crosssearch/all`, 'POST', {
limit: 20,
offset: 0,
columnFilters: [{ id: 'country', value: 'Spain' }],
sorting: [{ id: 'loc_name', desc: false }],
})
expect(status).toEqual(200)
expect(body).toHaveLength(10)
expect(body.every(row => row.country === 'Spain')).toBe(true)
})
it('Requesting get all with column filters has correct data', async () => {
const { body: responseBody1, status: responseStatus1 } = await send<CrossSearch[]>(
`crosssearch/all/20/0/[{"id": "lid_now_loc", "value": "21050"}]/[]`,
@@ -132,6 +145,18 @@ describe('Getting cross-search data', () => {
expect(lastCountries).toEqual(['Japan', 'Japan', 'Japan', 'Japan', 'Japan'])
})
it('Requesting localities with POST body has correct data', async () => {
const legacyResponse = await send(`crosssearch/localities/[{"id": "country", "value": "Spain"}]/[]`, 'GET')
const { body, status } = await send(`crosssearch/localities`, 'POST', {
columnFilters: [{ id: 'country', value: 'Spain' }],
sorting: [],
})
expect(status).toEqual(200)
expect(body.length).toBeGreaterThan(0)
expect(body).toEqual(legacyResponse.body)
})
it('Requesting get all with column filtering does not allow invalid arrays or column names', async () => {
const { body: responseBody1, status: responseStatus1 } = await send<CrossSearch[]>(
`crosssearch/all/20/0/[{"id": "invalid_column", "value": "21050"}]/[]`,
+84 -54
View File
@@ -1,8 +1,9 @@
import { Router } from 'express'
import { Request, Response, Router } from 'express'
import {
getCrossSearchRawSql,
getCrossSearchLocalitiesRawSql,
parseAndValidateCrossSearchRouteParameters,
CrossSearchRequestParameters,
} from '../services/crossSearch'
import { fixBigInt } from '../utils/common'
import { format, FormatterRow, FormatterRowTransformFunction } from 'fast-csv'
@@ -32,82 +33,101 @@ const transformFunction = (row: CrossSearch & { full_count?: number }) => {
return transformedRow
}
router.get(`/all/:limit/:offset/:columnfilters/:sorting`, async (req, res) => {
let validatedValues
const getValidatedValues = (parameters: CrossSearchRequestParameters) => {
const { validationErrors, ...values } = parseAndValidateCrossSearchRouteParameters(parameters)
const validatedValues = values
if (validationErrors.length > 0) {
return { validationErrors }
}
return { validatedValues }
}
const handleCrossSearchValidationError = (error: unknown, res: Response) => {
if (error instanceof Error) return res.status(403).send({ error: error.message })
return res.status(403).send('Unknown error')
}
const sendCrossSearchRows = async (req: Request, res: Response, parameters: CrossSearchRequestParameters) => {
let parsedValues
try {
const { validationErrors, ...values } = parseAndValidateCrossSearchRouteParameters({
limit: req.params.limit,
offset: req.params.offset,
columnFilters: req.params.columnfilters,
sorting: req.params.sorting,
})
validatedValues = values
if (validationErrors.length > 0) {
return res.status(403).send(validationErrors)
}
parsedValues = getValidatedValues(parameters)
} catch (error) {
if (error instanceof Error) return res.status(403).send({ error: error.message })
return res.status(403).send('Unknown error')
return handleCrossSearchValidationError(error, res)
}
if ('validationErrors' in parsedValues) {
return res.status(403).send(parsedValues.validationErrors)
}
try {
const result = await getCrossSearchRawSql(
req.user,
validatedValues.validatedLimit,
validatedValues.validatedOffset,
validatedValues.validatedColumnFilters,
validatedValues.validatedSorting
parsedValues.validatedValues.validatedLimit,
parsedValues.validatedValues.validatedOffset,
parsedValues.validatedValues.validatedColumnFilters,
parsedValues.validatedValues.validatedSorting
)
return res.status(200).send(fixBigInt(result))
} catch (error) {
if (error instanceof Error) return res.status(403).send({ error: error.message })
return res.status(403).send('Unknown error')
return handleCrossSearchValidationError(error, res)
}
})
}
router.get(`/localities/:columnfilters/:sorting`, async (req, res) => {
let validatedValues
const sendCrossSearchLocalities = async (req: Request, res: Response, parameters: CrossSearchRequestParameters) => {
let parsedValues
try {
const { validationErrors, ...values } = parseAndValidateCrossSearchRouteParameters({
columnFilters: req.params.columnfilters,
sorting: req.params.sorting,
})
validatedValues = values
if (validationErrors.length > 0) {
return res.status(403).send(validationErrors)
}
parsedValues = getValidatedValues(parameters)
} catch (error) {
if (error instanceof Error) return res.status(403).send({ error: error.message })
return res.status(403).send('Unknown error')
return handleCrossSearchValidationError(error, res)
}
if ('validationErrors' in parsedValues) {
return res.status(403).send(parsedValues.validationErrors)
}
try {
const result = await getCrossSearchLocalitiesRawSql(
req.user,
validatedValues.validatedColumnFilters,
validatedValues.validatedSorting
parsedValues.validatedValues.validatedColumnFilters,
parsedValues.validatedValues.validatedSorting
)
return res.status(200).send(fixBigInt(result))
} catch (error) {
if (error instanceof Error) return res.status(403).send({ error: error.message })
return res.status(403).send('Unknown error')
return handleCrossSearchValidationError(error, res)
}
}
router.post(`/all`, async (req, res) => {
return sendCrossSearchRows(req, res, req.body as CrossSearchRequestParameters)
})
router.get(`/export/:columnfilters/:sorting`, async (req, res) => {
let validatedValues
router.get(`/all/:limit/:offset/:columnfilters/:sorting`, async (req, res) => {
return sendCrossSearchRows(req, res, {
limit: req.params.limit,
offset: req.params.offset,
columnFilters: req.params.columnfilters,
sorting: req.params.sorting,
})
})
router.post(`/localities`, async (req, res) => {
return sendCrossSearchLocalities(req, res, req.body as CrossSearchRequestParameters)
})
router.get(`/localities/:columnfilters/:sorting`, async (req, res) => {
return sendCrossSearchLocalities(req, res, {
columnFilters: req.params.columnfilters,
sorting: req.params.sorting,
})
})
const streamCrossSearchExport = async (req: Request, res: Response, parameters: CrossSearchRequestParameters) => {
let parsedValues
try {
const { validationErrors, ...values } = parseAndValidateCrossSearchRouteParameters({
columnFilters: req.params.columnfilters,
sorting: req.params.sorting,
})
validatedValues = values
if (validationErrors.length > 0) {
return res.status(403).send(validationErrors)
}
parsedValues = getValidatedValues(parameters)
} catch (error) {
if (error instanceof Error) return res.status(403).send({ error: error.message })
return res.status(403).send('Unknown error')
return handleCrossSearchValidationError(error, res)
}
if ('validationErrors' in parsedValues) {
return res.status(403).send(parsedValues.validationErrors)
}
let dataArray: Partial<CrossSearch[][]>
@@ -116,12 +136,11 @@ router.get(`/export/:columnfilters/:sorting`, async (req, res) => {
req.user,
undefined,
undefined,
validatedValues.validatedColumnFilters,
validatedValues.validatedSorting
parsedValues.validatedValues.validatedColumnFilters,
parsedValues.validatedValues.validatedSorting
)) as Partial<CrossSearch[][]>
} catch (error) {
if (error instanceof Error) return res.status(403).send({ error: error.message })
return res.status(403).send('Unknown error')
return handleCrossSearchValidationError(error, res)
}
res.on('finish', () => {
@@ -163,6 +182,17 @@ router.get(`/export/:columnfilters/:sorting`, async (req, res) => {
}
return stream.end()
}
router.post(`/export`, async (req, res) => {
return streamCrossSearchExport(req, res, req.body as CrossSearchRequestParameters)
})
router.get(`/export/:columnfilters/:sorting`, async (req, res) => {
return streamCrossSearchExport(req, res, {
columnFilters: req.params.columnfilters,
sorting: req.params.sorting,
})
})
export default router
+27 -15
View File
@@ -13,6 +13,13 @@ import { generateCrossSearchLocalitiesSql, generateCrossSearchSql } from './quer
import { ValidationObject } from '../../../frontend/src/shared/validators/validator'
import { validateCrossSearchRouteParams } from '../../../frontend/src/shared/validators/crossSearch'
export type CrossSearchRequestParameters = {
limit?: string | number
offset?: string | number
columnFilters: unknown
sorting: unknown
}
const getAllowedLocalities = async (user: User) => {
const usersProjects = await nowDb.now_proj_people.findMany({
where: { initials: user.initials },
@@ -195,22 +202,27 @@ export const getCrossSearchLocalitiesRawSql = async (
return result
}
export const parseAndValidateCrossSearchRouteParameters = (parameters: CrossSearchRouteParameters) => {
const parseOptionalNumber = (value: string | number | undefined, name: string) => {
if (value === undefined) return undefined
const parsedValue = typeof value === 'number' ? value : parseInt(value)
if (isNaN(parsedValue)) throw new Error(`${name} is not a number.`)
return parsedValue
}
const parseArrayParameter = (value: unknown, name: string) => {
const parsedValue: unknown = typeof value === 'string' ? (JSON.parse(value) as unknown) : value
if (!Array.isArray(parsedValue)) throw new Error(`${name} is not an array.`)
return parsedValue as unknown[]
}
export const parseAndValidateCrossSearchRouteParameters = (
parameters: CrossSearchRouteParameters | CrossSearchRequestParameters
) => {
const { limit, offset, columnFilters, sorting } = parameters
let parsedLimit
let parsedOffset
if (limit) {
parsedLimit = parseInt(limit)
if (isNaN(parsedLimit)) throw new Error('Limit is not a number.')
}
if (offset) {
parsedOffset = parseInt(offset)
if (isNaN(parsedOffset)) throw new Error('Offset is not a number.')
}
const parsedColumnFilters = JSON.parse(columnFilters) as unknown
if (!Array.isArray(parsedColumnFilters)) throw new Error('ColumnFilters is not an array.')
const parsedSorting = JSON.parse(sorting) as unknown
if (!Array.isArray(parsedSorting)) throw new Error('Sorting is not an array.')
const parsedLimit = parseOptionalNumber(limit, 'Limit')
const parsedOffset = parseOptionalNumber(offset, 'Offset')
const parsedColumnFilters = parseArrayParameter(columnFilters, 'ColumnFilters')
const parsedSorting = parseArrayParameter(sorting, 'Sorting')
const parsedParameters = {
limit: parsedLimit,