Skip to content

Commit

Permalink
feat: vue limit, offset and measure filters support (#194)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TCBroad authored and paveltiunov committed Sep 5, 2019
1 parent 7abf504 commit 33f365a
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 21 deletions.
2 changes: 2 additions & 0 deletions docs/Cube.js-Frontend/@cubejs-client-vue.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ API from Cube.js Backend.
- `isQueryPresent` - Bool indicating whether is query ready to be displayed or
not.
- `query` - current query, based on selected members.
- `setLimit`, `removeLimit` - functions to control the number of results returned
- `setOffset`, `removeOffset` - functions to control the number of rows skipped before results returned. Use with limit to control pagination

### Example
[Open in CodeSandbox](https://codesandbox.io/s/3rlxjkv2p)
Expand Down
79 changes: 64 additions & 15 deletions packages/cubejs-vue/src/QueryBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default {
availableDimensions: [],
availableTimeDimensions: [],
availableSegments: [],
limit: null,
offset: null,
};

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

const { measures, dimensions, segments, timeDimensions, filters } = this.query;
const { measures, dimensions, segments, timeDimensions, filters, limit, offset } = this.query;

this.measures = (measures || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'measures') }));
this.dimensions = (dimensions || []).map((m, i) => ({ index: i, ...this.meta.resolveMember(m, 'dimensions') }));
Expand All @@ -54,8 +56,9 @@ export default {
}));
this.filters = (filters || []).map((m, i) => ({
...m,
dimension: this.meta.resolveMember(m.dimension, ['dimensions', 'measures']),
operators: this.meta.filterOperatorsForMember(m.dimension, ['dimensions', 'measures']),
// using 'dimension' is deprecated, 'member' should be specified instead
member: this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']),
operators: this.meta.filterOperatorsForMember(m.member || m.dimension, ['dimensions', 'measures']),
index: i
}));

Expand All @@ -64,6 +67,8 @@ export default {
this.availableTimeDimensions = (this.meta.membersForQuery({}, 'dimensions') || [])
.filter(m => m.type === 'time');
this.availableSegments = this.meta.membersForQuery({}, 'segments') || [];
this.limit = (limit || null);
this.offset = (offset || null);
},
render(createElement) {
const {
Expand All @@ -82,6 +87,12 @@ export default {
availableTimeDimensions,
availableDimensions,
availableMeasures,
limit,
offset,
setLimit,
removeLimit,
setOffset,
removeOffset,
} = this;

let builderProps = {};
Expand All @@ -102,6 +113,12 @@ export default {
availableDimensions,
availableMeasures,
updateChartType: this.updateChart,
limit,
offset,
setLimit,
removeLimit,
setOffset,
removeOffset,
};

QUERY_ELEMENTS.forEach((e) => {
Expand Down Expand Up @@ -147,9 +164,14 @@ export default {
validatedQuery() {
const validatedQuery = {};
let toQuery = member => member.name;
// TODO: implement order, limit, timezone, renewQuery
// TODO: implement order, timezone, renewQuery

let hasElements = false;
QUERY_ELEMENTS.forEach((e) => {
if (!this[e]) {
return;
}

if (e === 'timeDimensions') {
toQuery = (member) => ({
dimension: member.dimension.name,
Expand All @@ -158,14 +180,16 @@ export default {
});
} else if (e === 'filters') {
toQuery = (member) => ({
dimension: member.dimension.name,
member: member.member.name,
operator: member.operator,
values: member.values,
});
}

if (this[e].length > 0) {
validatedQuery[e] = this[e].map(x => toQuery(x));

hasElements = true;
}
});
// TODO: implement default heuristics
Expand All @@ -174,6 +198,19 @@ export default {
validatedQuery.filters = validatedQuery.filters.filter(f => f.operator);
}

// only set limit and offset if there are elements otherwise an invalid request with just limit/offset
// gets sent when the component is first mounted, but before the actual query is constructed.
if (hasElements) {
if (this.limit) {
validatedQuery.limit = this.limit;
}

if (this.offset) {
validatedQuery.offset = this.offset;
}
// add order
}

return validatedQuery;
},
},
Expand All @@ -199,13 +236,13 @@ export default {
};
}
} else if (element === 'filters') {
const dimension = {
...this.meta.resolveMember(member.dimension, 'dimensions'),
const filterMember = {
...this.meta.resolveMember(member.member || member.dimension, ['dimensions', 'measures']),
};

mem = {
...member,
dimension,
member: filterMember,
};
} else {
mem = this[`available${name}`].find(m => m.name === member);
Expand Down Expand Up @@ -254,13 +291,13 @@ export default {
}
} else if (element === 'filters') {
index = this[element].findIndex(x => x.dimension === old);
const dimension = {
...this.meta.resolveMember(member.dimension, 'dimensions'),
const filterMember = {
...this.meta.resolveMember(member.member || member.dimension, ['dimensions', 'measures']),
};

mem = {
...member,
dimension,
member: filterMember,
};
} else {
index = this[element].findIndex(x => x.name === old);
Expand Down Expand Up @@ -294,13 +331,13 @@ export default {
};
}
} else if (element === 'filters') {
const dimension = {
...this.meta.resolveMember(m.dimension, 'dimensions'),
const member = {
...this.meta.resolveMember(m.member || m.dimension, ['dimensions', 'measures']),
};

mem = {
...m,
dimension,
member,
};
} else {
mem = this[`available${name}`].find(x => x.name === m);
Expand All @@ -311,8 +348,20 @@ export default {

this[element] = elements;
},
setLimit(limit) {
this.limit = limit;
},
removeLimit() {
this.limit = null;
},
setOffset(offset) {
this.offset = offset;
},
removeOffset() {
this.offset = null;
},
updateChart(chartType) {
this.chartType = chartType;
},
},
};
};
94 changes: 88 additions & 6 deletions packages/cubejs-vue/tests/unit/QueryBuilder.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ describe('QueryBuilder.vue', () => {
values: ['valid']
});
expect(wrapper.vm.filters.length).toBe(1);
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
});

it('updates filters', async () => {
Expand Down Expand Up @@ -335,11 +335,11 @@ describe('QueryBuilder.vue', () => {
await flushPromises();

expect(wrapper.vm.filters.length).toBe(1);
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].values).toContain('invalid');
wrapper.vm.updateMember('filters', 'Orders.status', newFilter);
expect(wrapper.vm.filters.length).toBe(1);
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].values).toContain('valid');
});

