import { defineStore } from 'pinia' import { endsWith, find, get, initial, isEmpty, join, map, remove, set, size, slice, sortedIndexBy, split, sumBy, toUpper, } from 'lodash' import { AddHashField, AddListItem, AddStreamValue, AddZSetValue, CleanCmdHistory, CloseConnection, ConvertValue, DeleteKey, DeleteOneKey, ExportKey, FlushDB, GetCmdHistory, GetKeyDetail, GetKeySummary, GetKeyType, GetSlowLogs, LoadAllKeys, LoadNextKeys, OpenConnection, OpenDatabase, RemoveStreamValues, RenameKey, ServerInfo, SetHashValue, SetKeyTTL, SetKeyValue, SetListItem, SetSetItem, UpdateSetItem, UpdateZSetValue, } from 'wailsjs/go/services/browserService.js' import useTabStore from 'stores/tab.js' import { decodeRedisKey, nativeRedisKey } from '@/utils/key_convert.js' import { BrowserTabType } from '@/consts/browser_tab_type.js' import { KeyViewType } from '@/consts/key_view_type.js' import { ConnectionType } from '@/consts/connection_type.js' import useConnectionStore from 'stores/connections.js' import { decodeTypes, formatTypes } from '@/consts/value_view_type.js' import { isRedisGlob } from '@/utils/glob_pattern.js' import { i18nGlobal } from '@/utils/i18n.js' import { EventsEmit, EventsOff, EventsOn } from 'wailsjs/runtime/runtime.js' const useBrowserStore = defineStore('browser', { /** * @typedef {Object} DatabaseItem * @property {string} key - tree node unique key * @property {string} label * @property {string} [name] - server name, type != ConnectionType.Group only * @property {number} type * @property {number} [db] - database index, type == ConnectionType.RedisDB only * @property {string} [redisKey] - redis key, type == ConnectionType.RedisKey || type == ConnectionType.RedisValue only * @property {number[]} [redisKeyCode] - redis key char code array, optional for redis key which contains binary data * @property {number} [keys] - children key count * @property {number} [maxKeys] - max key count for database * @property {boolean} [isLeaf] * @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only * @property {boolean} [expanded] - current node is expanded * @property {DatabaseItem[]} [children] * @property {string} [redisType] - redis type name, 'loading' indicate that is in loading progress */ /** * @typedef {Object} FilterItem * @property {string} pattern key pattern filter * @property {string} type type filter */ /** * @typedef {Object} LoadingState * @property {string} loading indicated that is loading children now * @property {string} fullLoaded indicated that all children already loaded */ /** * @typedef {Object} HistoryItem * @property {string} time * @property {string} server * @property {string} cmd * @property {number} cost */ /** * @typedef {Object} BrowserState * @property {Object} serverStats * @property {Object.} filter * @property {Object.} viewType * @property {Object.} databases * @property {Object.>} nodeMap key format likes 'server#db', children key format likes 'key#type' */ /** * * @returns {BrowserState} */ state: () => ({ serverStats: {}, // current server status info filter: {}, // all filters in opened connections map by server and FilterItem loadingState: {}, // all loading state in opened connections map by server and LoadingState viewType: {}, // view type selection for all opened connections group by 'server' databases: {}, // all database lists in opened connections group by 'server name' nodeMap: {}, // all nodes in opened connections group by 'server#db' and 'type/key' keySet: {}, // all keys set in opened connections group by 'server#db openedDB: {}, // opened database map by server and database index }), getters: { anyConnectionOpened() { return !isEmpty(this.databases) }, selectedDatabases() { return this.openedDB || {} }, }, actions: { /** * check if connection is connected * @param name * @returns {boolean} */ isConnected(name) { let dbs = get(this.databases, name, []) return !isEmpty(dbs) }, /** * close all connections * @returns {Promise} */ async closeAllConnection() { for (const name in this.databases) { await CloseConnection(name) } this.databases = {} this.nodeMap.clear() this.keySet.clear() this.serverStats = {} const tabStore = useTabStore() tabStore.removeAllTab() }, /** * get database info list * @param server * @return {DatabaseItem[]} */ getDBList(server) { return this.databases[server] || [] }, /** * get database by server name and database index * @param {string} server * @param {number} db * @return {DatabaseItem|null} */ getDatabase(server, db) { const dbs = this.databases[server] if (dbs != null) { const selDB = find(dbs, (item) => item.db === db) if (selDB != null) { return selDB } } return null }, /** * get current selection database by server * @param server * @return {number} */ getSelectedDB(server) { return this.selectedDatabases[server] || 0 }, /** * get key list in current database * @param server * @return {DatabaseItem[]} */ getKeyList(server) { const db = this.getSelectedDB(server) const dbNodes = this.databases[server] const node = find(dbNodes, (n) => n.db === db) return node.children }, /** * switch key view * @param {string} connName * @param {number} viewType */ async switchKeyView(connName, viewType) { if (viewType !== KeyViewType.Tree && viewType !== KeyViewType.List) { return } const t = get(this.viewType, connName, KeyViewType.Tree) if (t === viewType) { return } this.viewType[connName] = viewType const dbs = get(this.databases, connName, []) for (const dbItem of dbs) { if (!dbItem.opened) { continue } dbItem.children = undefined dbItem.keys = 0 const { db = 0 } = dbItem this._getNodeMap(connName, db).clear() const keys = this._getKeySet(connName, db) this._addKeyNodes(connName, db, keys) this._tidyNode(connName, db, '') } }, /** * open connection * @param {string} name * @param {boolean} [reload] * @returns {Promise} */ async openConnection(name, reload) { if (this.isConnected(name)) { if (reload !== true) { return } else { // reload mode, try close connection first await CloseConnection(name) } } const { data, success, msg } = await OpenConnection(name) if (!success) { throw new Error(msg) } // append to db node to current connection // const connNode = this.getConnection(name) // if (connNode == null) { // throw new Error('no such connection') // } const { db, view = KeyViewType.Tree, lastDB } = data if (isEmpty(db)) { throw new Error('no db loaded') } const dbs = [] let containLastDB = false for (let i = 0; i < db.length; i++) { this._getNodeMap(name, i).clear() this._getKeySet(name, i).clear() dbs.push({ key: `${name}/${db[i].name}`, label: db[i].name, name: name, keys: 0, maxKeys: db[i].keys, db: db[i].index, type: ConnectionType.RedisDB, isLeaf: false, children: undefined, }) if (db[i].index === lastDB) { containLastDB = true } } this.databases[name] = dbs this.viewType[name] = view // get last selected db if (containLastDB) { this.openedDB[name] = lastDB } else { this.openedDB[name] = get(dbs, '0.db', 0) } }, /** * close connection * @param {string} name * @returns {Promise} */ async closeConnection(name) { const { success, msg } = await CloseConnection(name) if (!success) { // throw new Error(msg) return false } const dbs = this.databases[name] if (!isEmpty(dbs)) { for (const db of dbs) { this._getNodeMap(name, db.db).clear() this._getKeySet(name, db.db).clear() } } delete this.filter[name] delete this.databases[name] delete this.serverStats[name] const tabStore = useTabStore() tabStore.removeTabByName(name) return true }, /** * open database and load all keys * @param server * @param db * @returns {Promise} */ async openDatabase(server, db) { const { match: filterPattern, type: filterType } = this.getKeyFilter(server) const { data, success, msg } = await OpenDatabase(server, db, filterPattern, filterType) if (!success) { throw new Error(msg) } const { keys = [], end = false, maxKeys = 0 } = data const selDB = this.getDatabase(server, db) if (selDB == null) { return } selDB.opened = true selDB.maxKeys = maxKeys this.openedDB[server] = db set(this.loadingState, 'fullLoaded', end) if (isEmpty(keys)) { selDB.children = [] } else { // append db node to current connection's children this._addKeyNodes(server, db, keys) } this._tidyNode(server, db) }, /** * reopen database * @param connName * @param db * @returns {Promise} */ async reopenDatabase(connName, db) { const selDB = this.getDatabase(connName, db) if (selDB == null) { return } selDB.children = undefined selDB.isLeaf = false this._getNodeMap(connName, db).clear() this._getKeySet(connName, db).clear() delete this.filter[connName] }, /** * close database * @param server * @param db */ closeDatabase(server, db) { const selDB = this.getDatabase(server, db) if (selDB == null) { return } delete selDB.children selDB.isLeaf = false selDB.opened = false selDB.keys = 0 this._getNodeMap(server, db).clear() this._getKeySet(server, db).clear() delete this.filter[server] }, /** * * @param server * @returns {Promise<{}>} */ async getServerInfo(server) { try { const { success, data } = await ServerInfo(server) if (success) { this.serverStats[server] = data return data } } finally { } return {} }, /** * load key summary info * @param {string} server * @param {number} db * @param {string|number[]} [key] null or blank indicate that update tab to display normal content (blank content or server status) * @param {boolean} [clearValue] * @return {Promise} */ async loadKeySummary({ server, db, key, clearValue }) { try { const tab = useTabStore() if (!isEmpty(key)) { const { data, success, msg } = await GetKeySummary({ server, db, key, }) if (success) { const { type, ttl, size, length } = data const k = decodeRedisKey(key) const binaryKey = k !== key tab.upsertTab({ subTab: BrowserTabType.KeyDetail, server, db, type, ttl, keyCode: binaryKey ? key : undefined, key: k, size, length, clearValue, }) return } else { if (!isEmpty(msg)) { $message.error('load key summary fail: ' + msg) } // its danger to delete "non-exists" key, just remove from tree view await this.deleteKey(server, db, key, true) // TODO: show key not found page or check exists on server first? } } tab.upsertTab({ subTab: BrowserTabType.Status, server, db, type: 'none', ttl: -1, key: null, keyCode: null, size: 0, length: 0, clearValue, }) } catch (e) { $message.error('') } finally { } }, /** * load key type * @param {string} server * @param {number} db * @param {string} key * @param {number[]} keyCode * @return {Promise} */ async loadKeyType({ server, db, key, keyCode }) { const nodeMap = this._getNodeMap(server, db) const node = nodeMap.get(`${ConnectionType.RedisValue}/${key}`) if (node == null || !isEmpty(node.redisType)) { return } try { node.redisType = 'loading' const { data, success } = await GetKeyType({ server, db, key: keyCode || key }) if (success) { const { type } = data || {} node.redisType = type } else { node.redisType = 'NONE' } } catch (e) { node.redisType = 'NONE' } finally { } }, /** * reload key * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} [decode] * @param {string} [format] * @param {string} [matchPattern] * @return {Promise} */ async reloadKey({ server, db, key, decode, format, matchPattern }) { const tab = useTabStore() try { tab.updateLoading({ server, db, loading: true }) await this.loadKeySummary({ server, db, key, clearValue: true }) await this.loadKeyDetail({ server, db, key, decode, format, matchPattern, reset: true }) } finally { tab.updateLoading({ server, db, loading: false }) } }, /** * load key content * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} [format] * @param {string} [decode] * @param {string} [matchPattern] * @param {boolean} [reset] * @param {boolean} [full] * @return {Promise} */ async loadKeyDetail({ server, db, key, format, decode, matchPattern, reset, full }) { const tab = useTabStore() try { tab.updateLoading({ server, db, loading: true }) const { data, success, msg } = await GetKeyDetail({ server, db, key, format, decode, matchPattern, full: full === true, reset, lite: true, }) if (success) { const { value, decode: retDecode, format: retFormat, match: retMatch, reset: retReset, end } = data tab.updateValue({ server, db, key: decodeRedisKey(key), value, decode: retDecode, format: retFormat, reset: retReset, matchPattern: retMatch || '', end, }) } else { $message.error('load key detail fail:' + msg) } } finally { tab.updateLoading({ server, db, loading: false }) } }, /** * convert value by decode type or format * @param {string|number[]} value * @param {string} [decode] * @param {string} [format] * @return {Promise<{[format]: string, [decode]: string, value: string}>} */ async convertValue({ value, decode, format }) { try { const { data, success } = await ConvertValue(value, decode, format) if (success) { const { value: retVal, decode: retDecode, format: retFormat } = data return { value: retVal, decode: retDecode, format: retFormat } } } catch (e) {} return { value, decode, format } }, /** * scan keys with prefix * @param {string} connName * @param {number} db * @param {string} match * @param {string} [matchType] * @param {boolean} [full] * @returns {Promise<{keys: string[], maxKeys: number, end: boolean}>} */ async scanKeys(connName, db, match, matchType, full) { let resp if (full) { resp = await LoadAllKeys(connName, db, match || '*', matchType) } else { resp = await LoadNextKeys(connName, db, match || '*', matchType) } const { data, success, msg } = resp || {} if (!success) { throw new Error(msg) } const { keys = [], maxKeys, end } = data return { keys, end, maxKeys, success } }, /** * * @param {string} connName * @param {number} db * @param {string|null} prefix * @param {string|null} matchType * @param {boolean} [all] * @return {Promise<{keys: Array, maxKeys: number, end: boolean}>} * @private */ async _loadKeys(connName, db, prefix, matchType, all) { let match = prefix if (isEmpty(match)) { match = '*' } else if (!isRedisGlob(match)) { const separator = this._getSeparator(connName) if (!endsWith(prefix, separator + '*')) { match = prefix + separator + '*' } } return this.scanKeys(connName, db, match, matchType, all) }, /** * load more keys within the database * @param {string} connName * @param {number} db * @return {Promise} */ async loadMoreKeys(connName, db) { const { match, type: keyType } = this.getKeyFilter(connName) const { keys, maxKeys, end } = await this._loadKeys(connName, db, match, keyType, false) this._setDBMaxKeys(connName, db, maxKeys) // remove current keys below prefix this._addKeyNodes(connName, db, keys) this._tidyNode(connName, db, '') return end }, /** * load all left keys within the database * @param {string} connName * @param {number} db * @return {Promise} */ async loadAllKeys(connName, db) { const { match, type: keyType } = this.getKeyFilter(connName, db) const { keys, maxKeys } = await this._loadKeys(connName, db, match, keyType, true) this._setDBMaxKeys(connName, db, maxKeys) this._addKeyNodes(connName, db, keys) this._tidyNode(connName, db, '') }, /** * reload keys under layer * @param {string} connName * @param {number} db * @param {string} prefix * @return {Promise} */ async reloadLayer(connName, db, prefix) { if (isEmpty(prefix)) { return } let match = prefix const separator = this._getSeparator(connName) if (!endsWith(match, separator)) { match += separator + '*' } else { match += '*' } // FIXME: ignore original match pattern due to redis not support combination matching const { match: originMatch, type: keyType } = this.getKeyFilter(connName, db) const { keys, maxKeys, success } = await this._loadKeys(connName, db, match, keyType, true) if (!success) { return } this._setDBMaxKeys(connName, db, maxKeys) // remove current keys below prefix this._deleteKeyNode(connName, db, prefix, true) this._addKeyNodes(connName, db, keys) this._tidyNode(connName, db, prefix) }, /** * get custom separator of connection * @param server * @returns {string} * @private */ _getSeparator(server) { const connStore = useConnectionStore() const { keySeparator } = connStore.getDefaultSeparator(server) if (isEmpty(keySeparator)) { return ':' } return keySeparator }, /** * get node map * @param {string} connName * @param {number} db * @returns {Map} * @private */ _getNodeMap(connName, db) { if (!this.nodeMap.hasOwnProperty(`${connName}#${db}`)) { this.nodeMap[`${connName}#${db}`] = new Map() } // construct a tree node list, the format of item key likes 'server/db#type/key' return this.nodeMap[`${connName}#${db}`] }, /** * get all keys in a database * @param {string} connName * @param {number} db * @return {Set} * @private */ _getKeySet(connName, db) { if (!this.keySet.hasOwnProperty(`${connName}#${db}`)) { this.keySet[`${connName}#${db}`] = new Set() } // construct a key set return this.keySet[`${connName}#${db}`] }, /** * remove keys in db * @param {string} connName * @param {number} db * @param {Array|Set} keys * @param {boolean} [sortInsert] * @return {{success: boolean, newKey: number, newLayer: number, replaceKey: number}} * @private */ _addKeyNodes(connName, db, keys, sortInsert) { const result = { success: false, newLayer: 0, newKey: 0, replaceKey: 0, } if (isEmpty(keys)) { return result } const separator = this._getSeparator(connName) const selDB = this.getDatabase(connName, db) if (selDB == null) { return result } if (selDB.children == null) { selDB.children = [] } const nodeMap = this._getNodeMap(connName, db) const keySet = this._getKeySet(connName, db) const rootChildren = selDB.children const viewType = get(this.viewType, connName, KeyViewType.Tree) if (viewType === KeyViewType.List) { // construct list view data for (const key of keys) { const k = decodeRedisKey(key) const isBinaryKey = k !== key const nodeKey = `${ConnectionType.RedisValue}/${nativeRedisKey(key)}` const replaceKey = nodeMap.has(nodeKey) const selectedNode = { key: `${connName}/db${db}#${nodeKey}`, label: k, db, keys: 0, redisKey: k, redisKeyCode: isBinaryKey ? key : undefined, redisKeyType: undefined, type: ConnectionType.RedisValue, isLeaf: true, } nodeMap.set(nodeKey, selectedNode) keySet.add(key) if (!replaceKey) { if (sortInsert) { const index = sortedIndexBy(rootChildren, selectedNode, 'key') rootChildren.splice(index, 0, selectedNode) } else { rootChildren.push(selectedNode) } result.newKey += 1 } else { result.replaceKey += 1 } } } else { // construct tree view data for (const key of keys) { const k = decodeRedisKey(key) const isBinaryKey = k !== key const keyParts = isBinaryKey ? [nativeRedisKey(key)] : split(k, separator) const len = size(keyParts) const lastIdx = len - 1 let handlePath = '' let children = rootChildren for (let i = 0; i < len; i++) { handlePath += keyParts[i] if (i !== lastIdx) { // layer const nodeKey = `${ConnectionType.RedisKey}/${handlePath}` let selectedNode = nodeMap.get(nodeKey) if (selectedNode == null) { selectedNode = { key: `${connName}/db${db}#${nodeKey}`, label: keyParts[i], db, keys: 0, redisKey: handlePath, type: ConnectionType.RedisKey, isLeaf: false, children: [], } nodeMap.set(nodeKey, selectedNode) if (sortInsert) { const index = sortedIndexBy(children, selectedNode, 'key') children.splice(index, 0, selectedNode) } else { children.push(selectedNode) } result.newLayer += 1 } children = selectedNode.children handlePath += separator } else { // key const nodeKey = `${ConnectionType.RedisValue}/${handlePath}` const replaceKey = nodeMap.has(nodeKey) const selectedNode = { key: `${connName}/db${db}#${nodeKey}`, label: isBinaryKey ? k : keyParts[i], db, keys: 0, redisKey: handlePath, redisKeyCode: isBinaryKey ? key : undefined, redisKeyType: undefined, type: ConnectionType.RedisValue, isLeaf: true, } nodeMap.set(nodeKey, selectedNode) keySet.add(key) if (!replaceKey) { if (sortInsert) { const index = sortedIndexBy(children, selectedNode, 'key') children.splice(index, 0, selectedNode) } else { children.push(selectedNode) } result.newKey += 1 } else { result.replaceKey += 1 } } } } } return result }, /** * * @param {DatabaseItem[]} nodeList * @private */ _sortNodes(nodeList) { if (nodeList == null) { return } nodeList.sort((a, b) => { return a.key > b.key ? 1 : -1 }) }, /** * tidy node by key * @param {string} connName * @param {number} db * @param {string} [key] * @param {boolean} [skipResort] * @private */ _tidyNode(connName, db, key, skipResort) { const nodeMap = this._getNodeMap(connName, db) const dbNode = this.getDatabase(connName, db) || {} const separator = this._getSeparator(connName) const keyParts = split(key, separator) const totalParts = size(keyParts) let node // find last exists ancestor key let i = totalParts - 1 for (; i > 0; i--) { const parentKey = join(slice(keyParts, 0, i), separator) node = nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`) if (node != null) { break } } if (node == null) { node = dbNode } const keyCountUpdated = this._tidyNodeChildren(node, skipResort) if (keyCountUpdated) { // update key count of parent and above for (; i > 0; i--) { const parentKey = join(slice(keyParts, 0, i), separator) const parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${parentKey}`) if (parentNode == null) { break } parentNode.keys = sumBy(parentNode.children, 'keys') } // update key count of db dbNode.keys = sumBy(dbNode.children, 'keys') } return true }, /** * sort all node item's children and calculate keys count * @param {DatabaseItem} node * @param {boolean} skipSort skip sorting children * @returns {boolean} return whether key count changed * @private */ _tidyNodeChildren(node, skipSort) { let count = 0 if (!isEmpty(node.children)) { if (skipSort !== true) { this._sortNodes(node.children) } for (const elem of node.children) { this._tidyNodeChildren(elem, skipSort) count += elem.keys } } else { if (node.type === ConnectionType.RedisValue) { count += 1 } else { // no children in db node or layer node, set count to 0 count = 0 } } if (node.keys !== count) { node.keys = count return true } return false }, /** * update max key by increase/decrease value * @param {string} connName * @param {number} db * @param {number} [updateValue] * @private */ _updateDBMaxKeys(connName, db, updateValue) { if (updateValue === undefined) { return } const database = this.getDatabase(connName, db) if (database != null) { const maxKeys = get(database, 'maxKeys', 0) database.maxKeys = Math.max(0, maxKeys + updateValue) } }, /** * set db max keys value * @param {string} connName * @param {number} db * @param {number} maxKeys * @private */ _setDBMaxKeys(connName, db, maxKeys) { const database = this.getDatabase(connName, db) if (database != null) { set(database, 'maxKeys', maxKeys) } }, /** * get tree node by key name * @param key * @return {DatabaseItem|null} */ getNode(key) { let idx = key.indexOf('#') if (idx < 0) { idx = size(key) } const dbPart = key.substring(0, idx) // parse server and db index const idx2 = dbPart.lastIndexOf('/db') if (idx2 < 0) { return null } const server = dbPart.substring(0, idx2) const db = parseInt(dbPart.substring(idx2 + 3)) if (isNaN(db)) { return null } if (size(key) > idx + 1) { const keyPart = key.substring(idx + 1) // contains redis key const nodeMap = this._getNodeMap(server, db) return nodeMap.get(keyPart) } else { return this.getDatabase(server, db) } }, /** * set redis key * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} keyType * @param {any} value * @param {number} ttl * @param {string} [format] * @param {string} [decode] * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: {string}}>} */ async setKey({ server, db, key, keyType, value, ttl, format = formatTypes.RAW, decode = decodeTypes.NONE }) { try { const { data, success, msg } = await SetKeyValue({ server, db, key, keyType, value, ttl, format, decode, }) if (success) { // const { value } = data // update tree view data const { newKey = 0 } = this._addKeyNodes(server, db, [key], true) if (newKey > 0) { this._tidyNode(server, db, key) this._updateDBMaxKeys(server, db, newKey) } const tab = useTabStore() tab.updateValue({ server, db, key, value }) this.loadKeySummary({ server, db, key }) return { success, nodeKey: `${server}/db${db}#${ConnectionType.RedisValue}/${key}`, updatedValue: value, } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * update hash entry * when field is set, newField is null, delete field * when field is null, newField is set, add new field * when both field and newField are set, and field === newField, update field * when both field and newField are set, and field !== newField, delete field and add newField * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} field * @param {string} [newField] * @param {string} [value] * @param {decodeTypes} [decode] * @param {formatTypes} [format] * @param {decodeTypes} [retDecode] * @param {formatTypes} [retFormat] * @param {boolean} [refresh] * @param {number} [index] index for retrieve affect entries quickly * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} */ async setHash({ server, db, key, field, newField = '', value = '', decode = decodeTypes.NONE, format = formatTypes.RAW, retDecode, retFormat, index, reload, }) { try { const { data, success, msg } = await SetHashValue({ server, db, key, field, newField, value, decode, format, retDecode, retFormat, }) if (success) { /** * @type {{updated: HashEntryItem[], removed: HashEntryItem[], updated: HashEntryItem[], replaced: HashReplaceItem[]}} */ const { updated = [], removed = [], added = [], replaced = [] } = data const tab = useTabStore() if (!isEmpty(removed)) { const removedKeys = map(removed, 'k') tab.removeValueEntries({ server, db, key, type: 'hash', entries: removedKeys }) } if (!isEmpty(updated)) { tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated }) } if (!isEmpty(added)) { tab.insertValueEntries({ server, db, key, type: 'hash', entries: added }) } if (!isEmpty(replaced)) { tab.replaceValueEntries({ server, db, key, type: 'hash', entries: replaced, index: [index], }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success, updated } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * insert or update hash field item * @param {string} server * @param {number} db * @param {string|number[]} key * @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields * @param {string[]} fieldItems field1, value1, filed2, value2... * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean, [updated]: [], [added]: []}>} */ async addHashField({ server, db, key, action, fieldItems, reload }) { try { const { data, success, msg } = await AddHashField(server, db, key, action, fieldItems) if (success) { const { updated = [], added = [] } = data const tab = useTabStore() if (!isEmpty(updated)) { tab.updateValueEntries({ server, db, key, type: 'hash', entries: updated }) } if (!isEmpty(added)) { tab.insertValueEntries({ server, db, key, type: 'hash', entries: added }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success, updated, added } } else { return { success: false, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove hash field * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} field * @param {boolean} reload * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>} */ async removeHashField({ server, db, key, field, reload }) { try { const { data, success, msg } = await SetHashValue({ server, db, key, field, newField: '' }) if (success) { const { removed = [] } = data // if (!isEmpty(removed)) { // const tab = useTabStore() // tab.removeValueEntries({ server, db, key, type: 'hash', entries: removed }) // } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success, removed } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * prepend item to head of list * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string[]} values * @param {boolean} reload * @returns {Promise<{[msg]: string, success: boolean, [item]: []}>} */ async prependListItem({ server, db, key, values, reload }) { try { const { data, success, msg } = await AddListItem(server, db, key, 0, values) if (success) { const { left = [] } = data if (!isEmpty(left)) { const tab = useTabStore() tab.insertValueEntries({ server: server, db, key, type: 'list', entries: left, prepend: true, }) if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } } return { success, item: left } } else { return { success: false, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * append item to tail of list * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string[]} values * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean, [item]: any[]}>} */ async appendListItem({ server, db, key, values, reload }) { try { const { data, success, msg } = await AddListItem(server, db, key, 1, values) if (success) { const { right = [] } = data // FIXME: do not append items if not all items loaded if (!isEmpty(right)) { const tab = useTabStore() tab.insertValueEntries({ server: server, db, key, type: 'list', entries: right, prepend: false, }) if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } } return { success, item: right } } else { return { success: false, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * update value of list item by index * @param {string} server * @param {number} db * @param {string|number[]} key * @param {number} index * @param {string|number[]} value * @param {decodeTypes} decode * @param {formatTypes} format * @param {decodeTypes} [retDecode] * @param {formatTypes} [retFormat] * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean}>} */ async updateListItem({ server, db, key, index, value, decode = decodeTypes.NONE, format = formatTypes.RAW, retDecode, retFormat, reload, }) { try { const { data, success, msg } = await SetListItem({ server, db, key, index, value, decode, format, retDecode, retFormat, }) if (success) { /** @type {{replaced: ListReplaceItem[]}} **/ const { replaced = [], removed = [] } = data const tab = useTabStore() if (!isEmpty(replaced)) { tab.replaceValueEntries({ server, db, key, type: 'list', entries: replaced, }) } if (!isEmpty(removed)) { const removedIndex = map(removed, 'index') tab.removeValueEntries({ server, db, key, type: 'list', entries: removedIndex, }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove list item * @param {string} server * @param {number} db * @param {string|number[]} key * @param {number} index * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>} */ async removeListItem({ server, db, key, index, reload }) { try { const { data, success, msg } = await SetListItem({ server, db, key, index }) if (success) { const { removed = [] } = data const tab = useTabStore() if (!isEmpty(removed)) { const removedIndexes = map(removed, 'index') tab.removeValueEntries({ server, db, key, type: 'list', entries: removedIndexes, }) if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } } return { success, removed } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * add item to set * @param {string} server * @param {number} db * @param {string|number} key * @param {string|string[]} value * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean}>} */ async addSetItem({ server, db, key, value, reload }) { try { if ((!value) instanceof Array) { value = [value] } const { data, success, msg } = await SetSetItem(server, db, key, false, value) if (success) { const { added } = data if (!isEmpty(added)) { const tab = useTabStore() tab.insertValueEntries({ server, db, key, type: 'set', entries: added }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * update value of set item * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string|number[]} value * @param {string|number[]} newValue * @param {decodeTypes} [decode] * @param {formatTypes} [format] * @param {decodeTypes} [retDecode] * @param {formatTypes} [retFormat] * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean}>} */ async updateSetItem({ server, db, key, value, newValue, decode = decodeTypes.NONE, format = formatTypes.RAW, retDecode, retFormat, reload, }) { try { const { data, success, msg } = await UpdateSetItem({ server, db, key, value, newValue, decode, format, retDecode, retFormat, }) if (success) { const { added, removed } = data const tab = useTabStore() if (!isEmpty(removed)) { const removedValues = map(removed, 'v') tab.removeValueEntries({ server, db, key, type: 'set', entries: removedValues }) } if (!isEmpty(added)) { tab.insertValueEntries({ server, db, key, type: 'set', entries: added }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success } } else { return { success: false, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove item from set * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} value * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean}>} */ async removeSetItem({ server, db, key, value, reload }) { try { const { data, success, msg } = await SetSetItem(server, db, key, true, [value]) if (success) { const { removed } = data const tab = useTabStore() if (!isEmpty(removed)) { const removedValues = map(removed, 'v') tab.removeValueEntries({ server, db, key, type: 'set', entries: removedValues }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * add item to sorted set * @param {string} server * @param {number} db * @param {string|number[]} key * @param {number} action * @param {Object.} vs value: score * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean}>} */ async addZSetItem({ server, db, key, action, vs, reload }) { try { const { data, success, msg } = await AddZSetValue(server, db, key, action, vs) if (success) { const { added, updated } = data const tab = useTabStore() if (!isEmpty(added)) { tab.insertValueEntries({ server, db, key, type: 'zset', entries: added }) } if (!isEmpty(updated)) { tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * update item of sorted set * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} value * @param {string} newValue * @param {number} score * @param {decodeTypes} decode * @param {formatTypes} format * @param {decodeTypes} [retDecode] * @param {formatTypes} [retFormat] * @param {number} [index] index for retrieve affect entries quickly * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean}>} */ async updateZSetItem({ server, db, key, value = '', newValue, score, decode = decodeTypes.NONE, format = formatTypes.RAW, retDecode, retFormat, index, reload, }) { try { const { data, success, msg } = await UpdateZSetValue({ server, db, key, value, newValue, score, decode, format, retDecode, retFormat, }) if (success) { const { updated = [], added = [], removed = [], replaced = [] } = data const tab = useTabStore() if (!isEmpty(removed)) { const removedValues = map(removed, 'v') tab.removeValueEntries({ server, db, key, type: 'zset', entries: removedValues }) } if (!isEmpty(updated)) { tab.updateValueEntries({ server, db, key, type: 'zset', entries: updated }) } if (!isEmpty(added)) { tab.insertValueEntries({ server, db, key, type: 'zset', entries: added }) } if (!isEmpty(replaced)) { tab.replaceValueEntries({ server, db, key, type: 'zset', entries: replaced, index: [index] }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success, updated, removed } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove item from sorted set * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} value * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>} */ async removeZSetItem({ server, db, key, value, reload }) { try { const { data, success, msg } = await UpdateZSetValue({ server, db, key, value, newValue: '', score: 0 }) if (success) { const { removed } = data const tab = useTabStore() if (!isEmpty(removed)) { const removeValues = map(removed, 'v') tab.removeValueEntries({ server, db, key, type: 'zset', entries: removeValues }) } if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success, removed } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * insert new stream field item * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string} id * @param {string[]} values field1, value1, filed2, value2... * @param {boolean} [reload] * @returns {Promise<{[msg]: string, success: boolean}>} */ async addStreamValue({ server, db, key, id, values, reload }) { try { const { data = {}, success, msg } = await AddStreamValue(server, db, key, id, values) if (success) { const { added = [] } = data if (!isEmpty(added)) { const tab = useTabStore() tab.insertValueEntries({ server, db, key, type: 'stream', entries: added, }) if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } } return { success } } else { return { success: false, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove stream field * @param {string} server * @param {number} db * @param {string|number[]} key * @param {string[]|string} ids * @param {boolean} [reload] * @returns {Promise<{[msg]: {}, success: boolean}>} */ async removeStreamValues({ server, db, key, ids, reload }) { if (typeof ids === 'string') { ids = [ids] } try { const { data = {}, success, msg } = await RemoveStreamValues(server, db, key, ids) if (success) { const tab = useTabStore() tab.removeValueEntries({ server, db, key, type: 'stream', entries: ids }) if (reload === true) { this.reloadKey({ server, db, key }) } else { // reload summary only this.loadKeySummary({ server, db, key }) } return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * reset key's ttl * @param {string} connName * @param {number} db * @param {string} key * @param {number} ttl * @returns {Promise} */ async setTTL(connName, db, key, ttl) { try { const { success, msg } = await SetKeyTTL(connName, db, key, ttl) return success === true } catch (e) { return false } }, /** * * @param {string} server * @param {number} db * @param {string} key * @param {string} newKey * @private */ _renameKeyNode(server, db, key, newKey) { this._deleteKeyNode(server, db, key, false) const { success } = this._addKeyNodes(server, db, [newKey]) if (success) { const separator = this._getSeparator(server) const layer = initial(key.split(separator)).join(separator) this._tidyNode(server, db, layer) } }, /** * * @param {string} connName * @param {number} db * @param {string} [key] * @param {boolean} [isLayer] * @private */ _deleteKeyNode(connName, db, key, isLayer) { const dbRoot = this.getDatabase(connName, db) || {} const separator = this._getSeparator(connName) if (dbRoot == null) { return false } const nodeMap = this._getNodeMap(connName, db) const keySet = this._getKeySet(connName, db) if (isLayer === true) { this._deleteChildrenKeyNodes(nodeMap, keySet, key) } if (isEmpty(key)) { // clear all key nodes dbRoot.children = [] dbRoot.keys = 0 } else { const keyParts = split(key, separator) const totalParts = size(keyParts) // remove from parent in tree node const parentKey = slice(keyParts, 0, totalParts - 1) let parentNode if (isEmpty(parentKey)) { parentNode = dbRoot } else { parentNode = nodeMap.get(`${ConnectionType.RedisKey}/${join(parentKey, separator)}`) } // not found parent node if (parentNode == null) { return false } remove(parentNode.children, { type: isLayer ? ConnectionType.RedisKey : ConnectionType.RedisValue, redisKey: key, }) // check and remove empty layer node let i = totalParts - 1 for (; i >= 0; i--) { const anceKey = join(slice(keyParts, 0, i), separator) if (i > 0) { const anceNode = nodeMap.get(`${ConnectionType.RedisKey}/${anceKey}`) const redisKey = join(slice(keyParts, 0, i + 1), separator) remove(anceNode.children, { type: ConnectionType.RedisKey, redisKey }) if (isEmpty(anceNode.children)) { nodeMap.delete(`${ConnectionType.RedisKey}/${anceKey}`) keySet.delete(anceNode.redisKeyCode || anceNode.redisKey) } else { break } } else { // last one, remove from db node remove(dbRoot.children, { type: ConnectionType.RedisKey, redisKey: keyParts[0] }) const node = nodeMap.get(`${ConnectionType.RedisValue}/${keyParts[0]}`) if (node != null) { nodeMap.delete(`${ConnectionType.RedisValue}/${keyParts[0]}`) keySet.delete(node.redisKeyCode || node.redisKey) } } } } return true }, /** * delete node and all it's children from nodeMap * @param {Map} nodeMap * @param {Set} keySet * @param {string} [key] clean nodeMap if key is empty * @private */ _deleteChildrenKeyNodes(nodeMap, keySet, key) { if (isEmpty(key)) { nodeMap.clear() keySet.clear() } else { const mapKey = `${ConnectionType.RedisKey}/${key}` const node = nodeMap.get(mapKey) for (const child of node.children || []) { if (child.type === ConnectionType.RedisValue) { if (!nodeMap.delete(`${ConnectionType.RedisValue}/${child.redisKey}`)) { console.warn('delete:', `${ConnectionType.RedisValue}/${child.redisKey}`) } keySet.delete(child.redisKeyCode || child.redisKey) } else if (child.type === ConnectionType.RedisKey) { this._deleteChildrenKeyNodes(nodeMap, keySet, child.redisKey) } } if (!nodeMap.delete(mapKey)) { console.warn('delete map key', mapKey) } keySet.delete(node.redisKeyCode || node.redisKey) } }, /** * delete redis key * @param {string} connName * @param {number} db * @param {string|number[]} key * @param {boolean} [soft] do not try to remove from redis if true, just remove from tree data * @returns {Promise} */ async deleteKey(connName, db, key, soft) { try { let deleteCount = 0 if (soft !== true) { const { data } = await DeleteKey(connName, db, key) deleteCount = get(data, 'deleteCount', 0) } const k = nativeRedisKey(key) // update tree view data this._deleteKeyNode(connName, db, k) this._tidyNode(connName, db, k, true) this._updateDBMaxKeys(connName, db, -deleteCount) // set tab content empty const tab = useTabStore() tab.emptyTab(connName) return true } finally { } return false }, /** * delete multiple keys * @param {string} server * @param {number} db * @param {string[]|number[][]} keys * @return {Promise} */ async deleteKeys(server, db, keys) { const delMsgRef = $message.loading('', { duration: 0, closable: true }) let progress = 0 let count = size(keys) let deletedCount = 0 let failCount = 0 try { for (const key of keys) { delMsgRef.content = i18nGlobal.t('dialogue.deleting_key', { key: decodeRedisKey(key), index: ++progress, count, }) const { success } = await DeleteOneKey(server, db, key) if (success) { this._deleteKeyNode(server, db, key, false) deletedCount += 1 } else { failCount += 1 } } } finally { delMsgRef.destroy() // clear checked keys const tab = useTabStore() tab.setCheckedKeys(server) } // refresh model data this._tidyNode(server, db, '', true) this._updateDBMaxKeys(server, db, -deletedCount) if (failCount <= 0) { // no fail $message.success(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount })) } else if (failCount >= deletedCount) { // all fail $message.error(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount })) } else { // some fail $message.warn(i18nGlobal.t('dialogue.delete_completed', { success: deletedCount, fail: failCount })) } }, /** * export multiple keys * @param {string} server * @param {number} db * @param {string[]|number[][]} keys * @param {string} path * @returns {Promise} */ async exportKeys(server, db, keys, path) { const delMsgRef = $message.loading('', { duration: 0, closable: true }) let exported = 0 let failCount = 0 let canceled = false const eventName = 'exporting:' + path try { EventsOn(eventName, ({ total, progress, processing }) => { // update export progress delMsgRef.content = i18nGlobal.t('dialogue.export.exporting', { key: decodeRedisKey(processing), index: progress, count: total, }) }) delMsgRef.onClose = () => { EventsEmit('export:stop:' + path) } const { data, success, msg } = await ExportKey(server, db, keys, path) if (success) { canceled = get(data, 'canceled', false) exported = get(data, 'exported', 0) failCount = get(data, 'failed', 0) } else { $message.error(msg) } } finally { delMsgRef.destroy() EventsOff(eventName) } if (canceled) { $message.info(i18nGlobal.t('dialogue.handle_cancel')) } else if (failCount <= 0) { // no fail $message.success( i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount }), ) } else if (failCount >= exported) { // all fail $message.error(i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount })) } else { // some fail $message.warn(i18nGlobal.t('dialogue.export.export_completed', { success: exported, fail: failCount })) } }, /** * flush database * @param connName * @param db * @param async * @return {Promise} */ async flushDatabase(connName, db, async) { try { const { success = false } = await FlushDB(connName, db, async) if (success === true) { // update tree view data this._deleteKeyNode(connName, db) this._setDBMaxKeys(connName, db, 0) // set tab content empty const tab = useTabStore() tab.emptyTab(connName) return true } } finally { } return true }, /** * rename key * @param {string} server * @param {number} db * @param {string} key * @param {string} newKey * @returns {Promise<{[msg]: string, success: boolean, [nodeKey]: string}>} */ async renameKey(server, db, key, newKey) { const { success = false, msg } = await RenameKey(server, db, key, newKey) if (success) { // delete old key and add new key struct this._renameKeyNode(server, db, key, newKey) return { success: true, nodeKey: `${server}/db${db}#${ConnectionType.RedisValue}/${newKey}` } } else { return { success: false, msg } } }, /** * get command history * @param {number} [pageNo] * @param {number} [pageSize] * @returns {Promise} */ async getCmdHistory(pageNo, pageSize) { if (pageNo === undefined || pageSize === undefined) { pageNo = -1 pageSize = -1 } try { const { success, data = { list: [] } } = await GetCmdHistory(pageNo, pageSize) const { list } = data return list } catch { return [] } }, /** * clean cmd history * @return {Promise} */ async cleanCmdHistory() { try { const { success } = await CleanCmdHistory() return success === true } catch { return false } }, /** * get slow log list * @param {string} server * @param {number} db * @param {number} num * @return {Promise<[]>} */ async getSlowLog(server, db, num) { try { const { success, data = { list: [] } } = await GetSlowLogs(server, db, num) const { list } = data return list } catch { return [] } }, /** * get key filter pattern and filter type * @param {string} server * @returns {{match: string, type: string}} */ getKeyFilter(server) { let { pattern = '', type = '' } = this.filter[server] || {} if (isEmpty(pattern)) { // no custom match pattern, use default const conn = useConnectionStore() pattern = conn.getDefaultKeyFilter(server) } return { match: pattern, type: toUpper(type), } }, /** * * @param {string} server * @param {string} [type] * @param {string} [pattern] */ setKeyFilter(server, { type, pattern }) { const filter = this.filter[server] || {} filter.type = type === null ? filter.type : type filter.pattern = type === null ? filter.pattern : pattern this.filter[server] = filter }, }, }) export default useBrowserStore