You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
797 lines
29 KiB
797 lines
29 KiB
'use strict';
|
|
|
|
const { previewDataSourceQuery } = require('./dataSourceQuery');
|
|
|
|
const PREVIEW_GROUPED_X_ALIAS = '__preview_x_group_array__';
|
|
const PREVIEW_GROUPED_Y_ALIAS = '__preview_y_group_array__';
|
|
const MAX_STORED_PREVIEW_POINTS = 100000;
|
|
|
|
const normalizeOptionalString = value => {
|
|
const normalized = String(value || '').trim();
|
|
return normalized || null;
|
|
};
|
|
|
|
const ensureObject = value => {
|
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
};
|
|
|
|
const deepCloneJson = value => {
|
|
if (value === null || value === undefined) return value;
|
|
return JSON.parse(JSON.stringify(value));
|
|
};
|
|
|
|
const normalizeFilterSignature = filter => {
|
|
const field = normalizeOptionalString(filter?.field);
|
|
if (!field) return null;
|
|
const operator = normalizeOptionalString(filter?.operator) || '=';
|
|
const value = Object.prototype.hasOwnProperty.call(filter || {}, 'value')
|
|
? deepCloneJson(filter.value)
|
|
: null;
|
|
return JSON.stringify({ field, operator, value });
|
|
};
|
|
|
|
const mergeFilterLists = (...filterLists) => {
|
|
const result = [];
|
|
const seen = new Set();
|
|
filterLists.forEach(filters => {
|
|
(Array.isArray(filters) ? filters : []).forEach(filter => {
|
|
const signature = normalizeFilterSignature(filter);
|
|
if (!signature || seen.has(signature)) return;
|
|
seen.add(signature);
|
|
result.push(deepCloneJson(filter));
|
|
});
|
|
});
|
|
return result;
|
|
};
|
|
|
|
const collectCommonMetricFilters = metricList => {
|
|
const safeMetrics = Array.isArray(metricList) ? metricList : [];
|
|
if (!safeMetrics.length) return [];
|
|
const firstFilters = Array.isArray(safeMetrics[0]?.filters) ? safeMetrics[0].filters : [];
|
|
return firstFilters.filter(filter => {
|
|
const signature = normalizeFilterSignature(filter);
|
|
if (!signature) return false;
|
|
return safeMetrics.every(metric => {
|
|
const currentFilters = Array.isArray(metric?.filters) ? metric.filters : [];
|
|
return currentFilters.some(item => normalizeFilterSignature(item) === signature);
|
|
});
|
|
}).map(filter => deepCloneJson(filter));
|
|
};
|
|
|
|
const normalizeTimeRangeSignature = value => {
|
|
const timeRange = ensureObject(value);
|
|
const field = normalizeOptionalString(timeRange?.field);
|
|
const start = normalizeOptionalString(timeRange?.start);
|
|
const end = normalizeOptionalString(timeRange?.end);
|
|
if (!field || !start || !end) return null;
|
|
return JSON.stringify({ field, start, end });
|
|
};
|
|
|
|
const collectCommonMetricTimeRange = metricList => {
|
|
const safeMetrics = Array.isArray(metricList) ? metricList : [];
|
|
if (!safeMetrics.length) return null;
|
|
const firstRange = ensureObject(safeMetrics[0]?.timeRange);
|
|
const firstSignature = normalizeTimeRangeSignature(firstRange);
|
|
if (!firstSignature) return null;
|
|
const isCommon = safeMetrics.every(metric => (
|
|
normalizeTimeRangeSignature(metric?.timeRange) === firstSignature
|
|
));
|
|
return isCommon ? deepCloneJson(firstRange) : null;
|
|
};
|
|
|
|
const uniqueStringList = values => {
|
|
const result = [];
|
|
const seen = new Set();
|
|
(Array.isArray(values) ? values : []).forEach(value => {
|
|
const normalized = normalizeOptionalString(value);
|
|
if (!normalized || seen.has(normalized)) return;
|
|
seen.add(normalized);
|
|
result.push(normalized);
|
|
});
|
|
return result;
|
|
};
|
|
|
|
const normalizeChartPoints = (points = []) => {
|
|
return (Array.isArray(points) ? points : [])
|
|
.map((item, index) => {
|
|
const yValue = Number(item?.y);
|
|
if (!Number.isFinite(yValue)) return null;
|
|
const xValue = item?.x == null ? `${index + 1}` : String(item.x);
|
|
return { x: xValue, y: yValue };
|
|
})
|
|
.filter(Boolean)
|
|
.slice(0, MAX_STORED_PREVIEW_POINTS);
|
|
};
|
|
|
|
const normalizeChartSeries = (seriesList = []) => {
|
|
return (Array.isArray(seriesList) ? seriesList : [])
|
|
.map((series, index) => {
|
|
const points = normalizeChartPoints(series?.points || []);
|
|
if (!points.length) return null;
|
|
return {
|
|
name: normalizeOptionalString(series?.name) || `系列${index + 1}`,
|
|
points,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const normalizeChartContentSnapshot = snapshot => {
|
|
const safeSnapshot = ensureObject(snapshot) || {};
|
|
const seriesList = normalizeChartSeries(
|
|
safeSnapshot.seriesList
|
|
|| safeSnapshot.previewSeries
|
|
|| []
|
|
);
|
|
const points = normalizeChartPoints(
|
|
safeSnapshot.points
|
|
|| safeSnapshot.previewPoints
|
|
|| seriesList[0]?.points
|
|
|| []
|
|
);
|
|
if (!seriesList.length) return null;
|
|
return {
|
|
kind: 'chart',
|
|
seriesList,
|
|
points: points.length ? points : (seriesList[0]?.points || []),
|
|
};
|
|
};
|
|
|
|
const buildGroupedChartSeries = (rows = [], xAlias, yAlias, groupByField) => {
|
|
const seriesList = (Array.isArray(rows) ? rows : [])
|
|
.map((row, rowIndex) => {
|
|
const xArray = Array.isArray(row?.[xAlias]) ? row[xAlias] : [];
|
|
const yArray = Array.isArray(row?.[yAlias]) ? row[yAlias] : [];
|
|
const maxLength = Math.min(xArray.length, yArray.length);
|
|
if (!maxLength) return null;
|
|
|
|
const points = Array.from({ length: maxLength })
|
|
.map((_, index) => {
|
|
const xRaw = xArray[index];
|
|
const yValue = Number(yArray[index]);
|
|
if (!Number.isFinite(yValue)) return null;
|
|
return {
|
|
x: xRaw == null ? `${index + 1}` : String(xRaw),
|
|
y: yValue,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
if (!points.length) return null;
|
|
|
|
const rawName = row?.[groupByField];
|
|
const name = normalizeOptionalString(rawName) || `分组${rowIndex + 1}`;
|
|
return { name, points };
|
|
})
|
|
.filter(Boolean);
|
|
|
|
return normalizeChartSeries(seriesList);
|
|
};
|
|
|
|
const buildDetailChartSeries = (rows = [], xField, yField) => {
|
|
const points = (Array.isArray(rows) ? rows : [])
|
|
.map((row, rowIndex) => {
|
|
const yValue = Number(row?.[yField]);
|
|
if (!Number.isFinite(yValue)) return null;
|
|
const xRaw = row?.[xField];
|
|
return {
|
|
x: xRaw == null ? `${rowIndex + 1}` : String(xRaw),
|
|
y: yValue,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
|
|
if (!points.length) return [];
|
|
return normalizeChartSeries([{ name: '数据', points }]);
|
|
};
|
|
|
|
const deriveTableId = config => {
|
|
return normalizeOptionalString(
|
|
config?.previewQuery?.table
|
|
|| config?.query?.table
|
|
|| config?.query?.tableId
|
|
|| config?.query?.tableName
|
|
|| config?.table
|
|
|| config?.tableId
|
|
|| config?.tableName
|
|
);
|
|
};
|
|
|
|
const deriveGroupByField = config => {
|
|
const direct = normalizeOptionalString(config?.groupByField);
|
|
if (direct) return direct;
|
|
const groupByList = uniqueStringList(config?.previewQuery?.groupBy || config?.groupBy || config?.query?.groupBy);
|
|
return groupByList[0] || null;
|
|
};
|
|
|
|
const normalizeTableColumns = (columns = []) => {
|
|
return (Array.isArray(columns) ? columns : [])
|
|
.map((item, index) => {
|
|
const field = normalizeOptionalString(item?.field || item?.sourceField);
|
|
const title = normalizeOptionalString(item?.title || item?.label);
|
|
const calcMode = normalizeOptionalString(item?.calcMode || item?.aggregation) || 'none';
|
|
if (!field && !title) return null;
|
|
return {
|
|
id: normalizeOptionalString(item?.id) || `table-col-${index + 1}`,
|
|
title: title || (field === '__index__' ? '序号' : field),
|
|
field,
|
|
calcMode,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const buildTablePreviewDslFromConfig = config => {
|
|
const previewQuery = ensureObject(config?.previewQuery);
|
|
if (previewQuery) return deepCloneJson(previewQuery);
|
|
|
|
const baseQuery = ensureObject(config?.query) || ensureObject(config) || {};
|
|
const table = deriveTableId(config);
|
|
if (!table) throw '表格块缺少预览查询表/数据集';
|
|
|
|
const tableColumns = normalizeTableColumns(
|
|
config?.tableColumns || config?.previewColumns || []
|
|
);
|
|
const selectFieldSet = new Set(
|
|
uniqueStringList(baseQuery.selectFields || config?.selectFields || config?.fields)
|
|
);
|
|
tableColumns.forEach(item => {
|
|
const field = normalizeOptionalString(item?.field);
|
|
if (field && field !== '__index__') selectFieldSet.add(field);
|
|
});
|
|
|
|
const filters = Array.isArray(baseQuery.filters)
|
|
? deepCloneJson(baseQuery.filters)
|
|
: Array.isArray(config?.filters)
|
|
? deepCloneJson(config.filters)
|
|
: [];
|
|
const timeRange = ensureObject(baseQuery.timeRange) || ensureObject(config?.timeRange);
|
|
filters.forEach(item => {
|
|
const field = normalizeOptionalString(item?.field);
|
|
if (field) selectFieldSet.add(field);
|
|
});
|
|
if (timeRange?.field) {
|
|
const timeField = normalizeOptionalString(timeRange.field);
|
|
if (timeField) selectFieldSet.add(timeField);
|
|
}
|
|
|
|
const limit = Number(
|
|
baseQuery.limit || config?.limit || 20
|
|
);
|
|
const dsl = {
|
|
table,
|
|
selectFields: Array.from(selectFieldSet).filter(Boolean),
|
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
|
|
};
|
|
if (!dsl.selectFields.length) throw '表格块缺少预览查询字段配置';
|
|
if (Array.isArray(baseQuery.orderBy) && baseQuery.orderBy.length) {
|
|
dsl.orderBy = deepCloneJson(baseQuery.orderBy);
|
|
}
|
|
if (filters.length) dsl.filters = filters;
|
|
if (timeRange) dsl.timeRange = deepCloneJson(timeRange);
|
|
return dsl;
|
|
};
|
|
|
|
const buildKpiPreviewDslFromConfig = config => {
|
|
const previewQuery = ensureObject(config?.previewQuery);
|
|
const baseQuery = previewQuery || ensureObject(config?.query) || ensureObject(config) || {};
|
|
const table = deriveTableId(config);
|
|
if (!table) throw 'KPI 块缺少预览查询表/数据集';
|
|
|
|
const metricList = Array.isArray(config?.kpiMetrics) ? config.kpiMetrics : [];
|
|
const selectFieldSet = new Set(
|
|
uniqueStringList(baseQuery.selectFields || config?.selectFields || config?.fields)
|
|
);
|
|
const filters = mergeFilterLists(
|
|
baseQuery.filters,
|
|
config?.filters,
|
|
collectCommonMetricFilters(metricList),
|
|
);
|
|
const timeRange = (
|
|
ensureObject(baseQuery.timeRange)
|
|
|| ensureObject(config?.timeRange)
|
|
|| collectCommonMetricTimeRange(metricList)
|
|
);
|
|
|
|
metricList.forEach(metric => {
|
|
const field = normalizeOptionalString(metric?.field);
|
|
if (field) selectFieldSet.add(field);
|
|
|
|
(Array.isArray(metric?.filters) ? metric.filters : []).forEach(item => {
|
|
const filterField = normalizeOptionalString(item?.field);
|
|
if (filterField) selectFieldSet.add(filterField);
|
|
});
|
|
|
|
const metricTimeRange = ensureObject(metric?.timeRange);
|
|
if (metricTimeRange?.field) {
|
|
const timeField = normalizeOptionalString(metricTimeRange.field);
|
|
if (timeField) selectFieldSet.add(timeField);
|
|
}
|
|
});
|
|
|
|
filters.forEach(item => {
|
|
const field = normalizeOptionalString(item?.field);
|
|
if (field) selectFieldSet.add(field);
|
|
});
|
|
if (timeRange?.field) {
|
|
const timeField = normalizeOptionalString(timeRange.field);
|
|
if (timeField) selectFieldSet.add(timeField);
|
|
}
|
|
|
|
const limit = Number(baseQuery.limit || config?.limit || 100000);
|
|
const dsl = {
|
|
table,
|
|
selectFields: Array.from(selectFieldSet).filter(Boolean),
|
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 100000,
|
|
};
|
|
if (!dsl.selectFields.length) throw 'KPI 块缺少预览查询字段配置';
|
|
if (Array.isArray(baseQuery.orderBy) && baseQuery.orderBy.length) {
|
|
dsl.orderBy = deepCloneJson(baseQuery.orderBy);
|
|
}
|
|
if (filters.length) dsl.filters = filters;
|
|
if (timeRange) dsl.timeRange = deepCloneJson(timeRange);
|
|
return dsl;
|
|
};
|
|
|
|
const buildKpiMetricPreviewDslFromConfig = (config, metric = {}) => {
|
|
const previewQuery = ensureObject(config?.previewQuery);
|
|
const baseQuery = previewQuery || ensureObject(config?.query) || ensureObject(config) || {};
|
|
const table = deriveTableId(config);
|
|
if (!table) throw 'KPI 块缺少预览查询表/数据集';
|
|
|
|
const field = normalizeOptionalString(metric?.field);
|
|
if (!field) throw 'KPI 指标缺少字段配置';
|
|
|
|
const selectFieldSet = new Set(
|
|
uniqueStringList(baseQuery.selectFields || config?.selectFields || config?.fields)
|
|
);
|
|
selectFieldSet.add(field);
|
|
|
|
const filters = mergeFilterLists(baseQuery.filters, config?.filters, metric?.filters);
|
|
filters.forEach(item => {
|
|
const filterField = normalizeOptionalString(item?.field);
|
|
if (filterField) selectFieldSet.add(filterField);
|
|
});
|
|
|
|
const baseTimeRange = ensureObject(baseQuery.timeRange) || ensureObject(config?.timeRange);
|
|
const metricTimeRange = ensureObject(metric?.timeRange);
|
|
const previewTimeRange = baseTimeRange || metricTimeRange || null;
|
|
[baseTimeRange, metricTimeRange].forEach(item => {
|
|
const timeField = normalizeOptionalString(item?.field);
|
|
if (timeField) selectFieldSet.add(timeField);
|
|
});
|
|
|
|
const limit = Number(baseQuery.limit || config?.limit || 100000);
|
|
const dsl = {
|
|
table,
|
|
selectFields: Array.from(selectFieldSet).filter(Boolean),
|
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 100000,
|
|
};
|
|
if (!dsl.selectFields.length) throw 'KPI 块缺少预览查询字段配置';
|
|
if (Array.isArray(baseQuery.orderBy) && baseQuery.orderBy.length) {
|
|
dsl.orderBy = deepCloneJson(baseQuery.orderBy);
|
|
}
|
|
if (filters.length) dsl.filters = filters;
|
|
if (previewTimeRange) dsl.timeRange = deepCloneJson(previewTimeRange);
|
|
return dsl;
|
|
};
|
|
|
|
const buildPreviewDslFromConfig = config => {
|
|
const previewQuery = ensureObject(config?.previewQuery);
|
|
if (previewQuery) return deepCloneJson(previewQuery);
|
|
|
|
const baseQuery = ensureObject(config?.query) || ensureObject(config) || {};
|
|
const table = deriveTableId(config);
|
|
if (!table) throw '图表块缺少预览查询表/数据集';
|
|
|
|
const xField = normalizeOptionalString(config?.xField);
|
|
const yField = normalizeOptionalString(config?.yField);
|
|
if (!yField) throw '图表块缺少 Y 轴字段配置';
|
|
|
|
const filters = Array.isArray(baseQuery.filters)
|
|
? deepCloneJson(baseQuery.filters)
|
|
: Array.isArray(config?.filters)
|
|
? deepCloneJson(config.filters)
|
|
: [];
|
|
const timeRange = ensureObject(baseQuery.timeRange) || ensureObject(config?.timeRange);
|
|
const groupByField = deriveGroupByField(config);
|
|
const limit = Number(baseQuery.limit || config?.limit || 100000);
|
|
|
|
const dsl = {
|
|
table,
|
|
selectFields: [],
|
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 100000,
|
|
};
|
|
|
|
if (groupByField) {
|
|
if (!xField) throw '图表块缺少 X 轴字段配置';
|
|
dsl.selectFields = [groupByField];
|
|
dsl.groupBy = [groupByField];
|
|
dsl.aggregations = [
|
|
{ func: 'groupArray', field: xField, alias: PREVIEW_GROUPED_X_ALIAS },
|
|
{ func: 'groupArray', field: yField, alias: PREVIEW_GROUPED_Y_ALIAS },
|
|
];
|
|
dsl.sourceOrderBy = [{ field: groupByField, direction: 'ASC' }];
|
|
if (xField !== groupByField) {
|
|
dsl.sourceOrderBy.push({ field: xField, direction: 'ASC' });
|
|
}
|
|
dsl.orderBy = [{ field: groupByField, direction: 'ASC' }];
|
|
} else {
|
|
const selectFieldSet = new Set(uniqueStringList(baseQuery.selectFields || config?.selectFields || config?.fields));
|
|
if (xField) selectFieldSet.add(xField);
|
|
selectFieldSet.add(yField);
|
|
filters.forEach(item => {
|
|
const field = normalizeOptionalString(item?.field);
|
|
if (field) selectFieldSet.add(field);
|
|
});
|
|
if (timeRange?.field) {
|
|
const timeField = normalizeOptionalString(timeRange.field);
|
|
if (timeField) selectFieldSet.add(timeField);
|
|
}
|
|
dsl.selectFields = Array.from(selectFieldSet);
|
|
if (xField || yField) {
|
|
dsl.orderBy = [{ field: xField || yField, direction: 'ASC' }];
|
|
}
|
|
}
|
|
|
|
if (!dsl.selectFields.length) throw '图表块缺少预览查询字段配置';
|
|
if (filters.length) dsl.filters = filters;
|
|
if (timeRange) dsl.timeRange = deepCloneJson(timeRange);
|
|
return dsl;
|
|
};
|
|
|
|
const buildChartSeries = (rows = [], config, previewQuery) => {
|
|
const xField = normalizeOptionalString(config?.xField);
|
|
const yField = normalizeOptionalString(config?.yField);
|
|
if (!yField) throw '图表块缺少 Y 轴字段配置';
|
|
|
|
const aggregationList = Array.isArray(previewQuery?.aggregations)
|
|
? previewQuery.aggregations
|
|
: [];
|
|
const hasGroupedAggregation =
|
|
aggregationList.some(item => normalizeOptionalString(item?.alias) === PREVIEW_GROUPED_X_ALIAS)
|
|
&& aggregationList.some(item => normalizeOptionalString(item?.alias) === PREVIEW_GROUPED_Y_ALIAS);
|
|
|
|
if (hasGroupedAggregation) {
|
|
const groupByField = deriveGroupByField(config);
|
|
if (!groupByField) throw '图表块缺少分组字段配置';
|
|
return buildGroupedChartSeries(
|
|
rows,
|
|
PREVIEW_GROUPED_X_ALIAS,
|
|
PREVIEW_GROUPED_Y_ALIAS,
|
|
groupByField
|
|
);
|
|
}
|
|
|
|
return buildDetailChartSeries(rows, xField, yField);
|
|
};
|
|
|
|
const sanitizeQueryBlockConfig = rawConfig => {
|
|
const config = ensureObject(rawConfig);
|
|
if (!config) return {};
|
|
const nextConfig = deepCloneJson(config) || {};
|
|
delete nextConfig.previewSeries;
|
|
delete nextConfig.previewPoints;
|
|
delete nextConfig.previewRows;
|
|
delete nextConfig.previewColumns;
|
|
delete nextConfig.previewTotal;
|
|
return nextConfig;
|
|
};
|
|
|
|
const computeAggregateValue = (rows = [], field = '', calcMode = 'none') => {
|
|
const normalizedField = normalizeOptionalString(field);
|
|
if (!normalizedField || calcMode === 'none') return null;
|
|
|
|
const values = (Array.isArray(rows) ? rows : [])
|
|
.map(row => Number(row?.[normalizedField]))
|
|
.filter(value => Number.isFinite(value));
|
|
if (!values.length) return null;
|
|
|
|
if (calcMode === 'sum') {
|
|
return values.reduce((sum, value) => sum + value, 0);
|
|
}
|
|
if (calcMode === 'avg') {
|
|
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
}
|
|
if (calcMode === 'max') return Math.max(...values);
|
|
if (calcMode === 'min') return Math.min(...values);
|
|
if (calcMode === 'count') return values.length;
|
|
if (calcMode === 'latest') return values[values.length - 1];
|
|
return null;
|
|
};
|
|
|
|
const buildTableSummary = (columns = [], rows = []) => {
|
|
const safeColumns = Array.isArray(columns) ? columns : [];
|
|
if (!safeColumns.length) return null;
|
|
|
|
let hasComputedCell = false;
|
|
const cells = safeColumns.map((column, index) => {
|
|
const field = normalizeOptionalString(column?.field || column?.sourceField);
|
|
const calcMode = normalizeOptionalString(column?.calcMode || column?.aggregation) || 'none';
|
|
if (index === 0 && field === '__index__') return '汇总';
|
|
if (!field || field === '__index__' || calcMode === 'none') return null;
|
|
const value = computeAggregateValue(rows, field, calcMode);
|
|
if (value === null) return null;
|
|
hasComputedCell = true;
|
|
return value;
|
|
});
|
|
|
|
if (!hasComputedCell) return null;
|
|
return {
|
|
label: '汇总',
|
|
cells,
|
|
};
|
|
};
|
|
|
|
const normalizeStoredTime = value => {
|
|
const normalized = normalizeOptionalString(value);
|
|
if (!normalized) return null;
|
|
const parsed = new Date(normalized);
|
|
if (!Number.isNaN(parsed.getTime())) return parsed.getTime();
|
|
const numericValue = Number(normalized);
|
|
if (Number.isFinite(numericValue)) return numericValue;
|
|
return null;
|
|
};
|
|
|
|
const normalizeStoredFilterValue = (operator, value) => {
|
|
if (operator === 'IN') {
|
|
if (Array.isArray(value)) {
|
|
return value.map(item => String(item)).filter(Boolean);
|
|
}
|
|
return String(value || '')
|
|
.split(',')
|
|
.map(item => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
if (Array.isArray(value)) return value;
|
|
return value;
|
|
};
|
|
|
|
const matchStoredFilter = (row, filter) => {
|
|
if (!filter?.field) return true;
|
|
const operator = normalizeOptionalString(filter?.operator) || '=';
|
|
const rowValue = row?.[filter.field];
|
|
|
|
if (operator === 'BETWEEN') {
|
|
const rawRange = Array.isArray(filter?.value)
|
|
? filter.value
|
|
: Array.isArray(filter?.dateRange)
|
|
? filter.dateRange
|
|
: [];
|
|
if (rawRange.length < 2) return true;
|
|
const rowTime = normalizeStoredTime(rowValue);
|
|
const rangeStart = normalizeStoredTime(rawRange[0]);
|
|
const rangeEnd = normalizeStoredTime(rawRange[1]);
|
|
if (rowTime !== null && rangeStart !== null && rangeEnd !== null) {
|
|
return rowTime >= rangeStart && rowTime <= rangeEnd;
|
|
}
|
|
const numericValue = Number(rowValue);
|
|
const numericStart = Number(rawRange[0]);
|
|
const numericEnd = Number(rawRange[1]);
|
|
if (
|
|
Number.isFinite(numericValue)
|
|
&& Number.isFinite(numericStart)
|
|
&& Number.isFinite(numericEnd)
|
|
) {
|
|
return numericValue >= numericStart && numericValue <= numericEnd;
|
|
}
|
|
const textValue = String(rowValue ?? '');
|
|
return textValue >= String(rawRange[0]) && textValue <= String(rawRange[1]);
|
|
}
|
|
|
|
const normalizedValue = normalizeStoredFilterValue(operator, filter?.value);
|
|
if (
|
|
normalizedValue === null
|
|
|| normalizedValue === undefined
|
|
|| (typeof normalizedValue === 'string' && !normalizedValue.trim())
|
|
|| (Array.isArray(normalizedValue) && !normalizedValue.length)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
const rowText = String(rowValue ?? '');
|
|
const compareText = String(normalizedValue ?? '');
|
|
if (operator === '=') return rowText === compareText;
|
|
if (operator === '!=') return rowText !== compareText;
|
|
if (operator === '>') return Number(rowValue) > Number(normalizedValue);
|
|
if (operator === '>=') return Number(rowValue) >= Number(normalizedValue);
|
|
if (operator === '<') return Number(rowValue) < Number(normalizedValue);
|
|
if (operator === '<=') return Number(rowValue) <= Number(normalizedValue);
|
|
if (operator === 'IN') {
|
|
const valueList = Array.isArray(normalizedValue) ? normalizedValue : [normalizedValue];
|
|
return valueList.map(item => String(item)).includes(rowText);
|
|
}
|
|
if (operator === 'LIKE') return rowText.includes(compareText);
|
|
return true;
|
|
};
|
|
|
|
const applyStoredTimeRange = (rows = [], timeRange = null) => {
|
|
if (!timeRange?.field || !timeRange?.start || !timeRange?.end) return Array.isArray(rows) ? rows : [];
|
|
const startValue = normalizeStoredTime(timeRange.start);
|
|
const endValue = normalizeStoredTime(timeRange.end);
|
|
if (startValue === null || endValue === null) return Array.isArray(rows) ? rows : [];
|
|
return (Array.isArray(rows) ? rows : []).filter(row => {
|
|
const rowValue = normalizeStoredTime(row?.[timeRange.field]);
|
|
if (rowValue === null) return false;
|
|
return rowValue >= startValue && rowValue <= endValue;
|
|
});
|
|
};
|
|
|
|
const buildKpiMetricsSnapshot = (rows = [], kpiMetrics = []) => {
|
|
const safeMetrics = Array.isArray(kpiMetrics) ? kpiMetrics : [];
|
|
return safeMetrics
|
|
.map(metric => {
|
|
const field = normalizeOptionalString(metric?.field);
|
|
if (!field) return null;
|
|
let metricRows = Array.isArray(rows) ? rows : [];
|
|
(Array.isArray(metric?.filters) ? metric.filters : []).forEach(filter => {
|
|
metricRows = metricRows.filter(row => matchStoredFilter(row, filter));
|
|
});
|
|
metricRows = applyStoredTimeRange(metricRows, ensureObject(metric?.timeRange));
|
|
const calcMode = normalizeOptionalString(metric?.calcMode) || 'avg';
|
|
const value = computeAggregateValue(metricRows, field, calcMode);
|
|
if (value === null) return null;
|
|
return {
|
|
id: metric?.id,
|
|
title: normalizeOptionalString(metric?.title) || field || '指标',
|
|
field,
|
|
calcMode,
|
|
value,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
};
|
|
|
|
const buildSingleKpiMetricSnapshot = (rows = [], metric = {}, baseConfig = {}) => {
|
|
const field = normalizeOptionalString(metric?.field);
|
|
if (!field) return null;
|
|
|
|
let metricRows = Array.isArray(rows) ? rows : [];
|
|
const baseFilters = mergeFilterLists(baseConfig?.previewQuery?.filters, baseConfig?.filters);
|
|
baseFilters.forEach(filter => {
|
|
metricRows = metricRows.filter(row => matchStoredFilter(row, filter));
|
|
});
|
|
(Array.isArray(metric?.filters) ? metric.filters : []).forEach(filter => {
|
|
metricRows = metricRows.filter(row => matchStoredFilter(row, filter));
|
|
});
|
|
metricRows = applyStoredTimeRange(
|
|
metricRows,
|
|
ensureObject(baseConfig?.previewQuery?.timeRange) || ensureObject(baseConfig?.timeRange)
|
|
);
|
|
metricRows = applyStoredTimeRange(metricRows, ensureObject(metric?.timeRange));
|
|
|
|
const calcMode = normalizeOptionalString(metric?.calcMode) || 'avg';
|
|
const value = computeAggregateValue(metricRows, field, calcMode);
|
|
if (value === null) return null;
|
|
return {
|
|
id: metric?.id,
|
|
title: normalizeOptionalString(metric?.title) || field || '指标',
|
|
field,
|
|
calcMode,
|
|
value,
|
|
};
|
|
};
|
|
|
|
const buildChartContentSnapshotFromSeries = seriesList => {
|
|
const normalized = normalizeChartSeries(seriesList);
|
|
if (!normalized.length) {
|
|
return {
|
|
kind: 'chart',
|
|
seriesList: [],
|
|
points: [],
|
|
};
|
|
}
|
|
return {
|
|
kind: 'chart',
|
|
seriesList: normalized,
|
|
points: normalized[0]?.points || [],
|
|
};
|
|
};
|
|
|
|
const buildChartSnapshotPayload = async (
|
|
ctx,
|
|
dataSourceId,
|
|
rawConfig,
|
|
transaction = null,
|
|
rawContentSnapshot = null
|
|
) => {
|
|
const config = ensureObject(rawConfig);
|
|
if (!config) throw '图表块配置无效';
|
|
|
|
const previewQuery = buildPreviewDslFromConfig(config);
|
|
const previewPayload = await previewDataSourceQuery(ctx, dataSourceId, previewQuery, transaction);
|
|
const previewRows = Array.isArray(previewPayload?.rows) ? previewPayload.rows : [];
|
|
const previewSeries = buildChartSeries(previewRows, config, previewQuery);
|
|
const computedSnapshot = buildChartContentSnapshotFromSeries(previewSeries);
|
|
const fallbackSnapshot =
|
|
normalizeChartContentSnapshot(rawContentSnapshot)
|
|
|| normalizeChartContentSnapshot(config);
|
|
const resolvedSnapshot = computedSnapshot?.seriesList?.length
|
|
? computedSnapshot
|
|
: (fallbackSnapshot || computedSnapshot);
|
|
|
|
return {
|
|
config: {
|
|
...sanitizeQueryBlockConfig(config),
|
|
previewQuery,
|
|
},
|
|
contentSnapshot: resolvedSnapshot,
|
|
};
|
|
};
|
|
|
|
const buildTableSnapshotPayload = async (ctx, dataSourceId, rawConfig, transaction = null) => {
|
|
const config = ensureObject(rawConfig);
|
|
if (!config) throw '表格块配置无效';
|
|
|
|
const sanitizedConfig = sanitizeQueryBlockConfig(config);
|
|
const previewQuery = buildTablePreviewDslFromConfig(sanitizedConfig);
|
|
const previewPayload = await previewDataSourceQuery(ctx, dataSourceId, previewQuery, transaction);
|
|
const tableColumns = normalizeTableColumns(
|
|
sanitizedConfig.tableColumns
|
|
|| sanitizedConfig.previewColumns
|
|
|| []
|
|
);
|
|
return {
|
|
config: {
|
|
...sanitizedConfig,
|
|
previewQuery,
|
|
},
|
|
contentSnapshot: {
|
|
kind: 'table',
|
|
columns: tableColumns.length
|
|
? tableColumns
|
|
: Array.isArray(previewPayload?.columns)
|
|
? previewPayload.columns
|
|
: [],
|
|
rows: Array.isArray(previewPayload?.rows) ? previewPayload.rows : [],
|
|
total: previewPayload?.total ?? null,
|
|
summary: buildTableSummary(
|
|
tableColumns.length
|
|
? tableColumns
|
|
: Array.isArray(previewPayload?.columns)
|
|
? previewPayload.columns
|
|
: [],
|
|
Array.isArray(previewPayload?.rows) ? previewPayload.rows : []
|
|
),
|
|
},
|
|
};
|
|
};
|
|
|
|
const buildKpiSnapshotPayload = async (ctx, dataSourceId, rawConfig, transaction = null) => {
|
|
const config = ensureObject(rawConfig);
|
|
if (!config) throw 'KPI 块配置无效';
|
|
|
|
const sanitizedConfig = sanitizeQueryBlockConfig(config);
|
|
const metricList = Array.isArray(sanitizedConfig?.kpiMetrics) ? sanitizedConfig.kpiMetrics : [];
|
|
const previewQuery = buildKpiPreviewDslFromConfig(sanitizedConfig);
|
|
const metrics = [];
|
|
|
|
for (const metric of metricList) {
|
|
const metricPreviewQuery = buildKpiMetricPreviewDslFromConfig(sanitizedConfig, metric);
|
|
const previewPayload = await previewDataSourceQuery(ctx, dataSourceId, metricPreviewQuery, transaction);
|
|
const previewRows = Array.isArray(previewPayload?.rows) ? previewPayload.rows : [];
|
|
const metricSnapshot = buildSingleKpiMetricSnapshot(previewRows, metric, {
|
|
...sanitizedConfig,
|
|
previewQuery: metricPreviewQuery,
|
|
});
|
|
if (metricSnapshot) metrics.push(metricSnapshot);
|
|
}
|
|
|
|
return {
|
|
config: {
|
|
...sanitizedConfig,
|
|
previewQuery,
|
|
},
|
|
contentSnapshot: {
|
|
kind: 'kpi',
|
|
metrics,
|
|
},
|
|
};
|
|
};
|
|
|
|
module.exports = {
|
|
sanitizeQueryBlockConfig,
|
|
normalizeChartContentSnapshot,
|
|
buildChartSnapshotPayload,
|
|
buildTableSnapshotPayload,
|
|
buildKpiSnapshotPayload,
|
|
};
|
|
|