From e732937d77c05383810c58b999f07f80a086f0f6 Mon Sep 17 00:00:00 2001 From: Julien Deniau Date: Tue, 17 Sep 2024 14:08:37 +0200 Subject: [PATCH] Save SQL query into file --- src/configuration/filePaths.ts | 9 ++++ src/configuration/index.ts | 14 +++--- src/main-process/sqlFileStorage.ts | 48 +++++++++++++++++++++ src/main.ts | 2 + src/preload.ts | 3 ++ src/preload/sqlFileStorage.ts | 12 ++++++ src/preload/sqlFileStorageChannel.ts | 4 ++ src/renderer/routes/sql.$connectionSlug.tsx | 26 ++++++++--- 8 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 src/main-process/sqlFileStorage.ts create mode 100644 src/preload/sqlFileStorage.ts create mode 100644 src/preload/sqlFileStorageChannel.ts diff --git a/src/configuration/filePaths.ts b/src/configuration/filePaths.ts index cec3b6a..0a2b5de 100644 --- a/src/configuration/filePaths.ts +++ b/src/configuration/filePaths.ts @@ -1,4 +1,5 @@ import { app } from 'electron'; +import { existsSync, mkdirSync } from 'node:fs'; import path, { dirname, resolve } from 'node:path'; import { isDevApp } from '../main-process/helpers'; @@ -13,6 +14,14 @@ export function getConfigurationFolder() { return dirname(getConfigurationPath()); } +export function createConfigurationFolderIfNotExists() { + const configurationPah = getConfigurationPath(); + + if (!existsSync(dirname(configurationPah))) { + mkdirSync(dirname(configurationPah), { recursive: true }); + } +} + export function getLogPath() { return path.join( app.getPath('userData'), diff --git a/src/configuration/index.ts b/src/configuration/index.ts index a66e99b..fab137c 100644 --- a/src/configuration/index.ts +++ b/src/configuration/index.ts @@ -1,11 +1,13 @@ import { dialog, safeStorage } from 'electron'; -import { existsSync, mkdirSync, readFileSync, writeFile } from 'node:fs'; -import { dirname } from 'node:path'; +import { existsSync, readFileSync, writeFile } from 'node:fs'; import log from 'electron-log'; import { WindowState } from '../main-process/windowState'; import { CONFIGURATION_CHANNEL } from '../preload/configurationChannel'; import { ConnectionObject, ConnectionObjectWithoutSlug } from '../sql/types'; -import { getConfigurationPath } from './filePaths'; +import { + createConfigurationFolderIfNotExists, + getConfigurationPath, +} from './filePaths'; import { DEFAULT_LOCALE } from './locale'; import { DEFAULT_THEME } from './themes'; import { @@ -93,12 +95,8 @@ function writeConfiguration(config: Configuration): void { ), }; - // mkdirSync(configPath, { recursive: true }); - // create the folder of the `dataFilePath` if it does not exist - if (!existsSync(dirname(configurationPath))) { - mkdirSync(dirname(configurationPath), { recursive: true }); - } + createConfigurationFolderIfNotExists(); writeFile( configurationPath, diff --git a/src/main-process/sqlFileStorage.ts b/src/main-process/sqlFileStorage.ts new file mode 100644 index 0000000..a7c9f93 --- /dev/null +++ b/src/main-process/sqlFileStorage.ts @@ -0,0 +1,48 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { join } from 'path'; +import { + createConfigurationFolderIfNotExists, + getConfigurationFolder, +} from '../configuration/filePaths'; +import { SQL_FILE_STORAGE_CHANNEL } from '../preload/sqlFileStorageChannel'; + +const LATEST_SQL_FILENAME = 'latest.sql'; + +function getSqlFilePath(): string { + const configurationFolder = getConfigurationFolder(); + + return join(configurationFolder, LATEST_SQL_FILENAME); +} + +function loadLatest(): string | null { + const latestSqlFilePath = getSqlFilePath(); + + if (!existsSync(latestSqlFilePath)) { + return null; + } + + return readFileSync(latestSqlFilePath, 'utf-8'); +} + +function saveLatest(sql: string): void { + const latestSqlFilePath = getSqlFilePath(); + + createConfigurationFolderIfNotExists(); + + return writeFileSync(latestSqlFilePath, sql); +} + +const IPC_EVENT_BINDING = { + [SQL_FILE_STORAGE_CHANNEL.LOAD_LATEST]: loadLatest, + [SQL_FILE_STORAGE_CHANNEL.SAVE_LATEST]: saveLatest, +} as const; + +export function bindIpcMainSqlFileStorage(ipcMain: Electron.IpcMain): void { + for (const [channel, handler] of Object.entries(IPC_EVENT_BINDING)) { + ipcMain.handle(channel, (event, ...args: unknown[]) => + // convert the first argument to senderId and bind the rest + // @ts-expect-error issue with strict type in tsconfig, but seems to work at runtime + handler(...args) + ); + } +} diff --git a/src/main.ts b/src/main.ts index 3792a7f..ad8672a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,6 +11,7 @@ import { getLogPath } from './configuration/filePaths'; import { isDevApp, isMacPlatform } from './main-process/helpers'; import { installReactDevToolsExtension } from './main-process/installReactDevToolsExtension'; import { createMenu } from './main-process/menu'; +import { bindIpcMainSqlFileStorage } from './main-process/sqlFileStorage'; import WindowStateKeeper from './main-process/windowState'; import connectionStackInstance from './sql'; @@ -84,6 +85,7 @@ app.whenReady().then(() => { installReactDevToolsExtension(); bindIpcMainConfiguration(ipcMain); + bindIpcMainSqlFileStorage(ipcMain); connectionStackInstance.bindIpcMain(ipcMain); ipcMain.handle('get-is-dev', () => { diff --git a/src/preload.ts b/src/preload.ts index 4dee686..97ea080 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -5,9 +5,11 @@ import { contextBridge, ipcRenderer } from 'electron'; import { config } from './preload/config'; import { navigationListener } from './preload/navigationListener'; import { sql } from './preload/sql'; +import { sqlFileStorage } from './preload/sqlFileStorage'; contextBridge.exposeInMainWorld('config', config); contextBridge.exposeInMainWorld('sql', sql); +contextBridge.exposeInMainWorld('sqlFileStorage', sqlFileStorage); contextBridge.exposeInMainWorld('navigationListener', navigationListener); ipcRenderer.invoke('get-is-dev').then((isDev) => { @@ -25,6 +27,7 @@ declare global { isMac: boolean; config: typeof config; sql: typeof sql; + sqlFileStorage: typeof sqlFileStorage; navigationListener: typeof navigationListener; } } diff --git a/src/preload/sqlFileStorage.ts b/src/preload/sqlFileStorage.ts new file mode 100644 index 0000000..8623909 --- /dev/null +++ b/src/preload/sqlFileStorage.ts @@ -0,0 +1,12 @@ +import { bindChannel } from './bindChannel'; +import { SQL_FILE_STORAGE_CHANNEL } from './sqlFileStorageChannel'; + +interface SqlFileStorage { + loadLatest(): Promise; + saveLatest(sql: string): Promise; +} + +export const sqlFileStorage: SqlFileStorage = { + loadLatest: bindChannel(SQL_FILE_STORAGE_CHANNEL.LOAD_LATEST), + saveLatest: bindChannel(SQL_FILE_STORAGE_CHANNEL.SAVE_LATEST), +}; diff --git a/src/preload/sqlFileStorageChannel.ts b/src/preload/sqlFileStorageChannel.ts new file mode 100644 index 0000000..0cb06f8 --- /dev/null +++ b/src/preload/sqlFileStorageChannel.ts @@ -0,0 +1,4 @@ +export enum SQL_FILE_STORAGE_CHANNEL { + LOAD_LATEST = 'sql-file-storage:load-latest', + SAVE_LATEST = 'sql-file-storage:save-latest', +} diff --git a/src/renderer/routes/sql.$connectionSlug.tsx b/src/renderer/routes/sql.$connectionSlug.tsx index b52d3e2..2f389bb 100644 --- a/src/renderer/routes/sql.$connectionSlug.tsx +++ b/src/renderer/routes/sql.$connectionSlug.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react'; import { Button, Flex, Form } from 'antd'; import { ActionFunctionArgs, useFetcher } from 'react-router-dom'; import invariant from 'tiny-invariant'; @@ -9,10 +10,19 @@ import { RawSqlEditor } from '../component/MonacoEditor/RawSqlEditor'; import RawSqlResult from '../component/Query/RawSqlResult/RowDataPacketResult'; // const DEFAULT_VALUE = `SELECT * FROM employees e WHERE e.gender = 'F' LIMIT 10;`; -const DEFAULT_VALUE = `SELECT t. -FROM ticketing t -JOIN -LIMIT 10;`; +function useSqlFileStorage(): [string | null, (value: string) => void] { + const [sqlQuery, setSqlQuery] = useState(null); + + useEffect(() => { + window.sqlFileStorage.loadLatest().then((v) => setSqlQuery(v ?? '')); + }, []); + + const saveValue = (value: string) => { + window.sqlFileStorage.saveLatest(value); + }; + + return [sqlQuery, saveValue]; +} type SqlActionReturnTypes = | { @@ -54,15 +64,21 @@ export default function SqlPage() { const { t } = useTranslation(); const [form] = Form.useForm(); const fetcher = useFetcher(); + const [sqlQuery, saveSqlQuery] = useSqlFileStorage(); const { state } = fetcher; + if (sqlQuery === null) { + return null; + } + return (
{ + saveSqlQuery(values.raw); fetcher.submit(values, { method: 'post', });