From d2d7dbfa21ac514e86c5217e2b671d70be41de0b Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 25 Aug 2025 16:26:18 +0300 Subject: [PATCH 1/6] more types (SymbolResolver) --- .../transpilers/CubePropContextTranspiler.ts | 17 +++++++++-------- .../transpilers/transpiler.interface.ts | 2 ++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index 11ee57a6c0910..b1fee72bf731f 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -3,6 +3,7 @@ import R from 'ramda'; import type { NodePath } from '@babel/traverse'; import { + SymbolResolver, TranspilerCubeResolver, TranspilerInterface, TranspilerSymbolResolver, @@ -59,7 +60,7 @@ export class CubePropContextTranspiler implements TranspilerInterface { args[0].node.type === 'TemplateLiteral' && args[0].node.quasis.length && args[0].node.quasis[0].value.cooked; - args[args.length - 1].traverse(this.sqlAndReferencesFieldVisitor(cubeName)); + args[args.length - 1].traverse(this.sqlAndReferencesFieldVisitor(cubeName as string)); args[args.length - 1].traverse( this.knownIdentifiersInjectVisitor('extends', name => this.cubeDictionary.resolveCube(name)) ); @@ -72,11 +73,11 @@ export class CubePropContextTranspiler implements TranspilerInterface { }; } - protected transformObjectProperty(path: NodePath, resolveSymbol: (name: string) => void) { + protected transformObjectProperty(path: NodePath, resolveSymbol: SymbolResolver) { CubePropContextTranspiler.replaceValueWithArrowFunction(resolveSymbol, path.get('value')); } - public static replaceValueWithArrowFunction(resolveSymbol: (name: string) => any, value: NodePath) { + public static replaceValueWithArrowFunction(resolveSymbol: SymbolResolver, value: NodePath) { const knownIds = CubePropContextTranspiler.collectKnownIdentifiers( resolveSymbol, value, @@ -91,7 +92,7 @@ export class CubePropContextTranspiler implements TranspilerInterface { ); } - protected sqlAndReferencesFieldVisitor(cubeName): TraverseObject { + protected sqlAndReferencesFieldVisitor(cubeName: string | null | undefined): TraverseObject { const resolveSymbol = n => this.viewCompiler.resolveSymbol(cubeName, n) || this.cubeSymbols.resolveSymbol(cubeName, n) || this.cubeSymbols.isCurrentCube(n); @@ -171,7 +172,7 @@ export class CubePropContextTranspiler implements TranspilerInterface { return fp; } - protected knownIdentifiersInjectVisitor(field: RegExp | string, resolveSymbol: (name: string) => void): TraverseObject { + protected knownIdentifiersInjectVisitor(field: RegExp | string, resolveSymbol: SymbolResolver): TraverseObject { return { ObjectProperty: (path) => { if (path.node.key.type === 'Identifier' && path.node.key.name.match(field)) { @@ -181,8 +182,8 @@ export class CubePropContextTranspiler implements TranspilerInterface { }; } - protected static collectKnownIdentifiers(resolveSymbol, path: NodePath) { - const identifiers = []; + protected static collectKnownIdentifiers(resolveSymbol: SymbolResolver, path: NodePath): string[] { + const identifiers: string[] = []; if (path.node.type === 'Identifier') { CubePropContextTranspiler.matchAndPushIdentifier(path, resolveSymbol, identifiers); @@ -197,7 +198,7 @@ export class CubePropContextTranspiler implements TranspilerInterface { return R.uniq(identifiers); } - protected static matchAndPushIdentifier(path, resolveSymbol, identifiers) { + protected static matchAndPushIdentifier(path, resolveSymbol: SymbolResolver, identifiers: string[]) { if ( (!path.parent || (path.parent.type !== 'MemberExpression' || path.parent.type === 'MemberExpression' && path.key !== 'property') diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts index 803a1c0fa4082..292256ead97b0 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts @@ -15,3 +15,5 @@ export interface TranspilerSymbolResolver { export interface TranspilerCubeResolver { resolveCube(name): boolean; } + +export type SymbolResolver = (name: string) => any; From 4e0b11284461922975bb4d46e8dbf8f67e8b15b9 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 25 Aug 2025 16:41:05 +0300 Subject: [PATCH 2/6] add userAttributes to CONTEXT_SYMBOLS --- .../cubejs-schema-compiler/src/compiler/CubeSymbols.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 5330b8f9120ec..0e6c93e70cf44 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -188,7 +188,12 @@ export const CONTEXT_SYMBOLS = { securityContext: 'securityContext', FILTER_PARAMS: 'filterParams', FILTER_GROUP: 'filterGroup', - SQL_UTILS: 'sqlUtils' + SQL_UTILS: 'sqlUtils', + // This is syntax sugar for `securityContext.cubeCloud.userAttributes` + // while resolveSymbol won't return the exact path from securityContext + // it's not a problem, because this is only needed for the transpilation phase + // and then this shorthand will be transpiled with the full path + userAttributes: 'securityContext.cubeCloud.userAttributes' }; export const CURRENT_CUBE_CONSTANTS = ['CUBE', 'TABLE']; From bceaba84b9f283cb0061337c11812cd876168455 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 25 Aug 2025 18:39:48 +0300 Subject: [PATCH 3/6] add tests for CubePropContextTranspiler with userAttributes --- .../test/unit/transpilers.test.ts | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts b/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts index d0cccbd75d725..f8264aac49973 100644 --- a/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts +++ b/packages/cubejs-schema-compiler/test/unit/transpilers.test.ts @@ -5,6 +5,7 @@ import babelTraverse from '@babel/traverse'; import { prepareJsCompiler } from './PrepareCompiler'; import { ImportExportTranspiler } from '../../src/compiler/transpilers'; import { ErrorReporter } from '../../src/compiler/ErrorReporter'; +import { PostgresQuery } from '../../src'; describe('Transpilers', () => { it('CubeCheckDuplicatePropTranspiler', async () => { @@ -50,6 +51,72 @@ describe('Transpilers', () => { await compiler.compile(); }); + it('CubePropContextTranspiler with full path to userAttributes should work normally', async () => { + const { cubeEvaluator, compiler } = prepareJsCompiler(` + cube(\`Test\`, { + sql: 'SELECT * FROM users', + dimensions: { + userId: { + sql: \`userId\`, + type: 'string' + } + }, + accessPolicy: [ + { + role: \`*\`, + rowLevel: { + filters: [ + { + member: \`userId\`, + operator: \`equals\`, + values: [ securityContext.cubeCloud.userAttributes.userId ] + } + ] + } + } + ] + }) + `); + + await compiler.compile(); + + const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values; + expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.userAttributes.userId'); + }); + + it('CubePropContextTranspiler with shorthand userAttributes should work normally', async () => { + const { cubeEvaluator, compiler } = prepareJsCompiler(` + cube(\`Test\`, { + sql: 'SELECT * FROM users', + dimensions: { + userId: { + sql: \`userId\`, + type: 'string' + } + }, + accessPolicy: [ + { + role: \`*\`, + rowLevel: { + filters: [ + { + member: \`userId\`, + operator: \`equals\`, + values: [ userAttributes.userId ] + } + ] + } + } + ] + }) + `); + + await compiler.compile(); + + const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values; + expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.userAttributes.userId'); + }); + it('ImportExportTranspiler', async () => { const ieTranspiler = new ImportExportTranspiler(); const errorsReport = new ErrorReporter(); From ce8d67f845a7ff58d8772182e90ed8b2ae0cddab Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Mon, 25 Aug 2025 18:39:56 +0300 Subject: [PATCH 4/6] Rewrite shorthand for userAttributes to full path during transpilation --- .../transpilers/CubePropContextTranspiler.ts | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index b1fee72bf731f..13e9a189c13da 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -78,10 +78,11 @@ export class CubePropContextTranspiler implements TranspilerInterface { } public static replaceValueWithArrowFunction(resolveSymbol: SymbolResolver, value: NodePath) { - const knownIds = CubePropContextTranspiler.collectKnownIdentifiers( + const knownIds = CubePropContextTranspiler.collectKnownIdentifiersAndTransform( resolveSymbol, value, ); + value.replaceWith( t.arrowFunctionExpression( knownIds.map(i => t.identifier(i)), @@ -198,6 +199,25 @@ export class CubePropContextTranspiler implements TranspilerInterface { return R.uniq(identifiers); } + protected static collectKnownIdentifiersAndTransform(resolveSymbol: SymbolResolver, path: NodePath): string[] { + const identifiers: string[] = []; + + if (path.node.type === 'Identifier') { + CubePropContextTranspiler.matchAndPushIdentifier(path, resolveSymbol, identifiers); + } + + path.traverse({ + Identifier: (p) => { + CubePropContextTranspiler.matchAndTransformIdentifier(p, resolveSymbol, identifiers); + }, + MemberExpression: (p) => { + CubePropContextTranspiler.transformUserAttributesMemberExpression(p); + } + }); + + return R.uniq(identifiers); + } + protected static matchAndPushIdentifier(path, resolveSymbol: SymbolResolver, identifiers: string[]) { if ( (!path.parent || @@ -208,4 +228,33 @@ export class CubePropContextTranspiler implements TranspilerInterface { identifiers.push(path.node.name); } } + + protected static matchAndTransformIdentifier(path, resolveSymbol: SymbolResolver, identifiers: string[]) { + if ( + (!path.parent || + (path.parent.type !== 'MemberExpression' || path.parent.type === 'MemberExpression' && path.key !== 'property') + ) && + resolveSymbol(path.node.name) + ) { + // Special handling for userAttributes - replace in parameter list with securityContext + if (path.node.name === 'userAttributes') { + identifiers.push('securityContext'); + } else { + identifiers.push(path.node.name); + } + } + } + + protected static transformUserAttributesMemberExpression(path: NodePath) { + // Check if this is userAttributes.someProperty (object should be identifier named 'userAttributes') + if (t.isIdentifier(path.node.object, { name: 'userAttributes' })) { + // Replace userAttributes with securityContext.cubeCloud.userAttributes + const securityContext = t.identifier('securityContext'); + const cubeCloud = t.memberExpression(securityContext, t.identifier('cubeCloud')); + const userAttributes = t.memberExpression(cubeCloud, t.identifier('userAttributes')); + const newMemberExpression = t.memberExpression(userAttributes, path.node.property, path.node.computed); + + path.replaceWith(newMemberExpression); + } + } } From 2e43b8144c24135467ffd9635d44dada8c9b18c7 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Tue, 26 Aug 2025 15:05:55 +0300 Subject: [PATCH 5/6] remove unneded --- .../cubejs-schema-compiler/src/compiler/CubeSymbols.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts index 0e6c93e70cf44..5330b8f9120ec 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeSymbols.ts @@ -188,12 +188,7 @@ export const CONTEXT_SYMBOLS = { securityContext: 'securityContext', FILTER_PARAMS: 'filterParams', FILTER_GROUP: 'filterGroup', - SQL_UTILS: 'sqlUtils', - // This is syntax sugar for `securityContext.cubeCloud.userAttributes` - // while resolveSymbol won't return the exact path from securityContext - // it's not a problem, because this is only needed for the transpilation phase - // and then this shorthand will be transpiled with the full path - userAttributes: 'securityContext.cubeCloud.userAttributes' + SQL_UTILS: 'sqlUtils' }; export const CURRENT_CUBE_CONSTANTS = ['CUBE', 'TABLE']; From 607acabae20be9d16ee1c84d5f79b28b87fa915a Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Tue, 26 Aug 2025 16:00:40 +0300 Subject: [PATCH 6/6] reduce scope to ACL only --- .../transpilers/CubePropContextTranspiler.ts | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts index 13e9a189c13da..c3d57a19510b4 100644 --- a/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts +++ b/packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts @@ -101,14 +101,14 @@ 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); + const fullPath = CubePropContextTranspiler.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); + const fullPath = CubePropContextTranspiler.fullPath(path); // eslint-disable-next-line no-restricted-syntax for (const p of transpiledFieldsPatterns) { if (fullPath.match(p)) { @@ -154,7 +154,7 @@ export class CubePropContextTranspiler implements TranspilerInterface { valuePath.replaceWith(t.arrayExpression(elements)); } - protected fullPath(path: NodePath): string { + protected static fullPath(path: NodePath): string { // @ts-ignore let fp = path?.node?.key?.name || ''; let pp: NodePath | null | undefined = path?.parentPath; @@ -183,27 +183,11 @@ export class CubePropContextTranspiler implements TranspilerInterface { }; } - protected static collectKnownIdentifiers(resolveSymbol: SymbolResolver, path: NodePath): string[] { - const identifiers: string[] = []; - - if (path.node.type === 'Identifier') { - CubePropContextTranspiler.matchAndPushIdentifier(path, resolveSymbol, identifiers); - } - - path.traverse({ - Identifier: (p) => { - CubePropContextTranspiler.matchAndPushIdentifier(p, resolveSymbol, identifiers); - } - }); - - return R.uniq(identifiers); - } - protected static collectKnownIdentifiersAndTransform(resolveSymbol: SymbolResolver, path: NodePath): string[] { const identifiers: string[] = []; if (path.node.type === 'Identifier') { - CubePropContextTranspiler.matchAndPushIdentifier(path, resolveSymbol, identifiers); + CubePropContextTranspiler.matchAndTransformIdentifier(path, resolveSymbol, identifiers); } path.traverse({ @@ -218,17 +202,6 @@ export class CubePropContextTranspiler implements TranspilerInterface { return R.uniq(identifiers); } - protected static matchAndPushIdentifier(path, resolveSymbol: SymbolResolver, identifiers: string[]) { - if ( - (!path.parent || - (path.parent.type !== 'MemberExpression' || path.parent.type === 'MemberExpression' && path.key !== 'property') - ) && - resolveSymbol(path.node.name) - ) { - identifiers.push(path.node.name); - } - } - protected static matchAndTransformIdentifier(path, resolveSymbol: SymbolResolver, identifiers: string[]) { if ( (!path.parent || @@ -237,7 +210,8 @@ export class CubePropContextTranspiler implements TranspilerInterface { resolveSymbol(path.node.name) ) { // Special handling for userAttributes - replace in parameter list with securityContext - if (path.node.name === 'userAttributes') { + const fullPath = this.fullPath(path); + if (path.node.name === 'userAttributes' && (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))) { identifiers.push('securityContext'); } else { identifiers.push(path.node.name); @@ -247,7 +221,8 @@ export class CubePropContextTranspiler implements TranspilerInterface { protected static transformUserAttributesMemberExpression(path: NodePath) { // Check if this is userAttributes.someProperty (object should be identifier named 'userAttributes') - if (t.isIdentifier(path.node.object, { name: 'userAttributes' })) { + const fullPath = this.fullPath(path); + if (t.isIdentifier(path.node.object, { name: 'userAttributes' }) && (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))) { // Replace userAttributes with securityContext.cubeCloud.userAttributes const securityContext = t.identifier('securityContext'); const cubeCloud = t.memberExpression(securityContext, t.identifier('cubeCloud'));