Skip to content

Commit 33f365a

Browse files
TCBroadpaveltiunov
authored andcommitted
feat: vue limit, offset and measure filters support (#194)
* Fixed issue with filters not using 'members' and only matching against dimensions - Added support for limit and offset * Updated documentation for added features * Fixed existing tests, added tests for new features * Added old 'dimension' property back in to filters Fixes #188
1 parent 7abf504 commit 33f365a

File tree

3 files changed

+154
-21
lines changed

3 files changed

+154
-21
lines changed

docs/Cube.js-Frontend/@cubejs-client-vue.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ API from Cube.js Backend.
137137
- `isQueryPresent` - Bool indicating whether is query ready to be displayed or
138138
not.
139139
- `query` - current query, based on selected members.
140+
- `setLimit`, `removeLimit` - functions to control the number of results returned
141+
- `setOffset`, `removeOffset` - functions to control the number of rows skipped before results returned. Use with limit to control pagination
140142

141143
### Example
142144
[Open in CodeSandbox](https://codesandbox.io/s/3rlxjkv2p)

packages/cubejs-vue/src/QueryBuilder.js

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export default {
2727
availableDimensions: [],
2828
availableTimeDimensions: [],
2929
availableSegments: [],
30+
limit: null,
31+
offset: null,
3032
};
3133

3234
data.granularities = [
@@ -42,7 +44,7 @@ export default {
4244
async mounted() {
4345
this.meta = await this.cubejsApi.meta();
4446

45-
const { measures, dimensions, segments, timeDimensions, filters } = this.query;
47+
const { measures, dimensions, segments, timeDimensions, filters, limit, offset } = this.query;
4648

4749
this.measures = (measures || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'measures') }));
4850
this.dimensions = (dimensions || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'dimensions') }));
@@ -54,8 +56,9 @@ export default {
5456
}));
5557
this.filters = (filters || []).map((m, i) => ({
5658
...m,
57-
dimension: this.meta.resolveMember(m.dimension, ['dimensions', 'measures']),
58-
operators: this.meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']),
59+
// using 'dimension' is deprecated, 'member' should be specified instead
60+
member: this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']),
61+
operators: this.meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']),
5962
index: i
6063
}));
6164

