diff --git a/.github/workflows/build-npm.yaml b/.github/workflows/build-npm.yaml index cb7dbf2c..929dc5ab 100644 --- a/.github/workflows/build-npm.yaml +++ b/.github/workflows/build-npm.yaml @@ -133,3 +133,8 @@ jobs: working-directory: plugins/dbgate-plugin-sqlite run: | npm publish + + - name: Publish dbgate-plugin-redis + working-directory: plugins/dbgate-plugin-redis + run: | + npm publish diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index fb86362d..cf9147ee 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -240,6 +240,7 @@ module.exports = { get_meta: true, async get({ conid }) { + if (!conid) return null; if (portalConnections) return portalConnections.find(x => x._id == conid) || null; const res = await this.datastore.get(conid); return res || null; diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 21baa660..eea07817 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -151,6 +151,46 @@ module.exports = { return res.result; }, + loadKeys_meta: true, + async loadKeys({ conid, database, root }) { + const opened = await this.ensureOpened(conid, database); + const res = await this.sendRequest(opened, { msgtype: 'loadKeys', root }); + if (res.errorMessage) { + console.error(res.errorMessage); + } + return res.result || null; + }, + + loadKeyInfo_meta: true, + async loadKeyInfo({ conid, database, key }) { + const opened = await this.ensureOpened(conid, database); + const res = await this.sendRequest(opened, { msgtype: 'loadKeyInfo', key }); + if (res.errorMessage) { + console.error(res.errorMessage); + } + return res.result || null; + }, + + loadKeyTableRange_meta: true, + async loadKeyTableRange({ conid, database, key, cursor, count }) { + const opened = await this.ensureOpened(conid, database); + const res = await this.sendRequest(opened, { msgtype: 'loadKeyTableRange', key, cursor, count }); + if (res.errorMessage) { + console.error(res.errorMessage); + } + return res.result || null; + }, + + callMethod_meta: true, + async callMethod({ conid, database, method, args }) { + const opened = await this.ensureOpened(conid, database); + const res = await this.sendRequest(opened, { msgtype: 'callMethod', method, args }); + if (res.errorMessage) { + console.error(res.errorMessage); + } + return res.result || null; + }, + updateCollection_meta: true, async updateCollection({ conid, database, changeSet }) { const opened = await this.ensureOpened(conid, database); diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index c3ae8a3e..da730ba6 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -183,6 +183,50 @@ async function handleCollectionData({ msgid, options }) { } } +async function handleLoadKeys({ msgid, root }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + try { + const result = await driver.loadKeys(systemConnection, root); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + +async function handleLoadKeyInfo({ msgid, key }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + try { + const result = await driver.loadKeyInfo(systemConnection, key); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + +async function handleCallMethod({ msgid, method, args }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + try { + const result = await driver.callMethod(systemConnection, method, args); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + +async function handleLoadKeyTableRange({ msgid, key, cursor, count }) { + await waitConnected(); + const driver = requireEngineDriver(storedConnection); + try { + const result = await driver.loadKeyTableRange(systemConnection, key, cursor, count); + process.send({ msgtype: 'response', msgid, result }); + } catch (err) { + process.send({ msgtype: 'response', msgid, errorMessage: err.message }); + } +} + async function handleUpdateCollection({ msgid, changeSet }) { await waitConnected(); const driver = requireEngineDriver(storedConnection); @@ -248,6 +292,10 @@ const messageHandlers = { runScript: handleRunScript, updateCollection: handleUpdateCollection, collectionData: handleCollectionData, + loadKeys: handleLoadKeys, + loadKeyInfo: handleLoadKeyInfo, + callMethod: handleCallMethod, + loadKeyTableRange: handleLoadKeyTableRange, sqlPreview: handleSqlPreview, ping: handlePing, syncModel: handleSyncModel, diff --git a/packages/api/src/shell/tableReader.js b/packages/api/src/shell/tableReader.js index 46e7fe16..c0824ee6 100644 --- a/packages/api/src/shell/tableReader.js +++ b/packages/api/src/shell/tableReader.js @@ -9,7 +9,7 @@ async function tableReader({ connection, pureName, schemaName }) { const fullName = { pureName, schemaName }; - if (driver.dialect.nosql) { + if (driver.databaseEngineTypes.includes('document')) { // @ts-ignore console.log(`Reading collection ${fullNameToString(fullName)}`); // @ts-ignore diff --git a/packages/tools/src/createBulkInsertStreamBase.ts b/packages/tools/src/createBulkInsertStreamBase.ts index 37d0f6c5..01f8b1b2 100644 --- a/packages/tools/src/createBulkInsertStreamBase.ts +++ b/packages/tools/src/createBulkInsertStreamBase.ts @@ -14,7 +14,7 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options): writable.buffer = []; writable.structure = null; writable.columnNames = null; - writable.requireFixedStructure = !driver.dialect.nosql; + writable.requireFixedStructure = driver.databaseEngineTypes.includes('sql'); writable.addRow = async row => { if (writable.structure) { diff --git a/packages/tools/src/driverBase.ts b/packages/tools/src/driverBase.ts index b4e9dc98..6618ec48 100644 --- a/packages/tools/src/driverBase.ts +++ b/packages/tools/src/driverBase.ts @@ -20,6 +20,7 @@ export const driverBase = { analyserClass: null, dumperClass: SqlDumper, dialect, + databaseEngineTypes: ['sql'], async analyseFull(pool, version) { const analyser = new this.analyserClass(pool, this, version); @@ -45,7 +46,7 @@ export const driverBase = { } }, getNewObjectTemplates() { - if (!this.dialect?.nosql) { + if (this.databaseEngineTypes.includes('sql')) { return [{ label: 'New view', sql: 'CREATE VIEW myview\nAS\nSELECT * FROM table1' }]; } return []; diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index 47aecea4..dc8fa14d 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -59,3 +59,28 @@ export function safeJsonParse(json, defaultValue?, logError = false) { export function isJsonLikeLongString(value) { return _isString(value) && value.length > 100 && value.match(/^\s*\{.*\}\s*$|^\s*\[.*\]\s*$/); } + +export function getIconForRedisType(type) { + switch (type) { + case 'dir': + return 'img folder'; + case 'string': + return 'img type-string'; + case 'hash': + return 'img type-hash'; + case 'set': + return 'img type-set'; + case 'list': + return 'img type-list'; + case 'zset': + return 'img type-zset'; + case 'stream': + return 'img type-stream'; + case 'binary': + return 'img type-binary'; + case 'ReJSON-RL': + return 'img type-rejson'; + default: + return null; + } +} diff --git a/packages/types/dialect.d.ts b/packages/types/dialect.d.ts index f05dc052..71f496d0 100644 --- a/packages/types/dialect.d.ts +++ b/packages/types/dialect.d.ts @@ -10,7 +10,6 @@ export interface SqlDialect { explicitDropConstraint?: boolean; anonymousPrimaryKey?: boolean; enableConstraintsPerTable?: boolean; - nosql?: boolean; // mongo dropColumnDependencies?: string[]; changeColumnDependencies?: string[]; diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 2f1d2719..ab60bb2e 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -46,6 +46,8 @@ export interface EngineDriver { engine: string; title: string; defaultPort?: number; + databaseEngineTypes: string[]; + supportedKeyTypes: { name: string; label: string }[]; supportsDatabaseUrl?: boolean; isElectronOnly?: boolean; showConnectionField?: (field: string, values: any) => boolean; @@ -74,6 +76,9 @@ export interface EngineDriver { name: string; }[] >; + loadKeys(pool, root: string): Promise; + loadKeyInfo(pool, key): Promise; + loadKeyTableRange(pool, key, cursor, count): Promise; analyseFull(pool: any, serverVersion): Promise; analyseIncremental(pool: any, structure: DatabaseInfo, serverVersion): Promise; dialect: SqlDialect; @@ -87,6 +92,8 @@ export interface EngineDriver { getQuerySplitterOptions(usage: 'stream' | 'script'): any; script(pool: any, sql: string): Promise; getNewObjectTemplates(): NewObjectTemplate[]; + // direct call of pool method, only some methods could be supported, on only some drivers + callMethod(pool, method, args); analyserClass?: any; dumperClass?: any; diff --git a/packages/web/src/appobj/AppObjectCore.svelte b/packages/web/src/appobj/AppObjectCore.svelte index dc82c4e3..ed0e33bd 100644 --- a/packages/web/src/appobj/AppObjectCore.svelte +++ b/packages/web/src/appobj/AppObjectCore.svelte @@ -8,8 +8,8 @@ export let icon; export let title; - export let data; - export let module; + export let data = null; + export let module = null; export let isBold = false; export let isBusy = false; @@ -24,8 +24,10 @@ export let onPin = null; export let onUnpin = null; export let showPinnedInsteadOfUnpin = false; + export let indentLevel = 0; - $: isChecked = checkedObjectsStore && $checkedObjectsStore.find(x => module.extractKey(data) == module.extractKey(x)); + $: isChecked = + checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x)); function handleExpand() { dispatch('expand'); @@ -33,7 +35,7 @@ function handleClick() { if (checkedObjectsStore) { if (isChecked) { - checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y))); + checkedObjectsStore.update(x => x.filter(y => module?.extractKey(data) != module?.extractKey(y))); } else { checkedObjectsStore.update(x => [...x, data]); } @@ -52,12 +54,14 @@ function setChecked(value) { if (!value && isChecked) { - checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y))); + checkedObjectsStore.update(x => x.filter(y => module?.extractKey(data) != module?.extractKey(y))); } if (value && !isChecked) { checkedObjectsStore.update(x => [...x, data]); } } + + // $: console.log(title, indentLevel);
{/if} + {#if indentLevel} + + {/if} {#if isBusy} {:else} @@ -188,5 +195,4 @@ float: right; color: var(--theme-font-2); } - diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte index 7611f0fc..78e86a98 100644 --- a/packages/web/src/appobj/DatabaseAppObject.svelte +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -174,8 +174,8 @@ return [ { onClick: handleNewQuery, text: 'New query', isNewQuery: true }, - !driver?.dialect?.nosql && { onClick: handleNewTable, text: 'New table' }, - driver?.dialect?.nosql && { onClick: handleNewCollection, text: 'New collection' }, + driver?.databaseEngineTypes?.includes('sql') && { onClick: handleNewTable, text: 'New table' }, + driver?.databaseEngineTypes?.includes('document') && { onClick: handleNewCollection, text: 'New collection' }, { divider: true }, { onClick: handleImport, text: 'Import' }, { onClick: handleExport, text: 'Export' }, @@ -256,6 +256,7 @@ {...$$restProps} {data} title={data.name} + extInfo={data.extInfo} icon="img database" colorMark={passProps?.connectionColorFactory && passProps?.connectionColorFactory({ conid: _.get(data.connection, '_id'), database: data.name }, null, null, false)} diff --git a/packages/web/src/appobj/SubDatabaseList.svelte b/packages/web/src/appobj/SubDatabaseList.svelte index b719ac97..c733007c 100644 --- a/packages/web/src/appobj/SubDatabaseList.svelte +++ b/packages/web/src/appobj/SubDatabaseList.svelte @@ -15,7 +15,7 @@ filterName(filter, x.name)), - 'name' + x => x.sortOrder ?? x.name ).map(db => ({ ...db, connection: data }))} module={databaseAppObject} {passProps} diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index cae2c54d..b464a5c5 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -4,6 +4,7 @@ import { get } from 'svelte/store'; import { ThemeDefinition } from 'dbgate-types'; import ConnectionModal from '../modals/ConnectionModal.svelte'; import AboutModal from '../modals/AboutModal.svelte'; +import AddDbKeyModal from '../modals/AddDbKeyModal.svelte'; import SettingsModal from '../settings/SettingsModal.svelte'; import ImportExportModal from '../modals/ImportExportModal.svelte'; import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte'; @@ -158,7 +159,7 @@ registerCommand({ toolbarName: 'New table', testEnabled: () => { const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions()); - return !!get(currentDatabase) && !driver?.dialect?.nosql; + return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('sql'); }, onClick: () => { const $currentDatabase = get(currentDatabase); @@ -196,7 +197,7 @@ registerCommand({ toolbarName: 'New collection', testEnabled: () => { const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions()); - return !!get(currentDatabase) && driver?.dialect?.nosql; + return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('document'); }, onClick: async () => { const $currentDatabase = get(currentDatabase); @@ -217,6 +218,30 @@ registerCommand({ }, }); +registerCommand({ + id: 'new.dbKey', + category: 'New', + name: 'Key', + toolbar: true, + toolbarName: 'New key', + testEnabled: () => { + const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions()); + return !!get(currentDatabase) && driver?.databaseEngineTypes?.includes('keyvalue'); + }, + onClick: async () => { + const $currentDatabase = get(currentDatabase); + const connection = _.get($currentDatabase, 'connection') || {}; + const database = _.get($currentDatabase, 'name'); + const driver = findEngineDriver(get(currentDatabase)?.connection, getExtensions()); + + showModal(AddDbKeyModal, { + conid: connection._id, + database, + driver, + }); + }, +}); + registerCommand({ id: 'new.markdown', category: 'New', diff --git a/packages/web/src/datagrid/DataFilterControl.svelte b/packages/web/src/datagrid/DataFilterControl.svelte index 1afe6642..2f49ac42 100644 --- a/packages/web/src/datagrid/DataFilterControl.svelte +++ b/packages/web/src/datagrid/DataFilterControl.svelte @@ -266,7 +266,7 @@ class:isOk placeholder="Filter" /> - {#if conid && database && driver && !driver?.dialect?.nosql} + {#if conid && database && driver && driver?.databaseEngineTypes?.includes('sql')} {#if foreignKey} diff --git a/packages/web/src/datagrid/DbKeyTableControl.svelte b/packages/web/src/datagrid/DbKeyTableControl.svelte new file mode 100644 index 00000000..22c32f16 --- /dev/null +++ b/packages/web/src/datagrid/DbKeyTableControl.svelte @@ -0,0 +1,95 @@ + + + ({ + fieldName: column.name, + header: column.name, + })), + ]} + {rows} + onLoadNext={isLoadedAll ? null : loadNextRows} + selectable + singleLineRow + bind:selectedIndex +/> diff --git a/packages/web/src/dbkeyvalue/DbKeyItemDetail.svelte b/packages/web/src/dbkeyvalue/DbKeyItemDetail.svelte new file mode 100644 index 00000000..1b6174d0 --- /dev/null +++ b/packages/web/src/dbkeyvalue/DbKeyItemDetail.svelte @@ -0,0 +1,44 @@ + + +
+ {#each dbKeyFields as column} +
{column.name}
+
+ { + if (onChangeItem) { + onChangeItem({ + ...item, + [column.name]: e.detail, + }); + } + }} + /> +
+ {/each} +
+ + diff --git a/packages/web/src/elements/ScrollableTableControl.svelte b/packages/web/src/elements/ScrollableTableControl.svelte index fefdc3a7..10af4f3e 100644 --- a/packages/web/src/elements/ScrollableTableControl.svelte +++ b/packages/web/src/elements/ScrollableTableControl.svelte @@ -15,7 +15,7 @@ - + diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index c308bb22..1ff16ff9 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -167,6 +167,16 @@ 'img link': 'mdi mdi-link', 'img filter': 'mdi mdi-filter', 'img group': 'mdi mdi-group', + + 'img folder': 'mdi mdi-folder color-icon-yellow', + 'img type-string': 'mdi mdi-alphabetical color-icon-blue', + 'img type-hash': 'mdi mdi-pound color-icon-blue', + 'img type-set': 'mdi mdi-format-list-bulleted color-icon-blue', + 'img type-list': 'mdi mdi-format-list-numbered color-icon-blue', + 'img type-zset': 'mdi mdi-format-list-checks color-icon-blue', + 'img type-stream': 'mdi mdi-view-stream color-icon-blue', + 'img type-binary': 'mdi mdi-file color-icon-blue', + 'img type-rejson': 'mdi mdi-color-json color-icon-blue', }; diff --git a/packages/web/src/modals/AddDbKeyModal.svelte b/packages/web/src/modals/AddDbKeyModal.svelte new file mode 100644 index 00000000..78453ad5 --- /dev/null +++ b/packages/web/src/modals/AddDbKeyModal.svelte @@ -0,0 +1,58 @@ + + + + + Add item + +
+ ({ value: t.name, label: t.label }))} + value={type} + on:change={e => { + type = e.detail; + }} + /> + + x.name == type).dbKeyFields} + {item} + onChangeItem={value => { + item = value; + }} + /> +
+ + + handleSubmit()} /> + + +
+
+ + diff --git a/packages/web/src/modals/ConnectionModalDriverFields.svelte b/packages/web/src/modals/ConnectionModalDriverFields.svelte index 972a8509..41b6b719 100644 --- a/packages/web/src/modals/ConnectionModalDriverFields.svelte +++ b/packages/web/src/modals/ConnectionModalDriverFields.svelte @@ -26,6 +26,9 @@ $: disabledFields = (currentAuthType ? currentAuthType.disabledFields : null) || []; $: driver = $extensions.drivers.find(x => x.engine == engine); $: defaultDatabase = $values.defaultDatabase; + + $: showUser = !driver?.showConnectionField || driver.showConnectionField('user', $values); + $: showPassword = !driver?.showConnectionField || driver.showConnectionField('password', $values); {/if} -{#if !driver?.showConnectionField || driver.showConnectionField('user', $values)} +{#if showUser && showPassword}
-
- -
- {#if !driver?.showConnectionField || driver.showConnectionField('password', $values)} + {#if showUser} +
+ +
+ {/if} + {#if showPassword}
{/if} +{#if showUser && !showPassword} + +{/if} +{#if !showUser && showPassword} + +{/if} -{#if !disabledFields.includes('password') && (!driver?.showConnectionField || driver.showConnectionField('password', $values))} +{#if !disabledFields.includes('password') && showPassword} + import FormStyledButton from '../buttons/FormStyledButton.svelte'; + import DbKeyItemDetail from '../dbkeyvalue/DbKeyItemDetail.svelte'; + + import FormProvider from '../forms/FormProvider.svelte'; + import ModalBase from './ModalBase.svelte'; + import { closeCurrentModal } from './modalTools'; + + export let keyInfo; + export let label; + export let onConfirm; + + let item = {}; + + const handleSubmit = async () => { + closeCurrentModal(); + onConfirm(item); + }; + + + + + Add item + +
+ { + item = value; + }} + /> +
+ + + handleSubmit()} /> + + +
+
+ + diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index 70215782..3d51e9f2 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -93,6 +93,7 @@ export const loadingPluginStore = writable({ loaded: false, loadingPackageName: null, }); +export const activeDbKeysStore = writableWithStorage({}, 'activeDbKeysStore'); export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) => $extensions.themes.find(x => x.themeClassName == $currentTheme) diff --git a/packages/web/src/tabs/DbKeyDetailTab.svelte b/packages/web/src/tabs/DbKeyDetailTab.svelte new file mode 100644 index 00000000..830ee92f --- /dev/null +++ b/packages/web/src/tabs/DbKeyDetailTab.svelte @@ -0,0 +1,183 @@ + + + + +{#await apiCall('database-connections/load-key-info', { conid, database, key, refreshToken })} + +{:then keyInfo} +
+
+
+ + {keyInfo.type} +
+
+ +
+ handleChangeTtl(keyInfo)} /> + {#if keyInfo.type == 'string'} + + {/if} + {#if keyInfo.keyType?.addMethod && keyInfo.keyType?.showItemList} + addItem(keyInfo)} /> + {/if} + +
+ +
+ {#if keyInfo.keyType?.dbKeyFields && keyInfo.keyType?.showItemList} + + + { + currentRow = row; + }} + /> + + + + + + {:else} + { + editedValue = e.detail; + }} + /> + {/if} +
+
+{/await} + + diff --git a/packages/web/src/tabs/QueryTab.svelte b/packages/web/src/tabs/QueryTab.svelte index 669f3d95..94f50a8b 100644 --- a/packages/web/src/tabs/QueryTab.svelte +++ b/packages/web/src/tabs/QueryTab.svelte @@ -124,7 +124,7 @@ } export function isSqlEditor() { - return !driver?.dialect?.nosql; + return driver?.databaseEngineTypes?.includes('sql'); } export function canKill() { @@ -281,7 +281,7 @@ - {#if driver?.dialect?.nosql} + {#if driver?.databaseEngineTypes?.includes('document')} ({ }, }); +const databaseKeysLoader = ({ conid, database, root }) => ({ + url: 'database-connections/load-keys', + params: { conid, database, root }, + reloadTrigger: `database-keys-changed-${conid}-${database}`, +}); + const serverVersionLoader = ({ conid }) => ({ url: 'server-connections/version', params: { conid }, @@ -429,3 +435,10 @@ export function getAuthTypes(args) { export function useAuthTypes(args) { return useCore(authTypesLoader, args); } + +export function getDatabaseKeys(args) { + return getCore(databaseKeysLoader, args); +} +export function useDatabaseKeys(args) { + return useCore(databaseKeysLoader, args); +} diff --git a/packages/web/src/widgets/CellDataWidget.svelte b/packages/web/src/widgets/CellDataWidget.svelte index b5991b15..06e391bd 100644 --- a/packages/web/src/widgets/CellDataWidget.svelte +++ b/packages/web/src/widgets/CellDataWidget.svelte @@ -39,7 +39,7 @@ ]; function autodetect(selection) { - if (selection[0]?.engine?.dialect?.nosql) { + if (selection[0]?.engine?.databaseEngineTypes?.includes('document')) { return 'jsonRow'; } const value = selection.length == 1 ? selection[0].value : null; diff --git a/packages/web/src/widgets/DatabaseWidget.svelte b/packages/web/src/widgets/DatabaseWidget.svelte index 1f341f02..807fe4ca 100644 --- a/packages/web/src/widgets/DatabaseWidget.svelte +++ b/packages/web/src/widgets/DatabaseWidget.svelte @@ -5,10 +5,13 @@ import ConnectionList from './ConnectionList.svelte'; import PinnedObjectsList from './PinnedObjectsList.svelte'; - import SqlObjectListWrapper from './SqlObjectListWrapper.svelte'; + import ErrorInfo from '../elements/ErrorInfo.svelte'; + import WidgetsInnerContainer from './WidgetsInnerContainer.svelte'; import WidgetColumnBar from './WidgetColumnBar.svelte'; import WidgetColumnBarItem from './WidgetColumnBarItem.svelte'; + import SqlObjectList from './SqlObjectList.svelte'; + import DbKeysTree from './DbKeysTree.svelte'; export let hidden = false; @@ -16,6 +19,8 @@ $: connection = useConnectionInfo({ conid }); $: driver = findEngineDriver($connection, $extensions); $: config = useConfig(); + $: singleDatabase = $currentDatabase?.connection?.singleDatabase; + $: database = $currentDatabase?.name; @@ -34,11 +39,26 @@ > - - - + + {#if conid && (database || singleDatabase)} + {#if driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document')} + + + + {:else if driver?.databaseEngineTypes?.includes('keyvalue')} + + + + {/if} + {:else} + + + + + + {/if} diff --git a/packages/web/src/widgets/DbKeysSubTree.svelte b/packages/web/src/widgets/DbKeysSubTree.svelte new file mode 100644 index 00000000..68e7399b --- /dev/null +++ b/packages/web/src/widgets/DbKeysSubTree.svelte @@ -0,0 +1,34 @@ + + +{#each ($items || []).slice(0, maxShowCount) as item} + +{/each} + +{#if ($items || []).length > maxShowCount} + { + maxShowCount += SHOW_INCREMENT; + }} + /> +{/if} diff --git a/packages/web/src/widgets/DbKeysTree.svelte b/packages/web/src/widgets/DbKeysTree.svelte new file mode 100644 index 00000000..207bc305 --- /dev/null +++ b/packages/web/src/widgets/DbKeysTree.svelte @@ -0,0 +1,33 @@ + + + + + + runCommand('new.dbKey')} title="Add new key"> + + + + + + + + + diff --git a/packages/web/src/widgets/DbKeysTreeNode.svelte b/packages/web/src/widgets/DbKeysTreeNode.svelte new file mode 100644 index 00000000..f9e992b5 --- /dev/null +++ b/packages/web/src/widgets/DbKeysTreeNode.svelte @@ -0,0 +1,64 @@ + + + { + if (item.type == 'dir') { + isExpanded = !isExpanded; + } + }} + on:click={() => { + if (item.type == 'dir') { + isExpanded = !isExpanded; + } else { + openNewTab({ + tabComponent: 'DbKeyDetailTab', + title: 'Key: ' + database, + props: { + isDefaultBrowser: true, + conid, + database, + }, + }); + $activeDbKeysStore = { + ...$activeDbKeysStore, + [`${conid}:${database}`]: item.key, + }; + } + }} + extInfo={item.count ? `(${item.count})` : null} + {indentLevel} +/> + + +{#if isExpanded} + +{/if} diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte index 79903cd8..74769630 100644 --- a/packages/web/src/widgets/SqlObjectList.svelte +++ b/packages/web/src/widgets/SqlObjectList.svelte @@ -67,9 +67,10 @@ function createAddMenu() { const res = []; - if (driver?.dialect?.nosql) { + if (driver?.databaseEngineTypes?.includes('document')) { res.push({ command: 'new.collection' }); - } else { + } + if (driver?.databaseEngineTypes?.includes('sql')) { res.push({ command: 'new.table' }); } if (driver) @@ -100,11 +101,11 @@ />
Refresh - {#if !driver?.dialect?.nosql} + {#if driver?.databaseEngineTypes?.includes('sql')}
runCommand('new.table')}>New table {/if} - {#if driver?.dialect?.nosql} + {#if driver?.databaseEngineTypes?.includes('document')}
runCommand('new.collection')}>New collection {/if} diff --git a/packages/web/src/widgets/SqlObjectListWrapper.svelte b/packages/web/src/widgets/SqlObjectListWrapper.svelte deleted file mode 100644 index a7f7852e..00000000 --- a/packages/web/src/widgets/SqlObjectListWrapper.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - -{#if conid && (database || singleDatabase)} - -{:else} - - - -{/if} diff --git a/plugins/dbgate-plugin-mongo/src/frontend/driver.js b/plugins/dbgate-plugin-mongo/src/frontend/driver.js index 15381d9c..14676271 100644 --- a/plugins/dbgate-plugin-mongo/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mongo/src/frontend/driver.js @@ -1,4 +1,3 @@ -const _isString = require('lodash/isString'); const { driverBase } = global.DBGATE_TOOLS; const Dumper = require('./Dumper'); const { mongoSplitterOptions } = require('dbgate-query-splitter/lib/options'); @@ -17,7 +16,6 @@ const dialect = { offsetFetchRangeSyntax: true, stringEscapeChar: "'", fallbackDataType: 'nvarchar(max)', - nosql: true, quoteIdentifier(s) { return `[${s}]`; }, @@ -27,6 +25,7 @@ const dialect = { const driver = { ...driverBase, dumperClass: Dumper, + databaseEngineTypes: ['document'], dialect, engine: 'mongo@dbgate-plugin-mongo', title: 'MongoDB', diff --git a/plugins/dbgate-plugin-redis/.gitignore b/plugins/dbgate-plugin-redis/.gitignore new file mode 100644 index 00000000..1dad8826 --- /dev/null +++ b/plugins/dbgate-plugin-redis/.gitignore @@ -0,0 +1,25 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +build +dist +lib + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/plugins/dbgate-plugin-redis/README.md b/plugins/dbgate-plugin-redis/README.md new file mode 100644 index 00000000..3a232be5 --- /dev/null +++ b/plugins/dbgate-plugin-redis/README.md @@ -0,0 +1,6 @@ +[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier) +[![NPM version](https://img.shields.io/npm/v/dbgate-plugin-redis.svg)](https://www.npmjs.com/package/dbgate-plugin-redis) + +# dbgate-plugin-redis + +Use DbGate for install of this plugin diff --git a/plugins/dbgate-plugin-redis/icon.svg b/plugins/dbgate-plugin-redis/icon.svg new file mode 100644 index 00000000..cfe0335e --- /dev/null +++ b/plugins/dbgate-plugin-redis/icon.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + diff --git a/plugins/dbgate-plugin-redis/package.json b/plugins/dbgate-plugin-redis/package.json new file mode 100644 index 00000000..75f1f19a --- /dev/null +++ b/plugins/dbgate-plugin-redis/package.json @@ -0,0 +1,43 @@ +{ + "name": "dbgate-plugin-redis", + "main": "dist/backend.js", + "version": "1.0.0", + "license": "MIT", + "description": "Redis connector plugin for DbGate", + "homepage": "https://dbgate.org", + "repository": { + "type": "git", + "url": "https://github.com/dbgate/dbgate" + }, + "author": "Jan Prochazka", + "keywords": [ + "dbgate", + "dbgateplugin", + "redis" + ], + "files": [ + "dist" + ], + "scripts": { + "build:frontend": "webpack --config webpack-frontend.config", + "build:frontend:watch": "webpack --watch --config webpack-frontend.config", + "build:backend": "webpack --config webpack-backend.config.js", + "build": "yarn build:frontend && yarn build:backend", + "plugin": "yarn build && yarn pack && dbgate-plugin dbgate-plugin-redis", + "plugout": "dbgate-plugout dbgate-plugin-redis", + "copydist": "yarn build && yarn pack && dbgate-copydist ../dist/dbgate-plugin-redis", + "prepublishOnly": "yarn build" + }, + "devDependencies": { + "dbgate-plugin-tools": "^1.0.7", + "dbgate-query-splitter": "^4.1.1", + "dbgate-tools": "^4.1.1", + "lodash": "^4.17.21", + "webpack": "^4.42.0", + "webpack-cli": "^3.3.11" + }, + "dependencies": { + "async": "^3.2.3", + "ioredis": "^4.28.5" + } +} diff --git a/plugins/dbgate-plugin-redis/prettier.config.js b/plugins/dbgate-plugin-redis/prettier.config.js new file mode 100644 index 00000000..40648407 --- /dev/null +++ b/plugins/dbgate-plugin-redis/prettier.config.js @@ -0,0 +1,8 @@ +module.exports = { + trailingComma: 'es5', + tabWidth: 2, + semi: true, + singleQuote: true, + arrowParen: 'avoid', + printWidth: 120, +}; diff --git a/plugins/dbgate-plugin-redis/src/backend/Analyser.js b/plugins/dbgate-plugin-redis/src/backend/Analyser.js new file mode 100644 index 00000000..3e176ddc --- /dev/null +++ b/plugins/dbgate-plugin-redis/src/backend/Analyser.js @@ -0,0 +1,9 @@ +const { DatabaseAnalyser } = require('dbgate-tools'); + +class Analyser extends DatabaseAnalyser { + constructor(pool, driver) { + super(pool, driver); + } +} + +module.exports = Analyser; diff --git a/plugins/dbgate-plugin-redis/src/backend/driver.js b/plugins/dbgate-plugin-redis/src/backend/driver.js new file mode 100644 index 00000000..47df6bf8 --- /dev/null +++ b/plugins/dbgate-plugin-redis/src/backend/driver.js @@ -0,0 +1,230 @@ +const _ = require('lodash'); +const async = require('async'); +const stream = require('stream'); +const driverBase = require('../frontend/driver'); +const Analyser = require('./Analyser'); +const Redis = require('ioredis'); + +/** @type {import('dbgate-types').EngineDriver} */ +const driver = { + ...driverBase, + analyserClass: Analyser, + async connect({ server, port, password, database }) { + let db = 0; + if (_.isString(database) && database.startsWith('db')) db = parseInt(database.substring(2)); + if (_.isNumber(database)) db = database; + const pool = new Redis({ + host: server, + port, + password, + db, + }); + return pool; + }, + // @ts-ignore + async query(pool, sql) { + return { + rows: [], + columns: [], + }; + }, + async stream(pool, sql, options) { + return null; + }, + async readQuery(pool, sql, structure) { + const pass = new stream.PassThrough({ + objectMode: true, + highWaterMark: 100, + }); + + // pass.write(structure) + // pass.write(row1) + // pass.write(row2) + // pass.end() + + return pass; + }, + async writeTable(pool, name, options) { + return createBulkInsertStreamBase(this, stream, pool, name, options); + }, + async info(pool) { + const info = await pool.info(); + return _.fromPairs( + info + .split('\n') + .filter((x) => x.trim() && !x.trim().startsWith('#')) + .map((x) => x.split(':')) + ); + }, + async getVersion(pool) { + const info = await this.info(pool); + + return { + version: info.redis_version, + versionText: `Redis ${info.redis_version}`, + }; + }, + async listDatabases(pool) { + const info = await this.info(pool); + + return _.range(16).map((index) => ({ name: `db${index}`, extInfo: info[`db${index}`], sortOrder: index })); + }, + + async loadKeys(pool, root = '') { + const keys = await this.getKeys(pool, root); + const res = this.extractKeysFromLevel(root, keys); + await this.enrichKeyInfo(pool, res); + return res; + }, + + async getKeys(pool, root = '') { + const res = []; + let cursor = 0; + do { + const [strCursor, keys] = await pool.scan(cursor, 'MATCH', root ? `${root}:*` : '*', 'COUNT', 100); + res.push(...keys); + cursor = parseInt(strCursor); + } while (cursor > 0); + return res; + }, + + extractKeysFromLevel(root, keys) { + const prefix = root ? `${root}:` : ''; + const rootSplit = _.compact(root.split(':')); + const res = {}; + for (const key of keys) { + if (!key.startsWith(prefix)) continue; + const keySplit = key.split(':'); + if (keySplit.length > rootSplit.length) { + const text = keySplit[rootSplit.length]; + if (keySplit.length == rootSplit.length + 1) { + res[text] = { + text, + key, + }; + } else { + const dctKey = '::' + text; + if (res[dctKey]) { + res[dctKey].count++; + } else { + res[dctKey] = { + text: text + ':*', + type: 'dir', + root: keySplit.slice(0, rootSplit.length + 1).join(':'), + count: 1, + }; + } + } + } + } + return Object.values(res); + }, + + async getKeyCardinality(pool, key, type) { + switch (type) { + case 'list': + return pool.llen(key); + case 'set': + return pool.scard(key); + case 'zset': + return pool.zcard(key); + case 'stream': + return pool.xlen(key); + case 'hash': + return pool.hlen(key); + } + }, + + async enrichOneKeyInfo(pool, item) { + item.type = await pool.type(item.key); + item.count = await this.getKeyCardinality(pool, item.key, item.type); + }, + + async enrichKeyInfo(pool, levelInfo) { + await async.eachLimit( + levelInfo.filter((x) => x.key), + 10, + async (item) => await this.enrichOneKeyInfo(pool, item) + ); + }, + + async loadKeyInfo(pool, key) { + const res = {}; + const type = await pool.type(key); + + res.key = key; + res.type = type; + res.ttl = await pool.ttl(key); + res.count = await this.getKeyCardinality(pool, key, type); + + switch (type) { + case 'string': + res.value = await pool.get(key); + break; + // case 'list': + // res.tableColumns = [{ name: 'value' }]; + // res.addMethod = 'rpush'; + // break; + // case 'set': + // res.tableColumns = [{ name: 'value' }]; + // res.keyColumn = 'value'; + // res.addMethod = 'sadd'; + // break; + // case 'zset': + // res.tableColumns = [{ name: 'score' }, { name: 'value' }]; + // res.keyColumn = 'value'; + // res.addMethod = 'zadd'; + // break; + // case 'hash': + // res.tableColumns = [{ name: 'key' }, { name: 'value' }]; + // res.keyColumn = 'key'; + // res.addMethod = 'hset'; + // break; + } + + res.keyType = this.supportedKeyTypes.find((x) => x.name == type); + + return res; + }, + + async callMethod(pool, method, args) { + return await pool[method](...args); + }, + + async loadKeyTableRange(pool, key, cursor, count) { + const type = await pool.type(key); + switch (type) { + case 'list': { + const res = await pool.lrange(key, cursor, cursor + count); + return { + cursor: res.length > count ? cursor + count : 0, + items: res.map((value) => ({ value })).slice(0, count), + }; + } + case 'set': { + const res = await pool.sscan(key, cursor, 'COUNT', count); + return { + cursor: parseInt(res[0]), + items: res[1].map((value) => ({ value })), + }; + } + case 'zset': { + const res = await pool.zscan(key, cursor, 'COUNT', count); + return { + cursor: parseInt(res[0]), + items: _.chunk(res[1], 2).map((item) => ({ value: item[0], score: item[1] })), + }; + } + case 'hash': { + const res = await pool.hscan(key, cursor, 'COUNT', count); + return { + cursor: parseInt(res[0]), + items: _.chunk(res[1], 2).map((item) => ({ key: item[0], value: item[1] })), + }; + } + } + return null; + }, +}; + +module.exports = driver; diff --git a/plugins/dbgate-plugin-redis/src/backend/index.js b/plugins/dbgate-plugin-redis/src/backend/index.js new file mode 100644 index 00000000..cdad4131 --- /dev/null +++ b/plugins/dbgate-plugin-redis/src/backend/index.js @@ -0,0 +1,6 @@ +const driver = require('./driver'); + +module.exports = { + packageName: 'dbgate-plugin-redis', + drivers: [driver], +}; diff --git a/plugins/dbgate-plugin-redis/src/frontend/Dumper.js b/plugins/dbgate-plugin-redis/src/frontend/Dumper.js new file mode 100644 index 00000000..afcc6473 --- /dev/null +++ b/plugins/dbgate-plugin-redis/src/frontend/Dumper.js @@ -0,0 +1,6 @@ +const { SqlDumper } = require('dbgate-tools'); + +class Dumper extends SqlDumper { +} + +module.exports = Dumper; diff --git a/plugins/dbgate-plugin-redis/src/frontend/driver.js b/plugins/dbgate-plugin-redis/src/frontend/driver.js new file mode 100644 index 00000000..08d6380d --- /dev/null +++ b/plugins/dbgate-plugin-redis/src/frontend/driver.js @@ -0,0 +1,70 @@ +const { driverBase } = global.DBGATE_TOOLS; +const Dumper = require('./Dumper'); + +/** @type {import('dbgate-types').SqlDialect} */ +const dialect = { + limitSelect: true, + rangeSelect: true, + offsetFetchRangeSyntax: true, + stringEscapeChar: "'", + fallbackDataType: 'nvarchar(max)', + quoteIdentifier(s) { + return `[${s}]`; + }, +}; + +/** @type {import('dbgate-types').EngineDriver} */ +const driver = { + ...driverBase, + dumperClass: Dumper, + dialect, + engine: 'redis@dbgate-plugin-redis', + title: 'Redis', + defaultPort: 6379, + databaseEngineTypes: ['keyvalue'], + supportedKeyTypes: [ + { + name: 'string', + label: 'String', + dbKeyFields: [{ name: 'value' }], + addMethod: 'set', + }, + { + name: 'list', + label: 'List', + dbKeyFields: [{ name: 'value' }], + addMethod: 'rpush', + showItemList: true, + }, + { + name: 'set', + label: 'Set', + dbKeyFields: [{ name: 'value' }], + keyColumn: 'value', + addMethod: 'sadd', + showItemList: true, + }, + { + name: 'zset', + label: 'Sorted Set', + dbKeyFields: [{ name: 'score' }, { name: 'value' }], + keyColumn: 'value', + addMethod: 'zadd', + showItemList: true, + }, + { + name: 'hash', + label: 'Hash', + dbKeyFields: [{ name: 'key' }, { name: 'value' }], + keyColumn: 'key', + addMethod: 'hset', + showItemList: true, + }, + ], + + showConnectionField: (field, values) => { + return ['server', 'port', 'password'].includes(field); + }, +}; + +module.exports = driver; diff --git a/plugins/dbgate-plugin-redis/src/frontend/index.js b/plugins/dbgate-plugin-redis/src/frontend/index.js new file mode 100644 index 00000000..a7076224 --- /dev/null +++ b/plugins/dbgate-plugin-redis/src/frontend/index.js @@ -0,0 +1,6 @@ +import driver from './driver'; + +export default { + packageName: 'dbgate-plugin-redis', + drivers: [driver], +}; diff --git a/plugins/dbgate-plugin-redis/webpack-backend.config.js b/plugins/dbgate-plugin-redis/webpack-backend.config.js new file mode 100644 index 00000000..e75357df --- /dev/null +++ b/plugins/dbgate-plugin-redis/webpack-backend.config.js @@ -0,0 +1,23 @@ +var webpack = require('webpack'); +var path = require('path'); + +var config = { + context: __dirname + '/src/backend', + + entry: { + app: './index.js', + }, + target: 'node', + output: { + path: path.resolve(__dirname, 'dist'), + filename: 'backend.js', + libraryTarget: 'commonjs2', + }, + + // uncomment for disable minimalization + // optimization: { + // minimize: false, + // }, +}; + +module.exports = config; diff --git a/plugins/dbgate-plugin-redis/webpack-frontend.config.js b/plugins/dbgate-plugin-redis/webpack-frontend.config.js new file mode 100644 index 00000000..db07de29 --- /dev/null +++ b/plugins/dbgate-plugin-redis/webpack-frontend.config.js @@ -0,0 +1,24 @@ +var webpack = require("webpack"); +var path = require("path"); + +var config = { + context: __dirname + "/src/frontend", + + entry: { + app: "./index.js", + }, + target: "web", + output: { + path: path.resolve(__dirname, "dist"), + filename: "frontend.js", + libraryTarget: "var", + library: 'plugin', + }, + + // uncomment for disable minimalization + // optimization: { + // minimize: false, + // }, +}; + +module.exports = config; diff --git a/setCurrentVersion.js b/setCurrentVersion.js index 6b077950..fa3405e0 100644 --- a/setCurrentVersion.js +++ b/setCurrentVersion.js @@ -49,3 +49,4 @@ changePackageFile('plugins/dbgate-plugin-mysql', json.version); changePackageFile('plugins/dbgate-plugin-mongo', json.version); changePackageFile('plugins/dbgate-plugin-postgres', json.version); changePackageFile('plugins/dbgate-plugin-sqlite', json.version); +changePackageFile('plugins/dbgate-plugin-redis', json.version); diff --git a/yarn.lock b/yarn.lock index 99cbee28..7d8923f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1856,6 +1856,11 @@ async@^2.6.2: dependencies: lodash "^4.17.14" +async@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -2711,6 +2716,11 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -3222,6 +3232,13 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: dependencies: ms "^2.1.1" +debug@^4.3.1: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" @@ -3325,6 +3342,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +denque@^1.1.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + denque@^1.4.1: version "1.5.0" resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de" @@ -5192,6 +5214,23 @@ invert-kv@^2.0.0: resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== +ioredis@^4.28.5: + version "4.28.5" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f" + integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -6831,6 +6870,21 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + lodash.memoize@4.x: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7998,6 +8052,11 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-map@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + p-map@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" @@ -8851,6 +8910,23 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redis-commands@1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" + integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -9832,6 +9908,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"