cube.js 上线文 filter 处理的原理
cube.js 支持基于上下文的filter 处理,核心是依赖了js 的proxy
参考filter 使用模式
cube(`demoapp`, {
sql: ` SELECT
*
FROM
transactions AS ts
where ${SECURITY_CONTEXT.user_id.filter("total_amount")}
`,
measures: {
average_spend_per_customer: {
sql: `${sum}/${count}`,
type: `number`
},
count: {
sql: `total_amount`,
type: `count`,
},
sum: {
sql: `total_amount`,
type: `sum`,
}
},
dimensions: {
total_amount: {
sql: `total_amount`,
type: `number`
},
customer_id: {
sql: `customer_id`,
type: `string`
},
transaction_date: {
sql: `transaction_date`,
type: `time`,
shown: false
},
event_id: {
sql: `event_id`,
type: `string`
}
}
});
简单说明
因为请求是基于api-gateway的,需要通过编译器进行预编译处理,之后会获取sql,然后进行执行
参考gateway 处理
const sqlQuery = await this.getCompilerApi(context).getSql(this.coerceForSqlQuery(normalizedQuery, context));
同时会使用到编译api 提供的withQuery 函数(进行维度以及数据的处理)
return compilers.compiler.withQuery(sqlGenerator, () => ({
external: sqlGenerator.externalPreAggregationQuery(),
sql: sqlGenerator.buildSqlAndParams(),
timeDimensionAlias: sqlGenerator.timeDimensions[0] && sqlGenerator.timeDimensions[0].unescapedAliasName(),
timeDimensionField: sqlGenerator.timeDimensions[0] && sqlGenerator.timeDimensions[0].dimension,
order: sqlGenerator.order,
cacheKeyQueries: sqlGenerator.cacheKeyQueries(),
preAggregations: sqlGenerator.preAggregations.preAggregationsDescription(),
dataSource: sqlGenerator.dataSource,
aliasNameToMember: sqlGenerator.aliasNameToMember,
rollupMatchResults: includeDebugInfo ?
sqlGenerator.preAggregations.rollupMatchResultDescriptions() : undefined,
canUseTransformedQuery: sqlGenerator.preAggregations.canUseTransformedQuery()
}));
核心代码说明
@cubejs-backend/schema-compiler/dust/src/adapter/BaseQuery.js
evaluateSql(cubeName, sql, options) {
options = options || {};
const self = this;
const { cubeEvaluator } = this;
this.pushCubeNameForCollectionIfNecessary(cubeName);
return cubeEvaluator.resolveSymbolsCall(sql, (name) => {
const nextCubeName = cubeEvaluator.symbols[name] && name || cubeName;
this.pushCubeNameForCollectionIfNecessary(nextCubeName);
const resolvedSymbol = cubeEvaluator.resolveSymbol(cubeName, name);
// eslint-disable-next-line no-underscore-dangle
if (resolvedSymbol._objectWithResolvedProperties) {
return resolvedSymbol;
}
return self.evaluateSymbolSql(nextCubeName, name, resolvedSymbol);
}, {
sqlResolveFn: options.sqlResolveFn || ((symbol, cube, n) => self.evaluateSymbolSql(cube, n, symbol)),
cubeAliasFn: self.cubeAlias.bind(self),
contextSymbols: this.parametrizedContextSymbols(),
query: this
});
}
parametrizedContextSymbols 函数
parametrizedContextSymbols() {
if (!this.parametrizedContextSymbolsValue) {
this.parametrizedContextSymbolsValue = Object.assign({
filterParams: this.filtersProxy(),
sqlUtils: {
convertTz: this.convertTz.bind(this)
}
}, ramda_1.default.map((symbols) => this.contextSymbolsProxy(symbols), this.contextSymbols));
}
return this.parametrizedContextSymbolsValue;
}
proxy 函数
contextSymbolsProxy(symbols) {
return new Proxy(symbols, {
get: (target, name) => {
const propValue = target[name];
const methods = (paramValue) => ({
filter: (column) => {
if (paramValue) {
const value = Array.isArray(paramValue) ?
paramValue.map(this.paramAllocator.allocateParam.bind(this.paramAllocator)) :
this.paramAllocator.allocateParam(paramValue);
if (typeof column === 'function') {
return column(value);
}
else {
return `${column} = ${value}`;
}
}
else {
return '1 = 1';
}
},
requiredFilter: (column) => {
if (!paramValue) {
throw new UserError_1.UserError(`Filter for ${column} is required`);
}
return methods(paramValue).filter(column);
},
unsafeValue: () => paramValue
});
return methods(target)[name] ||
typeof propValue === 'object' && this.contextSymbolsProxy(propValue) ||
methods(propValue);
}
});
}
filtersProxy() {
const { allFilters } = this;
return new Proxy({}, {
get: (target, name) => {
if (name === '_objectWithResolvedProperties') {
return true;
}
const cubeName = this.cubeEvaluator.cubeNameFromPath(name);
return new Proxy({ cube: cubeName }, {
get: (cubeNameObj, propertyName) => {
const filter = allFilters.find(f => f.dimension === this.cubeEvaluator.pathFromArray([cubeNameObj.cube, propertyName]));
return {
filter: (column) => {
const filterParams = filter && filter.filterParams();
if (filterParams && filterParams.length) {
if (typeof column === 'function') {
// eslint-disable-next-line prefer-spread
return column.apply(null, filterParams.map(this.paramAllocator.allocateParam.bind(this.paramAllocator)));
}
else {
return filter.conditionSql(column);
}
}
else {
return '1 = 1';
}
}
};
}
});
}
});
}
cube.js 基于babel 的一些扩展
同时为了进行sql 函数的生成,cube.js 基于babel 开发了好多transpiler,比如import 的prop,validator 的
比如一些通用的属性转sql 函数的处理
protected knownIdentifiersInjectVisitor(field, resolveSymbol) {
const self = this;
return {
ObjectProperty(path) {
if (path.node.key.type === 'Identifier' && path.node.key.name.match(field)) {
const knownIds = self.collectKnownIdentifiers(
resolveSymbol,
path.get('value')
);
path.get('value').replaceWith(
t.arrowFunctionExpression(knownIds.map(i => t.identifier(i)), path.node.value, false)
);
}
}
};
}
编译使用babel 扩展
transpileFile(file, errorsReport) {
try {
const ast = parse(
file.content,
{
sourceFilename: file.fileName,
sourceType: 'module',
plugins: ['objectRestSpread']
},
);
// 此处使用了编写的babel扩展
this.transpilers.forEach((t) => {
errorsReport.inFile(file);
babelTraverse(ast, t.traverseObject(errorsReport));
errorsReport.exitFile();
});
const content = babelGenerator(ast, {}, file.content).code;
return Object.assign({}, file, { content });
} catch (e) {
if (e.toString().indexOf('SyntaxError') !== -1) {
const line = file.content.split('\n')[e.loc.line - 1];
const spaces = Array(e.loc.column).fill(' ').join('');
errorsReport.error(`Syntax error during '${file.fileName}' parsing: ${e.message}:\n${line}\n${spaces}^`);
} else {
errorsReport.error(e);
}
}
说明
以上是一个简单的使用说明,具体的可以多看看源码