Skip to content

Commit 2e7a802

Browse files
authored
chore(schema-compiler): Make cube joins to be an array instead of hashmap (#9800)
* more types in CubeSymbols * change joins schema in Validator * specify type for accessPolicy * make prepareJoins() aware of arrays * make joins in CubeSymbols / CubeEvaluator as array instead of hashmap * update JoinGraph to treat joins as array instead of hashmap * fix CubePropContextTranspiler to convert joins hashmap into array * add @types/js-yaml * make YamlCompiler aware of joins as array * fix/update snapshots for schema tests * fix transform yaml joins * add schema tests for joins as array/object * lint:fix * update snapshots * add a fallback workaround for joins as object in js models
1 parent 692c4ae commit 2e7a802

File tree

10 files changed

+391
-228
lines changed

10 files changed

+391
-228
lines changed

packages/cubejs-schema-compiler/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"@types/babel__traverse": "^7.20.5",
6868
"@types/inflection": "^1.5.28",
6969
"@types/jest": "^29",
70+
"@types/js-yaml": "^4.0.9",
7071
"@types/node": "^20",
7172
"@types/node-dijkstra": "^2.5.6",
7273
"@types/ramda": "^0.27.34",

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

Lines changed: 51 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import R from 'ramda';
33

44
import {
5+
AccessPolicyDefinition,
56
CubeDefinitionExtended,
67
CubeSymbols,
7-
HierarchyDefinition, JoinDefinition,
8-
PreAggregationDefinition, PreAggregationDefinitionRollup,
8+
HierarchyDefinition,
9+
JoinDefinition,
10+
PreAggregationDefinition,
11+
PreAggregationDefinitionRollup,
912
type ToString
1013
} from './CubeSymbols';
1114
import { UserError } from './UserError';
@@ -110,30 +113,6 @@ export type EvaluatedHierarchy = {
110113
[key: string]: any;
111114
};
112115

113-
export type Filter =
114-
| {
115-
member: string;
116-
memberReference?: string;
117-
[key: string]: any;
118-
}
119-
| {
120-
and?: Filter[];
121-
or?: Filter[];
122-
[key: string]: any;
123-
};
124-
125-
export type AccessPolicy = {
126-
rowLevel?: {
127-
filters: Filter[];
128-
};
129-
memberLevel?: {
130-
includes?: string | string[];
131-
excludes?: string | string[];
132-
includesMembers?: string[];
133-
excludesMembers?: string[];
134-
};
135-
};
136-
137116
export type EvaluatedFolder = {
138117
name: string;
139118
includes: (EvaluatedFolder | DimensionDefinition | MeasureDefinition)[];
@@ -145,15 +124,15 @@ export type EvaluatedCube = {
145124
measures: Record<string, MeasureDefinition>;
146125
dimensions: Record<string, DimensionDefinition>;
147126
segments: Record<string, SegmentDefinition>;
148-
joins: Record<string, JoinDefinition>;
127+
joins: JoinDefinition[];
149128
hierarchies: Record<string, HierarchyDefinition>;
150129
evaluatedHierarchies: EvaluatedHierarchy[];
151130
preAggregations: Record<string, PreAggregationDefinitionExtended>;
152131
dataSource?: string;
153132
folders: EvaluatedFolder[];
154133
sql?: (...args: any[]) => string;
155134
sqlTable?: (...args: any[]) => string;
156-
accessPolicy?: AccessPolicy[];
135+
accessPolicy?: AccessPolicyDefinition[];
157136
};
158137

159138
export class CubeEvaluator extends CubeSymbols {
@@ -200,7 +179,7 @@ export class CubeEvaluator extends CubeSymbols {
200179
);
201180
}
202181

203-
protected prepareCube(cube, errorReporter: ErrorReporter) {
182+
protected prepareCube(cube, errorReporter: ErrorReporter): EvaluatedCube {
204183
this.prepareJoins(cube, errorReporter);
205184
this.preparePreAggregations(cube, errorReporter);
206185
this.prepareMembers(cube.measures, cube, errorReporter);
@@ -443,36 +422,55 @@ export class CubeEvaluator extends CubeSymbols {
443422
}
444423
}
445424

446-
protected prepareJoins(cube: any, _errorReporter: ErrorReporter) {
447-
if (cube.joins) {
448-
// eslint-disable-next-line no-restricted-syntax
449-
for (const join of Object.values(cube.joins) as any[]) {
450-
// eslint-disable-next-line default-case
451-
switch (join.relationship) {
452-
case 'belongs_to':
453-
case 'many_to_one':
454-
case 'manyToOne':
455-
join.relationship = 'belongsTo';
456-
break;
457-
case 'has_many':
458-
case 'one_to_many':
459-
case 'oneToMany':
460-
join.relationship = 'hasMany';
461-
break;
462-
case 'has_one':
463-
case 'one_to_one':
464-
case 'oneToOne':
465-
join.relationship = 'hasOne';
466-
break;
467-
}
425+
protected prepareJoins(cube: any, errorReporter: ErrorReporter) {
426+
if (!cube.joins) {
427+
return;
428+
}
429+
430+
const transformRelationship = (relationship: string): string => {
431+
switch (relationship) {
432+
case 'belongs_to':
433+
case 'many_to_one':
434+
case 'manyToOne':
435+
return 'belongsTo';
436+
case 'has_many':
437+
case 'one_to_many':
438+
case 'oneToMany':
439+
return 'hasMany';
440+
case 'has_one':
441+
case 'one_to_one':
442+
case 'oneToOne':
443+
return 'hasOne';
444+
default:
445+
return relationship;
468446
}
447+
};
448+
449+
let joins: JoinDefinition[] = [];
450+
451+
if (Array.isArray(cube.joins)) {
452+
joins = cube.joins.map((join: JoinDefinition) => {
453+
join.relationship = transformRelationship(join.relationship);
454+
return join;
455+
});
456+
} else if (typeof cube.joins === 'object') {
457+
joins = Object.entries(cube.joins).map(([name, join]: [string, any]) => {
458+
join.relationship = transformRelationship(join.relationship);
459+
join.name = name;
460+
return join as JoinDefinition;
461+
});
462+
} else {
463+
errorReporter.error(`Invalid joins definition for cube '${cube.name}': expected an array or an object.`);
469464
}
465+
466+
cube.joins = joins;
470467
}
471468

472469
protected preparePreAggregations(cube: any, errorReporter: ErrorReporter) {
473470
if (cube.preAggregations) {
474471
// eslint-disable-next-line no-restricted-syntax
475472
for (const preAggregation of Object.values(cube.preAggregations) as any) {
473+
// preAggregation is actually (PreAggregationDefinitionRollup | PreAggregationDefinitionOriginalSql)
476474
if (preAggregation.timeDimension) {
477475
preAggregation.timeDimensionReference = preAggregation.timeDimension;
478476
delete preAggregation.timeDimension;
@@ -574,7 +572,7 @@ export class CubeEvaluator extends CubeSymbols {
574572
}
575573
}
576574

577-
public cubesByFileName(fileName) {
575+
public cubesByFileName(fileName): CubeDefinitionExtended[] {
578576
return this.byFileName[fileName] || [];
579577
}
580578

@@ -691,7 +689,7 @@ export class CubeEvaluator extends CubeSymbols {
691689
return this.preAggregations({ scheduled: true });
692690
}
693691

694-
public cubeNames() {
692+
public cubeNames(): string[] {
695693
return Object.keys(this.evaluatedCubes);
696694
}
697695

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

Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,35 @@ export type PreAggregationDefinitionRollup = BasePreAggregationDefinition & {
100100
export type PreAggregationDefinition = PreAggregationDefinitionRollup;
101101

102102
export type JoinDefinition = {
103+
name: string,
103104
relationship: string,
104105
sql: (...args: any[]) => string,
105106
};
106107

108+
export type Filter =
109+
| {
110+
member: string;
111+
memberReference?: string;
112+
[key: string]: any;
113+
}
114+
| {
115+
and?: Filter[];
116+
or?: Filter[];
117+
[key: string]: any;
118+
};
119+
120+
export type AccessPolicyDefinition = {
121+
rowLevel?: {
122+
filters: Filter[];
123+
};
124+
memberLevel?: {
125+
includes?: string | string[];
126+
excludes?: string | string[];
127+
includesMembers?: string[];
128+
excludesMembers?: string[];
129+
};
130+
};
131+
107132
export interface CubeDefinition {
108133
name: string;
109134
extends?: (...args: Array<unknown>) => { __cubeName: string };
@@ -119,8 +144,8 @@ export interface CubeDefinition {
119144
preAggregations?: Record<string, PreAggregationDefinitionRollup | PreAggregationDefinitionOriginalSql>;
120145
// eslint-disable-next-line camelcase
121146
pre_aggregations?: Record<string, PreAggregationDefinitionRollup | PreAggregationDefinitionOriginalSql>;
122-
joins?: Record<string, JoinDefinition>;
123-
accessPolicy?: any[];
147+
joins?: JoinDefinition[];
148+
accessPolicy?: AccessPolicyDefinition[];
124149
// eslint-disable-next-line camelcase
125150
access_policy?: any[];
126151
folders?: any[];
@@ -221,17 +246,17 @@ export class CubeSymbols {
221246
}
222247

223248
public createCube(cubeDefinition: CubeDefinition): CubeDefinitionExtended {
224-
let preAggregations: any;
225-
let joins: any;
226-
let measures: any;
227-
let dimensions: any;
228-
let segments: any;
229-
let hierarchies: any;
230-
let accessPolicy: any;
231-
let folders: any;
232-
let cubes: any;
233-
234-
const cubeObject = Object.assign({
249+
let preAggregations: CubeDefinition['preAggregations'];
250+
let joins: CubeDefinition['joins'];
251+
let measures: CubeDefinition['measures'];
252+
let dimensions: CubeDefinition['dimensions'];
253+
let segments: CubeDefinition['segments'];
254+
let hierarchies: CubeDefinition['hierarchies'];
255+
let accessPolicy: CubeDefinition['accessPolicy'];
256+
let folders: CubeDefinition['folders'];
257+
let cubes: CubeDefinition['cubes'];
258+
259+
const cubeObject: CubeDefinitionExtended = Object.assign({
235260
allDefinitions(type: string) {
236261
if (cubeDefinition.extends) {
237262
return {
@@ -297,7 +322,8 @@ export class CubeSymbols {
297322

298323
get joins() {
299324
if (!joins) {
300-
joins = this.allDefinitions('joins');
325+
const parentJoins = cubeDefinition.extends ? super.joins : [];
326+
joins = [...parentJoins, ...(cubeDefinition.joins || [])];
301327
}
302328
return joins;
303329
},
@@ -383,9 +409,10 @@ export class CubeSymbols {
383409
return cubeObject;
384410
}
385411

386-
protected transform(cubeName: string, errorReporter: ErrorReporter, splitViews: SplitViews) {
412+
protected transform(cubeName: string, errorReporter: ErrorReporter, splitViews: SplitViews): CubeSymbolsDefinition {
387413
const cube = this.getCubeDefinition(cubeName);
388-
const duplicateNames = R.compose(
414+
// @ts-ignore
415+
const duplicateNames: string[] = R.compose(
389416
R.map((nameToDefinitions: any) => nameToDefinitions[0]),
390417
R.toPairs,
391418
R.filter((definitionsByName: any) => definitionsByName.length > 1),
@@ -397,9 +424,7 @@ export class CubeSymbols {
397424
// @ts-ignore
398425
)([cube.measures, cube.dimensions, cube.segments, cube.preAggregations, cube.hierarchies]);
399426

400-
// @ts-ignore
401427
if (duplicateNames.length > 0) {
402-
// @ts-ignore
403428
errorReporter.error(`${duplicateNames.join(', ')} defined more than once`);
404429
}
405430

@@ -427,23 +452,24 @@ export class CubeSymbols {
427452
...cube.dimensions || {},
428453
...cube.segments || {},
429454
...cube.preAggregations || {}
430-
};
455+
} as CubeSymbolsDefinition;
431456
}
432457

433-
private camelCaseTypes(obj: Object | undefined) {
458+
private camelCaseTypes(obj: Object | Array<any> | undefined) {
434459
if (!obj) {
435460
return;
436461
}
437462

438-
// eslint-disable-next-line no-restricted-syntax
439-
for (const member of Object.values(obj)) {
463+
const members = Array.isArray(obj) ? obj : Object.values(obj);
464+
465+
members.forEach(member => {
440466
if (member.type && member.type.indexOf('_') !== -1) {
441467
member.type = camelize(member.type, true);
442468
}
443469
if (member.relationship && member.relationship.indexOf('_') !== -1) {
444470
member.relationship = camelize(member.relationship, true);
445471
}
446-
}
472+
});
447473
}
448474

449475
protected transformPreAggregations(preAggregations: Object) {
@@ -548,7 +574,7 @@ export class CubeSymbols {
548574
}
549575
}
550576

551-
const includeMembers = this.generateIncludeMembers(cubeIncludes, cube.name, type);
577+
const includeMembers = this.generateIncludeMembers(cubeIncludes, type);
552578
this.applyIncludeMembers(includeMembers, cube, type, errorReporter);
553579

554580
const existing = cube.includedMembers ?? [];
@@ -699,11 +725,7 @@ export class CubeSymbols {
699725
splitViewDef = splitViews[viewName];
700726
}
701727

702-
const includeMembers = this.generateIncludeMembers(
703-
finalIncludes,
704-
parentCube.name,
705-
type
706-
);
728+
const includeMembers = this.generateIncludeMembers(finalIncludes, type);
707729
this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter);
708730
} else {
709731
for (const member of finalIncludes) {
@@ -733,7 +755,7 @@ export class CubeSymbols {
733755
return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName];
734756
}
735757

736-
protected generateIncludeMembers(members: any[], cubeName: string, type: string) {
758+
protected generateIncludeMembers(members: any[], type: string) {
737759
return members.map(memberRef => {
738760
const path = memberRef.member.split('.');
739761
const resolvedMember = this.getResolvedMember(type, path[path.length - 2], path[path.length - 1]);
@@ -870,9 +892,8 @@ export class CubeSymbols {
870892

871893
/**
872894
* Split join path to member to join hint and member path: `A.B.C.D.E.dim` => `[A, B, C, D, E]` + `E.dim`
873-
* @param path
874895
*/
875-
public static joinHintFromPath(path: string): { path: string, joinHint: Array<string> } {
896+
public static joinHintFromPath(path: string): { path: string, joinHint: string[] } {
876897
const parts = path.split('.');
877898
if (parts.length > 2) {
878899
// Path contains join path
@@ -908,7 +929,7 @@ export class CubeSymbols {
908929
}
909930
}
910931

911-
protected withSymbolsCallContext(func, context) {
932+
protected withSymbolsCallContext(func: Function, context) {
912933
const oldContext = this.resolveSymbolsCallContext;
913934
this.resolveSymbolsCallContext = context;
914935
try {
@@ -933,7 +954,7 @@ export class CubeSymbols {
933954
return this.funcArgumentsValues[funcDefinition];
934955
}
935956

936-
protected joinHints() {
957+
protected joinHints(): string | string[] | undefined {
937958
const { joinHints } = this.resolveSymbolsCallContext || {};
938959
if (Array.isArray(joinHints)) {
939960
return R.uniq(joinHints);
@@ -1014,7 +1035,7 @@ export class CubeSymbols {
10141035
return (...filterParamArgs) => '';
10151036
}
10161037

1017-
public resolveSymbol(cubeName, name) {
1038+
public resolveSymbol(cubeName, name: string) {
10181039
const { sqlResolveFn, contextSymbols, collectJoinHints, depsResolveFn, currResolveIndexFn } = this.resolveSymbolsCallContext || {};
10191040
if (name === 'USER_CONTEXT') {
10201041
throw new Error('Support for USER_CONTEXT was removed, please migrate to SECURITY_CONTEXT.');

0 commit comments

Comments
 (0)