| |
| |
|
|
|
|
| import { v4 as uuidv4 } from "uuid";
|
| import { getDbInstance } from "./core";
|
| import { backupDbFile } from "./backup";
|
| import { invalidateDbCache } from "./readCache";
|
| import { normalizeComboRecord } from "@/lib/combos/steps";
|
|
|
| type JsonRecord = Record<string, unknown>;
|
|
|
| function asRecord(value: unknown): JsonRecord {
|
| return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {};
|
| }
|
|
|
| function getSerializedData(value: unknown): string | null {
|
| const row = asRecord(value);
|
| return typeof row.data === "string" ? row.data : null;
|
| }
|
|
|
| function getSortOrder(value: unknown): number | null {
|
| const row = asRecord(value);
|
| return typeof row.sort_order === "number" ? row.sort_order : null;
|
| }
|
|
|
| function withSortOrder(payload: string, sortOrder: number | null): JsonRecord {
|
| const parsed = JSON.parse(payload) as JsonRecord;
|
| if (typeof sortOrder === "number") {
|
| parsed.sortOrder = sortOrder;
|
| }
|
| return parsed;
|
| }
|
|
|
| function getComboNameSet(
|
| db: ReturnType<typeof getDbInstance>,
|
| extraNames: string[] = []
|
| ): Set<string> {
|
| const rows = db.prepare("SELECT name FROM combos").all();
|
| const names = new Set<string>();
|
|
|
| for (const row of rows) {
|
| const record = asRecord(row);
|
| if (typeof record.name === "string" && record.name.trim().length > 0) {
|
| names.add(record.name.trim());
|
| }
|
| }
|
|
|
| for (const name of extraNames) {
|
| if (typeof name === "string" && name.trim().length > 0) {
|
| names.add(name.trim());
|
| }
|
| }
|
|
|
| return names;
|
| }
|
|
|
| function normalizeStoredCombo(
|
| combo: JsonRecord,
|
| db: ReturnType<typeof getDbInstance>,
|
| extraNames: string[] = []
|
| ): JsonRecord {
|
| return normalizeComboRecord(combo, {
|
| allCombos: getComboNameSet(db, extraNames),
|
| }) as JsonRecord;
|
| }
|
|
|
| function parseComboRow(row: unknown): JsonRecord | null {
|
| const payload = getSerializedData(row);
|
| if (!payload) return null;
|
| return withSortOrder(payload, getSortOrder(row));
|
| }
|
|
|
| function getNextSortOrder() {
|
| const db = getDbInstance();
|
| const row = db.prepare("SELECT COALESCE(MAX(sort_order), 0) AS sort_order FROM combos").get();
|
| const sortOrder = getSortOrder(row);
|
| return (sortOrder ?? 0) + 1;
|
| }
|
|
|
| export async function getCombos() {
|
| const db = getDbInstance();
|
| const rawCombos = db
|
| .prepare("SELECT data, sort_order FROM combos ORDER BY sort_order ASC, name COLLATE NOCASE ASC")
|
| .all()
|
| .map((row) => parseComboRow(row))
|
| .filter((row): row is JsonRecord => row !== null);
|
|
|
| const comboNames = rawCombos
|
| .map((combo) => (typeof combo.name === "string" ? combo.name.trim() : ""))
|
| .filter((name): name is string => name.length > 0);
|
|
|
| return rawCombos.map((combo) =>
|
| normalizeComboRecord(combo, {
|
| allCombos: comboNames,
|
| })
|
| );
|
| }
|
|
|
| export async function getComboById(id: string) {
|
| const db = getDbInstance();
|
| const row = db.prepare("SELECT data, sort_order FROM combos WHERE id = ?").get(id);
|
| const combo = parseComboRow(row);
|
| if (!combo) return null;
|
| return normalizeStoredCombo(combo, db, typeof combo.name === "string" ? [combo.name] : []);
|
| }
|
|
|
| export async function getComboByName(name: string) {
|
| const db = getDbInstance();
|
| const row = db.prepare("SELECT data, sort_order FROM combos WHERE name = ?").get(name);
|
| const combo = parseComboRow(row);
|
| if (!combo) return null;
|
| return normalizeStoredCombo(combo, db, [name]);
|
| }
|
|
|
| export async function createCombo(data: JsonRecord) {
|
| const db = getDbInstance();
|
| const now = new Date().toISOString();
|
| const sortOrder = typeof data.sortOrder === "number" ? data.sortOrder : getNextSortOrder();
|
| const comboId = typeof data.id === "string" && data.id.trim().length > 0 ? data.id : uuidv4();
|
| const combo = normalizeStoredCombo(
|
| {
|
| ...data,
|
| id: comboId,
|
| name: data.name,
|
| models: data.models || [],
|
| strategy: data.strategy || "priority",
|
| config: data.config || {},
|
| isHidden: Boolean(data.isHidden),
|
| sortOrder,
|
| createdAt: now,
|
| updatedAt: now,
|
| },
|
| db,
|
| typeof data.name === "string" ? [data.name] : []
|
| );
|
|
|
| db.prepare(
|
| "INSERT INTO combos (id, name, data, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)"
|
| ).run(combo.id, combo.name, JSON.stringify(combo), sortOrder, now, now);
|
|
|
| invalidateDbCache("combos");
|
| backupDbFile("pre-write");
|
| return combo;
|
| }
|
|
|
| export async function updateCombo(id: string, data: JsonRecord) {
|
| const db = getDbInstance();
|
| const existing = db.prepare("SELECT data, sort_order FROM combos WHERE id = ?").get(id);
|
| if (!existing) return null;
|
|
|
| const current = parseComboRow(existing);
|
| if (!current) return null;
|
| const sortOrder =
|
| typeof data.sortOrder === "number"
|
| ? data.sortOrder
|
| : typeof current.sortOrder === "number"
|
| ? current.sortOrder
|
| : getNextSortOrder();
|
| const merged: JsonRecord = {
|
| ...current,
|
| ...data,
|
| sortOrder,
|
| updatedAt: new Date().toISOString(),
|
| };
|
|
|
| for (const key of Object.keys(data)) {
|
| if (data[key] === null) {
|
| delete merged[key];
|
| }
|
| }
|
| const currentName = typeof current.name === "string" ? current.name : "";
|
| const nextName =
|
| typeof merged["name"] === "string" && merged["name"].trim().length > 0
|
| ? merged["name"]
|
| : currentName;
|
| const normalizedMerged = normalizeStoredCombo({ ...merged, name: nextName }, db, [nextName]);
|
|
|
| db.prepare(
|
| "UPDATE combos SET name = ?, data = ?, sort_order = ?, updated_at = ? WHERE id = ?"
|
| ).run(nextName, JSON.stringify(normalizedMerged), sortOrder, normalizedMerged.updatedAt, id);
|
|
|
| invalidateDbCache("combos");
|
| backupDbFile("pre-write");
|
| return normalizedMerged;
|
| }
|
|
|
| export async function reorderCombos(comboIds: string[]) {
|
| const db = getDbInstance();
|
| const rows = db
|
| .prepare(
|
| "SELECT id, name, data, sort_order FROM combos ORDER BY sort_order ASC, name COLLATE NOCASE ASC"
|
| )
|
| .all();
|
| if (rows.length === 0) return [];
|
|
|
| const existingIds = new Set(
|
| rows
|
| .map((row) => {
|
| const record = asRecord(row);
|
| return typeof record.id === "string" ? record.id : null;
|
| })
|
| .filter((id): id is string => id !== null)
|
| );
|
|
|
| const seen = new Set<string>();
|
| const requestedIds = comboIds.filter((id) => {
|
| if (!existingIds.has(id) || seen.has(id)) return false;
|
| seen.add(id);
|
| return true;
|
| });
|
|
|
| const orderedIds = [
|
| ...requestedIds,
|
| ...rows
|
| .map((row) => {
|
| const record = asRecord(row);
|
| return typeof record.id === "string" ? record.id : null;
|
| })
|
| .filter((id): id is string => id !== null && !seen.has(id)),
|
| ];
|
|
|
| const update = db.prepare(
|
| "UPDATE combos SET data = ?, sort_order = ?, updated_at = ? WHERE id = ?"
|
| );
|
| const now = new Date().toISOString();
|
| const rowById = new Map(
|
| rows.map((row) => {
|
| const record = asRecord(row);
|
| return [String(record.id), row];
|
| })
|
| );
|
| const comboNames = rows
|
| .map((row) => {
|
| const combo = parseComboRow(row);
|
| return combo && typeof combo.name === "string" ? combo.name.trim() : "";
|
| })
|
| .filter((name): name is string => name.length > 0);
|
|
|
| const reorderTransaction = db.transaction(() => {
|
| orderedIds.forEach((id, index) => {
|
| const row = rowById.get(id);
|
| const combo = row ? parseComboRow(row) : null;
|
| if (!combo) return;
|
| const sortOrder = index + 1;
|
| const updatedCombo = normalizeComboRecord(
|
| { ...combo, sortOrder, updatedAt: now },
|
| { allCombos: comboNames }
|
| );
|
| update.run(JSON.stringify(updatedCombo), sortOrder, now, id);
|
| });
|
| });
|
|
|
| reorderTransaction();
|
| invalidateDbCache("combos");
|
| backupDbFile("pre-write");
|
| return getCombos();
|
| }
|
|
|
| export async function deleteCombo(id: string) {
|
| const db = getDbInstance();
|
| const result = db.prepare("DELETE FROM combos WHERE id = ?").run(id);
|
| if (result.changes === 0) return false;
|
| invalidateDbCache("combos");
|
| backupDbFile("pre-write");
|
| return true;
|
| }
|
|
|
| export async function deleteComboByName(name: string) {
|
| const combo = await getComboByName(name);
|
| if (!combo || typeof combo.id !== "string") return false;
|
| return deleteCombo(combo.id);
|
| }
|
|
|
| export function setActiveCombo(name: string, db = getDbInstance()) {
|
| db.prepare(
|
| "INSERT OR REPLACE INTO key_value (namespace, key, value) VALUES ('settings', 'activeCombo', ?)"
|
| ).run(JSON.stringify(name));
|
| }
|
|
|