import { defineStore } from 'pinia' import { get, isEmpty, last, remove, size, sortedIndexBy, split, uniq } from 'lodash' import { AddHashField, AddListItem, AddZSetValue, CloseConnection, GetConnection, GetKeyValue, ListConnection, OpenConnection, OpenDatabase, RemoveConnection, RemoveKey, RenameKey, SaveConnection, SetHashValue, SetKeyTTL, SetKeyValue, SetListItem, SetSetItem, UpdateSetItem, UpdateZSetValue } from '../../wailsjs/go/services/connectionService.js' import { ConnectionType } from '../consts/connection_type.js' import useTabStore from './tab.js' const separator = ':' const useConnectionStore = defineStore('connections', { /** * @typedef {Object} ConnectionItem * @property {string} key * @property {string} label display label * @property {string} name database name * @property {number} type * @property {ConnectionItem[]} children */ /** * @typedef {Object} DatabaseItem * @property {string} 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 {number} keys * @property {boolean} [opened] - redis db is opened, type == ConnectionType.RedisDB only * @property {boolean} [expanded] - current node is expanded */ /** * * @returns {{databases: Object, connections: ConnectionItem[]}} */ state: () => ({ groups: [], // all group name connections: [], // all connections databases: {, // all databases in opened connections group by name }), getters: { anyConnectionOpened() { return !isEmpty(this.databases) }, }, actions: { /** * load all store connections struct from local profile * @param {boolean} [force] * @returns {Promise} */ async initConnections(force) { if (!force && !isEmpty(this.connections)) { return } const conns = [] const groups = [] const { data = [{ groupName: '', connections: [] }] } = await ListConnection() for (let i = 0; i < data.length; i++) { const group = data[i] // Top level group if (isEmpty(group.groupName)) { const len = size(group.connections) for (let j = 0; j < len; j++) { const item = group.connections[j] conns.push({ key: item.name, label: item.name, name: item.name, type: ConnectionType.Server, // isLeaf: false, }) } } else { groups.push(group.groupName) // Custom group const children = [] const len = size(group.connections) for (let j = 0; j < len; j++) { const item = group.connections[j] const value = group.groupName + '/' + item.name children.push({ key: value, label: item.name, name: item.name, type: ConnectionType.Server, // isLeaf: false, }) } conns.push({ key: group.groupName, label: group.groupName, type: ConnectionType.Group, children }) } } this.connections = conns this.groups = uniq(groups) }, /** * get connection by name from local profile * @param name * @returns {Promise<{}|null>} */ async getConnectionProfile(name) { try { const { data, success } = await GetConnection(name) if (success) { return data } } finally { } return null }, /** * create a new default connection * @param {string} [name] * @returns {{}} */ newDefaultConnection(name) { return { group: '', name: name || '', addr: '127.0.0.1', port: 6379, username: '', password: '', defaultFilter: '*', keySeparator: ':', connTimeout: 60, execTimeout: 60, markColor: '' } }, /** * get database server by name * @param name * @returns {ConnectionItem|null} */ getConnection(name) { const conns = this.connections for (let i = 0; i < conns.length; i++) { if (conns[i].type === ConnectionType.Server && conns[i].key === name) { return conns[i] } else if (conns[i].type === ConnectionType.Group) { const children = conns[i].children for (let j = 0; j < children.length; j++) { if (children[j].type === ConnectionType.Server && conns[i].key === name) { return children[j] } } } } return null }, /** * Create a new connection or update current connection profile * @param {string} name set null if create a new connection * @param {{}} param * @returns {Promise<{success: boolean, [msg]: string}>} */ async saveConnection(name, param) { const { success, msg } = await SaveConnection(name, param) if (!success) { return { success: false, msg } } // reload connection list await this.initConnections(true) return { success: true } }, /** * Check if connection is connected * @param name * @returns {boolean} */ isConnected(name) { let dbs = get(this.databases, name, []) return !isEmpty(dbs) }, /** * Open connection * @param {string} name * @returns {Promise} */ async openConnection(name) { if (this.isConnected(name)) { return } 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 } = data if (isEmpty(db)) { throw new Error('no db loaded') } const dbs = [] for (let i = 0; i < db.length; i++) { dbs.push({ key: `${name}/${db[i].name}`, label: db[i].name, name: name, keys: db[i].keys, db: i, type: ConnectionType.RedisDB, isLeaf: false, }) } this.databases[name] = dbs }, /** * Close connection * @param {string} name * @returns {Promise} */ async closeConnection(name) { const { success, msg } = await CloseConnection(name) if (!success) { // throw new Error(msg) return false } delete this.databases[name] const tabStore = useTabStore() tabStore.removeTabByName(name) return true }, /** * Remove connection * @param name * @returns {Promise<{success: boolean, [msg]: string}>} */ async removeConnection(name) { // close connection first await this.closeConnection(name) const { success, msg } = await RemoveConnection(name) if (!success) { return { success: false, msg } } await this.initConnections(true) return { success: true } }, /** * Open database and load all keys * @param connName * @param db * @returns {Promise} */ async openDatabase(connName, db) { const { data, success, msg } = await OpenDatabase(connName, db) if (!success) { throw new Error(msg) } const { keys = [] } = data if (isEmpty(keys)) { const dbs = this.databases[connName] dbs[db].children = [] dbs[db].opened = true return } // insert child to children list by order const sortedInsertChild = (childrenList, item) => { const insertIdx = sortedIndexBy(childrenList, item, 'key') childrenList.splice(insertIdx, 0, item) // childrenList.push(item) } // update all node item's children num const updateChildrenNum = (node) => { let count = 0 const totalChildren = size(node.children) if (totalChildren > 0) { for (const elem of node.children) { updateChildrenNum(elem) count += elem.keys } } else { count += 1 } node.keys = count // node.children = sortBy(node.children, 'label') } const keyStruct = [] const mark = {} for (const key in keys) { const keyPart = split(key, separator) // const prefixLen = size(keyPart) - 1 const len = size(keyPart) let handlePath = '' let ks = keyStruct for (let i = 0; i < len; i++) { handlePath += keyPart[i] if (i !== len - 1) { // layer const treeKey = `${handlePath}@${ConnectionType.RedisKey}` if (!mark.hasOwnProperty(treeKey)) { mark[treeKey] = { key: `${connName}/db${db}/${treeKey}`, label: keyPart[i], name: connName, db, keys: 0, redisKey: handlePath, type: ConnectionType.RedisKey, children: [], } sortedInsertChild(ks, mark[treeKey]) } ks = mark[treeKey].children handlePath += separator } else { // key const treeKey = `${handlePath}@${ConnectionType.RedisValue}` mark[treeKey] = { key: `${connName}/db${db}/${treeKey}`, label: keyPart[i], name: connName, db, keys: 0, redisKey: handlePath, type: ConnectionType.RedisValue, } sortedInsertChild(ks, mark[treeKey]) } } } // append db node to current connection's children const dbs = this.databases[connName] dbs[db].children = keyStruct dbs[db].opened = true updateChildrenNum(dbs[db]) }, /** * select node * @param key * @param name * @param db * @param type * @param redisKey */ select({ key, name, db, type, redisKey }) { if (type === ConnectionType.RedisValue) { console.log(`[click]key:${key} db: ${db} redis key: ${redisKey}`) // async get value for key this.loadKeyValue(name, db, redisKey).then(() => {}) } }, /** * load redis key * @param server * @param db * @param key */ async loadKeyValue(server, db, key) { try { const { data, success, msg } = await GetKeyValue(server, db, key) if (success) { const { type, ttl, value } = data const tab = useTabStore() tab.upsertTab({ server, db, type, ttl, key, value, }) } else { console.warn('TODO: handle get key fail') } } finally { } }, /** * * @param {string} connName * @param {number} db * @param {string} key * @private */ _addKey(connName, db, key) { const dbs = this.databases[connName] const dbDetail = get(dbs, db, {}) if (dbDetail == null) { return } const descendantChain = [dbDetail] const keyPart = split(key, separator) let redisKey = '' const keyLen = size(keyPart) let added = false for (let i = 0; i < keyLen; i++) { redisKey += keyPart[i] const node = last(descendantChain) const nodeList = get(node, 'children', []) const len = size(nodeList) const isLastKeyPart = i === keyLen - 1 for (let j = 0; j < len + 1; j++) { const treeKey = get(nodeList[j], 'key') const isLast = j >= len - 1 const currentKey = `${connName}/db${db}/${redisKey}@${ isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey }` if (treeKey > currentKey || isLast) { // out of search range, add new item if (isLastKeyPart) { // key not exists, add new one const item = { key: currentKey, label: keyPart[i], name: connName, db, keys: 1, redisKey, type: ConnectionType.RedisValue, } if (isLast) { nodeList.push(item) } else { nodeList.splice(j, 0, item) } added = true } else { // layer not exists, add new one const item = { key: currentKey, label: keyPart[i], name: connName, db, keys: 0, redisKey, type: ConnectionType.RedisKey, children: [], } if (isLast) { nodeList.push(item) descendantChain.push(last(nodeList)) } else { nodeList.splice(j, 0, item) descendantChain.push(nodeList[j]) } redisKey += separator added = true } break } else if (treeKey === currentKey) { if (isLastKeyPart) { // same key exists, do nothing console.log('TODO: same key exist, do nothing now, should replace value later') } else { // same group exists, find into it's children descendantChain.push(nodeList[j]) redisKey += separator } break } } } // update ancestor node's info if (added) { const desLen = size(descendantChain) for (let i = 0; i < desLen; i++) { const children = get(descendantChain[i], 'children', []) let keys = 0 for (const child of children) { if (child.type === ConnectionType.RedisKey) { keys += get(child, 'keys', 1) } else if (child.type === ConnectionType.RedisValue) { keys += get(child, 'keys', 0) } } descendantChain[i].keys = keys } } }, /** * set redis key * @param {string} connName * @param {number} db * @param {string} key * @param {number} keyType * @param {any} value * @param {number} ttl * @returns {Promise<{[msg]: string, success: boolean}>} */ async setKey(connName, db, key, keyType, value, ttl) { try { const { data, success, msg } = await SetKeyValue(connName, db, key, keyType, value, ttl) if (success) { // update tree view data this._addKey(connName, db, key) return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * update hash field * 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} connName * @param {number} db * @param {string} key * @param {string} field * @param {string} newField * @param {string} value * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} */ async setHash(connName, db, key, field, newField, value) { try { const { data, success, msg } = await SetHashValue(connName, db, key, field, newField || '', value || '') if (success) { const { updated = {} } = data return { success, updated } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * insert or update hash field item * @param {string} connName * @param {number} db * @param {string} key * @param {number }action 0:ignore duplicated fields 1:overwrite duplicated fields * @param {string[]} fieldItems field1, value1, filed2, value2... * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} */ async addHashField(connName, db, key, action, fieldItems) { try { const { data, success, msg } = await AddHashField(connName, db, key, action, fieldItems) if (success) { const { updated = {} } = data return { success, updated } } else { return { success: false, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove hash field * @param {string} connName * @param {number} db * @param {string} key * @param {string} field * @returns {Promise<{[msg]: {}, success: boolean, [removed]: string[]}>} */ async removeHashField(connName, db, key, field) { try { const { data, success, msg } = await SetHashValue(connName, db, key, field, '', '') if (success) { const { removed = [] } = data return { success, removed } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * insert list item * @param {string} connName * @param {number} db * @param {string} key * @param {int} action 0: push to head, 1: push to tail * @param {string[]}values * @returns {Promise<*|{msg, success: boolean}>} */ async addListItem(connName, db, key, action, values) { try { return AddListItem(connName, db, key, action, values) } catch (e) { return { success: false, msg: e.message } } }, /** * prepend item to head of list * @param connName * @param db * @param key * @param values * @returns {Promise<[msg]: string, success: boolean, [item]: []>} */ async prependListItem(connName, db, key, values) { try { const { data, success, msg } = await AddListItem(connName, db, key, 0, values) if (success) { const { left = [] } = data return { success, item: left } } else { return { success: false, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * append item to tail of list * @param connName * @param db * @param key * @param values * @returns {Promise<[msg]: string, success: boolean, [item]: any[]>} */ async appendListItem(connName, db, key, values) { try { const { data, success, msg } = await AddListItem(connName, db, key, 1, values) if (success) { const { right = [] } = data 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} connName * @param {number} db * @param {string} key * @param {number} index * @param {string} value * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}}>} */ async updateListItem(connName, db, key, index, value) { try { const { data, success, msg } = await SetListItem(connName, db, key, index, value) if (success) { const { updated = {} } = data return { success, updated } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove list item * @param {string} connName * @param {number} db * @param {string} key * @param {number} index * @returns {Promise<{[msg]: string, success: boolean, [removed]: string[]}>} */ async removeListItem(connName, db, key, index) { try { const { data, success, msg } = await SetListItem(connName, db, key, index, '') if (success) { const { removed = [] } = data return { success, removed } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * add item to set * @param {string} connName * @param {number} db * @param {string} key * @param {string} value * @returns {Promise<{[msg]: string, success: boolean}>} */ async addSetItem(connName, db, key, value) { try { const { success, msg } = await SetSetItem(connName, db, key, false, [value]) if (success) { return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * update value of set item * @param {string} connName * @param {number} db * @param {string} key * @param {string} value * @param {string} newValue * @returns {Promise<{[msg]: string, success: boolean}>} */ async updateSetItem(connName, db, key, value, newValue) { try { const { success, msg } = await UpdateSetItem(connName, db, key, value, newValue) if (success) { return { success: true } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove item from set * @param connName * @param db * @param key * @param value * @returns {Promise<{[msg]: string, success: boolean}>} */ async removeSetItem(connName, db, key, value) { try { const { success, msg } = await SetSetItem(connName, db, key, true, [value]) if (success) { return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * add item to sorted set * @param {string} connName * @param {number} db * @param {string} key * @param {number} action * @param {Object.} vs value: score * @returns {Promise<{[msg]: string, success: boolean}>} */ async addZSetItem(connName, db, key, action, vs) { try { const { success, msg } = await AddZSetValue(connName, db, key, action, vs) if (success) { return { success } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * update item of sorted set * @param {string} connName * @param {number} db * @param {string} key * @param {string} value * @param {string} newValue * @param {number} score * @returns {Promise<{[msg]: string, success: boolean, [updated]: {}, [removed]: []}>} */ async updateZSetItem(connName, db, key, value, newValue, score) { try { const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, newValue, score) if (success) { const { updated, removed } = data return { success, updated, removed } } else { return { success, msg } } } catch (e) { return { success: false, msg: e.message } } }, /** * remove item from sorted set * @param {string} connName * @param {number} db * @param key * @param {string} value * @returns {Promise<{[msg]: string, success: boolean, [removed]: []}>} */ async removeZSetItem(connName, db, key, value) { try { const { data, success, msg } = await UpdateZSetValue(connName, db, key, value, '', 0) if (success) { const { removed } = data return { success, removed } } 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} connName * @param {number} db * @param {string} key * @private */ _removeKey(connName, db, key) { const dbs = this.databases[connName] const dbDetail = get(dbs, db, {}) if (dbDetail == null) { return } const descendantChain = [dbDetail] const keyPart = split(key, separator) let redisKey = '' const keyLen = size(keyPart) let deleted = false let forceBreak = false for (let i = 0; i < keyLen && !forceBreak; i++) { redisKey += keyPart[i] const node = last(descendantChain) const nodeList = get(node, 'children', []) const len = size(nodeList) const isLastKeyPart = i === keyLen - 1 for (let j = 0; j < len; j++) { const treeKey = get(nodeList[j], 'key') const currentKey = `${connName}/db${db}/${redisKey}@${ isLastKeyPart ? ConnectionType.RedisValue : ConnectionType.RedisKey }` if (treeKey > currentKey) { // out of search range, target not exists forceBreak = true break } else if (treeKey === currentKey) { if (isLastKeyPart) { // find target nodeList.splice(j, 1) node.keys -= 1 deleted = true forceBreak = true } else { // find into it's children descendantChain.push(nodeList[j]) redisKey += separator } break } } if (forceBreak) { break } } // console.log(JSON.stringify(descendantChain)) // update ancestor node's info if (deleted) { const desLen = size(descendantChain) for (let i = desLen - 1; i > 0; i--) { const children = get(descendantChain[i], 'children', []) const parent = descendantChain[i - 1] if (isEmpty(children)) { const parentChildren = get(parent, 'children', []) const k = get(descendantChain[i], 'key') remove(parentChildren, (item) => item.key === k) } parent.keys -= 1 } } }, /** * remove redis key * @param {string} connName * @param {number} db * @param {string} key * @returns {Promise} */ async removeKey(connName, db, key) { try { const { data, success, msg } = await RemoveKey(connName, db, key) if (success) { // update tree view data this._removeKey(connName, db, key) // set tab content empty const tab = useTabStore() tab.emptyTab(connName) return true } } finally { } return false }, /** * rename key * @param {string} connName * @param {number} db * @param {string} key * @param {string} newKey * @returns {Promise<{[msg]: string, success: boolean}>} */ async renameKey(connName, db, key, newKey) { const { success = false, msg } = await RenameKey(connName, db, key, newKey) if (success) { // delete old key and add new key struct this._removeKey(connName, db, key) this._addKey(connName, db, newKey) return { success: true } } else { return { success: false, msg } } }, }, }) export default useConnectionStore