diff --git a/packages/cubejs-schema-compiler/package.json b/packages/cubejs-schema-compiler/package.json index 5a461ae92da7f..049d63c3b8a1f 100644 --- a/packages/cubejs-schema-compiler/package.json +++ b/packages/cubejs-schema-compiler/package.json @@ -67,6 +67,7 @@ "@types/babel__traverse": "^7.20.5", "@types/inflection": "^1.5.28", "@types/jest": "^29", + "@types/js-yaml": "^4.0.9", "@types/node": "^20", "@types/node-dijkstra": "^2.5.6", "@types/ramda": "^0.27.34", diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts index 3d2e2e3a993ea..49a5c29bf3c2d 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts @@ -2,10 +2,13 @@ import R from 'ramda'; import { + AccessPolicyDefinition, CubeDefinitionExtended, CubeSymbols, - HierarchyDefinition, JoinDefinition, - PreAggregationDefinition, PreAggregationDefinitionRollup, + HierarchyDefinition, + JoinDefinition, + PreAggregationDefinition, + PreAggregationDefinitionRollup, type ToString } from './CubeSymbols'; import { UserError } from './UserError'; @@ -110,30 +113,6 @@ export type EvaluatedHierarchy = { [key: string]: any; }; -export type Filter = - | { - member: string; - memberReference?: string; - [key: string]: any; - } - | { - and?: Filter[]; - or?: Filter[]; - [key: string]: any; - }; - -export type AccessPolicy = { - rowLevel?: { - filters: Filter[]; - }; - memberLevel?: { - includes?: string | string[]; - excludes?: string | string[]; - includesMembers?: string[]; - excludesMembers?: string[]; - }; -}; - export type EvaluatedFolder = { name: string; includes: (EvaluatedFolder | DimensionDefinition | MeasureDefinition)[]; @@ -145,7 +124,7 @@ export type EvaluatedCube = { measures: Record; dimensions: Record; segments: Record; - joins: Record; + joins: JoinDefinition[]; hierarchies: Record; evaluatedHierarchies: EvaluatedHierarchy[]; preAggregations: Record; @@ -153,7 +132,7 @@ export type EvaluatedCube = { folders: EvaluatedFolder[]; sql?: (...args: any[]) => string; sqlTable?: (...args: any[]) => string; - accessPolicy?: AccessPolicy[]; + accessPolicy?: AccessPolicyDefinition[]; }; export class CubeEvaluator extends CubeSymbols { @@ -200,7 +179,7 @@ export class CubeEvaluator extends CubeSymbols { ); } - protected prepareCube(cube, errorReporter: ErrorReporter) { + protected prepareCube(cube, errorReporter: ErrorReporter): EvaluatedCube { this.prepareJoins(cube, errorReporter); this.preparePreAggregations(cube, errorReporter); this.prepareMembers(cube.measures, cube, errorReporter); @@ -443,36 +422,55 @@ export class CubeEvaluator extends CubeSymbols { } } - protected prepareJoins(cube: any, _errorReporter: ErrorReporter) { - if (cube.joins) { - // eslint-disable-next-line no-restricted-syntax - for (const join of Object.values(cube.joins) as any[]) { - // eslint-disable-next-line default-case - switch (join.relationship) { - case 'belongs_to': - case 'many_to_one': - case 'manyToOne': - join.relationship = 'belongsTo'; - break; - case 'has_many': - case 'one_to_many': - case 'oneToMany': - join.relationship = 'hasMany'; - break; - case 'has_one': - case 'one_to_one': - case 'oneToOne': - join.relationship = 'hasOne'; - break; - } + protected prepareJoins(cube: any, errorReporter: ErrorReporter) { + if (!cube.joins) { + return; + } + + const transformRelationship = (relationship: string): string => { + switch (relationship) { + case 'belongs_to': + case 'many_to_one': + case 'manyToOne': + return 'belongsTo'; + case 'has_many': + case 'one_to_many': + case 'oneToMany': + return 'hasMany'; + case 'has_one': + case 'one_to_one': + case 'oneToOne': + return 'hasOne'; + default: + return relationship; } + }; + + let joins: JoinDefinition[] = []; + + if (Array.isArray(cube.joins)) { + joins = cube.joins.map((join: JoinDefinition) => { + join.relationship = transformRelationship(join.relationship); + return join; + }); + } else if (typeof cube.joins === 'object') { + joins = Object.entries(cube.joins).map(([name, join]: [string, any]) => { + join.relationship = transformRelationship(join.relationship); + join.name = name; + return join as JoinDefinition; + }); + } else { + errorReporter.error(`Invalid joins definition for cube '${cube.name}': expected an array or an object.`); } + + cube.joins = joins; } protected preparePreAggregations(cube: any, errorReporter: ErrorReporter) { if (cube.preAggregations) { // eslint-disable-next-line no-restricted-syntax for (const preAggregation of Object.values(cube.preAggregations) as any) { + // preAggregation is actually (PreAggregationDefinitionRollup | PreAggregationDefinitionOriginalSql) if (preAggregation.timeDimension) { preAggregation.timeDimensionReference = preAggregation.timeDimension; delete preAggregation.timeDimension; @@ -574,7 +572,7 @@ export class CubeEvaluator extends CubeSymbols { } } - public cubesByFileName(fileName) { + public cubesByFileName(fileName): CubeDefinitionExtended[] { return this.byFileName[fileName] || []; } @@ -691,7 +689,7 @@ export class CubeEvaluator extends CubeSymbols { return this.preAggregations({ scheduled: true }); } - public cubeNames() { + public cubeNames(): string[] { return Object.keys(this.evaluatedCubes); } diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 1f39dc1ce7904..b730608b790a6 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -100,10 +100,35 @@ export type PreAggregationDefinitionRollup = BasePreAggregationDefinition & { export type PreAggregationDefinition = PreAggregationDefinitionRollup; export type JoinDefinition = { + name: string, relationship: string, sql: (...args: any[]) => string, }; +export type Filter = + | { + member: string; + memberReference?: string; + [key: string]: any; + } + | { + and?: Filter[]; + or?: Filter[]; + [key: string]: any; + }; + +export type AccessPolicyDefinition = { + rowLevel?: { + filters: Filter[]; + }; + memberLevel?: { + includes?: string | string[]; + excludes?: string | string[]; + includesMembers?: string[]; + excludesMembers?: string[]; + }; +}; + export interface CubeDefinition { name: string; extends?: (...args: Array) => { __cubeName: string }; @@ -119,8 +144,8 @@ export interface CubeDefinition { preAggregations?: Record; // eslint-disable-next-line camelcase pre_aggregations?: Record; - joins?: Record; - accessPolicy?: any[]; + joins?: JoinDefinition[]; + accessPolicy?: AccessPolicyDefinition[]; // eslint-disable-next-line camelcase access_policy?: any[]; folders?: any[]; @@ -221,17 +246,17 @@ export class CubeSymbols { } public createCube(cubeDefinition: CubeDefinition): CubeDefinitionExtended { - let preAggregations: any; - let joins: any; - let measures: any; - let dimensions: any; - let segments: any; - let hierarchies: any; - let accessPolicy: any; - let folders: any; - let cubes: any; - - const cubeObject = Object.assign({ + let preAggregations: CubeDefinition['preAggregations']; + let joins: CubeDefinition['joins']; + let measures: CubeDefinition['measures']; + let dimensions: CubeDefinition['dimensions']; + let segments: CubeDefinition['segments']; + let hierarchies: CubeDefinition['hierarchies']; + let accessPolicy: CubeDefinition['accessPolicy']; + let folders: CubeDefinition['folders']; + let cubes: CubeDefinition['cubes']; + + const cubeObject: CubeDefinitionExtended = Object.assign({ allDefinitions(type: string) { if (cubeDefinition.extends) { return { @@ -297,7 +322,8 @@ export class CubeSymbols { get joins() { if (!joins) { - joins = this.allDefinitions('joins'); + const parentJoins = cubeDefinition.extends ? super.joins : []; + joins = [...parentJoins, ...(cubeDefinition.joins || [])]; } return joins; }, @@ -383,9 +409,10 @@ export class CubeSymbols { return cubeObject; } - protected transform(cubeName: string, errorReporter: ErrorReporter, splitViews: SplitViews) { + protected transform(cubeName: string, errorReporter: ErrorReporter, splitViews: SplitViews): CubeSymbolsDefinition { const cube = this.getCubeDefinition(cubeName); - const duplicateNames = R.compose( + // @ts-ignore + const duplicateNames: string[] = R.compose( R.map((nameToDefinitions: any) => nameToDefinitions[0]), R.toPairs, R.filter((definitionsByName: any) => definitionsByName.length > 1), @@ -397,9 +424,7 @@ export class CubeSymbols { // @ts-ignore )([cube.measures, cube.dimensions, cube.segments, cube.preAggregations, cube.hierarchies]); - // @ts-ignore if (duplicateNames.length > 0) { - // @ts-ignore errorReporter.error(`${duplicateNames.join(', ')} defined more than once`); } @@ -427,23 +452,24 @@ export class CubeSymbols { ...cube.dimensions || {}, ...cube.segments || {}, ...cube.preAggregations || {} - }; + } as CubeSymbolsDefinition; } - private camelCaseTypes(obj: Object | undefined) { + private camelCaseTypes(obj: Object | Array | undefined) { if (!obj) { return; } - // eslint-disable-next-line no-restricted-syntax - for (const member of Object.values(obj)) { + const members = Array.isArray(obj) ? obj : Object.values(obj); + + members.forEach(member => { if (member.type && member.type.indexOf('_') !== -1) { member.type = camelize(member.type, true); } if (member.relationship && member.relationship.indexOf('_') !== -1) { member.relationship = camelize(member.relationship, true); } - } + }); } protected transformPreAggregations(preAggregations: Object) { @@ -548,7 +574,7 @@ export class CubeSymbols { } } - const includeMembers = this.generateIncludeMembers(cubeIncludes, cube.name, type); + const includeMembers = this.generateIncludeMembers(cubeIncludes, type); this.applyIncludeMembers(includeMembers, cube, type, errorReporter); const existing = cube.includedMembers ?? []; @@ -699,11 +725,7 @@ export class CubeSymbols { splitViewDef = splitViews[viewName]; } - const includeMembers = this.generateIncludeMembers( - finalIncludes, - parentCube.name, - type - ); + const includeMembers = this.generateIncludeMembers(finalIncludes, type); this.applyIncludeMembers(includeMembers, splitViewDef, type, errorReporter); } else { for (const member of finalIncludes) { @@ -733,7 +755,7 @@ export class CubeSymbols { return this.symbols[cubeName]?.cubeObj()?.[type]?.[memberName]; } - protected generateIncludeMembers(members: any[], cubeName: string, type: string) { + protected generateIncludeMembers(members: any[], type: string) { return members.map(memberRef => { const path = memberRef.member.split('.'); const resolvedMember = this.getResolvedMember(type, path[path.length - 2], path[path.length - 1]); @@ -870,9 +892,8 @@ export class CubeSymbols { /** * Split join path to member to join hint and member path: `A.B.C.D.E.dim` => `[A, B, C, D, E]` + `E.dim` - * @param path */ - public static joinHintFromPath(path: string): { path: string, joinHint: Array } { + public static joinHintFromPath(path: string): { path: string, joinHint: string[] } { const parts = path.split('.'); if (parts.length > 2) { // Path contains join path @@ -908,7 +929,7 @@ export class CubeSymbols { } } - protected withSymbolsCallContext(func, context) { + protected withSymbolsCallContext(func: Function, context) { const oldContext = this.resolveSymbolsCallContext; this.resolveSymbolsCallContext = context; try { @@ -933,7 +954,7 @@ export class CubeSymbols { return this.funcArgumentsValues[funcDefinition]; } - protected joinHints() { + protected joinHints(): string | string[] | undefined { const { joinHints } = this.resolveSymbolsCallContext || {}; if (Array.isArray(joinHints)) { return R.uniq(joinHints); @@ -1014,7 +1035,7 @@ export class CubeSymbols { return (...filterParamArgs) => ''; } - public resolveSymbol(cubeName, name) { + public resolveSymbol(cubeName, name: string) { const { sqlResolveFn, contextSymbols, collectJoinHints, depsResolveFn, currResolveIndexFn } = this.resolveSymbolsCallContext || {}; if (name === 'USER_CONTEXT') { throw new Error('Support for USER_CONTEXT was removed, please migrate to SECURITY_CONTEXT.'); diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 32540d4864ac6..a8b99fd77284e 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -802,14 +802,25 @@ const baseSchema = { shown: Joi.boolean().strict(), public: Joi.boolean().strict(), meta: Joi.any(), - joins: Joi.object().pattern(identifierRegex, Joi.object().keys({ - sql: Joi.func().required(), - relationship: Joi.any().valid( - 'belongsTo', 'belongs_to', 'many_to_one', 'manyToOne', - 'hasMany', 'has_many', 'one_to_many', 'oneToMany', - 'hasOne', 'has_one', 'one_to_one', 'oneToOne' - ).required() - })), + joins: Joi.alternatives([ + Joi.object().pattern(identifierRegex, Joi.object().keys({ + sql: Joi.func().required(), + relationship: Joi.any().valid( + 'belongsTo', 'belongs_to', 'many_to_one', 'manyToOne', + 'hasMany', 'has_many', 'one_to_many', 'oneToMany', + 'hasOne', 'has_one', 'one_to_one', 'oneToOne' + ).required() + })), + Joi.array().items(Joi.object().keys({ + name: identifier.required(), + sql: Joi.func().required(), + relationship: Joi.any().valid( + 'belongsTo', 'belongs_to', 'many_to_one', 'manyToOne', + 'hasMany', 'has_many', 'one_to_many', 'oneToMany', + 'hasOne', 'has_one', 'one_to_one', 'oneToOne' + ).required() + })) + ]), measures: MeasuresSchema, dimensions: DimensionsSchema, segments: SegmentsSchema, diff --git a/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts b/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts index 179450a0cfed9..39b9acc3296e7 100644 --- a/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts +++ b/packages/cubejs-schema-compiler/src/compiler/JoinGraph.ts @@ -109,70 +109,64 @@ export class JoinGraph { } protected buildJoinEdges(cube: CubeDefinition, errorReporter: ErrorReporter): Array<[string, JoinEdge]> { - // @ts-ignore - return R.compose( - // @ts-ignore - R.filter(R.identity), - R.map((join: [string, JoinEdge]) => { - const multipliedMeasures: ((m: Record) => MeasureDefinition[]) = R.compose( - R.filter( - (m: MeasureDefinition): boolean => m.sql && this.cubeEvaluator.funcArguments(m.sql).length === 0 && m.sql() === 'count(*)' || - ['sum', 'avg', 'count', 'number'].indexOf(m.type) !== -1 - ), - R.values as (input: Record) => MeasureDefinition[] - ); - const joinRequired = - (v) => `primary key for '${v}' is required when join is defined in order to make aggregates work properly`; - if ( - !this.cubeEvaluator.primaryKeys[join[1].from].length && - multipliedMeasures(this.cubeEvaluator.measuresForCube(join[1].from)).length > 0 - ) { - errorReporter.error(joinRequired(join[1].from)); - return null; + if (!cube.joins) { + return []; + } + + const getMultipliedMeasures = (cubeName: string): MeasureDefinition[] => { + const measures = this.cubeEvaluator.measuresForCube(cubeName); + return Object.values(measures).filter((m: MeasureDefinition) => (m.sql && + this.cubeEvaluator.funcArguments(m.sql).length === 0 && + m.sql() === 'count(*)') || + ['sum', 'avg', 'count', 'number'].includes(m.type)); + }; + + const joinRequired = + (v) => `primary key for '${v}' is required when join is defined in order to make aggregates work properly`; + + return cube.joins + .filter(join => { + if (!this.cubeEvaluator.cubeExists(join.name)) { + errorReporter.error(`Cube ${join.name} doesn't exist`); + return false; } - if (!this.cubeEvaluator.primaryKeys[join[1].to].length && - multipliedMeasures(this.cubeEvaluator.measuresForCube(join[1].to)).length > 0) { - errorReporter.error(joinRequired(join[1].to)); - return null; + + const fromMultipliedMeasures = getMultipliedMeasures(cube.name); + if (!this.cubeEvaluator.primaryKeys[cube.name].length && fromMultipliedMeasures.length > 0) { + errorReporter.error(joinRequired(cube.name)); + return false; } - return join; - }), - R.unnest, - R.map((join: [string, JoinDefinition]): [[string, JoinEdge]] => [ - [`${cube.name}-${join[0]}`, { - join: join[1], + + const toMultipliedMeasures = getMultipliedMeasures(join.name); + if (!this.cubeEvaluator.primaryKeys[join.name].length && toMultipliedMeasures.length > 0) { + errorReporter.error(joinRequired(join.name)); + return false; + } + + return true; + }) + .map(join => { + const joinEdge: JoinEdge = { + join, from: cube.name, - to: join[0], + to: join.name, originalFrom: cube.name, - originalTo: join[0] - }] - ]), - // @ts-ignore - R.filter(R.identity), - R.map((join: [string, JoinDefinition]) => { - if (!this.cubeEvaluator.cubeExists(join[0])) { - errorReporter.error(`Cube ${join[0]} doesn't exist`); - return undefined; - } - return join; - }), - // @ts-ignore - R.toPairs - // @ts-ignore - )(cube.joins || {}); + originalTo: join.name + }; + + return [`${cube.name}-${join.name}`, joinEdge] as [string, JoinEdge]; + }); } protected buildJoinNode(cube: CubeDefinition): Record { - return R.compose< - Record, - Array<[string, JoinDefinition]>, - Array<[string, 1]>, - Record - >( - R.fromPairs, - R.map(v => [v[0], 1]), - R.toPairs - )(cube.joins || {}); + if (!cube.joins) { + return {}; + } + + return cube.joins.reduce((acc, join) => { + acc[join.name] = 1; + return acc; + }, {} as Record); } public buildJoin(cubesToJoin: JoinHints): FinishedJoinTree | null { diff --git a/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts b/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts index d66224582cc0e..c36b648500244 100644 --- a/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts @@ -116,7 +116,7 @@ export class YamlCompiler { } } - private transpileAndPrepareJsFile(file, methodFn, cubeObj, errorsReport: ErrorReporter) { + private transpileAndPrepareJsFile(file: FileContent, methodFn: ('cube' | 'view'), cubeObj, errorsReport: ErrorReporter): FileContent { const yamlAst = this.transformYamlCubeObj(cubeObj, errorsReport); const cubeOrViewCall = t.callExpression(t.identifier(methodFn), [t.stringLiteral(cubeObj.name), yamlAst]); @@ -135,7 +135,13 @@ export class YamlCompiler { cubeObj.dimensions = this.yamlArrayToObj(cubeObj.dimensions || [], 'dimension', errorsReport); cubeObj.segments = this.yamlArrayToObj(cubeObj.segments || [], 'segment', errorsReport); cubeObj.preAggregations = this.yamlArrayToObj(cubeObj.preAggregations || [], 'preAggregation', errorsReport); - cubeObj.joins = this.yamlArrayToObj(cubeObj.joins || [], 'join', errorsReport); + + cubeObj.joins = cubeObj.joins || []; // For edge cases where joins are not defined/null + if (!Array.isArray(cubeObj.joins)) { + errorsReport.error('joins must be defined as array'); + cubeObj.joins = []; + } + cubeObj.hierarchies = this.yamlArrayToObj(cubeObj.hierarchies || [], 'hierarchies', errorsReport); return this.transpileYaml(cubeObj, [], cubeObj.name, errorsReport); diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index 2ffc96058b3ef..4fb43d08868d8 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -100,6 +100,13 @@ export class CubePropContextTranspiler implements TranspilerInterface { return { ObjectProperty: (path) => { + if (path.node.key.type === 'Identifier' && path.node.key.name === 'joins' && t.isObjectExpression(path.node.value)) { + const fullPath = this.fullPath(path); + if (fullPath === 'joins') { + this.convertJoinsObjectToArray(path); + } + } + if (path.node.key.type === 'Identifier' && transpiledFields.has(path.node.key.name)) { const fullPath = this.fullPath(path); // eslint-disable-next-line no-restricted-syntax @@ -114,6 +121,39 @@ export class CubePropContextTranspiler implements TranspilerInterface { }; } + protected convertJoinsObjectToArray(path: NodePath) { + const valuePath = path.get('value'); + if (!valuePath.isObjectExpression()) { + return; + } + + const elements: t.ObjectExpression[] = []; + + for (const prop of valuePath.get('properties')) { + if (!prop.isObjectProperty()) { + return; + } + const keyNode = prop.node.key; + const valueNode = prop.node.value; + + if ( + (t.isIdentifier(keyNode) || t.isStringLiteral(keyNode)) && + t.isObjectExpression(valueNode) + ) { + const nameProp = t.objectProperty( + t.identifier('name'), + t.stringLiteral(t.isIdentifier(keyNode) ? keyNode.name : keyNode.value) + ); + + const newProps = [nameProp, ...valueNode.properties]; + const objectExpr = t.objectExpression(newProps); + elements.push(objectExpr); + } + } + + valuePath.replaceWith(t.arrayExpression(elements)); + } + protected fullPath(path: NodePath): string { // @ts-ignore let fp = path?.node?.key?.name || ''; diff --git a/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap b/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap index a2083b8601b9e..7f79bc7421f29 100644 --- a/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap +++ b/packages/cubejs-schema-compiler/test/unit/__snapshots__/schema.test.ts.snap @@ -166,7 +166,7 @@ Object { }, "fileName": "custom_calendar.js", "hierarchies": Object {}, - "joins": Object {}, + "joins": Array [], "measures": Object { "count": Object { "ownedByCube": true, @@ -385,7 +385,7 @@ Object { "title": "Retail Calendar Hierarchy", }, }, - "joins": Object {}, + "joins": Array [], "measures": Object { "count": Object { "ownedByCube": true, @@ -585,25 +585,28 @@ Object { `; exports[`Schema Testing Inheritance CubeB.js correctly extends cubeA.js (with additions): joins 1`] = ` -Object { - "order_users": Object { +Array [ + Object { + "name": "order_users", "relationship": "belongsTo", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.js correctly extends cubeA.js (with additions): joins 2`] = ` -Object { - "line_items": Object { - "relationship": "hasMany", +Array [ + Object { + "name": "order_users", + "relationship": "belongsTo", "sql": [Function], }, - "order_users": Object { - "relationship": "belongsTo", + Object { + "name": "line_items", + "relationship": "hasMany", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.js correctly extends cubeA.js (with additions): measures 1`] = ` @@ -925,25 +928,28 @@ Object { `; exports[`Schema Testing Inheritance CubeB.js correctly extends cubeA.yml (with additions): joins 1`] = ` -Object { - "order_users": Object { +Array [ + Object { + "name": "order_users", "relationship": "belongsTo", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.js correctly extends cubeA.yml (with additions): joins 2`] = ` -Object { - "line_items": Object { - "relationship": "hasMany", +Array [ + Object { + "name": "order_users", + "relationship": "belongsTo", "sql": [Function], }, - "order_users": Object { - "relationship": "belongsTo", + Object { + "name": "line_items", + "relationship": "hasMany", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.js correctly extends cubeA.yml (with additions): measures 1`] = ` @@ -1223,25 +1229,28 @@ Object { `; exports[`Schema Testing Inheritance CubeB.yml correctly extends cubeA.js (with additions): joins 1`] = ` -Object { - "order_users": Object { +Array [ + Object { + "name": "order_users", "relationship": "belongsTo", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.yml correctly extends cubeA.js (with additions): joins 2`] = ` -Object { - "line_items": Object { - "relationship": "hasMany", +Array [ + Object { + "name": "order_users", + "relationship": "belongsTo", "sql": [Function], }, - "order_users": Object { - "relationship": "belongsTo", + Object { + "name": "line_items", + "relationship": "hasMany", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.yml correctly extends cubeA.js (with additions): measures 1`] = ` @@ -1563,25 +1572,28 @@ Object { `; exports[`Schema Testing Inheritance CubeB.yml correctly extends cubeA.yml (with additions): joins 1`] = ` -Object { - "order_users": Object { +Array [ + Object { + "name": "order_users", "relationship": "belongsTo", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.yml correctly extends cubeA.yml (with additions): joins 2`] = ` -Object { - "line_items": Object { - "relationship": "hasMany", +Array [ + Object { + "name": "order_users", + "relationship": "belongsTo", "sql": [Function], }, - "order_users": Object { - "relationship": "belongsTo", + Object { + "name": "line_items", + "relationship": "hasMany", "sql": [Function], }, -} +] `; exports[`Schema Testing Inheritance CubeB.yml correctly extends cubeA.yml (with additions): measures 1`] = ` @@ -1677,6 +1689,46 @@ Object { } `; +exports[`Schema Testing Joins join types (joins as array) 1`] = ` +Array [ + Object { + "name": "CubeB", + "relationship": "hasOne", + "sql": [Function], + }, + Object { + "name": "CubeC", + "relationship": "hasMany", + "sql": [Function], + }, + Object { + "name": "CubeD", + "relationship": "belongsTo", + "sql": [Function], + }, +] +`; + +exports[`Schema Testing Joins join types (joins as object) 1`] = ` +Array [ + Object { + "name": "CubeB", + "relationship": "hasOne", + "sql": [Function], + }, + Object { + "name": "CubeC", + "relationship": "hasMany", + "sql": [Function], + }, + Object { + "name": "CubeD", + "relationship": "belongsTo", + "sql": [Function], + }, +] +`; + exports[`Schema Testing Views allows to override \`title\`, \`description\`, \`meta\`, and \`format\` on includes members 1`] = ` Object { "accessPolicy": undefined, @@ -1813,7 +1865,7 @@ Object { }, ], "isView": true, - "joins": Object {}, + "joins": Array [], "measures": Object { "count": Object { "aggType": "count", diff --git a/packages/cubejs-schema-compiler/test/unit/schema.test.ts b/packages/cubejs-schema-compiler/test/unit/schema.test.ts index 9fb37a1ca66ed..43dc8cd6839bc 100644 --- a/packages/cubejs-schema-compiler/test/unit/schema.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/schema.test.ts @@ -416,41 +416,76 @@ describe('Schema Testing', () => { expect(gr.origin).toBe('2020-06-01 10:00:00'); }); - it('join types', async () => { - const { compiler, cubeEvaluator } = prepareJsCompiler([ - createCubeSchema({ - name: 'CubeA', - joins: `{ - CubeB: { - sql: \`SQL ON clause\`, - relationship: 'one_to_one' - }, - CubeC: { - sql: \`SQL ON clause\`, - relationship: 'one_to_many' - }, - CubeD: { - sql: \`SQL ON clause\`, - relationship: 'many_to_one' - }, - }` - }), - createCubeSchema({ - name: 'CubeB', - }), - createCubeSchema({ - name: 'CubeC', - }), - createCubeSchema({ - name: 'CubeD', - }), - ]); - await compiler.compile(); + describe('Joins', () => { + it('join types (joins as object)', async () => { + const { compiler, cubeEvaluator } = prepareJsCompiler([ + createCubeSchema({ + name: 'CubeA', + joins: `{ + CubeB: { + sql: \`SQL ON clause\`, + relationship: 'one_to_one' + }, + CubeC: { + sql: \`SQL ON clause\`, + relationship: 'one_to_many' + }, + CubeD: { + sql: \`SQL ON clause\`, + relationship: 'many_to_one' + }, + }` + }), + createCubeSchema({ + name: 'CubeB', + }), + createCubeSchema({ + name: 'CubeC', + }), + createCubeSchema({ + name: 'CubeD', + }), + ]); + await compiler.compile(); + + expect(cubeEvaluator.cubeFromPath('CubeA').joins).toMatchSnapshot(); + }); + + it('join types (joins as array)', async () => { + const { compiler, cubeEvaluator } = prepareJsCompiler([ + createCubeSchema({ + name: 'CubeA', + joins: `[ + { + name: 'CubeB', + sql: \`SQL ON clause\`, + relationship: 'one_to_one' + }, + { + name: 'CubeC', + sql: \`SQL ON clause\`, + relationship: 'one_to_many' + }, + { + name: 'CubeD', + sql: \`SQL ON clause\`, + relationship: 'many_to_one' + }, + ]` + }), + createCubeSchema({ + name: 'CubeB', + }), + createCubeSchema({ + name: 'CubeC', + }), + createCubeSchema({ + name: 'CubeD', + }), + ]); + await compiler.compile(); - expect(cubeEvaluator.cubeFromPath('CubeA').joins).toMatchObject({ - CubeB: { relationship: 'hasOne' }, - CubeC: { relationship: 'hasMany' }, - CubeD: { relationship: 'belongsTo' } + expect(cubeEvaluator.cubeFromPath('CubeA').joins).toMatchSnapshot(); }); }); diff --git a/yarn.lock b/yarn.lock index e574aef82b9e4..8e87eaaa67b45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7769,6 +7769,11 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/js-yaml@^4.0.9": + version "4.0.9" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2" + integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg== + "@types/json-bigint@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/json-bigint/-/json-bigint-1.0.1.tgz#201062a6990119a8cc18023cfe1fed12fc2fc8a7"