From f16b7e9895f69d7461fe3d2eb817c61b59816d42 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 21 Apr 2025 11:30:43 +0300 Subject: [PATCH 1/6] allow overrides in views validator --- .../src/compiler/CubeValidator.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts index 0055c78901f25..c8a427f6c95ca 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeValidator.ts @@ -110,6 +110,14 @@ const GranularityInterval = Joi.string().pattern(/^\d+\s+(second|minute|hour|day // Do not allow negative intervals for granularities, while offsets could be negative 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'); +const formatSchema = Joi.alternatives([ + Joi.string().valid('imageUrl', 'link', 'currency', 'percent', 'number', 'id'), + Joi.object().keys({ + type: Joi.string().valid('link'), + label: Joi.string().required() + }) +]); + const BaseDimensionWithoutSubQuery = { aliases: Joi.array().items(Joi.string()), type: Joi.any().valid('string', 'number', 'boolean', 'time', 'geo').required(), @@ -122,13 +130,7 @@ const BaseDimensionWithoutSubQuery = { description: Joi.string(), suggestFilterValues: Joi.boolean().strict(), enableSuggestions: Joi.boolean().strict(), - format: Joi.alternatives([ - Joi.string().valid('imageUrl', 'link', 'currency', 'percent', 'number', 'id'), - Joi.object().keys({ - type: Joi.string().valid('link'), - label: Joi.string().required() - }) - ]), + format: formatSchema, meta: Joi.any(), granularities: Joi.when('type', { is: 'time', @@ -796,7 +798,11 @@ const viewSchema = inherit(baseSchema, { Joi.string().required(), Joi.object().keys({ name: identifier.required(), - alias: identifier + alias: identifier, + title: Joi.string(), + description: Joi.string(), + format: formatSchema, + meta: Joi.any(), }) ])) ]).required(), From c5d747534aad1d89c30cbf1402c771fa3087cc58 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 21 Apr 2025 12:36:59 +0300 Subject: [PATCH 2/6] allow objects in view's includes without aliases --- packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 06bdcac184709..d13d5920eebaa 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -63,7 +63,7 @@ export class CubeSymbols { public cubeList: any[]; - private evaluateViews: boolean; + private readonly evaluateViews: boolean; private resolveSymbolsCallContext: any; @@ -447,13 +447,13 @@ export class CubeSymbols { includes = Object.keys(membersObj).map(memberName => ({ member: `${fullPath}.${memberName}`, name: fullMemberName(memberName) })); } else { includes = cubeInclude.includes.map((include: any) => { - const member = include.alias || include; + const member = include.alias || include.name || include; if (member.includes('.')) { errorReporter.error(`Paths aren't allowed in cube includes but '${member}' provided as include member`); } - const name = fullMemberName(include.alias || member); + const name = fullMemberName(member); memberSets.allMembers.add(name); const includedMemberName = include.name || include; From d644d4564416d062b95eff0165e02fdb04e4cd2b Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 21 Apr 2025 13:04:59 +0300 Subject: [PATCH 3/6] feat(schema-compiler): Support overriding `title`, `description`, `meta`, and `format` on view members --- .../src/compiler/CubeEvaluator.ts | 6 ++- .../src/compiler/CubeSymbols.ts | 49 +++++++++++-------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts index b95b75f92714f..759e0457ba2c3 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts @@ -257,7 +257,9 @@ export class CubeEvaluator extends CubeSymbols { } private prepareHierarchies(cube: any, errorReporter: ErrorReporter): void { - if (Object.keys(cube.hierarchies).length) { + // Hierarchies from views are not fully populated at this moment and are processed later, + // so we should not pollute the cube hierarchies definition here. + if (!cube.isView && Object.keys(cube.hierarchies).length) { cube.evaluatedHierarchies = Object.entries(cube.hierarchies).map(([name, hierarchy]) => ({ name, ...(typeof hierarchy === 'object' ? hierarchy : {}), @@ -306,6 +308,8 @@ export class CubeEvaluator extends CubeSymbols { throw new UserError(`Hierarchy '${it.name}' not found in cube '${cubeName}'`); } return { + // Title might be overridden in the view + title: cube.hierarchies?.[it.name]?.override?.title || it.title, ...it, name, levels diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index d13d5920eebaa..e4cdc8e5e4328 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -275,13 +275,14 @@ export class CubeSymbols { this.prepareIncludes(cube, errorReporter, splitViews); } - return Object.assign( - { cubeName: () => cube.name, cubeObj: () => cube }, - cube.measures || {}, - cube.dimensions || {}, - cube.segments || {}, - cube.preAggregations || {} - ); + return { + cubeName: () => cube.name, + cubeObj: () => cube, + ...cube.measures || {}, + ...cube.dimensions || {}, + ...cube.segments || {}, + ...cube.preAggregations || {} + }; } private camelCaseTypes(obj: Object) { @@ -425,7 +426,7 @@ export class CubeSymbols { for (const [memberName, memberDefinition] of includeMembers) { if (cube[type]?.[memberName]) { errorReporter.error(`Included member '${memberName}' conflicts with existing member of '${cube.name}'. Please consider excluding this member or assigning it an alias.`); - } else if (type !== 'hierarchies') { + } else { cube[type][memberName] = memberDefinition; } } @@ -461,6 +462,14 @@ export class CubeSymbols { const resolvedMember = this.getResolvedMember(type, cubeReference, includedMemberName) ? { member: `${fullPath}.${includedMemberName}`, name, + ...(include.title || include.description || include.format || include.meta) ? { + override: { + title: include.title, + description: include.description, + format: include.format, + meta: include.meta, + } + } : {} } : undefined; if (resolvedMember) { @@ -537,10 +546,10 @@ export class CubeSymbols { sql, type: CubeSymbols.toMemberDataType(resolvedMember.type), aggType: resolvedMember.type, - meta: resolvedMember.meta, - title: resolvedMember.title, - description: resolvedMember.description, - format: resolvedMember.format, + meta: memberRef.override?.meta || resolvedMember.meta, + title: memberRef.override?.title || resolvedMember.title, + description: memberRef.override?.description || resolvedMember.description, + format: memberRef.override?.format || resolvedMember.format, ...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }), ...(resolvedMember.timeShift && { timeShift: resolvedMember.timeShift }), ...(resolvedMember.orderBy && { orderBy: resolvedMember.orderBy }), @@ -549,24 +558,24 @@ export class CubeSymbols { memberDefinition = { sql, type: resolvedMember.type, - meta: resolvedMember.meta, - title: resolvedMember.title, - description: resolvedMember.description, - format: resolvedMember.format, + meta: memberRef.override?.meta || resolvedMember.meta, + title: memberRef.override?.title || resolvedMember.title, + description: memberRef.override?.description || resolvedMember.description, + format: memberRef.override?.format || resolvedMember.format, ...(resolvedMember.granularities ? { granularities: resolvedMember.granularities } : {}), ...(resolvedMember.multiStage && { multiStage: resolvedMember.multiStage }), }; } else if (type === 'segments') { memberDefinition = { sql, - meta: resolvedMember.meta, - title: resolvedMember.title, - description: resolvedMember.description, + meta: memberRef.override?.meta || resolvedMember.meta, + description: memberRef.override?.description || resolvedMember.description, + title: memberRef.override?.title || resolvedMember.title, aliases: resolvedMember.aliases, }; } else if (type === 'hierarchies') { memberDefinition = { - title: resolvedMember.title, + title: memberRef.override?.title || resolvedMember.title, levels: resolvedMember.levels, }; } else { From 5cc936fd3697cef8289ce0cdef41328470a04c35 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 21 Apr 2025 13:12:17 +0300 Subject: [PATCH 4/6] code polish --- .../src/compiler/CubeSymbols.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index e4cdc8e5e4328..e8420f9e1e05d 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -440,7 +440,7 @@ export class CubeSymbols { const cubeReference = split[split.length - 1]; const cubeName = cubeInclude.alias || cubeReference; - let includes; + let includes: any[]; const fullMemberName = (memberName: string) => (cubeInclude.prefix ? `${cubeName}_${memberName}` : memberName); if (cubeInclude.includes === '*') { @@ -587,7 +587,7 @@ export class CubeSymbols { /** * This method is mainly used for evaluating RLS conditions and filters. - * It allows referencing security_context (lowecase) in dynamic conditions or filter values. + * It allows referencing security_context (lowercase) in dynamic conditions or filter values. * * It currently does not support async calls because inner resolveSymbol and * resolveSymbolsCall are sync. Async support may be added later with deeper @@ -596,7 +596,7 @@ export class CubeSymbols { protected evaluateContextFunction(cube: any, contextFn: any, context: any = {}) { const cubeEvaluator = this; - const res = cubeEvaluator.resolveSymbolsCall(contextFn, (name: string) => { + return cubeEvaluator.resolveSymbolsCall(contextFn, (name: string) => { const resolvedSymbol = this.resolveSymbol(cube, name); if (resolvedSymbol) { return resolvedSymbol; @@ -609,8 +609,6 @@ export class CubeSymbols { securityContext: context.securityContext, } }); - - return res; } protected evaluateReferences>( @@ -766,15 +764,14 @@ export class CubeSymbols { } protected depsContextSymbols() { - return Object.assign({ + return { filterParams: this.filtersProxyDep(), filterGroup: this.filterGroupFunctionDep(), securityContext: CubeSymbols.contextSymbolsProxyFrom({}, (param) => param), sqlUtils: { convertTz: (f) => f - }, - }); + }; } protected filtersProxyDep() { @@ -819,7 +816,7 @@ export class CubeSymbols { if (CONTEXT_SYMBOLS[name]) { // always resolves if contextSymbols aren't passed for transpile step - const symbol = contextSymbols && contextSymbols[CONTEXT_SYMBOLS[name]] || {}; + const symbol = contextSymbols?.[CONTEXT_SYMBOLS[name]] || {}; // eslint-disable-next-line no-underscore-dangle symbol._objectWithResolvedProperties = true; return symbol; @@ -851,12 +848,12 @@ export class CubeSymbols { const parentIndex = currResolveIndexFn(); cube = this.cubeDependenciesProxy(parentIndex, newCubeName); return cube; - } else if (this.symbols[cubeName] && this.symbols[cubeName][name] && this.symbols[cubeName][name].type === 'time') { + } else if (this.symbols[cubeName]?.[name] && this.symbols[cubeName][name].type === 'time') { const parentIndex = currResolveIndexFn(); return this.timeDimDependenciesProxy(parentIndex); } } - return cube || (this.symbols[cubeName] && this.symbols[cubeName][name]); + return cube || this.symbols[cubeName]?.[name]; } protected cubeReferenceProxy(cubeName, joinHints?: any[], refProperty?: any) { @@ -946,7 +943,7 @@ export class CubeSymbols { return { interval: `1 ${granName}` }; } - return cube && cube[dimName] && cube[dimName][gr] && cube[dimName][gr][granName]; + return cube?.[dimName]?.[gr]?.[granName]; } protected cubeDependenciesProxy(parentIndex, cubeName) { From 2bcedd1aa0e05c8fb3c28fc442da9c528e55c9cd Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 21 Apr 2025 13:50:12 +0300 Subject: [PATCH 5/6] add tests --- .../unit/__snapshots__/schema.test.ts.snap | 165 ++++++++++++++++++ .../test/unit/cube-validator.test.ts | 41 +++++ .../test/unit/schema.test.ts | 58 ++++++ 3 files changed, 264 insertions(+) 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 69aa7248564fb..768626a10e7fc 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 @@ -1408,3 +1408,168 @@ Object { "type": "number", } `; + +exports[`Schema Testing Views allows to override \`title\`, \`description\`, \`meta\`, and \`format\` on includes members 1`] = ` +Object { + "accessPolicy": undefined, + "allDefinitions": [Function], + "cubes": Array [ + Object { + "includes": Array [ + Object { + "alias": "my_beloved_status", + "description": "Don't you believe this?", + "meta": Array [ + Object { + "whose": "mine", + }, + Object { + "what": "status", + }, + ], + "name": "status", + "title": "My Favorite and not Beloved Status!", + }, + Object { + "alias": "my_beloved_created_at", + "description": "Created at this point in time", + "meta": Array [ + Object { + "c1": "iddqd", + }, + Object { + "c2": "idkfa", + }, + ], + "name": "created_at", + "title": "My Favorite and not Beloved created_at!", + }, + Object { + "description": "It's not possible!", + "format": "percent", + "meta": Array [ + Object { + "whose": "bread", + }, + Object { + "what": "butter", + }, + Object { + "why": "cheese", + }, + ], + "name": "count", + "title": "My Overridden Count!", + }, + Object { + "name": "hello", + "title": "My Overridden hierarchy!", + }, + ], + "joinPath": [Function], + }, + ], + "dimensions": Object { + "my_beloved_created_at": Object { + "aliasMember": "orders.created_at", + "description": "Created at this point in time", + "format": undefined, + "meta": Array [ + Object { + "c1": "iddqd", + }, + Object { + "c2": "idkfa", + }, + ], + "ownedByCube": false, + "sql": [Function], + "title": "My Favorite and not Beloved created_at!", + "type": "time", + }, + "my_beloved_status": Object { + "aliasMember": "orders.status", + "description": "Don't you believe this?", + "format": undefined, + "meta": Array [ + Object { + "whose": "mine", + }, + Object { + "what": "status", + }, + ], + "ownedByCube": false, + "sql": [Function], + "title": "My Favorite and not Beloved Status!", + "type": "string", + }, + }, + "evaluatedHierarchies": Array [ + Object { + "levels": Array [ + "orders_view.my_beloved_status", + ], + "name": "hello", + "title": "World", + }, + ], + "fileName": "order_view.yml", + "hierarchies": Object { + "hello": Object { + "levels": [Function], + "title": "My Overridden hierarchy!", + }, + }, + "includedMembers": Array [ + Object { + "memberPath": "orders.hello", + "name": "hello", + "type": "hierarchies", + }, + Object { + "memberPath": "orders.count", + "name": "count", + "type": "measures", + }, + Object { + "memberPath": "orders.status", + "name": "my_beloved_status", + "type": "dimensions", + }, + Object { + "memberPath": "orders.created_at", + "name": "my_beloved_created_at", + "type": "dimensions", + }, + ], + "isView": true, + "joins": Object {}, + "measures": Object { + "count": Object { + "aggType": "count", + "aliasMember": "orders.count", + "description": "It's not possible!", + "format": "percent", + "meta": Array [ + Object { + "whose": "bread", + }, + Object { + "what": "butter", + }, + Object { + "why": "cheese", + }, + ], + "ownedByCube": false, + "sql": [Function], + "title": "My Overridden Count!", + "type": "number", + }, + }, + "name": "orders_view", + "preAggregations": Object {}, + "segments": Object {}, +} +`; diff --git a/packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts b/packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts index 628a5189883b9..2c39328166b15 100644 --- a/packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/cube-validator.test.ts @@ -165,6 +165,47 @@ describe('Cube Validation', () => { expect(validationResult.error).toBeTruthy(); }); + it('view with overridden included members properties', async () => { + const cubeValidator = new CubeValidator(new CubeSymbols()); + const cube = { + name: 'name', + // it's a hidden field which we use internally + isView: true, + fileName: 'fileName', + cubes: [ + { + joinPath: () => '', + prefix: false, + includes: [ + 'member_by_name', + { + name: 'member_by_alias', + alias: 'correct_alias' + }, + { + name: 'member_by_alias_with_overrides', + title: 'Overridden title', + description: 'Overridden description', + format: 'percent', + meta: { + f1: 'Overridden 1', + f2: 'Overridden 2', + }, + } + ] + } + ] + }; + + const validationResult = cubeValidator.validate(cube, { + error: (message: any, _e: any) => { + console.log(message); + } + } as any); + + expect(validationResult.error).toBeFalsy(); + }); + it('refreshKey alternatives', async () => { const cubeValidator = new CubeValidator(new CubeSymbols()); const cube = { diff --git a/packages/cubejs-schema-compiler/test/unit/schema.test.ts b/packages/cubejs-schema-compiler/test/unit/schema.test.ts index dd16b2b6e12b5..d4c51b088a76b 100644 --- a/packages/cubejs-schema-compiler/test/unit/schema.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/schema.test.ts @@ -685,6 +685,64 @@ describe('Schema Testing', () => { expect(e.toString()).toMatch(/Included member 'id' conflicts with existing member of 'orders_view'\. Please consider excluding this member or assigning it an alias/); } }); + + it('allows to override `title`, `description`, `meta`, and `format` on includes members', async () => { + const orders = fs.readFileSync( + path.join(process.cwd(), '/test/unit/fixtures/orders.js'), + 'utf8' + ); + const ordersView = ` + views: + - name: orders_view + cubes: + - join_path: orders + includes: + - name: status + alias: my_beloved_status + title: My Favorite and not Beloved Status! + description: Don't you believe this? + meta: + - whose: mine + - what: status + + - name: created_at + alias: my_beloved_created_at + title: My Favorite and not Beloved created_at! + description: Created at this point in time + meta: + - c1: iddqd + - c2: idkfa + + - name: count + title: My Overridden Count! + description: It's not possible! + format: percent + meta: + - whose: bread + - what: butter + - why: cheese + + - name: hello + title: My Overridden hierarchy! + `; + + const { compiler, cubeEvaluator } = prepareCompiler([ + { + content: orders, + fileName: 'orders.js', + }, + { + content: ordersView, + fileName: 'order_view.yml', + }, + ]); + + await compiler.compile(); + compiler.throwIfAnyErrors(); + + const cubeB = cubeEvaluator.cubeFromPath('orders_view'); + expect(cubeB).toMatchSnapshot(); + }); }); describe('Inheritance', () => { From 0d568d48089aa29099ac40e4e8ad8d4f11a0b454 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Wed, 23 Apr 2025 15:01:10 +0300 Subject: [PATCH 6/6] code polish --- .../src/compiler/CubeSymbols.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index e8420f9e1e05d..3461cf69da18e 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -147,7 +147,7 @@ export class CubeSymbols { } return preAggregations; }, - set preAggregations(v) { + set preAggregations(_v) { // Dont allow to modify }, @@ -157,7 +157,7 @@ export class CubeSymbols { } return joins; }, - set joins(v) { + set joins(_v) { // Dont allow to modify }, @@ -167,7 +167,7 @@ export class CubeSymbols { } return measures; }, - set measures(v) { + set measures(_v) { // Dont allow to modify }, @@ -177,7 +177,7 @@ export class CubeSymbols { } return dimensions; }, - set dimensions(v) { + set dimensions(_v) { // Dont allow to modify }, @@ -187,7 +187,7 @@ export class CubeSymbols { } return segments; }, - set segments(v) { + set segments(_v) { // Dont allow to modify }, @@ -197,7 +197,7 @@ export class CubeSymbols { } return hierarchies; }, - set hierarchies(v) { + set hierarchies(_v) { // Dont allow to modify }, @@ -213,7 +213,7 @@ export class CubeSymbols { return undefined; } }, - set accessPolicy(v) { + set accessPolicy(_v) { // Dont allow to modify } },