From 9743569ca7e8a66601b3f1077fcf688ec0cd79b9 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Sat, 20 Feb 2021 17:47:48 +0100 Subject: [PATCH] metadata loaders as svelte stream --- packages/web-svelte/package.json | 10 +- packages/web-svelte/src/utility/axios.js | 14 + packages/web-svelte/src/utility/cache.ts | 40 ++ packages/web-svelte/src/utility/getAsArray.ts | 7 + .../web-svelte/src/utility/getElectron.ts | 7 + .../web-svelte/src/utility/metadataLoaders.ts | 378 ++++++++++++++++++ packages/web-svelte/src/utility/resolveApi.ts | 21 + packages/web-svelte/src/utility/socket.js | 8 + .../src/widgets/ConnectionList.svelte | 16 +- .../src/widgets/SearchBoxWrapper.svelte | 8 + .../src/widgets/SqlObjectList.svelte | 22 + packages/web-svelte/tsconfig.json | 4 +- 12 files changed, 522 insertions(+), 13 deletions(-) create mode 100644 packages/web-svelte/src/utility/axios.js create mode 100644 packages/web-svelte/src/utility/cache.ts create mode 100644 packages/web-svelte/src/utility/getAsArray.ts create mode 100644 packages/web-svelte/src/utility/getElectron.ts create mode 100644 packages/web-svelte/src/utility/metadataLoaders.ts create mode 100644 packages/web-svelte/src/utility/resolveApi.ts create mode 100644 packages/web-svelte/src/utility/socket.js create mode 100644 packages/web-svelte/src/widgets/SearchBoxWrapper.svelte create mode 100644 packages/web-svelte/src/widgets/SqlObjectList.svelte diff --git a/packages/web-svelte/package.json b/packages/web-svelte/package.json index 97d4cf222..b8f15630b 100644 --- a/packages/web-svelte/package.json +++ b/packages/web-svelte/package.json @@ -22,10 +22,16 @@ "svelte-check": "^1.0.0", "svelte-preprocess": "^4.0.0", "tslib": "^2.0.0", - "typescript": "^3.9.3" + "typescript": "^3.9.3", + "socket.io-client": "^2.3.0", + "sql-formatter": "^2.3.3", + "uuid": "^3.4.0", + "json-stable-stringify": "^1.0.1", + "localforage": "^1.9.0", + "lodash": "^4.17.15" }, "dependencies": { "@mdi/font": "^5.9.55", "sirv-cli": "^1.0.0" } -} +} \ No newline at end of file diff --git a/packages/web-svelte/src/utility/axios.js b/packages/web-svelte/src/utility/axios.js new file mode 100644 index 000000000..9a587ccb9 --- /dev/null +++ b/packages/web-svelte/src/utility/axios.js @@ -0,0 +1,14 @@ +import axios from 'axios'; +import resolveApi from './resolveApi'; + +const axiosInstance = axios.create({ + baseURL: resolveApi(), +}); + +axiosInstance.defaults.headers = { + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + Expires: '0', +}; + +export default axiosInstance; diff --git a/packages/web-svelte/src/utility/cache.ts b/packages/web-svelte/src/utility/cache.ts new file mode 100644 index 000000000..8dd251bf0 --- /dev/null +++ b/packages/web-svelte/src/utility/cache.ts @@ -0,0 +1,40 @@ +import getAsArray from './getAsArray'; + +let cachedByKey = {}; +let cachedPromisesByKey = {}; +const cachedKeysByReloadTrigger = {}; + +export function cacheGet(key) { + return cachedByKey[key]; +} + +export function cacheSet(key, value, reloadTrigger) { + cachedByKey[key] = value; + for (const item of getAsArray(reloadTrigger)) { + if (!(item in cachedKeysByReloadTrigger)) { + cachedKeysByReloadTrigger[item] = []; + } + cachedKeysByReloadTrigger[item].push(key); + } + delete cachedPromisesByKey[key]; +} + +export function cacheClean(reloadTrigger) { + for (const item of getAsArray(reloadTrigger)) { + const keys = cachedKeysByReloadTrigger[item]; + if (keys) { + for (const key of keys) { + delete cachedByKey[key]; + delete cachedPromisesByKey[key]; + } + } + delete cachedKeysByReloadTrigger[item]; + } +} + +export function getCachedPromise(key, func) { + if (key in cachedPromisesByKey) return cachedPromisesByKey[key]; + const promise = func(); + cachedPromisesByKey[key] = promise; + return promise; +} diff --git a/packages/web-svelte/src/utility/getAsArray.ts b/packages/web-svelte/src/utility/getAsArray.ts new file mode 100644 index 000000000..623baebc0 --- /dev/null +++ b/packages/web-svelte/src/utility/getAsArray.ts @@ -0,0 +1,7 @@ +import _ from 'lodash'; + +export default function getAsArray(obj) { + if (_.isArray(obj)) return obj; + if (obj != null) return [obj]; + return []; +} diff --git a/packages/web-svelte/src/utility/getElectron.ts b/packages/web-svelte/src/utility/getElectron.ts new file mode 100644 index 000000000..7f19571cb --- /dev/null +++ b/packages/web-svelte/src/utility/getElectron.ts @@ -0,0 +1,7 @@ +export default function getElectron() { + if (window['require']) { + const electron = window['require']('electron'); + return electron; + } + return null; +} diff --git a/packages/web-svelte/src/utility/metadataLoaders.ts b/packages/web-svelte/src/utility/metadataLoaders.ts new file mode 100644 index 000000000..c5339c152 --- /dev/null +++ b/packages/web-svelte/src/utility/metadataLoaders.ts @@ -0,0 +1,378 @@ +import axios from './axios'; +import _ from 'lodash'; +import { cacheGet, cacheSet, getCachedPromise } from './cache'; +import stableStringify from 'json-stable-stringify'; +import { cacheClean } from './cache'; +import socket from './socket'; +import getAsArray from './getAsArray'; + +const databaseInfoLoader = ({ conid, database }) => ({ + url: 'database-connections/structure', + params: { conid, database }, + reloadTrigger: `database-structure-changed-${conid}-${database}`, + transform: db => { + const allForeignKeys = _.flatten(db.tables.map(x => x.foreignKeys)); + return { + ...db, + tables: db.tables.map(table => ({ + ...table, + dependencies: allForeignKeys.filter( + (x: any) => x.refSchemaName == table.schemaName && x.refTableName == table.pureName + ), + })), + }; + }, +}); + +// const tableInfoLoader = ({ conid, database, schemaName, pureName }) => ({ +// url: 'metadata/table-info', +// params: { conid, database, schemaName, pureName }, +// reloadTrigger: `database-structure-changed-${conid}-${database}`, +// }); + +// const sqlObjectInfoLoader = ({ objectTypeField, conid, database, schemaName, pureName }) => ({ +// url: 'metadata/sql-object-info', +// params: { objectTypeField, conid, database, schemaName, pureName }, +// reloadTrigger: `database-structure-changed-${conid}-${database}`, +// }); + +const connectionInfoLoader = ({ conid }) => ({ + url: 'connections/get', + params: { conid }, + reloadTrigger: 'connection-list-changed', +}); + +const configLoader = () => ({ + url: 'config/get', + params: {}, + reloadTrigger: 'config-changed', +}); + +const platformInfoLoader = () => ({ + url: 'config/platform-info', + params: {}, + reloadTrigger: 'platform-info-changed', +}); + +const favoritesLoader = () => ({ + url: 'files/favorites', + params: {}, + reloadTrigger: 'files-changed-favorites', +}); + +// const sqlObjectListLoader = ({ conid, database }) => ({ +// url: 'metadata/list-objects', +// params: { conid, database }, +// reloadTrigger: `database-structure-changed-${conid}-${database}`, +// }); + +const databaseStatusLoader = ({ conid, database }) => ({ + url: 'database-connections/status', + params: { conid, database }, + reloadTrigger: `database-status-changed-${conid}-${database}`, +}); + +const databaseListLoader = ({ conid }) => ({ + url: 'server-connections/list-databases', + params: { conid }, + reloadTrigger: `database-list-changed-${conid}`, +}); + +const archiveFoldersLoader = () => ({ + url: 'archive/folders', + params: {}, + reloadTrigger: `archive-folders-changed`, +}); + +const archiveFilesLoader = ({ folder }) => ({ + url: 'archive/files', + params: { folder }, + reloadTrigger: `archive-files-changed-${folder}`, +}); + +const serverStatusLoader = () => ({ + url: 'server-connections/server-status', + params: {}, + reloadTrigger: `server-status-changed`, +}); + +const connectionListLoader = () => ({ + url: 'connections/list', + params: {}, + reloadTrigger: `connection-list-changed`, +}); + +const installedPluginsLoader = () => ({ + url: 'plugins/installed', + params: {}, + reloadTrigger: `installed-plugins-changed`, +}); + +const filesLoader = ({ folder }) => ({ + url: 'files/list', + params: { folder }, + reloadTrigger: `files-changed-${folder}`, +}); +const allFilesLoader = () => ({ + url: 'files/list-all', + params: {}, + reloadTrigger: `all-files-changed`, +}); + +async function getCore(loader, args) { + const { url, params, reloadTrigger, transform } = loader(args); + const key = stableStringify({ url, ...params }); + + async function doLoad() { + const resp = await axios.request({ + method: 'get', + url, + params, + }); + return (transform || (x => x))(resp.data); + } + + const fromCache = cacheGet(key); + if (fromCache) return fromCache; + const res = await getCachedPromise(key, doLoad); + + cacheSet(key, res, reloadTrigger); + return res; +} + +function useCore(loader, args) { + const { url, params, reloadTrigger, transform } = loader(args); + const cacheKey = stableStringify({ url, ...params }); + + return { + subscribe: onChange => { + async function handleReload() { + async function doLoad() { + const resp = await axios.request({ + method: 'get', + params, + url, + }); + return (transform || (x => x))(resp.data); + } + + if (cacheKey) { + const fromCache = cacheGet(cacheKey); + if (fromCache) { + onChange(fromCache); + } else { + try { + const res = await getCachedPromise(cacheKey, doLoad); + cacheSet(cacheKey, res, reloadTrigger); + onChange(res); + } catch (err) { + console.error('Error when using cached promise', err); + cacheClean(cacheKey); + const res = await doLoad(); + cacheSet(cacheKey, res, reloadTrigger); + onChange(res); + } + } + } else { + const res = await doLoad(); + onChange(res); + } + } + + if (reloadTrigger && !socket) { + console.error('Socket not available, reloadTrigger not planned'); + } + handleReload(); + if (reloadTrigger && socket) { + for (const item of getAsArray(reloadTrigger)) { + socket.on(item, handleReload); + } + return () => { + for (const item of getAsArray(reloadTrigger)) { + socket.off(item, handleReload); + } + }; + } + }, + }; + + // const useTrack = track => ({ + // subscribe: onChange => { + // onChange('TRACK ' + track); + // if (track) { + // const handle = setInterval(() => onChange('TRACK ' + track + ';' + new Date()), 1000); + // // console.log("ON", track); + // const oldTrack = track; + // return () => { + // clearInterval(handle); + // // console.log("OFF", oldTrack); + // }; + // } + // }, + // }); + + // const res = useFetch({ + // url, + // params, + // reloadTrigger, + // cacheKey, + // transform, + // }); + + // return res; +} + +/** @returns {Promise} */ +export function getDatabaseInfo(args) { + return getCore(databaseInfoLoader, args); +} + +/** @returns {import('dbgate-types').DatabaseInfo} */ +export function useDatabaseInfo(args) { + return useCore(databaseInfoLoader, args); +} + +export async function getDbCore(args, objectTypeField = undefined) { + const db = await getDatabaseInfo(args); + if (!db) return null; + return db[objectTypeField || args.objectTypeField].find( + x => x.pureName == args.pureName && x.schemaName == args.schemaName + ); +} + +export function useDbCore(args, objectTypeField = undefined) { + const db = useDatabaseInfo(args); + if (!db) return null; + return db[objectTypeField || args.objectTypeField].find( + x => x.pureName == args.pureName && x.schemaName == args.schemaName + ); +} + +/** @returns {Promise} */ +export function getTableInfo(args) { + return getDbCore(args, 'tables'); +} + +/** @returns {import('dbgate-types').TableInfo} */ +export function useTableInfo(args) { + return useDbCore(args, 'tables'); +} + +/** @returns {Promise} */ +export function getViewInfo(args) { + return getDbCore(args, 'views'); +} + +/** @returns {import('dbgate-types').ViewInfo} */ +export function useViewInfo(args) { + return useDbCore(args, 'views'); +} + +export function getSqlObjectInfo(args) { + return getDbCore(args); +} + +export function useSqlObjectInfo(args) { + return useDbCore(args); +} + +/** @returns {Promise} */ +export function getConnectionInfo(args) { + return getCore(connectionInfoLoader, args); +} + +/** @returns {import('dbgate-types').StoredConnection} */ +export function useConnectionInfo(args) { + return useCore(connectionInfoLoader, args); +} + +// export function getSqlObjectList(args) { +// return getCore(sqlObjectListLoader, args); +// } +// export function useSqlObjectList(args) { +// return useCore(sqlObjectListLoader, args); +// } + +export function getDatabaseStatus(args) { + return getCore(databaseStatusLoader, args); +} +export function useDatabaseStatus(args) { + return useCore(databaseStatusLoader, args); +} + +export function getDatabaseList(args) { + return getCore(databaseListLoader, args); +} +export function useDatabaseList(args) { + return useCore(databaseListLoader, args); +} + +export function getServerStatus() { + return getCore(serverStatusLoader, {}); +} +export function useServerStatus() { + return useCore(serverStatusLoader, {}); +} + +export function getConnectionList() { + return getCore(connectionListLoader, {}); +} +export function useConnectionList() { + return useCore(connectionListLoader, {}); +} + +export function getConfig() { + return getCore(configLoader, {}) || {}; +} +export function useConfig() { + return useCore(configLoader, {}) || {}; +} + +export function getPlatformInfo() { + return getCore(platformInfoLoader, {}) || {}; +} +export function usePlatformInfo() { + return useCore(platformInfoLoader, {}) || {}; +} + +export function getArchiveFiles(args) { + return getCore(archiveFilesLoader, args); +} +export function useArchiveFiles(args) { + return useCore(archiveFilesLoader, args); +} + +export function getArchiveFolders(args) { + return getCore(archiveFoldersLoader, args); +} +export function useArchiveFolders(args) { + return useCore(archiveFoldersLoader, args); +} + +export function getInstalledPlugins(args) { + return getCore(installedPluginsLoader, args) || []; +} +export function useInstalledPlugins(args) { + return useCore(installedPluginsLoader, args) || []; +} + +export function getFiles(args) { + return getCore(filesLoader, args); +} +export function useFiles(args) { + return useCore(filesLoader, args); +} + +export function getAllFiles(args) { + return getCore(allFilesLoader, args); +} +export function useAllFiles(args) { + return useCore(allFilesLoader, args); +} + +export function getFavorites(args) { + return getCore(favoritesLoader, args); +} +export function useFavorites(args) { + return useCore(favoritesLoader, args); +} diff --git a/packages/web-svelte/src/utility/resolveApi.ts b/packages/web-svelte/src/utility/resolveApi.ts new file mode 100644 index 000000000..1005e664e --- /dev/null +++ b/packages/web-svelte/src/utility/resolveApi.ts @@ -0,0 +1,21 @@ +export default function resolveApi() { + if (window['require']) { + const electron = window['require']('electron'); + + if (electron) { + const port = electron.remote.getGlobal('port'); + if (port) { + return `http://localhost:${port}`; + } + } + } + + // // eslint-disable-next-line + // const apiUrl = process.env.REACT_APP_API_URL; + // if (apiUrl) { + // if (apiUrl == 'ORIGIN') return window.location.origin; + // return apiUrl; + // } + + return 'http://localhost:3000'; +} diff --git a/packages/web-svelte/src/utility/socket.js b/packages/web-svelte/src/utility/socket.js new file mode 100644 index 000000000..dd3e1d741 --- /dev/null +++ b/packages/web-svelte/src/utility/socket.js @@ -0,0 +1,8 @@ +import io from 'socket.io-client'; +import resolveApi from './resolveApi'; +import { cacheClean } from './cache'; + +const socket = io(resolveApi()); +socket.on('clean-cache', reloadTrigger => cacheClean(reloadTrigger)); + +export default socket; diff --git a/packages/web-svelte/src/widgets/ConnectionList.svelte b/packages/web-svelte/src/widgets/ConnectionList.svelte index b1c4f76e5..3f9305cdc 100644 --- a/packages/web-svelte/src/widgets/ConnectionList.svelte +++ b/packages/web-svelte/src/widgets/ConnectionList.svelte @@ -2,17 +2,15 @@ import InlineButton from './InlineButton.svelte'; import SearchInput from './SearchInput.svelte'; import WidgetsInnerContainer from './WidgetsInnerContainer.svelte'; + import { useConnectionList } from '../utility/metadataLoaders'; +import SearchBoxWrapper from './SearchBoxWrapper.svelte'; + + const connections = useConnectionList(); + $: console.log('CONNECTIONS', $connections); - + CONNECTIONS - - diff --git a/packages/web-svelte/src/widgets/SearchBoxWrapper.svelte b/packages/web-svelte/src/widgets/SearchBoxWrapper.svelte new file mode 100644 index 000000000..693c2cb9d --- /dev/null +++ b/packages/web-svelte/src/widgets/SearchBoxWrapper.svelte @@ -0,0 +1,8 @@ +
+ + diff --git a/packages/web-svelte/src/widgets/SqlObjectList.svelte b/packages/web-svelte/src/widgets/SqlObjectList.svelte new file mode 100644 index 000000000..6db4d33ef --- /dev/null +++ b/packages/web-svelte/src/widgets/SqlObjectList.svelte @@ -0,0 +1,22 @@ + + + + + Refresh + +CONNECTIONS diff --git a/packages/web-svelte/tsconfig.json b/packages/web-svelte/tsconfig.json index b082e9685..784d828ff 100644 --- a/packages/web-svelte/tsconfig.json +++ b/packages/web-svelte/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "@tsconfig/svelte/tsconfig.json", + // "extends": "@tsconfig/svelte/tsconfig.json", "include": ["src/**/*"], - "exclude": ["node_modules/*", "__sapper__/*", "public/*"] + "exclude": ["node_modules/*", "public/*"] } \ No newline at end of file