Skip to content

Commit ffc852e

Browse files
KSDaemonigorlukanin
authored andcommitted
feat(schema-compiler): Support shorthand for userAttributes properties in models (#9921)
* more types (SymbolResolver) * add userAttributes to CONTEXT_SYMBOLS * add tests for CubePropContextTranspiler with userAttributes * Rewrite shorthand for userAttributes to full path during transpilation * remove unneded * reduce scope to ACL only
1 parent 15dacc2 commit ffc852e

File tree

3 files changed

+109
-15
lines changed

3 files changed

+109
-15
lines changed

packages/cubejs-schema-compiler/src/compiler/transpilers/CubePropContextTranspiler.ts

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import R from 'ramda';
33

44
import type { NodePath } from '@babel/traverse';
55
import {
6+
SymbolResolver,
67
TranspilerCubeResolver,
78
TranspilerInterface,
89
TranspilerSymbolResolver,
@@ -59,7 +60,7 @@ export class CubePropContextTranspiler implements TranspilerInterface {
5960
args[0].node.type === 'TemplateLiteral' &&
6061
args[0].node.quasis.length &&
6162
args[0].node.quasis[0].value.cooked;
62-
args[args.length - 1].traverse(this.sqlAndReferencesFieldVisitor(cubeName));
63+
args[args.length - 1].traverse(this.sqlAndReferencesFieldVisitor(cubeName as string));
6364
args[args.length - 1].traverse(
6465
this.knownIdentifiersInjectVisitor('extends', name => this.cubeDictionary.resolveCube(name))
6566
);
@@ -72,15 +73,16 @@ export class CubePropContextTranspiler implements TranspilerInterface {
7273
};
7374
}
7475

75-
protected transformObjectProperty(path: NodePath<t.ObjectProperty>, resolveSymbol: (name: string) => void) {
76+
protected transformObjectProperty(path: NodePath<t.ObjectProperty>, resolveSymbol: SymbolResolver) {
7677
CubePropContextTranspiler.replaceValueWithArrowFunction(resolveSymbol, path.get('value'));
7778
}
7879

79-
public static replaceValueWithArrowFunction(resolveSymbol: (name: string) => any, value: NodePath<any>) {
80-
const knownIds = CubePropContextTranspiler.collectKnownIdentifiers(
80+
public static replaceValueWithArrowFunction(resolveSymbol: SymbolResolver, value: NodePath<any>) {
81+
const knownIds = CubePropContextTranspiler.collectKnownIdentifiersAndTransform(
8182
resolveSymbol,
8283
value,
8384
);
85+
8486
value.replaceWith(
8587
t.arrowFunctionExpression(
8688
knownIds.map(i => t.identifier(i)),
@@ -91,22 +93,22 @@ export class CubePropContextTranspiler implements TranspilerInterface {
9193
);
9294
}
9395

94-
protected sqlAndReferencesFieldVisitor(cubeName): TraverseObject {
96+
protected sqlAndReferencesFieldVisitor(cubeName: string | null | undefined): TraverseObject {
9597
const resolveSymbol = n => this.viewCompiler.resolveSymbol(cubeName, n) ||
9698
this.cubeSymbols.resolveSymbol(cubeName, n) ||
9799
this.cubeSymbols.isCurrentCube(n);
98100

99101
return {
100102
ObjectProperty: (path) => {
101103
if (path.node.key.type === 'Identifier' && path.node.key.name === 'joins' && t.isObjectExpression(path.node.value)) {
102-
const fullPath = this.fullPath(path);
104+
const fullPath = CubePropContextTranspiler.fullPath(path);
103105
if (fullPath === 'joins') {
104106
this.convertJoinsObjectToArray(path);
105107
}
106108
}
107109

108110
if (path.node.key.type === 'Identifier' && transpiledFields.has(path.node.key.name)) {
109-
const fullPath = this.fullPath(path);
111+
const fullPath = CubePropContextTranspiler.fullPath(path);
110112
// eslint-disable-next-line no-restricted-syntax
111113
for (const p of transpiledFieldsPatterns) {
112114
if (fullPath.match(p)) {
@@ -152,7 +154,7 @@ export class CubePropContextTranspiler implements TranspilerInterface {
152154
valuePath.replaceWith(t.arrayExpression(elements));
153155
}
154156

155-
protected fullPath(path: NodePath<t.ObjectProperty>): string {
157+
protected static fullPath(path: NodePath): string {
156158
// @ts-ignore
157159
let fp = path?.node?.key?.name || '';
158160
let pp: NodePath<t.Node> | null | undefined = path?.parentPath;
@@ -171,7 +173,7 @@ export class CubePropContextTranspiler implements TranspilerInterface {
171173
return fp;
172174
}
173175

174-
protected knownIdentifiersInjectVisitor(field: RegExp | string, resolveSymbol: (name: string) => void): TraverseObject {
176+
protected knownIdentifiersInjectVisitor(field: RegExp | string, resolveSymbol: SymbolResolver): TraverseObject {
175177
return {
176178
ObjectProperty: (path) => {
177179
if (path.node.key.type === 'Identifier' && path.node.key.name.match(field)) {
@@ -181,30 +183,53 @@ export class CubePropContextTranspiler implements TranspilerInterface {
181183
};
182184
}
183185

184-
protected static collectKnownIdentifiers(resolveSymbol, path: NodePath) {
185-
const identifiers = [];
186+
protected static collectKnownIdentifiersAndTransform(resolveSymbol: SymbolResolver, path: NodePath): string[] {
187+
const identifiers: string[] = [];
186188

187189
if (path.node.type === 'Identifier') {
188-
CubePropContextTranspiler.matchAndPushIdentifier(path, resolveSymbol, identifiers);
190+
CubePropContextTranspiler.matchAndTransformIdentifier(path, resolveSymbol, identifiers);
189191
}
190192

191193
path.traverse({
192194
Identifier: (p) => {
193-
CubePropContextTranspiler.matchAndPushIdentifier(p, resolveSymbol, identifiers);
195+
CubePropContextTranspiler.matchAndTransformIdentifier(p, resolveSymbol, identifiers);
196+
},
197+
MemberExpression: (p) => {
198+
CubePropContextTranspiler.transformUserAttributesMemberExpression(p);
194199
}
195200
});
196201

197202
return R.uniq(identifiers);
198203
}
199204

200-
protected static matchAndPushIdentifier(path, resolveSymbol, identifiers) {
205+
protected static matchAndTransformIdentifier(path, resolveSymbol: SymbolResolver, identifiers: string[]) {
201206
if (
202207
(!path.parent ||
203208
(path.parent.type !== 'MemberExpression' || path.parent.type === 'MemberExpression' && path.key !== 'property')
204209
) &&
205210
resolveSymbol(path.node.name)
206211
) {
207-
identifiers.push(path.node.name);
212+
// Special handling for userAttributes - replace in parameter list with securityContext
213+
const fullPath = this.fullPath(path);
214+
if (path.node.name === 'userAttributes' && (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))) {
215+
identifiers.push('securityContext');
216+
} else {
217+
identifiers.push(path.node.name);
218+
}
219+
}
220+
}
221+
222+
protected static transformUserAttributesMemberExpression(path: NodePath<t.MemberExpression>) {
223+
// Check if this is userAttributes.someProperty (object should be identifier named 'userAttributes')
224+
const fullPath = this.fullPath(path);
225+
if (t.isIdentifier(path.node.object, { name: 'userAttributes' }) && (fullPath.startsWith('accessPolicy') || fullPath.startsWith('access_policy'))) {
226+
// Replace userAttributes with securityContext.cubeCloud.userAttributes
227+
const securityContext = t.identifier('securityContext');
228+
const cubeCloud = t.memberExpression(securityContext, t.identifier('cubeCloud'));
229+
const userAttributes = t.memberExpression(cubeCloud, t.identifier('userAttributes'));
230+
const newMemberExpression = t.memberExpression(userAttributes, path.node.property, path.node.computed);
231+
232+
path.replaceWith(newMemberExpression);
208233
}
209234
}
210235
}

packages/cubejs-schema-compiler/src/compiler/transpilers/transpiler.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ export interface TranspilerSymbolResolver {
1515
export interface TranspilerCubeResolver {
1616
resolveCube(name): boolean;
1717
}
18+
19+
export type SymbolResolver = (name: string) => any;

packages/cubejs-schema-compiler/test/unit/transpilers.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import babelTraverse from '@babel/traverse';
55
import { prepareJsCompiler } from './PrepareCompiler';
66
import { ImportExportTranspiler } from '../../src/compiler/transpilers';
77
import { ErrorReporter } from '../../src/compiler/ErrorReporter';
8+
import { PostgresQuery } from '../../src';
89

910
describe('Transpilers', () => {
1011
it('CubeCheckDuplicatePropTranspiler', async () => {
@@ -50,6 +51,72 @@ describe('Transpilers', () => {
5051
await compiler.compile();
5152
});
5253

54+
it('CubePropContextTranspiler with full path to userAttributes should work normally', async () => {
55+
const { cubeEvaluator, compiler } = prepareJsCompiler(`
56+
cube(\`Test\`, {
57+
sql: 'SELECT * FROM users',
58+
dimensions: {
59+
userId: {
60+
sql: \`userId\`,
61+
type: 'string'
62+
}
63+
},
64+
accessPolicy: [
65+
{
66+
role: \`*\`,
67+
rowLevel: {
68+
filters: [
69+
{
70+
member: \`userId\`,
71+
operator: \`equals\`,
72+
values: [ securityContext.cubeCloud.userAttributes.userId ]
73+
}
74+
]
75+
}
76+
}
77+
]
78+
})
79+
`);
80+
81+
await compiler.compile();
82+
83+
const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values;
84+
expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.userAttributes.userId');
85+
});
86+
87+
it('CubePropContextTranspiler with shorthand userAttributes should work normally', async () => {
88+
const { cubeEvaluator, compiler } = prepareJsCompiler(`
89+
cube(\`Test\`, {
90+
sql: 'SELECT * FROM users',
91+
dimensions: {
92+
userId: {
93+
sql: \`userId\`,
94+
type: 'string'
95+
}
96+
},
97+
accessPolicy: [
98+
{
99+
role: \`*\`,
100+
rowLevel: {
101+
filters: [
102+
{
103+
member: \`userId\`,
104+
operator: \`equals\`,
105+
values: [ userAttributes.userId ]
106+
}
107+
]
108+
}
109+
}
110+
]
111+
})
112+
`);
113+
114+
await compiler.compile();
115+
116+
const transpiledValues = cubeEvaluator.cubeFromPath('Test').accessPolicy?.[0].rowLevel?.filters?.[0].values;
117+
expect(transpiledValues.toString()).toMatch('securityContext.cubeCloud.userAttributes.userId');
118+
});
119+
53120
it('ImportExportTranspiler', async () => {
54121
const ieTranspiler = new ImportExportTranspiler();
55122
const errorsReport = new ErrorReporter();

0 commit comments

Comments
 (0)