Expand Down Expand Up @@ -367,7 +367,7 @@ describe('QueryBuilder.vue', () => {
await flushPromises();

expect(wrapper.vm.filters.length).toBe(1);
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].values).toContain('invalid');
wrapper.vm.removeMember('filters', 'Orders.status');
expect(wrapper.vm.filters.length).toBe(0);
Expand Down Expand Up @@ -403,12 +403,94 @@ describe('QueryBuilder.vue', () => {
await flushPromises();

expect(wrapper.vm.filters.length).toBe(1);
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].values).toContain('invalid');
wrapper.vm.setMembers('filters', [newFilter]);
expect(wrapper.vm.filters.length).toBe(1);
expect(wrapper.vm.filters[0].dimension.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].member.name).toBe('Orders.status');
expect(wrapper.vm.filters[0].values).toContain('valid');
});

it('sets filters when using measure', async () => {
const cube = CubejsApi('token');
jest.spyOn(cube, 'request')
.mockImplementation(fetchMock(load))
.mockImplementationOnce(fetchMock(meta));

const filter = {
member: 'Orders.number',
operator: 'gt',
values: ['1'],
};

const wrapper = mount(QueryBuilder, {
propsData: {
cubejsApi: cube,
query: {
filters: [filter],
},
},
});

await flushPromises();

expect(wrapper.vm.filters.length).toBe(1);
expect(wrapper.vm.filters[0].member.name).toBe('Orders.number');
expect(wrapper.vm.filters[0].values).toContain('1');
});

it('sets limit', async () => {
const cube = CubejsApi('token');
jest.spyOn(cube, 'request')
.mockImplementation(fetchMock(load))
.mockImplementationOnce(fetchMock(meta));

const filter = {
member: 'Orders.status',
operator: 'equals',
values: ['invalid'],
};

const wrapper = mount(QueryBuilder, {
propsData: {
cubejsApi: cube,
query: {
filters: [filter],
limit: 10
},
},
});

await flushPromises();

expect(wrapper.vm.limit).toBe(10);
});

it('sets offset', async () => {
const cube = CubejsApi('token');
jest.spyOn(cube, 'request')
.mockImplementation(fetchMock(load))
.mockImplementationOnce(fetchMock(meta));

const filter = {
member: 'Orders.status',
operator: 'equals',
values: ['invalid'],
};

const wrapper = mount(QueryBuilder, {
propsData: {
cubejsApi: cube,
query: {
filters: [filter],
offset: 10
},
},
});

await flushPromises();

expect(wrapper.vm.offset).toBe(10);
});
});
});

0 comments on commit 33f365a

Please sign in to comment.