Files
dbx/apps/desktop/src/stores/sqlExecutionTargetGroupStore.ts
T

195 lines
7.7 KiB
TypeScript

import { defineStore } from "pinia";
import { ref } from "vue";
import { uuid } from "@/lib/common/utils";
import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage";
import { dedupeMultiDbExecutionTargets, executionTargetGroupNameKey, normalizeMultiDbExecutionTarget, type MultiDbExecutionTarget, type SqlExecutionTargetDatabaseTypeResolver, type SqlExecutionTargetGroup } from "@/types/sqlExecution";
import type { DatabaseType } from "@/types/database";
const STORAGE_KEY = "dbx-sql-execution-target-groups";
const STORAGE_VERSION = 1;
interface PersistedTargetGroupState {
version: number;
groups: SqlExecutionTargetGroup[];
}
function nowIso(): string {
return new Date().toISOString();
}
function normalizeGroup(value: Partial<SqlExecutionTargetGroup>): SqlExecutionTargetGroup | undefined {
const name = typeof value.name === "string" ? value.name.trim() : "";
const databaseType = value.databaseType;
if (!value.id || !name || !databaseType || !Array.isArray(value.targets)) return undefined;
const targets = dedupeMultiDbExecutionTargets(value.targets);
if (targets.length === 0) return undefined;
const createdAt = typeof value.createdAt === "string" && value.createdAt ? value.createdAt : nowIso();
const updatedAt = typeof value.updatedAt === "string" && value.updatedAt ? value.updatedAt : createdAt;
return {
id: value.id,
name,
databaseType,
targets,
createdAt,
updatedAt,
lastUsedAt: typeof value.lastUsedAt === "string" && value.lastUsedAt ? value.lastUsedAt : undefined,
};
}
function sortGroups(groups: readonly SqlExecutionTargetGroup[]): SqlExecutionTargetGroup[] {
return [...groups].sort((a, b) => {
const aTime = a.lastUsedAt ?? a.updatedAt;
const bTime = b.lastUsedAt ?? b.updatedAt;
const timeDiff = bTime.localeCompare(aTime);
if (timeDiff !== 0) return timeDiff;
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" });
});
}
function readGroups(): SqlExecutionTargetGroup[] {
const raw = safeLocalStorageGet(STORAGE_KEY);
if (!raw) return [];
try {
const parsed = JSON.parse(raw) as Partial<PersistedTargetGroupState> | SqlExecutionTargetGroup[];
const values = Array.isArray(parsed) ? parsed : Array.isArray(parsed.groups) ? parsed.groups : [];
const groups: SqlExecutionTargetGroup[] = [];
const names = new Set<string>();
for (const value of values) {
const group = normalizeGroup(value);
if (!group) continue;
const nameKey = executionTargetGroupNameKey(group.name);
if (names.has(nameKey)) continue;
names.add(nameKey);
groups.push(group);
}
return sortGroups(groups);
} catch (error) {
console.warn("[DBX][sql-execution-target-groups:load] invalid persisted state", error);
return [];
}
}
function writeGroups(groups: readonly SqlExecutionTargetGroup[]): void {
const state: PersistedTargetGroupState = {
version: STORAGE_VERSION,
groups: sortGroups(groups),
};
safeLocalStorageSet(STORAGE_KEY, JSON.stringify(state));
}
export const useSqlExecutionTargetGroupStore = defineStore("sqlExecutionTargetGroup", () => {
const groups = ref<SqlExecutionTargetGroup[]>(readGroups());
function hasName(name: string, exceptId?: string): boolean {
const key = executionTargetGroupNameKey(name);
return groups.value.some((group) => group.id !== exceptId && executionTargetGroupNameKey(group.name) === key);
}
function assertGroupName(name: string, exceptId?: string): string {
const normalized = name.trim();
if (!normalized) throw new Error("目标组名称不能为空");
if (hasName(normalized, exceptId)) throw new Error("目标组名称已存在");
return normalized;
}
function assertTargets(targets: readonly MultiDbExecutionTarget[], expectedDatabaseType?: DatabaseType, resolveDatabaseType?: SqlExecutionTargetDatabaseTypeResolver): MultiDbExecutionTarget[] {
const normalized = dedupeMultiDbExecutionTargets(targets);
if (normalized.length === 0) throw new Error("目标组至少需要一个执行目标");
if (expectedDatabaseType && resolveDatabaseType) {
const mismatched = normalized.find((target) => resolveDatabaseType(target) !== expectedDatabaseType);
if (mismatched) throw new Error("目标组只能包含同一种数据库类型的执行目标");
}
return normalized;
}
function persist(): void {
groups.value = sortGroups(groups.value);
writeGroups(groups.value);
}
function createGroup(input: { name: string; databaseType: DatabaseType; targets: readonly MultiDbExecutionTarget[]; resolveDatabaseType: SqlExecutionTargetDatabaseTypeResolver }): SqlExecutionTargetGroup {
const timestamp = nowIso();
const group: SqlExecutionTargetGroup = {
id: uuid(),
name: assertGroupName(input.name),
databaseType: input.databaseType,
targets: assertTargets(input.targets, input.databaseType, input.resolveDatabaseType),
createdAt: timestamp,
updatedAt: timestamp,
};
groups.value = [...groups.value, group];
persist();
return group;
}
function updateGroup(id: string, input: { name?: string; targets?: readonly MultiDbExecutionTarget[]; resolveDatabaseType: SqlExecutionTargetDatabaseTypeResolver }): SqlExecutionTargetGroup | undefined {
const existing = groups.value.find((group) => group.id === id);
if (!existing) return undefined;
const updated: SqlExecutionTargetGroup = {
...existing,
...(input.name !== undefined ? { name: assertGroupName(input.name, id) } : {}),
...(input.targets !== undefined ? { targets: assertTargets(input.targets, existing.databaseType, input.resolveDatabaseType) } : {}),
updatedAt: nowIso(),
};
groups.value = groups.value.map((group) => (group.id === id ? updated : group));
persist();
return updated;
}
function deleteGroup(id: string): void {
groups.value = groups.value.filter((group) => group.id !== id);
persist();
}
function markGroupUsed(id: string): SqlExecutionTargetGroup | undefined {
const existing = groups.value.find((group) => group.id === id);
if (!existing) return undefined;
const updated = { ...existing, lastUsedAt: nowIso() };
groups.value = groups.value.map((group) => (group.id === id ? updated : group));
persist();
return updated;
}
function getGroup(id: string): SqlExecutionTargetGroup | undefined {
return groups.value.find((group) => group.id === id);
}
function getGroupsByDatabaseType(databaseType?: DatabaseType): SqlExecutionTargetGroup[] {
return sortGroups(databaseType ? groups.value.filter((group) => group.databaseType === databaseType) : groups.value);
}
function replaceTargets(id: string, targets: readonly MultiDbExecutionTarget[], resolveDatabaseType: SqlExecutionTargetDatabaseTypeResolver): SqlExecutionTargetGroup | undefined {
return updateGroup(id, { targets, resolveDatabaseType });
}
function cloneGroup(id: string, name: string, targets: readonly MultiDbExecutionTarget[] | undefined, resolveDatabaseType: SqlExecutionTargetDatabaseTypeResolver): SqlExecutionTargetGroup | undefined {
const existing = getGroup(id);
if (!existing) return undefined;
return createGroup({
name,
databaseType: existing.databaseType,
targets: targets ?? existing.targets,
resolveDatabaseType,
});
}
function normalizeTarget(target: MultiDbExecutionTarget): MultiDbExecutionTarget | undefined {
return normalizeMultiDbExecutionTarget(target);
}
return {
groups,
createGroup,
updateGroup,
replaceTargets,
cloneGroup,
deleteGroup,
markGroupUsed,
getGroup,
getGroupsByDatabaseType,
normalizeTarget,
};
});
export { STORAGE_KEY as SQL_EXECUTION_TARGET_GROUPS_STORAGE_KEY };