YANG QIA b36586f324
fix(data-vi): fix issues when using external oracle data source for data visualization (#5436)
* fix(data-vi): fix issues when using external oracle data source for data visualization

* fix: bug

* fix: bug

* fix: oracle group by

* fix: oracle formatter
2024-10-23 10:18:58 +08:00

121 lines
3.9 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Context, Next } from '@nocobase/actions';
import { DimensionProps, MeasureProps, OrderProps } from '../types';
import { Formatter } from '../formatter/formatter';
import { Database } from '@nocobase/database';
const AllowedAggFuncs = ['sum', 'count', 'avg', 'min', 'max'];
export class QueryParser {
db: Database;
formatter: Formatter;
constructor(db: Database) {
this.db = db;
this.formatter = {
format: ({ field }) => db.sequelize.col(field),
} as Formatter;
}
parseMeasures(ctx: Context, measures: MeasureProps[]) {
let hasAgg = false;
const sequelize = this.db.sequelize;
const attributes = [];
const fieldMap = {};
measures.forEach((measure: MeasureProps & { field: string }) => {
const { field, aggregation, alias, distinct } = measure;
const attribute = [];
const col = sequelize.col(field);
if (aggregation) {
if (!AllowedAggFuncs.includes(aggregation)) {
throw new Error(`Invalid aggregation function: ${aggregation}`);
}
hasAgg = true;
attribute.push(sequelize.fn(aggregation, distinct ? sequelize.fn('DISTINCT', col) : col));
} else {
attribute.push(col);
}
if (alias) {
attribute.push(alias);
}
attributes.push(attribute.length > 1 ? attribute : attribute[0]);
fieldMap[alias || field] = measure;
});
return { attributes, fieldMap, hasAgg };
}
parseDimensions(ctx: Context, dimensions: (DimensionProps & { field: string })[], hasAgg: boolean, timezone: string) {
const sequelize = this.db.sequelize;
const attributes = [];
const group = [];
const fieldMap = {};
dimensions.forEach((dimension: DimensionProps & { field: string }) => {
const { field, format, alias, type, options } = dimension;
const attribute = [];
const col = sequelize.col(field);
if (format) {
attribute.push(this.formatter.format({ type, field, format, timezone, options }));
} else {
attribute.push(col);
}
if (alias) {
attribute.push(alias);
}
attributes.push(attribute.length > 1 ? attribute : attribute[0]);
if (hasAgg) {
group.push(attribute[0]);
}
fieldMap[alias || field] = dimension;
});
return { attributes, group, fieldMap };
}
parseOrders(ctx: Context, orders: OrderProps[], hasAgg: boolean) {
const sequelize = this.db.sequelize;
const order = [];
orders.forEach((item: OrderProps) => {
const alias = sequelize.getQueryInterface().quoteIdentifier(item.alias);
const name = hasAgg ? sequelize.literal(alias) : sequelize.col(item.field as string);
order.push([name, item.order || 'ASC']);
});
return order;
}
parse() {
return async (ctx: Context, next: Next) => {
const { measures, dimensions, orders, include, where, limit } = ctx.action.params.values;
const { attributes: measureAttributes, fieldMap: measureFieldMap, hasAgg } = this.parseMeasures(ctx, measures);
const {
attributes: dimensionAttributes,
group,
fieldMap: dimensionFieldMap,
} = this.parseDimensions(ctx, dimensions, hasAgg, ctx.timezone);
const order = this.parseOrders(ctx, orders, hasAgg);
ctx.action.params.values = {
...ctx.action.params.values,
queryParams: {
where,
attributes: [...measureAttributes, ...dimensionAttributes],
include,
group,
order,
limit: limit || 2000,
subQuery: false,
raw: true,
},
fieldMap: { ...measureFieldMap, ...dimensionFieldMap },
};
await next();
};
}
}