diff --git a/CHANGELOG.md b/CHANGELOG.md index c4544b03a..3c8f1bd47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Not released +- react-api: stats request uses POST for big queries/queryParameters [#656](https://github.com/CartoDB/carto-react/pull/656) - Improve upgrade guide documentation [#651](https://github.com/CartoDB/carto-react/pull/651) - Fix storybook publication with Node 18 [#654](https://github.com/CartoDB/carto-react/pull/654) diff --git a/packages/react-api/__tests__/api/stats.test.js b/packages/react-api/__tests__/api/stats.test.js index 474382c9c..a3e0a21ee 100644 --- a/packages/react-api/__tests__/api/stats.test.js +++ b/packages/react-api/__tests__/api/stats.test.js @@ -135,6 +135,82 @@ describe('stats', () => { }); }); + test('big query source - should return stats using POST', async () => { + const hugeQuery = Array.from(Array(5000)).join(','); + const QUERY_TEST = { + input: { + source: { ...QUERY_SOURCE, data: hugeQuery }, + column: 'injuries' + }, + url: `https://gcp-us-east1.api.carto.com/v3/stats/carto-ps-bq-developers/injuries`, + output: TABLE_AND_QUERY_STATS + }; + + const fetchMock = (global.fetch = jest.fn().mockImplementation(async () => { + return { + ok: true, + json: async () => QUERY_TEST.output + }; + })); + + const abortController = new AbortController(); + + const res = await getStats({ ...QUERY_TEST.input, opts: { abortController } }); + + expect(res).toEqual(QUERY_TEST.output); + + expect(fetchMock).toBeCalledWith(QUERY_TEST.url, { + method: 'POST', + headers: { + Authorization: `Bearer ${QUERY_TEST.input.source.credentials.accessToken}`, + 'Content-Type': 'application/json' + }, + signal: abortController.signal, + body: JSON.stringify({ q: hugeQuery }) + }); + }); + + test('query source - big queryParameters, should return stats using POST', async () => { + const huugeQueryParameters = Array.from(Array(5000)).reduce((r, _, i) => { + r[`param${i}`] = i; + return r; + }, {}); + const QUERY_TEST = { + input: { + source: { ...QUERY_SOURCE, queryParameters: huugeQueryParameters }, + column: 'injuries' + }, + url: `https://gcp-us-east1.api.carto.com/v3/stats/carto-ps-bq-developers/injuries`, + output: TABLE_AND_QUERY_STATS + }; + + const fetchMock = (global.fetch = jest.fn().mockImplementation(async () => { + return { + ok: true, + json: async () => QUERY_TEST.output + }; + })); + + const abortController = new AbortController(); + + const res = await getStats({ ...QUERY_TEST.input, opts: { abortController } }); + + expect(res).toEqual(QUERY_TEST.output); + + expect(fetchMock).toBeCalledWith(QUERY_TEST.url, { + method: 'POST', + headers: { + Authorization: `Bearer ${QUERY_TEST.input.source.credentials.accessToken}`, + 'Content-Type': 'application/json' + }, + signal: abortController.signal, + body: JSON.stringify({ + q: QUERY_SOURCE.data, + queryParameters: huugeQueryParameters + }) + }); + }); + test('tileset source - should return stats', async () => { const TILESET_TEST = { input: { diff --git a/packages/react-api/src/api/stats.js b/packages/react-api/src/api/stats.js index 00456c0de..a4d12187f 100644 --- a/packages/react-api/src/api/stats.js +++ b/packages/react-api/src/api/stats.js @@ -1,7 +1,11 @@ import { checkCredentials, makeCall } from './common'; import { MAP_TYPES, API_VERSIONS } from '@deck.gl/carto'; import { getTileJson } from './tilejson'; -import { InvalidColumnError, _assert as assert } from '@carto/react-core/'; +import { + InvalidColumnError, + REQUEST_GET_MAX_URL_LENGTH, + _assert as assert +} from '@carto/react-core/'; /** * Execute a stats service request. @@ -34,35 +38,41 @@ export async function getStats(props) { } return columnStats; - } else { - const url = buildUrl(source, column); - - return makeCall({ - url, - credentials: source.credentials, - opts, - queryParameters: source.queryParameters - }); } -} -// Aux -function buildUrl(source, column) { const isQuery = source.type === MAP_TYPES.QUERY; - - const url = new URL( + const baseUrl = new URL( `${source.credentials.apiBaseUrl}/v3/stats/${source.connection}/${ isQuery ? column : `${source.data}/${column}` }` ); + const queryParams = {}; if (isQuery) { - url.searchParams.set('q', source.data); - + queryParams.q = source.data; if (source.queryParameters) { - url.searchParams.set('queryParameters', JSON.stringify(source.queryParameters)); + queryParams.queryParameters = JSON.stringify(source.queryParameters); } } - return url; + const queryParamsFormatted = new URLSearchParams(queryParams).toString(); + const getUrl = `${baseUrl}${queryParamsFormatted ? '?' + queryParamsFormatted : ''}`; + if (getUrl.length <= REQUEST_GET_MAX_URL_LENGTH) { + return makeCall({ + url: getUrl, + credentials: source.credentials, + opts + }); + } else { + queryParams.queryParameters = source.queryParameters; + return makeCall({ + url: baseUrl, + credentials: source.credentials, + opts: { + ...opts, + method: 'POST', + body: JSON.stringify(queryParams) + } + }); + } }