diff --git a/packages/cubejs-api-gateway/src/query.js b/packages/cubejs-api-gateway/src/query.js index 9922708d7024..de8bf345f8bc 100644 --- a/packages/cubejs-api-gateway/src/query.js +++ b/packages/cubejs-api-gateway/src/query.js @@ -70,7 +70,7 @@ const oneFilter = Joi.object().keys({ dimension: id, member: id, operator: Joi.valid(...operators).required(), - values: Joi.array().items(Joi.string().allow('', null), Joi.link('...'), Joi.number(), Joi.boolean()) + values: Joi.array().items(Joi.string().allow('', null), Joi.number(), Joi.boolean(), Joi.link('...')) }).xor('dimension', 'member'); const oneCondition = Joi.object().keys({ @@ -118,15 +118,16 @@ const normalizeQueryOrder = order => { const DateRegex = /^\d\d\d\d-\d\d-\d\d$/; -const checkQueryFilters = (filter) => { - filter.find(f => { +const normalizeQueryFilters = (filter) => ( + filter.map(f => { + const res = { ...f }; if (f.or) { - checkQueryFilters(f.or); - return false; + res.or = normalizeQueryFilters(f.or); + return res; } if (f.and) { - checkQueryFilters(f.and); - return false; + res.and = normalizeQueryFilters(f.and); + return res; } if (!f.operator) { @@ -137,18 +138,27 @@ const checkQueryFilters = (filter) => { throw new UserError(`Operator ${f.operator} not supported for filter: ${JSON.stringify(f)}`); } - if (!f.values && ['set', 'notSet', 'measureFilter'].indexOf(f.operator) === -1) { + if ((!f.values || f.values.length === 0) && ['set', 'notSet', 'measureFilter'].indexOf(f.operator) === -1) { throw new UserError(`Values required for filter: ${JSON.stringify(f)}`); } - return false; - }); - return true; -}; + if (f.values) { + res.values = f.values.map(v => (v != null ? v.toString() : v)); + } + + if (f.dimension) { + res.member = f.dimension; + delete res.dimension; + } + + return res; + }) +); /** * Normalize incoming network query. * @param {Query} query + * @param {boolean} persistent * @throws {UserError} * @returns {NormalizedQuery} */ @@ -166,8 +176,6 @@ const normalizeQuery = (query, persistent) => { ); } - checkQueryFilters(query.filters || []); - const regularToTimeDimension = (query.dimensions || []).filter(d => d.split('.').length === 3).map(d => ({ dimension: d.split('.').slice(0, 2).join('.'), granularity: d.split('.')[2] @@ -198,14 +206,7 @@ const normalizeQuery = (query, persistent) => { limit: newLimit, timezone, order: normalizeQueryOrder(query.order), - filters: (query.filters || []).map(f => { - const { dimension, member, ...filter } = f; - return { - ...filter, - member: member || dimension, - values: filter.values?.map(v => (v != null ? v.toString() : v)) - }; - }), + filters: normalizeQueryFilters(query.filters || []), dimensions: (query.dimensions || []).filter(d => d.split('.').length !== 3), timeDimensions: (query.timeDimensions || []).map(td => { let dateRange; diff --git a/packages/cubejs-api-gateway/test/index.test.ts b/packages/cubejs-api-gateway/test/index.test.ts index e0e1b5f6a6c1..1251d422f9dd 100644 --- a/packages/cubejs-api-gateway/test/index.test.ts +++ b/packages/cubejs-api-gateway/test/index.test.ts @@ -297,6 +297,16 @@ describe('API Gateway', () => { member: 'Foo.bar', operator: 'gte', values: [0] + }, { + or: [{ + member: 'Foo.bar', + operator: 'gte', + values: [10.5] + }, { + member: 'Foo.bar', + operator: 'gte', + values: [0] + }] }] }; @@ -317,6 +327,16 @@ describe('API Gateway', () => { member: 'Foo.bar', operator: 'gte', values: ['0'] + }, { + or: [{ + member: 'Foo.bar', + operator: 'gte', + values: ['10.5'] + }, { + member: 'Foo.bar', + operator: 'gte', + values: ['0'] + }] }], rowLimit: 10000, limit: 10000, @@ -329,6 +349,19 @@ describe('API Gateway', () => { ); }); + test('normalize empty filters', async () => { + const { app } = createApiGateway(); + + const res = await request(app) + .get( + '/cubejs-api/v1/load?query={"measures":["Foo.bar"],"filters":[{"member":"Foo.bar","operator":"equals","values":[]}]}' + ) + .set('Authorization', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.t-IDcSemACt8x4iTMCda8Yhe3iZaWbvV5XKSTbuAn0M') + .expect(400); + console.log(res.body); + expect(res.body.error).toMatch(/Values required for filter/); + }); + test('normalize queryRewrite limit', async () => { const { app } = createApiGateway( new AdapterApiMock(),