Skip to content

Commit 2012281

Browse files
authored
feat(schema-compiler): folders support, hierarchies improvements (#9018)
1 parent a74818e commit 2012281

File tree

13 files changed

+553
-276
lines changed

13 files changed

+553
-276
lines changed

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

Lines changed: 76 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ export class CubeEvaluator extends CubeSymbols {
112112
this.evaluateMultiStageReferences(cube.name, cube.measures);
113113
this.evaluateMultiStageReferences(cube.name, cube.dimensions);
114114

115-
this.prepareHierarchies(cube);
115+
this.prepareHierarchies(cube, errorReporter);
116+
this.prepareFolders(cube, errorReporter);
116117

117118
this.prepareAccessPolicy(cube, errorReporter);
118119

@@ -179,36 +180,91 @@ export class CubeEvaluator extends CubeSymbols {
179180
}
180181
}
181182

182-
private prepareHierarchies(cube: any) {
183+
private prepareFolders(cube: any, errorReporter: ErrorReporter) {
184+
if (Array.isArray(cube.folders)) {
185+
cube.folders = cube.folders.map(it => {
186+
const includedMembers = this.allMembersOrList(cube, it.includes);
187+
const includes = includedMembers.map(memberName => {
188+
if (memberName.includes('.')) {
189+
errorReporter.error(
190+
`Paths aren't allowed in the 'folders' but '${memberName}' has been provided for ${cube.name}`
191+
);
192+
}
193+
194+
const member = cube.includedMembers.find(m => m.name === memberName);
195+
if (!member) {
196+
errorReporter.error(
197+
`Member '${memberName}' included in folder '${it.name}' not found`
198+
);
199+
return null;
200+
}
201+
202+
return member;
203+
})
204+
.filter(Boolean);
205+
206+
return ({
207+
...it,
208+
includes
209+
});
210+
});
211+
}
212+
213+
return [];
214+
}
215+
216+
private prepareHierarchies(cube: any, errorReporter: ErrorReporter) {
217+
const uniqueHierarchyNames = new Set();
183218
if (Array.isArray(cube.hierarchies)) {
184-
cube.hierarchies = cube.hierarchies.map(hierarchy => ({
185-
...hierarchy,
186-
levels: this.evaluateReferences(
187-
cube.name, hierarchy.levels, { originalSorting: true }
188-
)
189-
}));
219+
cube.hierarchies = cube.hierarchies.map(hierarchy => {
220+
if (uniqueHierarchyNames.has(hierarchy.name)) {
221+
errorReporter.error(`Duplicate hierarchy name '${hierarchy.name}' in cube '${cube.name}'`);
222+
}
223+
uniqueHierarchyNames.add(hierarchy.name);
224+
225+
return ({
226+
...hierarchy,
227+
levels: this.evaluateReferences(
228+
cube.name,
229+
hierarchy.levels,
230+
{ originalSorting: true }
231+
)
232+
});
233+
});
190234
}
191235

192236
if (cube.isView && (cube.includedMembers || []).length) {
193-
const includedCubeNames: string[] = R.uniq(cube.includedMembers.map(it => it.memberPath.split('.')[0]));
194237
const includedMemberPaths: string[] = R.uniq(cube.includedMembers.map(it => it.memberPath));
195-
196-
if (!cube.hierarchies) {
197-
for (const cubeName of includedCubeNames) {
198-
const { hierarchies } = this.evaluatedCubes[cubeName] || {};
199-
200-
if (Array.isArray(hierarchies) && hierarchies.length) {
201-
const filteredHierarchies = hierarchies.map(it => {
202-
const levels = it.levels.filter(level => includedMemberPaths.includes(level));
238+
const includedCubeNames: string[] = R.uniq(includedMemberPaths.map(it => it.split('.')[0]));
239+
const includedHierarchyNames = cube.includedMembers.filter(it => it.type === 'hierarchies').map(it => it.memberPath.split('.')[1]);
240+
241+
for (const cubeName of includedCubeNames) {
242+
const { hierarchies } = this.evaluatedCubes[cubeName] || {};
243+
244+
if (Array.isArray(hierarchies) && hierarchies.length) {
245+
const filteredHierarchies = hierarchies
246+
.filter(it => includedHierarchyNames.includes(it.name))
247+
.map(it => {
248+
const levels = it.levels.filter(level => {
249+
const member = cube.includedMembers.find(m => m.memberPath === level);
250+
if (member && member.type !== 'dimensions') {
251+
const memberName = level.split('.')[1] || level;
252+
errorReporter.error(`Only dimensions can be part of a hierarchy. Please remove the '${memberName}' member from the '${it.name}' hierarchy.`);
253+
} else if (member) {
254+
return includedMemberPaths.includes(level);
255+
}
256+
257+
return null;
258+
}).filter(Boolean);
203259

204260
return {
205261
...it,
206262
levels
207263
};
208-
}).filter(it => it.levels.length);
264+
})
265+
.filter(it => it.levels.length);
209266

210-
cube.hierarchies = [...(cube.hierarchies || []), ...filteredHierarchies];
211-
}
267+
cube.hierarchies = [...(cube.hierarchies || []), ...filteredHierarchies];
212268
}
213269
}
214270

packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,15 @@ export class CubeSymbols {
226226
return;
227227
}
228228

229-
const types = ['measures', 'dimensions', 'segments'];
229+
const memberSets = {
230+
resolvedMembers: new Set(),
231+
allMembers: new Set(),
232+
};
233+
234+
const types = ['measures', 'dimensions', 'segments', 'hierarchies'];
230235
for (const type of types) {
231-
const cubeIncludes = cube.cubes && this.membersFromCubes(cube, cube.cubes, type, errorReporter, splitViews) || [];
236+
const cubeIncludes = cube.cubes && this.membersFromCubes(cube, cube.cubes, type, errorReporter, splitViews, memberSets) || [];
237+
232238
const includes = cube.includes && this.membersFromIncludeExclude(cube.includes, cube.name, type) || [];
233239
const excludes = cube.excludes && this.membersFromIncludeExclude(cube.excludes, cube.name, type) || [];
234240

@@ -245,18 +251,23 @@ export class CubeSymbols {
245251
const split = it.member.split('.');
246252
const memberPath = this.pathFromArray([split[split.length - 2], split[split.length - 1]]);
247253
return {
254+
type,
248255
memberPath,
249256
name: it.name
250257
};
251258
})))];
252259
}
260+
261+
[...memberSets.allMembers].filter(it => !memberSets.resolvedMembers.has(it)).forEach(it => {
262+
errorReporter.error(`Member '${it}' is included in '${cube.name}' but not defined in any cube`);
263+
});
253264
}
254265

255266
applyIncludeMembers(includeMembers, cube, type, errorReporter) {
256267
for (const [memberName, memberDefinition] of includeMembers) {
257268
if (cube[type]?.[memberName]) {
258269
errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member.`);
259-
} else {
270+
} else if (type !== 'hierarchies') {
260271
cube[type][memberName] = memberDefinition;
261272
}
262273
}
@@ -265,7 +276,7 @@ export class CubeSymbols {
265276
/**
266277
* @protected
267278
*/
268-
membersFromCubes(parentCube, cubes, type, errorReporter, splitViews) {
279+
membersFromCubes(parentCube, cubes, type, errorReporter, splitViews, memberSets) {
269280
return R.unnest(cubes.map(cubeInclude => {
270281
const fullPath = this.evaluateReferences(null, cubeInclude.joinPath, { collectJoinHints: true });
271282
const split = fullPath.split('.');
@@ -277,37 +288,43 @@ export class CubeSymbols {
277288

278289
if (cubeInclude.includes === '*') {
279290
const membersObj = this.symbols[cubeReference]?.cubeObj()?.[type] || {};
280-
includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) }));
291+
if (Array.isArray(membersObj)) {
292+
includes = membersObj.map(it => ({ member: `${fullPath}.${it.name}`, name: fullMemberName(it.name) }));
293+
} else {
294+
includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) }));
295+
}
281296
} else {
282297
includes = cubeInclude.includes.map(include => {
283298
const member = include.alias || include;
284-
if (member.indexOf('.') !== -1) {
299+
300+
if (member.includes('.')) {
285301
errorReporter.error(`Paths aren't allowed in cube includes but '${member}' provided as include member`);
286302
}
287303

288304
const name = fullMemberName(include.alias || member);
289-
if (include.name) {
290-
const resolvedMember = this.symbols[cubeReference]?.cubeObj()?.[type]?.[include.name];
291-
return resolvedMember ? {
292-
member: `${fullPath}.${include.name}`,
293-
name,
294-
} : undefined;
295-
} else {
296-
const resolvedMember = this.symbols[cubeReference]?.cubeObj()?.[type]?.[include];
297-
return resolvedMember ? {
298-
member: `${fullPath}.${include}`,
299-
name
300-
} : undefined;
305+
memberSets.allMembers.add(name);
306+
307+
const includedMemberName = include.name || include;
308+
309+
const resolvedMember = this.getResolvedMember(type, cubeReference, includedMemberName) ? {
310+
member: `${fullPath}.${includedMemberName}`,
311+
name,
312+
} : undefined;
313+
314+
if (resolvedMember) {
315+
memberSets.resolvedMembers.add(name);
301316
}
317+
318+
return resolvedMember;
302319
});
303320
}
304321

305322
const excludes = (cubeInclude.excludes || []).map(exclude => {
306-
if (exclude.indexOf('.') !== -1) {
323+
if (exclude.includes('.')) {
307324
errorReporter.error(`Paths aren't allowed in cube excludes but '${exclude}' provided as exclude member`);
308325
}
309326

310-
const resolvedMember = this.symbols[cubeReference]?.cubeObj()?.[type]?.[exclude];
327+
const resolvedMember = this.getResolvedMember(type, cubeReference, exclude);
311328
return resolvedMember ? {
312329
member: `${fullPath}.${exclude}`
313330
} : undefined;
@@ -356,21 +373,32 @@ export class CubeSymbols {
356373
const membersObj = this.symbols[path[0]]?.cubeObj()?.[type] || {};
357374
return Object.keys(membersObj).map(memberName => ({ member: `${ref}.${memberName}` }));
358375
} else if (path.length === 2) {
359-
const resolvedMember = this.symbols[path[0]]?.cubeObj()?.[type]?.[path[1]];
376+
const resolvedMember = this.getResolvedMember(type, path[0], path[1]);
360377
return resolvedMember ? [{ member: ref }] : undefined;
361378
} else {
362379
throw new Error(`Unexpected path length ${path.length} for ${ref}`);
363380
}
364381
})).filter(Boolean);
365382
}
366383

