Skip to content

Commit 04f6e18

Browse files
KSDaemonmarianore-muttdata
authored andcommitted
feat(schema-compiler): Support overriding title, description, meta, and format on view members (cube-js#9496)
* allow overrides in views validator * allow objects in view's includes without aliases * feat(schema-compiler): Support overriding `title`, `description`, `meta`, and `format` on view members * code polish * add tests * code polish
1 parent 9e4a2df commit 04f6e18

File tree

6 files changed

+331
-51
lines changed

6 files changed

+331
-51
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,9 @@ export class CubeEvaluator extends CubeSymbols {
257257
}
258258

259259
private prepareHierarchies(cube: any, errorReporter: ErrorReporter): void {
260-
if (Object.keys(cube.hierarchies).length) {
260+
// Hierarchies from views are not fully populated at this moment and are processed later,
261+
// so we should not pollute the cube hierarchies definition here.
262+
if (!cube.isView && Object.keys(cube.hierarchies).length) {
261263
cube.evaluatedHierarchies = Object.entries(cube.hierarchies).map(([name, hierarchy]) => ({
262264
name,
263265
...(typeof hierarchy === 'object' ? hierarchy : {}),
@@ -306,6 +308,8 @@ export class CubeEvaluator extends CubeSymbols {
306308
throw new UserError(`Hierarchy '${it.name}' not found in cube '${cubeName}'`);
307309
}
308310
return {
311+
// Title might be overridden in the view
312+
title: cube.hierarchies?.[it.name]?.override?.title || it.title,
309313
...it,
310314
name,
311315
levels

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

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class CubeSymbols {
6363

6464
public cubeList: any[];
6565

66-
private evaluateViews: boolean;
66+
private readonly evaluateViews: boolean;
6767

6868
private resolveSymbolsCallContext: any;
6969

@@ -147,7 +147,7 @@ export class CubeSymbols {
147147
}
148148
return preAggregations;
149149
},
150-
set preAggregations(v) {
150+
set preAggregations(_v) {
151151
// Dont allow to modify
152152
},
153153

@@ -157,7 +157,7 @@ export class CubeSymbols {
157157
}
158158
return joins;
159159
},
160-
set joins(v) {
160+
set joins(_v) {
161161
// Dont allow to modify
162162
},
163163

@@ -167,7 +167,7 @@ export class CubeSymbols {
167167
}
168168
return measures;
169169
},
170-
set measures(v) {
170+
set measures(_v) {
171171
// Dont allow to modify
172172
},
173173

@@ -177,7 +177,7 @@ export class CubeSymbols {
177177
}
178178
return dimensions;
179179
},
180-
set dimensions(v) {
180+
set dimensions(_v) {
181181
// Dont allow to modify
182182
},
183183

@@ -187,7 +187,7 @@ export class CubeSymbols {
187187
}
188188
return segments;
189189
},
190-
set segments(v) {
190+
set segments(_v) {
191191
// Dont allow to modify
192192
},
193193

@@ -197,7 +197,7 @@ export class CubeSymbols {
197197
}
198198
return hierarchies;
199199
},
200-
set hierarchies(v) {
200+
set hierarchies(_v) {
201201
// Dont allow to modify
202202
},
203203

@@ -213,7 +213,7 @@ export class CubeSymbols {
213213
return undefined;
214214
}
215215
},
216-
set accessPolicy(v) {
216+
set accessPolicy(_v) {
217217
// Dont allow to modify
218218
}
219219
},
@@ -275,13 +275,14 @@ export class CubeSymbols {
275275
this.prepareIncludes(cube, errorReporter, splitViews);
276276
}
277277

278-
return Object.assign(
279-
{ cubeName: () => cube.name, cubeObj: () => cube },
280-
cube.measures || {},
281-
cube.dimensions || {},
282-
cube.segments || {},
283-
cube.preAggregations || {}
284-
);
278+
return {
279+
cubeName: () => cube.name,
280+
cubeObj: () => cube,
281+
...cube.measures || {},
282+
...cube.dimensions || {},
283+
...cube.segments || {},
284+
...cube.preAggregations || {}
285+
};
285286
}
286287

287288
private camelCaseTypes(obj: Object) {
@@ -425,7 +426,7 @@ export class CubeSymbols {
425426
for (const [memberName, memberDefinition] of includeMembers) {
426427
if (cube[type]?.[memberName]) {
427428
errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member or assigning it an alias.`);
428-
} else if (type !== 'hierarchies') {
429+
} else {
429430
cube[type][memberName] = memberDefinition;
430431
}
431432
}
@@ -439,28 +440,36 @@ export class CubeSymbols {
439440
const cubeReference = split[split.length - 1];
440441
const cubeName = cubeInclude.alias || cubeReference;
441442

442-
let includes;
443+
let includes: any[];
443444
const fullMemberName = (memberName: string) => (cubeInclude.prefix ? `${cubeName}_${memberName}` : memberName);
444445

445446
if (cubeInclude.includes === '*') {
446447
const membersObj = this.symbols[cubeReference]?.cubeObj()?.[type] || {};
447448
includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) }));
448449
} else {
449450
includes = cubeInclude.includes.map((include: any) => {
450-
const member = include.alias || include;
451+
const member = include.alias || include.name || include;
451452

452453
if (member.includes('.')) {
453454
errorReporter.error(`Paths aren't allowed in cube includes but '${member}' provided as include member`);
454455
}
455456

456-
const name = fullMemberName(include.alias || member);
457+
const name = fullMemberName(member);
457458
memberSets.allMembers.add(name);
458459

459460
const includedMemberName = include.name || include;
460461

461462
const resolvedMember = this.getResolvedMember(type, cubeReference, includedMemberName) ? {
462463
member: `${fullPath}.${includedMemberName}`,
463464
name,
465+
...(include.title || include.description || include.format || include.meta) ? {
466+
override: {
467+
title: include.title,
468+
description: include.description,
469+
format: include.format,
470+
meta: include.meta,
471+
}
472+
} : {}
464473
} : undefined;
465474

466475
if (resolvedMember) {
@@ -537,10 +546,10 @@ export class CubeSymbols {
537546
sql,
538547
type: CubeSymbols.toMemberDataType(resolvedMember.type),
539548
aggType: resolvedMember.type,
540-
meta: resolvedMember.meta,
541-
title: resolvedMember.title,
542-
description: resolvedMember.description,
543-
format: resolvedMember.format,
549+
meta: memberRef.override?.meta || resolvedMember.meta,
550+
title: memberRef.override?.title || resolvedMember.title,
551+
description: memberRef.override?.description || resolvedMember.description,
552+
format: memberRef.override?.format || resolvedMember.format,
544553
...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }),
545554
...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }),
546555
...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }),
@@ -549,24 +558,24 @@ export class CubeSymbols {
549558
memberDefinition = {
550559
sql,
551560
type: resolvedMember.type,
552-
meta: resolvedMember.meta,
553-
title: resolvedMember.title,
554-
description: resolvedMember.description,
555-
format: resolvedMember.format,
561+
meta: memberRef.override?.meta || resolvedMember.meta,
562+
title: memberRef.override?.title || resolvedMember.title,
563+
description: memberRef.override?.description || resolvedMember.description,
564+
format: memberRef.override?.format || resolvedMember.format,
556565
...(resolvedMember.granularities ? { granularities: resolvedMember.granularities } : {}),
557566
...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }),
558567
};
559568
} else if (type === 'segments') {
560569
memberDefinition = {
561570
sql,
562-
meta: resolvedMember.meta,
563-
title: resolvedMember.title,
564-
description: resolvedMember.description,
571+
meta: memberRef.override?.meta || resolvedMember.meta,
572+
description: memberRef.override?.description || resolvedMember.description,
573+
title: memberRef.override?.title || resolvedMember.title,
565574
aliases: resolvedMember.aliases,
566575
};
567576
} else if (type === 'hierarchies') {
568577
memberDefinition = {
569-
title: resolvedMember.title,
578+
title: memberRef.override?.title || resolvedMember.title,
570579
levels: resolvedMember.levels,
571580
};
572581
} else {
@@ -578,7 +587,7 @@ export class CubeSymbols {
578587

579588
/**
580589
* This method is mainly used for evaluating RLS conditions and filters.
581-
* It allows referencing security_context (lowecase) in dynamic conditions or filter values.
590+
* It allows referencing security_context (lowercase) in dynamic conditions or filter values.
582591
*
583592
* It currently does not support async calls because inner resolveSymbol and
584593
* resolveSymbolsCall are sync. Async support may be added later with deeper
@@ -587,7 +596,7 @@ export class CubeSymbols {
587596
protected evaluateContextFunction(cube: any, contextFn: any, context: any = {}) {
588597
const cubeEvaluator = this;
589598

590-
const res = cubeEvaluator.resolveSymbolsCall(contextFn, (name: string) => {
599+
return cubeEvaluator.resolveSymbolsCall(contextFn, (name: string) => {
591600
const resolvedSymbol = this.resolveSymbol(cube, name);
592601
if (resolvedSymbol) {
593602
return resolvedSymbol;
@@ -600,8 +609,6 @@ export class CubeSymbols {
600609
securityContext: context.securityContext,
601610
}
602611
});
603-
604-
return res;
605612
}
606613

607614
protected evaluateReferences<T extends ToString | Array<ToString>>(
@@ -757,15 +764,14 @@ export class CubeSymbols {
757764
}
758765

759766
protected depsContextSymbols() {
760-
return Object.assign({
767+
return {
761768
filterParams: this.filtersProxyDep(),
762769
filterGroup: this.filterGroupFunctionDep(),
763770
securityContext: CubeSymbols.contextSymbolsProxyFrom({}, (param) => param),
764771
sqlUtils: {
765772
convertTz: (f) => f
766-
767773
},
768-
});
774+
};
769775
}
770776

771777
protected filtersProxyDep() {
@@ -810,7 +816,7 @@ export class CubeSymbols {
810816

811817
if (CONTEXT_SYMBOLS[name]) {
812818
// always resolves if contextSymbols aren't passed for transpile step
813-
const symbol = contextSymbols && contextSymbols[CONTEXT_SYMBOLS[name]] || {};
819+
const symbol = contextSymbols?.[CONTEXT_SYMBOLS[name]] || {};
814820
// eslint-disable-next-line no-underscore-dangle
815821
symbol._objectWithResolvedProperties = true;
816822
return symbol;
@@ -842,12 +848,12 @@ export class CubeSymbols {
842848
const parentIndex = currResolveIndexFn();
843849
cube = this.cubeDependenciesProxy(parentIndex, newCubeName);
844850
return cube;
845-
} else if (this.symbols[cubeName] && this.symbols[cubeName][name] && this.symbols[cubeName][name].type === 'time') {
851+
} else if (this.symbols[cubeName]?.[name] && this.symbols[cubeName][name].type === 'time') {
846852
const parentIndex = currResolveIndexFn();
847853
return this.timeDimDependenciesProxy(parentIndex);
848854
}
849855
}
850-
return cube || (this.symbols[cubeName] && this.symbols[cubeName][name]);
856+
return cube || this.symbols[cubeName]?.[name];
851857
}
852858

853859
protected cubeReferenceProxy(cubeName, joinHints?: any[], refProperty?: any) {
@@ -937,7 +943,7 @@ export class CubeSymbols {
937943
return { interval: `1 ${granName}` };
938944
}
939945

940-
return cube && cube[dimName] && cube[dimName][gr] && cube[dimName][gr][granName];
946+
return cube?.[dimName]?.[gr]?.[granName];
941947
}
942948

943949
protected cubeDependenciesProxy(parentIndex, cubeName) {

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ const GranularityInterval = Joi.string().pattern(/^\d+\s+(second|minute|hour|day
110110
// Do not allow negative intervals for granularities, while offsets could be negative
111111
const GranularityOffset = Joi.string().pattern(/^-?(\d+\s+)(second|minute|hour|day|week|month|quarter|year)s?(\s-?\d+\s+(second|minute|hour|day|week|month|quarter|year)s?){0,7}$/, 'granularity offset');
112112

113+
const formatSchema = Joi.alternatives([
114+
Joi.string().valid('imageUrl', 'link', 'currency', 'percent', 'number', 'id'),
115+
Joi.object().keys({
116+
type: Joi.string().valid('link'),
117+
label: Joi.string().required()
118+
})
119+
]);
120+
113121
const BaseDimensionWithoutSubQuery = {
114122
aliases: Joi.array().items(Joi.string()),
115123
type: Joi.any().valid('string', 'number', 'boolean', 'time', 'geo').required(),
@@ -122,13 +130,7 @@ const BaseDimensionWithoutSubQuery = {
122130
description: Joi.string(),
123131
suggestFilterValues: Joi.boolean().strict(),
124132
enableSuggestions: Joi.boolean().strict(),
125-
format: Joi.alternatives([
126-
Joi.string().valid('imageUrl', 'link', 'currency', 'percent', 'number', 'id'),
127-
Joi.object().keys({
128-
type: Joi.string().valid('link'),
129-
label: Joi.string().required()
130-
})
131-
]),
133+
format: formatSchema,
132134
meta: Joi.any(),
133135
granularities: Joi.when('type', {
134136
is: 'time',
@@ -796,7 +798,11 @@ const viewSchema = inherit(baseSchema, {
796798
Joi.string().required(),
797799
Joi.object().keys({
798800
name: identifier.required(),
799-
alias: identifier
801+
alias: identifier,
802+
title: Joi.string(),
803+
description: Joi.string(),
804+
format: formatSchema,
805+
meta: Joi.any(),
800806
})
801807
]))
802808
]).required(),

0 commit comments

Comments
 (0)