mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Use POST bodies for cross-search filters
This commit is contained in:
@@ -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"}]/[]`,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user