384+
/**
385+
* @protected
386+
*/
387+
getResolvedMember(type, cubeName, memberName) {
388+
if (Array.isArray(this.symbols[cubeName]?.cubeObj()?.[type])) {
389+
return this.symbols[cubeName]?.cubeObj()?.[type]?.find((it) => it.name === memberName);
390+
}
391+
392+
return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName];
393+
}
394+
367395
/**
368396
* @protected
369397
*/
370398
generateIncludeMembers(members, cubeName, type) {
371399
return members.map(memberRef => {
372400
const path = memberRef.member.split('.');
373-
const resolvedMember = this.symbols[path[path.length - 2]]?.cubeObj()?.[type]?.[path[path.length - 1]];
401+
const resolvedMember = this.getResolvedMember(type, path[path.length - 2], path[path.length - 1]);
374402
if (!resolvedMember) {
375403
throw new Error(`Can't resolve '${memberRef.member}' while generating include members`);
376404
}
@@ -404,6 +432,11 @@ export class CubeSymbols {
404432
meta: resolvedMember.meta,
405433
description: resolvedMember.description,
406434
};
435+
} else if (type === 'hierarchies') {
436+
memberDefinition = {
437+
title: resolvedMember.title,
438+
levels: resolvedMember.levels,
439+
};
407440
} else {
408441
throw new Error(`Unexpected member type: ${type}`);
409442
}
@@ -458,6 +491,7 @@ export class CubeSymbols {
458491
name
459492
);
460493
// eslint-disable-next-line no-underscore-dangle
494+
// if (resolvedSymbol && resolvedSymbol._objectWithResolvedProperties) {
461495
if (resolvedSymbol._objectWithResolvedProperties) {
462496
return resolvedSymbol;
463497
}

packages/cubejs-schema-compiler/src/compiler/CubeToMetaTransformer.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,15 @@ export class CubeToMetaTransformer {
106106
})),
107107
R.toPairs
108108
)(cube.segments || {}),
109-
hierarchies: cube.hierarchies || []
109+
hierarchies: (cube.hierarchies || []).map((it) => ({
110+
...it,
111+
public: it.public ?? true,
112+
name: `${cube.name}.${it.name}`,
113+
})),
114+
folders: (cube.folders || []).map((it) => ({
115+
name: it.name,
116+
members: it.includes.map(member => `${cube.name}.${member.name}`),
117+
})),
110118
},
111119
};
112120
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -745,17 +745,25 @@ const baseSchema = {
745745
)),
746746
segments: SegmentsSchema,
747747
preAggregations: PreAggregationsAlternatives,
748-
hierarchies: Joi.array().items(Joi.object().keys({
748+
folders: Joi.array().items(Joi.object().keys({
749749
name: Joi.string().required(),
750-
title: Joi.string(),
751-
levels: Joi.func()
750+
includes: Joi.alternatives([
751+
Joi.string().valid('*'),
752+
Joi.array().items(Joi.string().required())
753+
]).required(),
752754
})),
753755
accessPolicy: Joi.array().items(RolePolicySchema.required()),
754756
};
755757

756758
const cubeSchema = inherit(baseSchema, {
757759
sql: Joi.func(),
758760
sqlTable: Joi.func(),
761+
hierarchies: Joi.array().items(Joi.object().keys({
762+
name: identifier,
763+
title: Joi.string(),
764+
public: Joi.boolean().strict(),
765+
levels: Joi.func()
766+
})),
759767
}).xor('sql', 'sqlTable').messages({
760768
'object.xor': 'You must use either sql or sqlTable within a model, but not both'
761769
});

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class YamlCompiler {
131131
cubeObj.segments = this.yamlArrayToObj(cubeObj.segments || [], 'segment', errorsReport);
132132
cubeObj.preAggregations = this.yamlArrayToObj(cubeObj.preAggregations || [], 'preAggregation', errorsReport);
133133
cubeObj.joins = this.yamlArrayToObj(cubeObj.joins || [], 'join', errorsReport);
134+
// cubeObj.hierarchies = this.yamlArrayToObj(cubeObj.hierarchies || [], 'hierarchies', errorsReport);
134135

135136
return this.transpileYaml(cubeObj, [], cubeObj.name, errorsReport);
136137
}

packages/cubejs-schema-compiler/test/unit/__snapshots__/views.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Object {
4040
"type": "number",
4141
},
4242
],
43+
"folders": Array [],
4344
"hierarchies": Array [],
4445
"isVisible": true,
4546
"measures": Array [
@@ -118,6 +119,7 @@ Object {
118119
"type": "number",
119120
},
120121
],
122+
"folders": Array [],
121123
"hierarchies": Array [],
122124
"isVisible": true,
123125
"measures": Array [
@@ -230,6 +232,7 @@ Object {
230232
"type": "number",
231233
},
232234
],
235+
"folders": Array [],
233236
"hierarchies": Array [],
234237
"isVisible": true,
235238
"measures": Array [
@@ -308,6 +311,7 @@ Object {
308311
"type": "number",
309312
},
310313
],
314+
"folders": Array [],
311315
"hierarchies": Array [],
312316
"isVisible": true,
313317
"measures": Array [

0 commit comments

Comments
 (0)