@@ -64,6 +67,8 @@ export default {
6467
this.availableTimeDimensions = (this.meta.membersForQuery({}, 'dimensions') || [])
6568
.filter(m => m.type === 'time');
6669
this.availableSegments = this.meta.membersForQuery({}, 'segments') || [];
70+
this.limit = (limit || null);
71+
this.offset = (offset || null);
6772
},
6873
render(createElement) {
6974
const {
@@ -82,6 +87,12 @@ export default {
8287
availableTimeDimensions,
8388
availableDimensions,
8489
availableMeasures,
90+
limit,
91+
offset,
92+
setLimit,
93+
removeLimit,
94+
setOffset,
95+
removeOffset,
8596
} = this;
8697

8798
let builderProps = {};
@@ -102,6 +113,12 @@ export default {
102113
availableDimensions,
103114
availableMeasures,
104115
updateChartType: this.updateChart,
116+
limit,
117+
offset,
118+
setLimit,
119+
removeLimit,
120+
setOffset,
121+
removeOffset,
105122
};
106123

107124
QUERY_ELEMENTS.forEach((e) => {
@@ -147,9 +164,14 @@ export default {
147164
validatedQuery() {
148165
const validatedQuery = {};
149166
let toQuery = member => member.name;
150-
// TODO: implement order, limit, timezone, renewQuery
167+
// TODO: implement order, timezone, renewQuery
151168

169+
let hasElements = false;
152170
QUERY_ELEMENTS.forEach((e) => {
171+
if (!this[e]) {
172+
return;
173+
}
174+
153175
if (e === 'timeDimensions') {
154176
toQuery = (member) => ({
155177
dimension: member.dimension.name,
@@ -158,14 +180,16 @@ export default {
158180
});
159181
} else if (e === 'filters') {
160182
toQuery = (member) => ({
161-
dimension: member.dimension.name,
183+
member: member.member.name,
162184
operator: member.operator,
163185
values: member.values,
164186
});
165187
}
166188

167189
if (this[e].length > 0) {
168190
validatedQuery[e] = this[e].map(x => toQuery(x));
191+
192+
hasElements = true;
169193
}
170194
});
171195
// TODO: implement default heuristics
@@ -174,6 +198,19 @@ export default {
174198
validatedQuery.filters = validatedQuery.filters.filter(f => f.operator);
175199
}
176200

201+
// only set limit and offset if there are elements otherwise an invalid request with just limit/offset
202+
// gets sent when the component is first mounted, but before the actual query is constructed.
203+
if (hasElements) {
204+
if (this.limit) {
205+
validatedQuery.limit = this.limit;
206+
}
207+
208+
if (this.offset) {
209+
validatedQuery.offset = this.offset;
210+
}
211+
// add order
212+
}
213+
177214
return validatedQuery;
178215
},
179216
},
@@ -199,13 +236,13 @@ export default {
199236
};
200237
}
201238
} else if (element === 'filters') {
202-
const dimension = {
203-
...this.meta.resolveMember(member.dimension, 'dimensions'),
239+
const filterMember = {
240+
...this.meta.resolveMember(member.member || member.dimension, ['dimensions', 'measures']),
204241
};
205242

206243
mem = {
207244
...member,
208-
dimension,
245+
member: filterMember,
209246
};
210247
} else {
211248
mem = this[`available${name}`].find(m => m.name === member);
@@ -254,13 +291,13 @@ export default {
254291
}
255292
} else if (element === 'filters') {
256293
index = this[element].findIndex(x => x.dimension === old);
257-
const dimension = {
258-
...this.meta.resolveMember(member.dimension, 'dimensions'),
294+
const filterMember = {
295+
...this.meta.resolveMember(member.member || member.dimension, ['dimensions', 'measures']),
259296
};
260297

261298
mem = {
262299
...member,
263-
dimension,
300+
member: filterMember,
264301
};
265302
} else {
266303
index = this[element].findIndex(x => x.name === old);
@@ -294,13 +331,13 @@ export default {
294331
};
295332
}
296333
} else if (element === 'filters') {
297-
const dimension = {
298-
...this.meta.resolveMember(m.dimension, 'dimensions'),
334+
const member = {
335+
...this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']),
299336
};
300337

301338
mem = {
302339
...m,
303-
dimension,
340+
member,
304341
};
305342
} else {
306343
mem = this[`available${name}`].find(x => x.name === m);
@@ -311,8 +348,20 @@ export default {
311348

312349
this[element] = elements;
313350
},
351+
setLimit(limit) {
352+
this.limit = limit;
353+
},
354+
removeLimit() {
355+
this.limit = null;
356+
},
357+
setOffset(offset) {
358+
this.offset = offset;
359+
},
360+
removeOffset() {
361+
this.offset = null;
362+
},
314363
updateChart(chartType) {
315364
this.chartType = chartType;
316365
},
317366
},
318-
};
367+
};

packages/cubejs-vue/tests/unit/QueryBuilder.spec.js

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ describe('QueryBuilder.vue', () => {
302302
values: ['valid']
303303
});
304304
expect(wrapper.vm.filters.length).toBe(1);
305-
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
305+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
306306
});
307307

308308
it('updates filters', async () => {
@@ -335,11 +335,11 @@ describe('QueryBuilder.vue', () => {
335335
await flushPromises();
336336

337337
expect(wrapper.vm.filters.length).toBe(1);
338-
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
338+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
339339
expect(wrapper.vm.filters[0].values).toContain('invalid');
340340
wrapper.vm.updateMember('filters', 'Orders.status', newFilter);
341341
expect(wrapper.vm.filters.length).toBe(1);
342-
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
342+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
343343
expect(wrapper.vm.filters[0].values).toContain('valid');
344344
});
345345

@@ -367,7 +367,7 @@ describe('QueryBuilder.vue', () => {
367367
await flushPromises();
368368

369369
expect(wrapper.vm.filters.length).toBe(1);
370-
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
370+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
371371
expect(wrapper.vm.filters[0].values).toContain('invalid');
372372
wrapper.vm.removeMember('filters', 'Orders.status');
373373
expect(wrapper.vm.filters.length).toBe(0);
@@ -403,12 +403,94 @@ describe('QueryBuilder.vue', () => {
403403
await flushPromises();
404404

405405
expect(wrapper.vm.filters.length).toBe(1);
406-
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
406+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
407407
expect(wrapper.vm.filters[0].values).toContain('invalid');
408408
wrapper.vm.setMembers('filters', [newFilter]);
409409
expect(wrapper.vm.filters.length).toBe(1);
410-
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
410+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
411411
expect(wrapper.vm.filters[0].values).toContain('valid');
412+
});
413+
414+
it('sets filters when using measure', async () => {
415+
const cube = CubejsApi('token');
416+
jest.spyOn(cube, 'request')
417+
.mockImplementation(fetchMock(load))
418+
.mockImplementationOnce(fetchMock(meta));
419+
420+
const filter = {
421+
member: 'Orders.number',
422+
operator: 'gt',
423+
values: ['1'],
424+
};
425+
426+
const wrapper = mount(QueryBuilder, {
427+
propsData: {
428+
cubejsApi: cube,
429+
query: {
430+
filters: [filter],
431+
},
432+
},
433+
});
434+
435+
await flushPromises();
436+
437+
expect(wrapper.vm.filters.length).toBe(1);
438+
expect(wrapper.vm.filters[0].member.name).toBe('Orders.number');
439+
expect(wrapper.vm.filters[0].values).toContain('1');
440+
});
441+
442+
it('sets limit', async () => {
443+
const cube = CubejsApi('token');
444+
jest.spyOn(cube, 'request')
445+
.mockImplementation(fetchMock(load))
446+
.mockImplementationOnce(fetchMock(meta));
447+
448+
const filter = {
449+
member: 'Orders.status',
450+
operator: 'equals',
451+
values: ['invalid'],
452+
};
453+
454+
const wrapper = mount(QueryBuilder, {
455+
propsData: {
456+
cubejsApi: cube,
457+
query: {
458+
filters: [filter],
459+
limit: 10
460+
},
461+
},
462+
});
463+
464+
await flushPromises();
465+
466+
expect(wrapper.vm.limit).toBe(10);
467+
});
468+
469+
it('sets offset', async () => {
470+
const cube = CubejsApi('token');
471+
jest.spyOn(cube, 'request')
472+
.mockImplementation(fetchMock(load))
473+
.mockImplementationOnce(fetchMock(meta));
474+
475+
const filter = {
476+
member: 'Orders.status',
477+
operator: 'equals',
478+
values: ['invalid'],
479+
};
480+
481+
const wrapper = mount(QueryBuilder, {
482+
propsData: {
483+
cubejsApi: cube,
484+
query: {
485+
filters: [filter],
486+
offset: 10
487+
},
488+
},
489+
});
490+
491+
await flushPromises();
492+
493+
expect(wrapper.vm.offset).toBe(10);
412494
});
413495
});
414496
});

0 commit comments

Comments
 (0)