feat(settings): configure SQL file size limits
This commit is contained in:
34 files changed
+554
-79
No files matched your search
+45
-1
@@ -1,7 +1,21 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ExternalSqlFileTooLargeError, externalSqlFileDisplayTitles, externalSqlFileOpenErrorMessage, formatSqlFileSize, isSqlFilePath, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES, normalizeExternalSqlPath, readBrowserSqlFile } from "@/lib/sql/sqlFileOpen";
|
||||
import {
|
||||
ExternalSqlFileTooLargeError,
|
||||
clampExternalSqlEditorMaxMbInput,
|
||||
externalSqlEditorMaxBytes,
|
||||
externalSqlFileDisplayTitles,
|
||||
externalSqlFileOpenErrorMessage,
|
||||
formatSqlFileSize,
|
||||
isSqlFilePath,
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES,
|
||||
MAX_EXTERNAL_SQL_EDITOR_FILE_MB,
|
||||
MIN_EXTERNAL_SQL_EDITOR_FILE_MB,
|
||||
normalizeExternalSqlEditorMaxMb,
|
||||
normalizeExternalSqlPath,
|
||||
readBrowserSqlFile,
|
||||
} from "@/lib/sql/sqlFileOpen";
|
||||
|
||||
describe("external SQL file paths", () => {
|
||||
it("normalizes Windows separators for identity checks", () => {
|
||||
@@ -41,6 +55,36 @@ describe("external SQL file editor limit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("honors a custom browser editor limit", async () => {
|
||||
const file = new Blob(["select 1;"]);
|
||||
Object.defineProperty(file, "size", { value: 8 });
|
||||
|
||||
await expect(readBrowserSqlFile(file, 4)).rejects.toMatchObject({
|
||||
name: "ExternalSqlFileTooLargeError",
|
||||
sizeBytes: 8,
|
||||
maxSizeBytes: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes the configurable editor size limit in MB", () => {
|
||||
expect(normalizeExternalSqlEditorMaxMb(undefined)).toBe(64);
|
||||
expect(normalizeExternalSqlEditorMaxMb(0)).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(normalizeExternalSqlEditorMaxMb(128)).toBe(128);
|
||||
expect(normalizeExternalSqlEditorMaxMb(99999)).toBe(MAX_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(normalizeExternalSqlEditorMaxMb(12.6)).toBe(13);
|
||||
});
|
||||
|
||||
it("clamps the editor size limit input", () => {
|
||||
expect(clampExternalSqlEditorMaxMbInput("abc")).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(clampExternalSqlEditorMaxMbInput(0)).toBe(MIN_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
expect(clampExternalSqlEditorMaxMbInput(256)).toBe(256);
|
||||
expect(clampExternalSqlEditorMaxMbInput(99999)).toBe(MAX_EXTERNAL_SQL_EDITOR_FILE_MB);
|
||||
});
|
||||
|
||||
it("converts the editor size limit to bytes", () => {
|
||||
expect(externalSqlEditorMaxBytes(64)).toBe(64 * 1024 * 1024);
|
||||
});
|
||||
|
||||
it("formats large file sizes", () => {
|
||||
expect(formatSqlFileSize(64 * 1024 * 1024)).toBe("64.0 MB");
|
||||
expect(formatSqlFileSize(50 * 1024 * 1024 * 1024)).toBe("50.0 GB");
|
||||
|
||||
@@ -313,6 +313,8 @@ export const saveDesktopSettings = forward("saveDesktopSettings");
|
||||
export const loadMcpGlobalPolicy = forward("loadMcpGlobalPolicy");
|
||||
export const saveMcpGlobalPolicy = forward("saveMcpGlobalPolicy");
|
||||
export const loadMaxAgentTurns = forward("loadMaxAgentTurns");
|
||||
export const loadSqlFileUploadMaxBytes = forward("loadSqlFileUploadMaxBytes");
|
||||
export const saveSqlFileUploadMaxMb = forward("saveSqlFileUploadMaxMb");
|
||||
export const saveMaxAgentTurns = forward("saveMaxAgentTurns");
|
||||
export const loadMaxRetries = forward("loadMaxRetries");
|
||||
export const saveMaxRetries = forward("saveMaxRetries");
|
||||
|
||||
@@ -1848,6 +1848,19 @@ export async function loadMaxAgentTurns(): Promise<number> {
|
||||
return get("/api/app-settings/max-agent-turns");
|
||||
}
|
||||
|
||||
export async function loadSqlFileUploadMaxBytes(): Promise<number> {
|
||||
return get("/api/app-settings/sql-file-upload-max-bytes");
|
||||
}
|
||||
|
||||
export async function saveSqlFileUploadMaxMb(sqlFileUploadMaxMb: number): Promise<void> {
|
||||
const res = await fetch(apiUrl("/api/app-settings/sql-file-upload-max-bytes"), {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ sqlFileUploadMaxMb }),
|
||||
});
|
||||
if (!res.ok) throw await backendResponseError(res);
|
||||
}
|
||||
|
||||
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
|
||||
const res = await fetch(apiUrl("/api/app-settings/max-agent-turns"), {
|
||||
method: "PUT",
|
||||
@@ -2270,11 +2283,11 @@ export async function pendingOpenAiConfigLinks(): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(_path: string): Promise<string> {
|
||||
export async function readExternalSqlFile(_path: string, _maxSizeBytes?: number): Promise<string> {
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
export async function readExternalSqlFileSnapshot(_path: string): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
|
||||
export async function readExternalSqlFileSnapshot(_path: string, _maxSizeBytes?: number): Promise<import("@/lib/backend/tauri").ExternalSqlFileSnapshot> {
|
||||
throw new Error("Opening external SQL file paths is only available in the desktop app");
|
||||
}
|
||||
|
||||
|
||||
@@ -717,6 +717,14 @@ export async function loadMaxAgentTurns(): Promise<number> {
|
||||
return invoke("load_max_agent_turns");
|
||||
}
|
||||
|
||||
export async function loadSqlFileUploadMaxBytes(): Promise<number> {
|
||||
return 200 * 1024 * 1024;
|
||||
}
|
||||
|
||||
export async function saveSqlFileUploadMaxMb(_sqlFileUploadMaxMb: number): Promise<void> {
|
||||
// No-op on desktop: SQL files are streamed directly from disk, no server upload cap applies.
|
||||
}
|
||||
|
||||
export async function saveMaxAgentTurns(maxAgentTurns: number): Promise<void> {
|
||||
return invoke("save_max_agent_turns", { maxAgentTurns });
|
||||
}
|
||||
@@ -970,16 +978,16 @@ export type ExternalSqlFileStatus = { kind: "present"; sizeBytes: number; modifi
|
||||
|
||||
export type ExternalSqlFileWriteResult = { kind: "written"; version: ExternalSqlFileVersion } | { kind: "conflict"; currentVersion: ExternalSqlFileVersion } | { kind: "missing" };
|
||||
|
||||
export async function readExternalSqlFileSnapshot(path: string): Promise<ExternalSqlFileSnapshot> {
|
||||
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path });
|
||||
export async function readExternalSqlFileSnapshot(path: string, maxSizeBytes?: number): Promise<ExternalSqlFileSnapshot> {
|
||||
const result = await invoke<{ kind: "content"; content: string; version: ExternalSqlFileVersion } | { kind: "tooLarge"; sizeBytes: number; maxSizeBytes: number }>("read_external_sql_file", { path, maxSizeBytes });
|
||||
if (result.kind === "tooLarge") {
|
||||
throw new ExternalSqlFileTooLargeError(result.sizeBytes, result.maxSizeBytes);
|
||||
}
|
||||
return { content: result.content, version: result.version };
|
||||
}
|
||||
|
||||
export async function readExternalSqlFile(path: string): Promise<string> {
|
||||
return (await readExternalSqlFileSnapshot(path)).content;
|
||||
export async function readExternalSqlFile(path: string, maxSizeBytes?: number): Promise<string> {
|
||||
return (await readExternalSqlFileSnapshot(path, maxSizeBytes)).content;
|
||||
}
|
||||
|
||||
export async function inspectExternalSqlFile(path: string): Promise<ExternalSqlFileStatus> {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { EditorSettings } from "@/stores/settingsStore";
|
||||
import { normalizeBackgroundImageSettings } from "@/lib/app/appBackgroundImage";
|
||||
import { normalizeResultPageSize } from "@/lib/dataGrid/paginationPageSize";
|
||||
import { normalizeQueryResultMaxRows } from "@/lib/dataGrid/queryResultRowLimit";
|
||||
import { normalizeExternalSqlEditorMaxMb } from "@/lib/sql/sqlFileOpen";
|
||||
import { normalizeCompletionTriggerMode } from "@/lib/sql/sqlCompletionTriggerPolicy";
|
||||
import { normalizeRedisKeyTemplates } from "@/lib/redis/redisKeyTemplates";
|
||||
|
||||
@@ -59,6 +60,7 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
|
||||
"tableOpenPageSize",
|
||||
"queryResultMaxRowsEnabled",
|
||||
"queryResultMaxRows",
|
||||
"externalSqlEditorMaxMb",
|
||||
"infiniteScroll",
|
||||
"regexMaxMatchCount",
|
||||
"autoCalculateTotalRows",
|
||||
@@ -131,6 +133,7 @@ export function normalizeQueryResultMaxRowsDraft(value: unknown): number {
|
||||
function normalizedDraftValue(key: EditorSettingsDraftKey, value: unknown): unknown {
|
||||
if (key === "pageSize" || key === "tableOpenPageSize") return normalizeTableOpenPageSizeDraft(value);
|
||||
if (key === "queryResultMaxRows") return normalizeQueryResultMaxRowsDraft(value);
|
||||
if (key === "externalSqlEditorMaxMb") return normalizeExternalSqlEditorMaxMb(value);
|
||||
if (key === "completionTriggerMode") return normalizeCompletionTriggerMode(value);
|
||||
if (key === "redisKeyTemplates") return normalizeRedisKeyTemplates(value);
|
||||
if (key === "backgroundImage") return normalizeBackgroundImageSettings(value);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
export type SettingsCategory = "editor" | "formatter" | "appearance" | "navigation" | "data" | "sqlFile" | "backups" | "tunnels" | "shortcuts" | "snippets" | "sync" | "ai" | "mcp" | "security" | "about";
|
||||
|
||||
export interface SettingsSearchContext {
|
||||
isWeb: boolean;
|
||||
@@ -199,6 +199,8 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
|
||||
{ id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" },
|
||||
{ id: "data-grid-filter-view", category: "data", titleKey: "settings.dataGridFilterView", descriptionKey: "settings.dataGridFilterViewDescription", targetId: "data-grid-filter-view" },
|
||||
{ id: "data-grid-flattening-multi-line", category: "data", titleKey: "settings.flatteningMultiLineText", descriptionKey: "settings.flatteningMultiLineTextDescription", targetId: "data" },
|
||||
{ id: "sql-file-editor-max-mb", category: "sqlFile", titleKey: "settings.externalSqlEditorMaxMb", descriptionKey: "settings.externalSqlEditorMaxMbDescription", targetId: "sqlFile" },
|
||||
{ id: "sql-file-web-upload-max-mb", category: "sqlFile", titleKey: "settings.webSqlFileUploadMaxMb", descriptionKey: "settings.webSqlFileUploadMaxMbDescription", targetId: "sqlFile", visible: (context) => context.isWeb },
|
||||
{ id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" },
|
||||
{ id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" },
|
||||
...createToolbarVisibilitySettingsSearchDefinitions(),
|
||||
|
||||
@@ -152,6 +152,7 @@ const SETTINGS_TRANSFER_CATEGORY_KEYS: Record<SettingsTransferCategoryId, readon
|
||||
"tableOpenPageSize",
|
||||
"queryResultMaxRowsEnabled",
|
||||
"queryResultMaxRows",
|
||||
"externalSqlEditorMaxMb",
|
||||
"infiniteScroll",
|
||||
"regexMaxMatchCount",
|
||||
"autoCalculateTotalRows",
|
||||
|
||||
@@ -49,6 +49,25 @@ export function externalSqlFilePaths(paths: string[]): string[] {
|
||||
|
||||
export const MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES = 64 * 1024 * 1024;
|
||||
|
||||
export const MIN_EXTERNAL_SQL_EDITOR_FILE_MB = 1;
|
||||
export const MAX_EXTERNAL_SQL_EDITOR_FILE_MB = 4096;
|
||||
|
||||
export function normalizeExternalSqlEditorMaxMb(value: unknown): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES / (1024 * 1024);
|
||||
return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, Math.round(parsed)));
|
||||
}
|
||||
|
||||
export function externalSqlEditorMaxBytes(maxMb: number): number {
|
||||
return Math.round(maxMb * 1024 * 1024);
|
||||
}
|
||||
|
||||
export function clampExternalSqlEditorMaxMbInput(value: unknown): number {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return MIN_EXTERNAL_SQL_EDITOR_FILE_MB;
|
||||
return Math.min(MAX_EXTERNAL_SQL_EDITOR_FILE_MB, Math.max(MIN_EXTERNAL_SQL_EDITOR_FILE_MB, Math.round(parsed)));
|
||||
}
|
||||
|
||||
export class ExternalSqlFileTooLargeError extends Error {
|
||||
constructor(
|
||||
readonly sizeBytes: number,
|
||||
@@ -63,9 +82,9 @@ export function isExternalSqlFileTooLargeError(error: unknown): error is Externa
|
||||
return error instanceof ExternalSqlFileTooLargeError;
|
||||
}
|
||||
|
||||
export function readBrowserSqlFile(file: Blob): Promise<string> {
|
||||
if (file.size > MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES) {
|
||||
return Promise.reject(new ExternalSqlFileTooLargeError(file.size, MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES));
|
||||
export function readBrowserSqlFile(file: Blob, maxSizeBytes = MAX_EXTERNAL_SQL_EDITOR_FILE_BYTES): Promise<string> {
|
||||
if (file.size > maxSizeBytes) {
|
||||
return Promise.reject(new ExternalSqlFileTooLargeError(file.size, maxSizeBytes));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
Reference in new issue
Block a user