Skip to content

Commit a83f812

Browse files
committed
feat(tesseract): add default value filter schema for views
Adds a view-only top-level `filters` field accepting entries with `member`, `operator`, `values`, and optional `unless`. Wires the new member/values/unless paths into the JS and YAML transpilers, and rejects `filters` on regular cubes via Joi's unknown-key validation.
1 parent d54d02b commit a83f812

3 files changed

Lines changed: 181 additions & 0 deletions

File tree

packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,41 @@ const folderSchema = Joi.object().keys({
11301130
]).required(),
11311131
}).id('folderSchema');
11321132

1133+
const ViewFilterSchema = Joi.object().keys({
1134+
member: Joi.func().required(),
1135+
operator: Joi.any().valid(
1136+
'equals',
1137+
'notEquals',
1138+
'contains',
1139+
'notContains',
1140+
'startsWith',
1141+
'notStartsWith',
1142+
'endsWith',
1143+
'notEndsWith',
1144+
'in',
1145+
'notIn',
1146+
'gt',
1147+
'gte',
1148+
'lt',
1149+
'lte',
1150+
'set',
1151+
'notSet',
1152+
'inDateRange',
1153+
'notInDateRange',
1154+
'onTheDate',
1155+
'beforeDate',
1156+
'beforeOrOnDate',
1157+
'afterDate',
1158+
'afterOrOnDate',
1159+
).required(),
1160+
values: Joi.when('operator', {
1161+
is: Joi.valid('set', 'notSet'),
1162+
then: Joi.func().optional(),
1163+
otherwise: Joi.func().required()
1164+
}),
1165+
unless: Joi.func(),
1166+
});
1167+
11331168
const viewSchema = inherit(baseSchema, {
11341169
isView: Joi.boolean().strict(),
11351170
viewGroup: Joi.alternatives([Joi.string(), Joi.func()]),
@@ -1160,6 +1195,7 @@ const viewSchema = inherit(baseSchema, {
11601195
})
11611196
),
11621197
folders: Joi.array().items(folderSchema),
1198+
filters: Joi.array().items(ViewFilterSchema),
11631199
});
11641200

11651201
function formatErrorMessageFromDetails(explain, d) {

packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export const transpiledFieldsPatterns: Array<RegExp> = [
3737
/^(accessPolicy|access_policy)\.[0-9]+\.(rowLevel|row_level)\.filters\.[0-9]+.*\.member$/,
3838
/^(accessPolicy|access_policy)\.[0-9]+\.(rowLevel|row_level)\.filters\.[0-9]+.*\.values$/,
3939
/^(accessPolicy|access_policy)\.[0-9]+\.conditions.[0-9]+\.if$/,
40+
/^filters\.[0-9]+\.member$/,
41+
/^filters\.[0-9]+\.values$/,
42+
/^filters\.[0-9]+\.unless$/,
4043
/^(measures|dimensions)\.[_a-zA-Z][_a-zA-Z0-9]*\.mask\.sql$/,
4144
];
4245

packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,148 @@ describe('Cube Validation', () => {
210210
expect(validationResult.error).toBeFalsy();
211211
});
212212

213+
describe('view default value filters', () => {
214+
const silentReporter = new ConsoleErrorReporter();
215+
216+
it('view with default value filter - correct', async () => {
217+
const cubeValidator = new CubeValidator(new CubeSymbols());
218+
const cube = {
219+
name: 'orders_view',
220+
isView: true,
221+
fileName: 'fileName',
222+
filters: [
223+
{
224+
member: () => 'currency',
225+
operator: 'equals',
226+
values: () => ['USD'],
227+
},
228+
],
229+
};
230+
231+
const validationResult = cubeValidator.validate(cube, silentReporter);
232+
233+
expect(validationResult.error).toBeFalsy();
234+
});
235+
236+
it('view with default value filter and unless - correct', async () => {
237+
const cubeValidator = new CubeValidator(new CubeSymbols());
238+
const cube = {
239+
name: 'orders_view',
240+
isView: true,
241+
fileName: 'fileName',
242+
filters: [
243+
{
244+
member: () => 'currency',
245+
operator: 'equals',
246+
values: () => ['USD'],
247+
unless: () => ['currency'],
248+
},
249+
],
250+
};
251+
252+
const validationResult = cubeValidator.validate(cube, silentReporter);
253+
254+
expect(validationResult.error).toBeFalsy();
255+
});
256+
257+
it('view filter with set operator does not require values - correct', async () => {
258+
const cubeValidator = new CubeValidator(new CubeSymbols());
259+
const cube = {
260+
name: 'orders_view',
261+
isView: true,
262+
fileName: 'fileName',
263+
filters: [
264+
{
265+
member: () => 'currency',
266+
operator: 'set',
267+
},
268+
],
269+
};
270+
271+
const validationResult = cubeValidator.validate(cube, silentReporter);
272+
273+
expect(validationResult.error).toBeFalsy();
274+
});
275+
276+
it('view filter with missing required values - error', async () => {
277+
const cubeValidator = new CubeValidator(new CubeSymbols());
278+
const cube = {
279+
name: 'orders_view',
280+
isView: true,
281+
fileName: 'fileName',
282+
filters: [
283+
{
284+
member: () => 'currency',
285+
operator: 'equals',
286+
},
287+
],
288+
};
289+
290+
const validationResult = cubeValidator.validate(cube, silentReporter);
291+
292+
expect(validationResult.error).toBeTruthy();
293+
});
294+
295+
it('view filter with missing member - error', async () => {
296+
const cubeValidator = new CubeValidator(new CubeSymbols());
297+
const cube = {
298+
name: 'orders_view',
299+
isView: true,
300+
fileName: 'fileName',
301+
filters: [
302+
{
303+
operator: 'equals',
304+
values: () => ['USD'],
305+
},
306+
],
307+
};
308+
309+
const validationResult = cubeValidator.validate(cube, silentReporter);
310+
311+
expect(validationResult.error).toBeTruthy();
312+
});
313+
314+
it('view filter with invalid operator - error', async () => {
315+
const cubeValidator = new CubeValidator(new CubeSymbols());
316+
const cube = {
317+
name: 'orders_view',
318+
isView: true,
319+
fileName: 'fileName',
320+
filters: [
321+
{
322+
member: () => 'currency',
323+
operator: 'someInvalidOperator',
324+
values: () => ['USD'],
325+
},
326+
],
327+
};
328+
329+
const validationResult = cubeValidator.validate(cube, silentReporter);
330+
331+
expect(validationResult.error).toBeTruthy();
332+
});
333+
334+
it('regular cube with default value filter - error', async () => {
335+
const cubeValidator = new CubeValidator(new CubeSymbols());
336+
const cube = {
337+
name: 'orders',
338+
sql: () => 'SELECT * FROM orders',
339+
fileName: 'fileName',
340+
filters: [
341+
{
342+
member: () => 'currency',
343+
operator: 'equals',
344+
values: () => ['USD'],
345+
},
346+
],
347+
};
348+
349+
const validationResult = cubeValidator.validate(cube, silentReporter);
350+
351+
expect(validationResult.error).toBeTruthy();
352+
});
353+
});
354+
213355
it('refreshKey alternatives', async () => {
214356
const cubeValidator = new CubeValidator(new CubeSymbols());
215357
const cube = {

0 commit comments

Comments
 (0)