'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, };