diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 161ab1e32..79785f644 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -494,6 +494,20 @@ module.exports = { return res.result || null; }, + saveRedisData_meta: true, + async saveRedisData({ conid, database, changeSet }, req) { + await testConnectionPermission(conid, req); + + const opened = await this.ensureOpened(conid, database); + const res = await this.sendRequest(opened, { msgtype: 'saveRedisData', changeSet }); + if (res.errorMessage) { + return { + errorMessage: res.errorMessage, + }; + } + return res.result || null; + }, + status_meta: true, async status({ conid, database }, req) { if (!conid) { diff --git a/packages/api/src/proc/databaseConnectionProcess.js b/packages/api/src/proc/databaseConnectionProcess.js index bed349355..050694b21 100644 --- a/packages/api/src/proc/databaseConnectionProcess.js +++ b/packages/api/src/proc/databaseConnectionProcess.js @@ -368,6 +368,90 @@ async function handleSaveTableData({ msgid, changeSet }) { } } +async function handleSaveRedisData({ msgid, changeSet }) { + try { + const driver = requireEngineDriver(storedConnection); + + if (!changeSet || !changeSet.changes || !Array.isArray(changeSet.changes)) { + throw new Error('Invalid changeSet structure'); + } + + for (const change of changeSet.changes) { + if (change.type === 'string') { + await driver.query(dbhan, `SET "${change.key}" "${change.value}"`); + } else if (change.type === 'json') { + await driver.query(dbhan, `JSON.SET "${change.key}" $ '${change.value.replace(/'/g, "\\'")}'`); + } else if (change.type === 'hash') { + if (change.updates && Array.isArray(change.updates)) { + for (const update of change.updates) { + await driver.query(dbhan, `HSET "${change.key}" "${update.key}" "${update.value}"`); + + if (update.ttl !== undefined && update.ttl !== null && update.ttl !== -1) { + try { + await dbhan.client.call('HEXPIRE', change.key, update.ttl, 'FIELDS', 1, update.key); + } catch (e) {} + } + } + } + if (change.inserts && Array.isArray(change.inserts)) { + for (const insert of change.inserts) { + await driver.query(dbhan, `HSET "${change.key}" "${insert.key}" "${insert.value}"`); + + if (insert.ttl !== undefined && insert.ttl !== null && insert.ttl !== -1) { + try { + await dbhan.client.call('HEXPIRE', change.key, insert.ttl, 'FIELDS', 1, insert.key); + } catch (e) {} + } + } + } + } else if (change.type === 'zset') { + if (change.updates && Array.isArray(change.updates)) { + for (const update of change.updates) { + await driver.query(dbhan, `ZADD "${change.key}" ${update.score} "${update.member}"`); + } + } + if (change.inserts && Array.isArray(change.inserts)) { + for (const insert of change.inserts) { + await driver.query(dbhan, `ZADD "${change.key}" ${insert.score} "${insert.member}"`); + } + } + } else if (change.type === 'list') { + if (change.updates && Array.isArray(change.updates)) { + for (const update of change.updates) { + await driver.query(dbhan, `LSET "${change.key}" ${update.index} "${update.value}"`); + } + } + if (change.inserts && Array.isArray(change.inserts)) { + for (const insert of change.inserts) { + await driver.query(dbhan, `RPUSH "${change.key}" "${insert.value}"`); + } + } + } else if (change.type === 'set') { + if (change.inserts && Array.isArray(change.inserts)) { + for (const insert of change.inserts) { + await driver.query(dbhan, `SADD "${change.key}" "${insert.value}"`); + } + } + } else if (change.type === 'stream') { + if (change.inserts && Array.isArray(change.inserts)) { + for (const insert of change.inserts) { + const streamId = insert.id === '*' || !insert.id ? '*' : insert.id; + await driver.query(dbhan, `XADD "${change.key}" ${streamId} value "${insert.value}"`); + } + } + } + } + + process.send({ msgtype: 'response', msgid }); + } catch (err) { + process.send({ + msgtype: 'response', + msgid, + errorMessage: extractErrorMessage(err, 'Error saving Redis data'), + }); + } +} + async function handleSqlPreview({ msgid, objects, options }) { await waitStructure(); const driver = requireEngineDriver(storedConnection); @@ -501,6 +585,7 @@ const messageHandlers = { schemaList: handleSchemaList, executeSessionQuery: handleExecuteSessionQuery, evalJsonScript: handleEvalJsonScript, + saveRedisData: handleSaveRedisData, // runCommand: handleRunCommand, }; diff --git a/packages/datalib/src/ChangeSetRedis.ts b/packages/datalib/src/ChangeSetRedis.ts new file mode 100644 index 000000000..cd8d39d9f --- /dev/null +++ b/packages/datalib/src/ChangeSetRedis.ts @@ -0,0 +1,55 @@ +export interface ChangeSetRedis_String { + key: string; + type: 'string'; + value: string; +} + +export interface ChangeSetRedis_JSON { + key: string; + type: 'json'; + value: string; +} + +export interface ChangeSetRedis_Hash { + key: string; + type: 'hash'; + inserts: { key: string; value: string, ttl: number }[]; + updates: { key: string; value: string, ttl: number }[]; + deletes: string[]; +} + +export interface ChangeSetRedis_List { + key: string; + type: 'list'; + inserts: { index: number; value: string }[]; + updates: { index: number; value: string }[]; + deletes: number[]; +} + +export interface ChangeSetRedis_Set { + key: string; + type: 'set'; + inserts: string[]; + deletes: string[]; +} + +export interface ChangeSetRedis_ZSet { + key: string; + type: 'zset'; + inserts: { member: string; score: number }[]; + updates: { member: string; score: number }[]; + deletes: string[]; +} + +export type ChangeSetRedisType = + | ChangeSetRedis_String + | ChangeSetRedis_JSON + | ChangeSetRedis_Hash + | ChangeSetRedis_List + | ChangeSetRedis_Set + | ChangeSetRedis_ZSet; + + +export interface ChangeSetRedis { + changes: ChangeSetRedisType[]; +} diff --git a/packages/datalib/src/index.ts b/packages/datalib/src/index.ts index 7012e70d2..0883eb6eb 100644 --- a/packages/datalib/src/index.ts +++ b/packages/datalib/src/index.ts @@ -25,3 +25,4 @@ export * from './CustomGridDisplay'; export * from './ScriptDrivedDeployer'; export * from './chartDefinitions'; export * from './chartProcessor'; +export * from './ChangeSetRedis'; diff --git a/packages/tools/src/stringTools.ts b/packages/tools/src/stringTools.ts index c5f24f690..82370568b 100644 --- a/packages/tools/src/stringTools.ts +++ b/packages/tools/src/stringTools.ts @@ -504,6 +504,7 @@ export function getIconForRedisType(type) { case 'binary': return 'img type-binary'; case 'ReJSON-RL': + case 'JSON': return 'img type-rejson'; default: return null; diff --git a/packages/web/src/datagrid/DbKeyTableControl.svelte b/packages/web/src/datagrid/DbKeyTableControl.svelte index 22c32f16c..4fe8db978 100644 --- a/packages/web/src/datagrid/DbKeyTableControl.svelte +++ b/packages/web/src/datagrid/DbKeyTableControl.svelte @@ -9,6 +9,8 @@ export let database; export let keyInfo; export let onChangeSelected; + export let modifyRow = null; + export let changeSetRedis = null; let rows = []; let cursor = 0; @@ -73,6 +75,12 @@ onMount(() => { loadNextRows(); }); + + $: displayRows = modifyRow ? rows.map(row => modifyRow(row)) : rows; + $: { + changeSetRedis; + displayRows = modifyRow ? rows.map(row => modifyRow(row)) : rows; + } {#each dbKeyFields as column} { onChangeItem?.({ ...item, @@ -29,5 +29,8 @@ flex: 1; display: flex; flex-direction: column; + gap: 10px; + padding: 10px; + overflow: auto; } diff --git a/packages/web/src/dbkeyvalue/DbKeyItemEdit.svelte b/packages/web/src/dbkeyvalue/DbKeyItemEdit.svelte new file mode 100644 index 000000000..395b80753 --- /dev/null +++ b/packages/web/src/dbkeyvalue/DbKeyItemEdit.svelte @@ -0,0 +1,53 @@ + + +
+ {#each dbKeyFields as column} +
+ { + onChangeItem?.({ + ...item, + [column.name]: value, + }); + } + : null} + /> +
+ {/each} +
+ + \ No newline at end of file diff --git a/packages/web/src/dbkeyvalue/DbKeyValueDetail.svelte b/packages/web/src/dbkeyvalue/DbKeyValueDetail.svelte index 20d543743..83fc05eef 100644 --- a/packages/web/src/dbkeyvalue/DbKeyValueDetail.svelte +++ b/packages/web/src/dbkeyvalue/DbKeyValueDetail.svelte @@ -12,6 +12,7 @@ export let columnTitle; export let value; export let onChangeValue = null; + export let keyType = null;
@@ -30,13 +31,16 @@
{#if display == 'text'} - { - onChangeValue?.(e.detail); - }} - /> +
+ { + onChangeValue?.(e.detail); + }} + /> +
{/if} {#if display == 'json'}
@@ -64,6 +68,13 @@ justify-content: space-between; } + .editor-wrapper { + flex: 1; + position: relative; + min-height: 60px; + max-height: 1000px; + } + .outer { flex: 1; position: relative; diff --git a/packages/web/src/dbkeyvalue/DbKeyValueHashEdit.svelte b/packages/web/src/dbkeyvalue/DbKeyValueHashEdit.svelte new file mode 100644 index 000000000..15674d41f --- /dev/null +++ b/packages/web/src/dbkeyvalue/DbKeyValueHashEdit.svelte @@ -0,0 +1,173 @@ + + +
+ {#each records as record, index} +
+
+ + handleFieldChange(index, 'key', e.target.value)} + disabled={keyColumn === 'key'} + /> + +
+
+ + handleFieldChange(index, 'value', e.target.value)} + disabled={keyColumn === 'value'} + /> + +
+
+ + handleFieldChange(index, 'ttl', e.target.value)} + disabled={keyColumn === 'ttl'} + /> + +
+
+ +
+
+ {/each} + +
+ +
+
+ + \ No newline at end of file diff --git a/packages/web/src/dbkeyvalue/DbKeyValueListEdit.svelte b/packages/web/src/dbkeyvalue/DbKeyValueListEdit.svelte new file mode 100644 index 000000000..a64520594 --- /dev/null +++ b/packages/web/src/dbkeyvalue/DbKeyValueListEdit.svelte @@ -0,0 +1,151 @@ + + +
+ {#each records as record, index} +
+
+ + handleFieldChange(index, 'value', e.target.value)} + disabled={keyColumn === 'value'} + /> + +
+
+ +
+
+ {/each} + +
+ +
+
+ + diff --git a/packages/web/src/dbkeyvalue/DbKeyValueSetEdit.svelte b/packages/web/src/dbkeyvalue/DbKeyValueSetEdit.svelte new file mode 100644 index 000000000..198130a36 --- /dev/null +++ b/packages/web/src/dbkeyvalue/DbKeyValueSetEdit.svelte @@ -0,0 +1,151 @@ + + +
+ {#each records as record, index} +
+
+ + handleFieldChange(index, 'value', e.target.value)} + disabled={keyColumn === 'value'} + /> + +
+
+ +
+
+ {/each} + +
+ +
+
+ + diff --git a/packages/web/src/dbkeyvalue/DbKeyValueStreamEdit.svelte b/packages/web/src/dbkeyvalue/DbKeyValueStreamEdit.svelte new file mode 100644 index 000000000..8ad79c354 --- /dev/null +++ b/packages/web/src/dbkeyvalue/DbKeyValueStreamEdit.svelte @@ -0,0 +1,166 @@ + + +
+ {#each records as record, index} +
+
+ + handleFieldChange(index, 'id', e.target.value)} + disabled={keyColumn === 'id'} + placeholder="* for auto" + /> + +
+
+ + handleFieldChange(index, 'value', e.target.value)} + disabled={keyColumn === 'value'} + /> + +
+
+ +
+
+ {/each} + +
+ +
+
+ + diff --git a/packages/web/src/dbkeyvalue/DbKeyValueZSetEdit.svelte b/packages/web/src/dbkeyvalue/DbKeyValueZSetEdit.svelte new file mode 100644 index 000000000..9bdb4c7c5 --- /dev/null +++ b/packages/web/src/dbkeyvalue/DbKeyValueZSetEdit.svelte @@ -0,0 +1,165 @@ + + +
+ {#each records as record, index} +
+
+ + handleFieldChange(index, 'member', e.target.value)} + disabled={keyColumn === 'member'} + /> + +
+
+ + handleFieldChange(index, 'score', e.target.value)} + disabled={keyColumn === 'score'} + /> + +
+
+ +
+
+ {/each} + +
+ +
+
+ + diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index d9f678404..b34362557 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -337,7 +337,7 @@ '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', + 'img type-rejson': 'mdi mdi-code-json color-icon-blue', 'img keydb': 'mdi mdi-key color-icon-blue', 'img replicator': 'mdi mdi-content-duplicate color-icon-green', diff --git a/packages/web/src/modals/AddDbKeyModal.svelte b/packages/web/src/modals/AddDbKeyModal.svelte index d255130a2..a43fd128b 100644 --- a/packages/web/src/modals/AddDbKeyModal.svelte +++ b/packages/web/src/modals/AddDbKeyModal.svelte @@ -1,6 +1,7 @@ {#await apiCall('database-connections/load-key-info', { conid, database, key, refreshToken })} {:then keyInfo} -
-
-
- - {keyInfo.type} + +
+
+
+ + {keyInfo.keyType?.label || keyInfo.type} +
+
+ +
+ handleKeyRename(keyInfo)} /> + handleChangeTtl(keyInfo)} />
-
- -
- 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; +
+ {#if keyInfo.keyType?.dbKeyFields && keyInfo.keyType?.showItemList} + + + { + currentRow = row; + showAddForm = false; + }} + modifyRow={row => getDisplayRow(row, keyInfo)} + /> + + + {#if showAddForm} + {#if keyInfo.type === 'list'} + { + if (item && item.records && item.records.length > 0) { + const existingChange = changeSetRedis.changes.find( + c => c.key === keyInfo.key && c.type === keyInfo.type + ); + // @ts-ignore + const listChange = existingChange || { key: keyInfo.key, type: 'list', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + listChange.inserts = item.records.filter(r => r.value.trim() !== '').map(r => ({ value: r.value })); + addOrUpdateChange(listChange); + } + }} + /> + {:else if keyInfo.type === 'hash'} + { + if (item && item.records && item.records.length > 0) { + const existingChange = changeSetRedis.changes.find( + c => c.key === keyInfo.key && c.type === keyInfo.type + ); + // @ts-ignore + const hashChange = existingChange || { key: keyInfo.key, type: 'hash', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + hashChange.inserts = item.records.filter(r => r.key.trim() !== '' && r.value.trim() !== '').map(r => ({ key: r.key, value: r.value, ttl: r.ttl ? parseInt(r.ttl) : undefined })); + addOrUpdateChange(hashChange); + } + }} + /> + {:else if keyInfo.type === 'zset'} + { + if (item && item.records && item.records.length > 0) { + const existingChange = changeSetRedis.changes.find( + c => c.key === keyInfo.key && c.type === keyInfo.type + ); + // @ts-ignore + const zsetChange = existingChange || { key: keyInfo.key, type: 'zset', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + zsetChange.inserts = item.records.filter(r => r.member.trim() !== '' && r.score.trim() !== '').map(r => ({ member: r.member, score: parseFloat(r.score) })); + addOrUpdateChange(zsetChange); + } + }} + /> + {:else if keyInfo.type === 'set'} + { + if (item && item.records && item.records.length > 0) { + const existingChange = changeSetRedis.changes.find( + c => c.key === keyInfo.key && c.type === keyInfo.type + ); + // @ts-ignore + const setChange = existingChange || { key: keyInfo.key, type: 'set', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + setChange.inserts = item.records.filter(r => r.value.trim() !== '').map(r => ({ value: r.value })); + addOrUpdateChange(setChange); + } + }} + /> + {:else if keyInfo.type === 'stream'} + { + if (item && item.records && item.records.length > 0) { + const existingChange = changeSetRedis.changes.find( + c => c.key === keyInfo.key && c.type === keyInfo.type + ); + // @ts-ignore + const streamChange = existingChange || { key: keyInfo.key, type: 'stream', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + streamChange.inserts = item.records.filter(r => r.value.trim() !== '').map(r => ({ id: r.id.trim() || '*', value: r.value })); + addOrUpdateChange(streamChange); + } + }} + /> + {/if} + {:else} + { + const existingChange = changeSetRedis.changes.find( + c => c.key === keyInfo.key && c.type === keyInfo.type + ); + + if (keyInfo.type === 'hash') { + // @ts-ignore + const hashChange = existingChange || { key: keyInfo.key, type: 'hash', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + const updateIndex = hashChange.updates?.findIndex(u => u.key === item.key) ?? -1; + if (updateIndex >= 0) { + // @ts-ignore + hashChange.updates[updateIndex] = { key: item.key, value: item.value, ttl: item.TTL }; + } else { + // @ts-ignore + hashChange.updates = [...(hashChange.updates || []), { key: item.key, value: item.value, ttl: item.TTL }]; + } + addOrUpdateChange(hashChange); + } else if (keyInfo.type === 'list') { + // @ts-ignore + const listChange = existingChange || { key: keyInfo.key, type: 'list', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + const updateIndex = listChange.updates?.findIndex(u => u.index === item.rowNumber) ?? -1; + if (updateIndex >= 0) { + // @ts-ignore + listChange.updates[updateIndex] = { index: item.rowNumber, value: item.value }; + } else { + // @ts-ignore + listChange.updates = [...(listChange.updates || []), { index: item.rowNumber, value: item.value }]; + } + addOrUpdateChange(listChange); + } else if (keyInfo.type === 'zset') { + // @ts-ignore + const zsetChange = existingChange || { key: keyInfo.key, type: 'zset', inserts: [], updates: [], deletes: [] }; + // @ts-ignore + const updateIndex = zsetChange.updates?.findIndex(u => u.member === item.member) ?? -1; + if (updateIndex >= 0) { + // @ts-ignore + zsetChange.updates[updateIndex] = { member: item.member, score: item.score }; + } else { + // @ts-ignore + zsetChange.updates = [...(zsetChange.updates || []), { member: item.member, score: item.score }]; + } + addOrUpdateChange(zsetChange); + } + }} + /> + {/if} + + + {:else} +
+ { + if (keyInfo.type === 'string') { + addOrUpdateChange({ + key: key, + type: 'string', + value: value, + }); + } else if (keyInfo.type === 'JSON') { + addOrUpdateChange({ + key: key, + type: 'json', + value: value, + }); + } }} /> - - - - - - {:else} -
- { - editedValue = value; - }} - /> -
- {/if} +
+ {/if} +
-
+ + + {_t('common.save', { defaultMessage: 'Save' })} + {#if keyInfo.keyType?.addMethod && keyInfo.keyType?.showItemList} + { showAddForm = true; }}>Add field + {/if} + {_t('common.refresh', { defaultMessage: 'Refresh' })} + + {/await} \ No newline at end of file diff --git a/packages/web/src/tabs/index.js b/packages/web/src/tabs/index.js index 1e34e5102..00d1fcb6a 100644 --- a/packages/web/src/tabs/index.js +++ b/packages/web/src/tabs/index.js @@ -18,6 +18,7 @@ import * as JsonTab from './JsonTab.svelte'; import * as ChangelogTab from './ChangelogTab.svelte'; import * as DiagramTab from './DiagramTab.svelte'; import * as DbKeyDetailTab from './DbKeyDetailTab.svelte'; +import * as DbKeyTab from './DbKeyTab.svelte'; import * as QueryDataTab from './QueryDataTab.svelte'; import * as ConnectionTab from './ConnectionTab.svelte'; import * as MapTab from './MapTab.svelte'; @@ -50,6 +51,7 @@ export default { ChangelogTab, DiagramTab, DbKeyDetailTab, + DbKeyTab, QueryDataTab, ConnectionTab, MapTab, diff --git a/packages/web/src/widgets/DbKeysTree.svelte b/packages/web/src/widgets/DbKeysTree.svelte index 512198a44..e1cab368a 100644 --- a/packages/web/src/widgets/DbKeysTree.svelte +++ b/packages/web/src/widgets/DbKeysTree.svelte @@ -51,25 +51,48 @@ function handleAddKey() { const connection = $currentDatabase?.connection; const database = $currentDatabase?.name; - const driver = findEngineDriver(connection, getExtensions()); + const focusedKey = $focusedTreeDbKey; + + let initialKeyName = ''; + if (focusedKey) { + if (focusedKey.type === 'dir' && focusedKey.key) { + initialKeyName = focusedKey.key + treeKeySeparator; + } else if (focusedKey.key) { + const lastSeparatorIndex = focusedKey.key.lastIndexOf(treeKeySeparator); + if (lastSeparatorIndex !== -1) { + initialKeyName = focusedKey.key.substring(0, lastSeparatorIndex + 1); + } + } + } - showModal(AddDbKeyModal, { - conid: connection._id, - database, - driver, - onConfirm: async item => { - const type = driver.supportedKeyTypes.find(x => x.name == item.type); - - await apiCall('database-connections/call-method', { - conid: connection._id, - database, - method: type.addMethod, - args: [item.keyName, ...type.dbKeyFields.map(fld => item[fld.name])], - }); - - reloadModel(); + openNewTab({ + tabComponent: 'DbKeyTab', + title: 'Add key', + icon: 'img keydb', + props: { + conid: connection?._id, + database, + initialKeyName, }, }); + + // showModal(AddDbKeyModal, { + // conid: connection._id, + // database, + // driver, + // onConfirm: async item => { + // const type = driver.supportedKeyTypes.find(x => x.name == item.type); + + // await apiCall('database-connections/call-method', { + // conid: connection._id, + // database, + // method: type.addMethod, + // args: [item.keyName, ...type.dbKeyFields.map(fld => item[fld.name])], + // }); + + // reloadModel(); + // }, + // }); } $: differentFocusedDb = diff --git a/plugins/dbgate-plugin-redis/src/backend/driver.js b/plugins/dbgate-plugin-redis/src/backend/driver.js index 5a9ead2b8..bc3304c91 100644 --- a/plugins/dbgate-plugin-redis/src/backend/driver.js +++ b/plugins/dbgate-plugin-redis/src/backend/driver.js @@ -459,6 +459,15 @@ const driver = { case 'string': res.value = await dbhan.client.get(key); break; + case 'ReJSON-RL': + res.type = 'JSON'; + try { + const jsonData = await dbhan.client.call('JSON.GET', key); + res.value = JSON.stringify(JSON.parse(jsonData), null, 2); + } catch (e) { + res.value = ''; + } + break; // case 'list': // res.tableColumns = [{ name: 'value' }]; // res.addMethod = 'rpush'; @@ -495,6 +504,10 @@ const driver = { switch (method) { case 'mdel': return await this.deleteBranch(dbhan, args[0]); + case 'zadd': + return await dbhan.client.zadd(args[0], args[2], args[1]); + case 'json.set': + return await dbhan.client.call('JSON.SET', args[0], '$', args[1]); case 'xaddjson': let json; try { @@ -528,14 +541,28 @@ const driver = { const res = await dbhan.client.zscan(key, cursor, 'COUNT', count); return { cursor: parseInt(res[0]), - items: _.chunk(res[1], 2).map((item) => ({ value: item[0], score: item[1] })), + items: _.chunk(res[1], 2).map((item) => ({ member: item[0], score: item[1] })), }; } case 'hash': { const res = await dbhan.client.hscan(key, cursor, 'COUNT', count); + const fields = _.chunk(res[1], 2); + + // Get TTL for each hash field (Redis 7.4+) + const items = await Promise.all( + fields.map(async ([fieldKey, fieldValue]) => { + try { + const ttl = await dbhan.client.call('HTTL', key, 'FIELDS', 1, fieldKey); + return { key: fieldKey, value: fieldValue, TTL: ttl && ttl[0] !== undefined ? ttl[0] : null }; + } catch (e) { + return { key: fieldKey, value: fieldValue }; + } + }) + ); + return { cursor: parseInt(res[0]), - items: _.chunk(res[1], 2).map((item) => ({ key: item[0], value: item[1] })), + items, }; } case 'stream': { diff --git a/plugins/dbgate-plugin-redis/src/frontend/driver.js b/plugins/dbgate-plugin-redis/src/frontend/driver.js index d5355c161..82e5299f6 100644 --- a/plugins/dbgate-plugin-redis/src/frontend/driver.js +++ b/plugins/dbgate-plugin-redis/src/frontend/driver.js @@ -49,7 +49,7 @@ const driver = { { name: 'set', label: 'Set', - dbKeyFields: [{ name: 'value' }], + dbKeyFields: [{ name: 'value', readOnly: true }], keyColumn: 'value', addMethod: 'sadd', showItemList: true, @@ -57,15 +57,15 @@ const driver = { { name: 'zset', label: 'Sorted Set', - dbKeyFields: [{ name: 'score' }, { name: 'value' }], - keyColumn: 'value', + dbKeyFields: [{ name: 'member', readOnly: true }, { name: 'score' }], + keyColumn: 'member', addMethod: 'zadd', showItemList: true, }, { name: 'hash', label: 'Hash', - dbKeyFields: [{ name: 'key' }, { name: 'value' }], + dbKeyFields: [{ name: 'key', readOnly: true }, { name: 'value' }, { name: 'TTL' }], keyColumn: 'key', addMethod: 'hset', showItemList: true, @@ -73,11 +73,17 @@ const driver = { { name: 'stream', label: 'Stream', - dbKeyFields: [{ name: 'id' }, { name: 'value' }], + dbKeyFields: [{ name: 'id', readOnly: true }, { name: 'value', readOnly: true }], keyColumn: 'id', addMethod: 'xaddjson', showItemList: true, }, + { + name: 'json', + label: 'JSON', + dbKeyFields: [{ name: 'value' }], + addMethod: 'json.set', + } ], showConnectionField: (field, values) => {