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);
      }
    }

说明

以上是一个简单的使用说明,具体的可以多看看源码

参考资料

https://github.com/cube-js/cube.js

posted on 2021-02-02 20:46  荣锋亮  阅读(198)  评论(0编辑  收藏  举报

导航