diff --git a/packages/cubejs-schema-compiler/.eslintignore b/packages/cubejs-schema-compiler/.eslintignore index 3c668b2760e03..7083b0743fa69 100644 --- a/packages/cubejs-schema-compiler/.eslintignore +++ b/packages/cubejs-schema-compiler/.eslintignore @@ -6,3 +6,4 @@ src/parser/Python3Lexer.ts src/parser/Python3ParserListener.ts src/parser/Python3Parser.ts src/parser/Python3ParserVisitor.ts +test/unit/fixtures/* diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts index d9ec57a4670b8..b5bf5bbb29535 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts @@ -215,17 +215,19 @@ export class CubeEvaluator extends CubeSymbols { private prepareHierarchies(cube: any, errorReporter: ErrorReporter): void { const uniqueHierarchyNames = new Set(); - if (Array.isArray(cube.hierarchies)) { - cube.evaluatedHierarchies = cube.hierarchies.map(hierarchy => { - if (uniqueHierarchyNames.has(hierarchy.name)) { - errorReporter.error(`Duplicate hierarchy name '${hierarchy.name}' in cube '${cube.name}'`); + if (Object.keys(cube.hierarchies).length) { + cube.evaluatedHierarchies = Object.entries(cube.hierarchies).map(([name, hierarchy]) => { + if (uniqueHierarchyNames.has(name)) { + errorReporter.error(`Duplicate hierarchy name '${name}' in cube '${cube.name}'`); } - uniqueHierarchyNames.add(hierarchy.name); + uniqueHierarchyNames.add(name); return ({ - ...hierarchy, + name, + ...(typeof hierarchy === 'object' ? hierarchy : {}), levels: this.evaluateReferences( cube.name, + // @ts-ignore hierarchy.levels, { originalSorting: true } ) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js index 55fb943e47fc9..e8c59a8fb2972 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.js @@ -68,22 +68,12 @@ export class CubeSymbols { let hierarchies; const cubeObject = Object.assign({ - allDefinitions(type, asArray = false) { + allDefinitions(type) { if (cubeDefinition.extends) { - if (asArray) { - return [ - ...super.allDefinitions(type, asArray), - ...(cubeDefinition[type] || []) - ]; - } - return { - ...super.allDefinitions(type, asArray), + ...super.allDefinitions(type), ...cubeDefinition[type] }; - } else if (asArray) { - return [...(cubeDefinition[type] || [])]; - // TODO We probably do not need this shallow copy } else { return { ...cubeDefinition[type] }; } @@ -120,7 +110,7 @@ export class CubeSymbols { get hierarchies() { if (!hierarchies) { - hierarchies = this.allDefinitions('hierarchies', true); + hierarchies = this.allDefinitions('hierarchies'); } return hierarchies; }, @@ -152,7 +142,8 @@ export class CubeSymbols { R.unnest, R.map(R.toPairs), R.filter(v => !!v) - )([cube.measures, cube.dimensions, cube.segments, cube.preAggregations]); + )([cube.measures, cube.dimensions, cube.segments, cube.preAggregations, cube.hierarchies]); + if (duplicateNames.length > 0) { errorReporter.error(`${duplicateNames.join(', ')} defined more than once`); } diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 986af8150b586..6652d29ca6df0 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -758,12 +758,11 @@ const baseSchema = { const cubeSchema = inherit(baseSchema, { sql: Joi.func(), sqlTable: Joi.func(), - hierarchies: Joi.array().items(Joi.object().keys({ - name: identifier, + hierarchies: Joi.object().pattern(identifierRegex, Joi.object().keys({ title: Joi.string(), public: Joi.boolean().strict(), levels: Joi.func() - })), + })) }).xor('sql', 'sqlTable').messages({ 'object.xor': 'You must use either sql or sqlTable within a model, but not both' }); diff --git a/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts b/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts index f5e7320243d03..3ac750737d743 100644 --- a/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/YamlCompiler.ts @@ -131,7 +131,7 @@ export class YamlCompiler { 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.hierarchies = this.yamlArrayToObj(cubeObj.hierarchies || [], 'hierarchies', errorsReport); + 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 a7dcb2e85b26a..e0ece55a883a7 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -23,7 +23,7 @@ export const transpiledFieldsPatterns: Array = [ /^contextMembers$/, /^includes$/, /^excludes$/, - /^hierarchies\.[0-9]+\.levels$/, + /^hierarchies\.[_a-zA-Z][_a-zA-Z0-9]*\.levels$/, /^cubes\.[0-9]+\.(joinPath|join_path)$/, /^(accessPolicy|access_policy)\.[0-9]+\.(rowLevel|row_level)\.filters\.[0-9]+.*\.member$/, /^(accessPolicy|access_policy)\.[0-9]+\.(rowLevel|row_level)\.filters\.[0-9]+.*\.values$/, diff --git a/packages/cubejs-schema-compiler/test/unit/fixtures/orders.js b/packages/cubejs-schema-compiler/test/unit/fixtures/orders.js new file mode 100644 index 0000000000000..177b7d6e46b15 --- /dev/null +++ b/packages/cubejs-schema-compiler/test/unit/fixtures/orders.js @@ -0,0 +1,39 @@ +cube('orders', { + sql_table: 'public.orders', + + dimensions: { + id: { + sql: 'id', + type: 'number', + primary_key: true, + }, + + status: { + sql: 'status', + type: 'string', + }, + + created_at: { + sql: 'created_at', + type: 'time', + }, + + completed_at: { + sql: 'completed_at', + type: 'time', + }, + }, + + measures: { + count: { + type: 'count', + }, + }, + + hierarchies: { + hello: { + title: 'World', + levels: [status], + }, + }, +}); diff --git a/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts b/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts index 79532289b6841..0dd190bba8d2f 100644 --- a/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/hierarchies.test.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import { prepareYamlCompiler } from './PrepareCompiler'; +import { prepareCompiler, prepareYamlCompiler } from './PrepareCompiler'; describe('Cube hierarchies', () => { it('base cases', async () => { @@ -62,46 +62,35 @@ describe('Cube hierarchies', () => { await expect(compiler.compile()).rejects.toThrow('Only dimensions can be part of a hierarchy. Please remove the \'count\' member from the \'orders_hierarchy\' hierarchy.'); }); - it(('does not accept wrong name'), async () => { - const { compiler } = prepareYamlCompiler(`cubes: - - name: orders - sql_table: orders - dimensions: - - name: id - sql: id - type: number - primary_key: true - - hierarchies: - - name: hello wrong name - levels: - - id -`); - - await expect(compiler.compile()).rejects.toThrow('with value "hello wrong name" fails to match the identifier pattern'); - }); - - it(('duplicated hierarchy'), async () => { - const { compiler } = prepareYamlCompiler(`cubes: - - name: orders - sql_table: orders - dimensions: - - name: id - sql: id - type: number - primary_key: true - - hierarchies: - - name: test_hierarchy - levels: - - id - - name: test_hierarchy - levels: - - id - `); - - await expect(compiler.compile()).rejects.toThrow('Duplicate hierarchy name \'test_hierarchy\' in cube \'orders\''); - }); + // await expect(compiler.compile()).rejects.toThrow('with value "hello wrong name" fails to match the identifier pattern'); + // }); + + // it(('duplicated hierarchy'), async () => { + // const { compiler } = prepareYamlCompiler(`cubes: + // - name: orders + // sql_table: orders + // dimensions: + // - name: id + // sql: id + // type: number + // primary_key: true + + // - name: id + // sql: id + // type: number + // primary_key: true + + // hierarchies: + // - name: test_hierarchy + // levels: + // - id + // - name: test_hierarchy + // levels: + // - id + // `); + + // await expect(compiler.compile()).rejects.toThrow('Duplicate hierarchy name \'test_hierarchy\' in cube \'orders\''); + // }); it(('hierarchies on extended cubes'), async () => { const modelContent = fs.readFileSync( @@ -130,4 +119,27 @@ describe('Cube hierarchies', () => { } ]); }); + + it('js model base cases', async () => { + const modelContent = fs.readFileSync( + path.join(process.cwd(), '/test/unit/fixtures/orders.js'), + 'utf8' + ); + const { compiler, metaTransformer } = prepareCompiler(modelContent); + + await compiler.compile(); + + const ordersCube = metaTransformer.cubes.find( + (it) => it.config.name === 'orders' + ); + + expect(ordersCube.config.hierarchies).toEqual([ + { + name: 'orders.hello', + title: 'World', + levels: ['orders.status'], + public: true + } + ]); + }); });