Files
MyGoNavi/frontend/src/utils/rowLocator.ts
Syngnat c1ebce4ef5 feat(query-editor): 放宽单表查询结果列级编辑边界
- 查询编辑:支持简单表列与表达式列混合展示
- 编辑安全:仅允许真实表列编辑,表达式列保持只读
- 提交流程:支持结果列别名映射回真实表字段
- 测试覆盖:补充聚合查询静默只读与列级提交用例
2026-04-29 20:07:22 +08:00

153 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { IndexDefinition } from '../types';
import { resolveUniqueKeyGroupsFromIndexes } from '../components/dataGridCopyInsert';
import { isOracleLikeDialect } from './sqlDialect';
export const ORACLE_ROWID_LOCATOR_COLUMN = '__gonavi_oracle_rowid__';
export type RowLocatorStrategy = 'primary-key' | 'unique-key' | 'oracle-rowid' | 'none';
export type EditRowLocator = {
strategy: RowLocatorStrategy;
columns: string[];
valueColumns: string[];
hiddenColumns?: string[];
writableColumns?: Record<string, string>;
readOnly: boolean;
reason?: string;
};
export type ResolveEditRowLocatorParams = {
dbType: string;
resultColumns: string[];
primaryKeys?: string[];
indexes?: IndexDefinition[];
allowOracleRowID?: boolean;
};
export type ResolveRowLocatorValuesResult =
| { ok: true; values: Record<string, any> }
| { ok: false; error: string };
const normalizeColumnName = (value: string): string => String(value || '').trim();
const hasColumn = (columns: string[], target: string): boolean => {
const normalizedTarget = normalizeColumnName(target).toLowerCase();
return columns.some((column) => normalizeColumnName(column).toLowerCase() === normalizedTarget);
};
const findColumn = (columns: string[], target: string): string => {
const normalizedTarget = normalizeColumnName(target).toLowerCase();
return columns.find((column) => normalizeColumnName(column).toLowerCase() === normalizedTarget) || target;
};
const buildReadOnlyLocator = (reason: string): EditRowLocator => ({
strategy: 'none',
columns: [],
valueColumns: [],
readOnly: true,
reason,
});
export const resolveEditRowLocator = ({
dbType,
resultColumns,
primaryKeys = [],
indexes,
allowOracleRowID = false,
}: ResolveEditRowLocatorParams): EditRowLocator => {
const columns = (resultColumns || []).map(normalizeColumnName).filter(Boolean);
const primaryKeyColumns = (primaryKeys || []).map(normalizeColumnName).filter(Boolean);
if (primaryKeyColumns.length > 0) {
const missing = primaryKeyColumns.filter((column) => !hasColumn(columns, column));
if (missing.length === 0) {
return {
strategy: 'primary-key',
columns: primaryKeyColumns,
valueColumns: primaryKeyColumns.map((column) => findColumn(columns, column)),
readOnly: false,
};
}
return buildReadOnlyLocator(`结果集中缺少主键列 ${missing.join(', ')},无法安全提交修改。`);
}
const uniqueKeyGroups = resolveUniqueKeyGroupsFromIndexes(indexes);
const uniqueKeyGroup = uniqueKeyGroups.find((group) => group.length > 0 && group.every((column) => hasColumn(columns, column)));
if (uniqueKeyGroup) {
return {
strategy: 'unique-key',
columns: uniqueKeyGroup,
valueColumns: uniqueKeyGroup.map((column) => findColumn(columns, column)),
readOnly: false,
};
}
if (allowOracleRowID && isOracleLikeDialect(dbType) && hasColumn(columns, ORACLE_ROWID_LOCATOR_COLUMN)) {
const rowIDColumn = findColumn(columns, ORACLE_ROWID_LOCATOR_COLUMN);
return {
strategy: 'oracle-rowid',
columns: ['ROWID'],
valueColumns: [rowIDColumn],
hiddenColumns: [rowIDColumn],
readOnly: false,
};
}
if (allowOracleRowID && isOracleLikeDialect(dbType)) {
return buildReadOnlyLocator('未检测到主键或可用唯一索引,且结果中缺少 Oracle ROWID无法安全提交修改。');
}
return buildReadOnlyLocator('未检测到主键或可用唯一索引,无法安全提交修改。');
};
export const resolveRowLocatorValues = (
locator: EditRowLocator | undefined,
row: Record<string, any>,
): ResolveRowLocatorValuesResult => {
if (!locator || locator.readOnly || locator.strategy === 'none') {
return { ok: false, error: '当前结果没有可用的安全行定位方式,无法提交修改。' };
}
const values: Record<string, any> = {};
for (let index = 0; index < locator.columns.length; index++) {
const column = locator.columns[index];
const valueColumn = locator.valueColumns[index] || column;
const value = row?.[valueColumn];
if (value === null || value === undefined || value === '') {
return { ok: false, error: `定位列 ${column} 的值为空,无法安全提交修改。` };
}
values[column] = value;
}
return { ok: true, values };
};
export const filterHiddenLocatorColumns = (columns: string[], locator?: EditRowLocator): string[] => {
const hidden = new Set((locator?.hiddenColumns || []).map((column) => normalizeColumnName(column).toLowerCase()));
if (hidden.size === 0) return columns;
return (columns || []).filter((column) => !hidden.has(normalizeColumnName(column).toLowerCase()));
};
export const isHiddenLocatorColumn = (column: string, locator?: EditRowLocator): boolean => {
const normalized = normalizeColumnName(column).toLowerCase();
return (locator?.hiddenColumns || []).some((hidden) => normalizeColumnName(hidden).toLowerCase() === normalized);
};
export const resolveWritableColumnName = (column: string, locator?: EditRowLocator): string | undefined => {
const normalized = normalizeColumnName(column);
if (!normalized || isHiddenLocatorColumn(normalized, locator)) return undefined;
const writableColumns = locator?.writableColumns;
if (!writableColumns) return normalized;
const normalizedTarget = normalized.toLowerCase();
const matchedEntry = Object.entries(writableColumns).find(([resultColumn]) => (
normalizeColumnName(resultColumn).toLowerCase() === normalizedTarget
));
const tableColumnName = normalizeColumnName(matchedEntry?.[1] || '');
return tableColumnName || undefined;
};
export const isWritableResultColumn = (column: string, locator?: EditRowLocator): boolean => (
resolveWritableColumnName(column, locator) !== undefined
);