mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 09:13:36 +08:00
- 复用查询页提供 ES 控制台、模板、AI 辅助及原始与表格结果 - 新增统一解析分类、危险确认、连接保护和受控 REST 执行 - 扩展 driver agent、版本适配、Wails 绑定、多语言与文档 - 修复完整回归中发现的跨平台和前端基线用例问题 Refs #782
254 lines
9.5 KiB
TypeScript
254 lines
9.5 KiB
TypeScript
import type { IndexDefinition } from '../types';
|
|
import { resolveUniqueKeyGroupsFromIndexes } from '../components/dataGridCopyInsert';
|
|
import { isOracleLikeDialect } from './sqlDialect';
|
|
import { t as translateCatalog, type I18nParams } from '../i18n';
|
|
|
|
export const ORACLE_ROWID_LOCATOR_COLUMN = '__gonavi_oracle_rowid__';
|
|
export const DUCKDB_ROWID_LOCATOR_COLUMN = '__gonavi_duckdb_rowid__';
|
|
|
|
export type RowLocatorStrategy = 'primary-key' | 'unique-key' | 'oracle-rowid' | 'duckdb-rowid' | 'all-columns' | '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;
|
|
allowDuckDBRowID?: boolean;
|
|
translate?: RowLocatorTranslator;
|
|
};
|
|
|
|
export type ResolveRowLocatorValuesResult =
|
|
| { ok: true; values: Record<string, any> }
|
|
| { ok: false; error: string };
|
|
|
|
export type RowLocatorMessages = {
|
|
noSafeLocator?: () => string;
|
|
emptyLocatorValue?: (column: string) => string;
|
|
};
|
|
|
|
type RowLocatorTranslator = (key: string, params?: I18nParams) => 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 GONAVI_INTERNAL_COLUMN_PREFIX = '__gonavi';
|
|
|
|
// Values longer than this are excluded from all-columns locators because
|
|
// LOB-typed columns cannot be compared with "=" on several dialects (Oracle).
|
|
const ALL_COLUMNS_LOCATOR_MAX_VALUE_LENGTH = 4000;
|
|
|
|
const ALL_COLUMNS_LOCATOR_HINT_KEY = 'data_viewer.edit_hint.all_columns_locator';
|
|
|
|
const isInternalLocatorColumn = (column: string): boolean => (
|
|
normalizeColumnName(column).toLowerCase().startsWith(GONAVI_INTERNAL_COLUMN_PREFIX)
|
|
);
|
|
|
|
const isScalarLocatorValue = (value: any): boolean => (
|
|
typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint'
|
|
);
|
|
|
|
export const buildAllColumnsLocator = (
|
|
resultColumns: string[],
|
|
options?: {
|
|
hiddenColumns?: string[];
|
|
writableColumns?: Record<string, string>;
|
|
translate?: RowLocatorTranslator;
|
|
},
|
|
): EditRowLocator => {
|
|
const hidden = new Set((options?.hiddenColumns || []).map((column) => normalizeColumnName(column).toLowerCase()));
|
|
const columns = (resultColumns || [])
|
|
.map(normalizeColumnName)
|
|
.filter(Boolean)
|
|
.filter((column) => !isInternalLocatorColumn(column))
|
|
.filter((column) => !hidden.has(column.toLowerCase()));
|
|
return {
|
|
strategy: 'all-columns',
|
|
columns,
|
|
valueColumns: [...columns],
|
|
hiddenColumns: options?.hiddenColumns,
|
|
writableColumns: options?.writableColumns,
|
|
readOnly: false,
|
|
reason: translateReason(options?.translate, ALL_COLUMNS_LOCATOR_HINT_KEY),
|
|
};
|
|
};
|
|
|
|
const translateReason = (
|
|
translate: RowLocatorTranslator | undefined,
|
|
key: string,
|
|
params?: I18nParams,
|
|
): string => {
|
|
const fallback = translateCatalog(key, params, 'en-US');
|
|
const translated = translate ? translate(key, params) : fallback;
|
|
return translated && translated !== key ? translated : fallback;
|
|
};
|
|
|
|
export const resolveEditRowLocator = ({
|
|
dbType,
|
|
resultColumns,
|
|
primaryKeys = [],
|
|
indexes,
|
|
allowOracleRowID = false,
|
|
allowDuckDBRowID = false,
|
|
translate,
|
|
}: 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 buildAllColumnsLocator(columns, { translate });
|
|
}
|
|
|
|
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 (allowDuckDBRowID && String(dbType || '').trim().toLowerCase() === 'duckdb' && hasColumn(columns, DUCKDB_ROWID_LOCATOR_COLUMN)) {
|
|
const rowIDColumn = findColumn(columns, DUCKDB_ROWID_LOCATOR_COLUMN);
|
|
return {
|
|
strategy: 'duckdb-rowid',
|
|
columns: ['rowid'],
|
|
valueColumns: [rowIDColumn],
|
|
hiddenColumns: [rowIDColumn],
|
|
readOnly: false,
|
|
};
|
|
}
|
|
|
|
return buildAllColumnsLocator(columns, { translate });
|
|
};
|
|
|
|
const resolveAllColumnsLocatorValues = (
|
|
locator: EditRowLocator,
|
|
row: Record<string, any>,
|
|
messages?: RowLocatorMessages,
|
|
): ResolveRowLocatorValuesResult => {
|
|
const hidden = new Set((locator.hiddenColumns || []).map((column) => normalizeColumnName(column).toLowerCase()));
|
|
const rowKeys = Object.keys(row || {});
|
|
const rowKeyByLower = new Map<string, string>();
|
|
rowKeys.forEach((key) => rowKeyByLower.set(normalizeColumnName(key).toLowerCase(), key));
|
|
|
|
const candidateColumns = (locator.columns.length > 0 ? locator.columns : rowKeys)
|
|
.map(normalizeColumnName)
|
|
.filter(Boolean)
|
|
.filter((column) => !isInternalLocatorColumn(column))
|
|
.filter((column) => !hidden.has(column.toLowerCase()));
|
|
|
|
const values: Record<string, any> = {};
|
|
candidateColumns.forEach((column) => {
|
|
const rowKey = rowKeyByLower.get(column.toLowerCase());
|
|
if (rowKey === undefined) return;
|
|
const value = row?.[rowKey];
|
|
if (value === null || value === undefined || value === '') return;
|
|
if (!isScalarLocatorValue(value)) return;
|
|
if (typeof value === 'string' && value.length > ALL_COLUMNS_LOCATOR_MAX_VALUE_LENGTH) return;
|
|
values[column] = value;
|
|
});
|
|
|
|
if (Object.keys(values).length === 0) {
|
|
return { ok: false, error: messages?.noSafeLocator?.() || 'No usable locator values were found on this row, so changes cannot be submitted safely.' };
|
|
}
|
|
return { ok: true, values };
|
|
};
|
|
|
|
export const resolveRowLocatorValues = (
|
|
locator: EditRowLocator | undefined,
|
|
row: Record<string, any>,
|
|
messages?: RowLocatorMessages,
|
|
): ResolveRowLocatorValuesResult => {
|
|
if (!locator || locator.readOnly || locator.strategy === 'none') {
|
|
return { ok: false, error: messages?.noSafeLocator?.() || 'No safe row locator is available for this result set.' };
|
|
}
|
|
|
|
if (locator.strategy === 'all-columns') {
|
|
return resolveAllColumnsLocatorValues(locator, row, messages);
|
|
}
|
|
|
|
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: messages?.emptyLocatorValue?.(column) || `Locator column ${column} is empty, so changes cannot be submitted safely.` };
|
|
}
|
|
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
|
|
);
|