diff --git a/.circleci/config.yml b/.circleci/config.yml index 2db7d0a781..8eff6f505b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -432,7 +432,7 @@ jobs: exit 0; fi - SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:<< parameters.env >> + UPGRADES_LINK=$UPGRADES_LINK_STAGE SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:<< parameters.env >> - persist_to_workspace: root: . paths: @@ -474,7 +474,7 @@ jobs: exit 0; fi - SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:<< parameters.env >> + UPGRADES_LINK=$UPGRADES_LINK_STAGE SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:<< parameters.env >> rm -rf release/mac no_output_timeout: 15m - persist_to_workspace: @@ -514,7 +514,7 @@ jobs: exit 0; fi - SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:<< parameters.env >> + UPGRADES_LINK=$UPGRADES_LINK_STAGE SEGMENT_WRITE_KEY=$SEGMENT_WRITE_KEY_STAGE yarn package:<< parameters.env >> rm -rf release/win-unpacked shell: bash.exe no_output_timeout: 20m diff --git a/.eslintignore b/.eslintignore index 699437a49e..0f5f5970d1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,6 @@ # Ignores folders covered with custom linters configs redisinsight/api -tests +tests/e2e # Logs logs diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cf8821fbf9..67c0ce5056 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve title: "[Bug]:" -labels: '' +labels: bug assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index aafa701581..bfd698f44a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -2,7 +2,7 @@ name: Feature request about: Request a feature or submit an idea title: "[Feature Request]:" -labels: '' +labels: feature assignees: '' --- diff --git a/.github/redisinsight_browser.png b/.github/redisinsight_browser.png new file mode 100644 index 0000000000..96b9348d4a Binary files /dev/null and b/.github/redisinsight_browser.png differ diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000000..0c9660b8dd --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main, latest, release/*, codeql ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '37 11 * * 3' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'javascript' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/Dockerfile b/Dockerfile index 6474ad4903..e39716367a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,8 @@ WORKDIR /usr/src/app COPY redisinsight/api/package.json redisinsight/api/yarn.lock ./ RUN yarn install COPY redisinsight/api ./ -COPY --from=front /usr/src/app/redisinsight/api/src/static ./src/static +COPY --from=front /usr/src/app/redisinsight/api/static ./static +COPY --from=front /usr/src/app/redisinsight/api/defaults ./defaults RUN yarn run build:prod FROM node:14.17-slim diff --git a/LICENSE b/LICENSE index ea3921393f..3c1393f656 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ Server Side Public License VERSION 1, OCTOBER 16, 2018 - Copyright © 2018 MongoDB, Inc. + Copyright © 2021 Redis, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -500,7 +500,7 @@ 14. Revised Versions of this License. - MongoDB, Inc. may publish revised and/or new versions of the Server Side + Redis, Inc. may publish revised and/or new versions of the Server Side Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. @@ -509,9 +509,9 @@ specifies that a certain numbered version of the Server Side Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of - any later version published by MongoDB, Inc. If the Program does not + any later version published by Redis, Inc. If the Program does not specify a version number of the Server Side Public License, you may - choose any version ever published by MongoDB, Inc. + choose any version ever published by Redis, Inc. If the Program specifies that a proxy can decide which future versions of the Server Side Public License can be used, that proxy's public statement diff --git a/README.md b/README.md index 5ea4879156..29f0749ddf 100644 --- a/README.md +++ b/README.md @@ -1,233 +1,76 @@ -# RedisInsight +[![Release](https://img.shields.io/github/v/release/RedisInsight/RedisInsight.svg?sort=semver)](https://github.com/RedisInsight/RedisInsight/releases) +[![CircleCI](https://circleci.com/gh/RedisInsight/RedisInsight/tree/master.svg?style=svg)](https://circleci.com/gh/RedisInsight/RedisInsighth/tree/master) +[![Total alerts](https://img.shields.io/lgtm/alerts/g/RedisInsight/RedisInsight.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/RedisInsight/RedisInsight/alerts/) -[![CircleCI](https://circleci.com/gh/RedisInsight/RedisInsight/tree/latest.svg?style=svg)](https://circleci.com/gh/RedisInsight/RedisInsight/tree/latest) +# logo RedisInsight - Developer GUI for Redis, by Redis. +[![Forum](https://img.shields.io/badge/Forum-RedisInsight-red)](https://forum.redis.com/c/redisinsight/65) +[![Discord](https://img.shields.io/discord/697882427875393627?style=flat-square)](https://discord.gg/QUkjSsk) -Awesome Redis GUI written in Electron, NodeJS and React -## Directory Structure +RedisInsight is a visual tool that provides capabilities to design, develop and optimize your Redis application. +Query, analyse and interact with your Redis data. [Download it here](https://redis.com/redis-enterprise/redis-insight/#insight-form)! -- `redisinsight/ui` - Contains the frontend code -- `redisinsight/api` - Contains the backend code -- `docs` - Contains the documentation -- `scripts` - Build scripts and other build-related files -- `configs` - Webpack configuration files and other build-related files -- `tests` - Contains the e2e +![RedisInsight Browser screenshot](/.github/redisinsight_browser.png) -## Plugins documentation +Built with love using [Electron](https://www.electronjs.org/), [Elastic UI](https://elastic.github.io/eui/#/), [Monaco Editor](https://microsoft.github.io/monaco-editor/) and NodeJS. -* [Introduction](docs/plugins/introduction.md) -* [Installation and Usage](docs/plugins/installation.md) -* [Plugin Development](docs/plugins/development.md) +## Overview -## Prerequisites +RedisInsight is an intuitive and efficient GUI for Redis, allowing you to interact with your databases and manage your data—with built-in support for Redis modules. -Make sure you have installed following packages: -* [Node](https://nodejs.org/en/download/) >=14.x and <16 -* [yarn](https://www.npmjs.com/package/yarn) >=1.21.3 +### RedisInsight Highlights: -## Installation +* Browse, filter and visualise your key-value Redis data structures +* CRUD support for Lists, Hashes, Strings, Sets, Sorted Sets +* CRUD support for [RedisJSON](https://oss.redis.com/redisjson/) +* Introducing Workbench - advanced command line interface with intelligent command auto-complete and complex data visualizations +* Command auto-complete support for [RediSearch](https://oss.redis.com/redisearch/), [RedisJSON](https://oss.redis.com/redisjson/), [RedisGraph](https://oss.redis.com/redisgraph/), [RedisTimeSeries](https://oss.redis.com/redistimeseries/), [RedisAI](https://oss.redis.com/redisai/) +* Visualizations of your [RediSearch](https://oss.redis.com/redisearch/) index, queries, and aggregations +* Ability to build your own data visualization plugins +* Built-in click-through guides for Redis capabilities +* Oficially supported for Redis OSS, [Redis Cloud](https://redis.com/try-free/). Works with Microsoft Azure Cache for Redis (official support upcoming). +* Available for macOS, Windows and Linux -Before development or build you have to install required dependencies +Check out the [release notes](https://docs.redis.com/latest/ri/release-notes/). -```bash -$ yarn install -$ yarn --cwd redisinsight/api/ -``` +## Get started with RedisInsight -## Development +This repository includes the code for RedisInsight 2.0, Currently available in public preview. Check out the [blogpost](https://redis.com/blog/introducing-redisinsight-2/) announcing it. -There are 2 ways to develop: +The current GA version of RedisInsight is 1.11. You can install RedisInsight 2.0 along with the GA version. -### Developing using electron app +### Installable +Available to download for free from [here](https://redis.com/redis-enterprise/redis-insight/#insight-form). -After you have installed all dependencies you can now run the app. -Run `yarn start` to start an electron application that will watch and build for you. +### Build +Alternatively you can also build from source. See our wiki for instructions. -```bash -# Development -$ yarn start -``` +* [How to build](https://github.com/RedisInsight/RedisInsight/wiki/How-to-build-and-contribute) -### Developing using web +## Feedback -#### Running backend part of the app +* Request a new [feature](https://github.com/RedisInsight/RedisInsight/issues/new?assignees=&labels=&template=feature_request.md&title=%5BFeature+Request%5D%3A) +* Upvote [popular feature requests](https://github.com/RedisInsight/RedisInsight/issues?q=is%3Aopen+is%3Aissue+label%3Afeature+sort%3Areactions-%2B1-desc) +* File a [bug](https://github.com/RedisInsight/RedisInsight/issues/new?assignees=&labels=&template=bug_report.md&title=%5BBug%5D%3A) -Run `yarn --cwd redisinsight/api/ start:dev` to start a local API at `localhost:5000`. -```bash -# Development -$ yarn --cwd redisinsight/api/ start:dev -``` +## RedisInsight Plugins -While the API is running, open your browser and navigate to http://localhost:5000/api/docs. You should see the Swagger UI. +With RedisInsight you can now also extend the core functionality by building your own data visualizations. See our wiki for more information. -#### Running frontend part of the app +* [Plugin Documentation](https://github.com/RedisInsight/RedisInsight/wiki/Plugin-Documentation) -Run `yarn start:web` to start a local server for UI. +## Contributing -```bash -# Development -$ yarn start:web -``` +If you would like to contribute to the code base or fix and issue, please consult the wiki. -Web interface will be available at http://localhost:8080 +* [How to build and contribute](https://github.com/RedisInsight/RedisInsight/wiki/How-to-build-and-contribute) -Now servers will watch for changes and automatically build for you +## Telemetry -## Build +RedisInsight includes an opt-in telemetry system, that is leveraged to help improve the developer experience (DX) within the app. We value your privacy, so stay assured, that all the data collected is anonymised. -### Packaging the desktop app +## License -#### Building statics for enablement area and default plugins +RedisInsight is licensed under [SSPL](/LICENSE) license. -Run `yarn build:statics` or `yarn build:statics:win` for Windows - -After you have installed all dependencies you can package the app. -Run `yarn package:prod` to package app for the local platform: - -```bash -# Production -$ yarn package:prod -``` - -And packaged installer will be in the folder _./release_ - -### Create docker image - -There are 2 different docker images available - -- Image with API and UI -- Image with API only - -#### Build Docker image with UI - -```bash - docker build . -``` - -Image exposes 5000 port - -Api docs - /api/docs - -Main UI - / - -Example: - -```bash - docker build -t redisinsight . -``` - -```bash - docker run -p 5000:5000 -d --cap-add ipc_lock redisinsight -``` - -Then api docs and main ui should be available on http://localhost:5000/api/docs and http://localhost:5000 - -#### Build Docker with API only - -Image exposes 5000 port - -Api docs - /api/docs - -Example: - -```bash - docker build -f api.Dockerfile -t api.redisinsight . -``` - -```bash - docker run -p 5000:5000 -d --cap-add ipc_lock api.redisinsight -``` - -Then api docs and main ui should be available on http://localhost:5000/api/docs - -## Tests - -### Running frontend tests - -#### Run UI unit tests - -```bash - yarn test -``` - -### Running backend tests - -#### Run backend unit tests - -```bash - # Plain tests - yarn --cwd redisinsight/api test - - # Tests with coverage - yarn --cwd redisinsight/api test:cov -``` - -### Run backend integration tests (using local server) - -```bash - # Plain tests - yarn --cwd redisinsight/api test:api - - # Tests with coverage - yarn --cwd redisinsight/api test:api:cov -``` - -> **_NOTE_**: Using `yarn test:api*` scripts you should have redis server up and running. -By default tests will look on `localhost:6379` without any auth -To customize tests configs you should run test with proper environment variables - -Example: - -If you have redis server running on a different host or port `somehost:7777` with default user pass `somepass` - -You should run test commands with such environment variables - -```bash - # Plain tests - TEST_REDIS_HOST=somehost \ - TEST_REDIS_PORT=7777 \ - TEST_REDIS_PASSWORD-somepass \ - yarn --cwd redisinsight/api test:api -``` - -You can find all possible environment variable available in the [constants.ts](redisinsight/api/test/helpers/constants.ts) file - -### Run backend integration tests (using docker) - -Here you should not care about tests and local redis database configuration - -We will spin up server inside docker container and run tests over it - -```bash - # run this this command - ./redisinsight/api/test/test-runs/start-test-run.sh -r oss-st-6 -``` -- -r - is the Redis Test Environment name - -We are supporting several test environments to run tests on various Redis databases: -- **oss-st-5** - _OSS Standalone v5_ -- **oss-st-5-pass** - _OSS Standalone v5 with admin pass required_ -- **oss-st-6** - _OSS Standalone v6 and all modules_ -- **oss-st-6-tls** - _OSS Standalone v6 with TLS enabled_ -- **oss-st-6-tls-auth** - _OSS Standalone v6 with TLS auth required_ -- **oss-clu** - _OSS Cluster_ -- **oss-clu-tls** - _OSS Cluster with TLS enabled_ -- **oss-sent** - _OSS Sentinel_ -- **re-st** - _Redis Enterprise with Standalone inside_ -- **re-clu** - _Redis Enterprise with Cluster inside_ - - -### Running E2E tests - -Install E2E tests deps - -```bash - yarn --cwd tests/e2e -``` - -Run E2E tests - -```bash - yarn --cwd tests/e2e test:chrome -``` diff --git a/configs/webpack.config.main.prod.babel.js b/configs/webpack.config.main.prod.babel.js index 0c679cfb97..76aa522547 100644 --- a/configs/webpack.config.main.prod.babel.js +++ b/configs/webpack.config.main.prod.babel.js @@ -1,7 +1,6 @@ import path from 'path'; import webpack from 'webpack'; import { merge } from 'webpack-merge'; -import TerserPlugin from 'terser-webpack-plugin'; import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; import baseConfig from './webpack.config.base'; import DeleteSourceMaps from '../scripts/DeleteSourceMaps'; diff --git a/configs/webpack.config.web.prod.babel.js b/configs/webpack.config.web.prod.babel.js index 45a17ac15c..acb39ccb29 100644 --- a/configs/webpack.config.web.prod.babel.js +++ b/configs/webpack.config.web.prod.babel.js @@ -122,6 +122,20 @@ export default merge(commonConfig, { test: /\.css$/, use: ['style-loader', 'css-loader'], }, + // WOFF2 Font + { + test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, + use: [ + { + loader: 'file-loader', + options: { + name: '[hash]-[name].[ext]', + outputPath: 'static', + publicPath: 'static', + }, + }, + ], + }, // TTF Font { test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, diff --git a/docs/plugins/installation.md b/docs/plugins/installation.md index cff18847f4..5192a6a173 100644 --- a/docs/plugins/installation.md +++ b/docs/plugins/installation.md @@ -9,9 +9,9 @@ authors to avoid automatic execution of malicious code. 1. Download the plugin for the Workbench. 2. Open the `plugins` folder with the following path - * For MacOs: `/.redisinsight-v2.0/plugins` - * For Windows: `C:\Users\{Username}\.redisinsight-v2.0\plugins` - * For Linux: `/.redisinsight-v2.0/plugins` + * For MacOs: `/.redisinsight-preview/plugins` + * For Windows: `C:/Users/{Username}/.redisinsight-preview/plugins` + * For Linux: `/.redisinsight-preview/plugins` 3. Add the folder with plugin to the `plugins` folder To see the uploaded plugin visualizations in the command results, reload the Workbench diff --git a/electron-builder.json b/electron-builder.json index 6d1a875a27..fc4e11687c 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -63,9 +63,14 @@ "extraResources": [ "./resources/**", { - "from": "./redisinsight/api/src/static", + "from": "./redisinsight/api/static", "to": "static", "filter": ["**/*"] + }, + { + "from": "./redisinsight/api/defaults", + "to": "defaults", + "filter": ["**/*"] } ] } diff --git a/package.json b/package.json index 63969e6921..a737e8a4d9 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,9 @@ "build:main": "webpack --config ./configs/webpack.config.main.prod.babel.js", "build:main:stage": "webpack --config ./configs/webpack.config.main.stage.babel.js", "build:web": "webpack --config ./configs/webpack.config.web.prod.babel.js", - "build:statics": "sh ./scripts/build-statics.sh", - "build:statics:win": "./scripts/build-statics.cmd", + "build:defaults": "yarn --cwd redisinsight/api build:defaults", + "build:statics": "yarn build:defaults & sh ./scripts/build-statics.sh", + "build:statics:win": "yarn build:defaults & ./scripts/build-statics.cmd", "build:renderer": "webpack --config ./configs/webpack.config.renderer.prod.babel.js", "build:renderer:stage": "webpack --config ./configs/webpack.config.renderer.stage.babel.js", "rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir redisinsight/ui", @@ -48,7 +49,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/RedisLabs/redisinsight-v2.git" + "url": "git+https://github.com/RedisInsight/RedisInsight.git" }, "author": { "name": "Redis Ltd.", @@ -56,7 +57,7 @@ "url": "https://redis.com/redis-enterprise/redis-insight" }, "bugs": { - "url": "https://github.com/RedisLabs/redisinsight-v2/issues" + "url": "https://github.com/RedisInsight/RedisInsight/issues" }, "keywords": [ "redisinsight", @@ -68,14 +69,19 @@ "sass", "webpack" ], - "homepage": "https://github.com/RedisLabs/redisinsight-v2#readme", + "homepage": "https://github.com/RedisInsight/RedisInsight#readme", "jest": { "testURL": "http://localhost/", "moduleNameMapper": { "\\.(jpg|jpeg|png|ico|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/redisinsight/__mocks__/fileMock.js", "\\.(css|less|sass|scss)$": "identity-obj-proxy", "uiSrc/(.*)": "/redisinsight/ui/src/$1", - "monaco-editor": "/redisinsight/__mocks__/monacoMock.js" + "monaco-editor": "/redisinsight/__mocks__/monacoMock.js", + "unified": "/redisinsight/__mocks__/unified.js", + "remark-parse": "/redisinsight/__mocks__/remarkParse.js", + "remark-rehype": "/redisinsight/__mocks__/remarkRehype.js", + "rehype-stringify": "/redisinsight/__mocks__/rehypeStringify.js", + "unist-util-visit": "/redisinsight/__mocks__/unistUtilsVisit.js" }, "setupFilesAfterEnv": [ "/redisinsight/ui/src/setup-tests.ts" @@ -161,8 +167,8 @@ "cross-env": "^7.0.2", "css-loader": "^5.0.1", "css-minimizer-webpack-plugin": "^1.2.0", - "electron": "^15.3.1", - "electron-builder": "^22.14.5", + "electron": "^16.0.5", + "electron-builder": "^22.14.10", "electron-builder-notarize": "^1.2.0", "electron-debug": "^3.1.0", "electron-devtools-installer": "^3.2.0", @@ -230,7 +236,7 @@ "electron-context-menu": "^3.1.0", "electron-log": "^4.2.4", "electron-store": "^8.0.0", - "electron-updater": "4.6.1", + "electron-updater": "4.6.5", "formik": "^2.2.9", "html-entities": "^2.3.2", "html-react-parser": "^1.2.4", @@ -239,11 +245,16 @@ "react-contenteditable": "^3.3.5", "react-dom": "^17.0.1", "react-hotkeys-hook": "^3.3.1", + "react-jsx-parser": "^1.28.4", "react-monaco-editor": "^0.44.0", "react-redux": "^7.2.2", - "react-jsx-parser": "^1.28.4", "react-router-dom": "^5.2.0", - "react-virtualized": "^9.22.2" + "react-virtualized": "^9.22.2", + "rehype-stringify": "^9.0.2", + "remark-parse": "^10.0.1", + "remark-rehype": "^10.0.1", + "unified": "^10.1.1", + "unist-util-visit": "^4.1.0" }, "devEngines": { "node": ">=14.x <16", diff --git a/redisinsight/__mocks__/rehypeStringify.js b/redisinsight/__mocks__/rehypeStringify.js new file mode 100644 index 0000000000..bbabe6d0d1 --- /dev/null +++ b/redisinsight/__mocks__/rehypeStringify.js @@ -0,0 +1 @@ +export default jest.fn() diff --git a/redisinsight/__mocks__/remarkParse.js b/redisinsight/__mocks__/remarkParse.js new file mode 100644 index 0000000000..bbabe6d0d1 --- /dev/null +++ b/redisinsight/__mocks__/remarkParse.js @@ -0,0 +1 @@ +export default jest.fn() diff --git a/redisinsight/__mocks__/remarkRehype.js b/redisinsight/__mocks__/remarkRehype.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/redisinsight/__mocks__/unified.js b/redisinsight/__mocks__/unified.js new file mode 100644 index 0000000000..127615fa42 --- /dev/null +++ b/redisinsight/__mocks__/unified.js @@ -0,0 +1 @@ +export const unified = jest.fn() diff --git a/redisinsight/__mocks__/unistUtilsVisit.js b/redisinsight/__mocks__/unistUtilsVisit.js new file mode 100644 index 0000000000..31557f85e7 --- /dev/null +++ b/redisinsight/__mocks__/unistUtilsVisit.js @@ -0,0 +1 @@ +export const visit = jest.fn(); diff --git a/redisinsight/api/.gitignore b/redisinsight/api/.gitignore index 0fc65df77f..690310b2bf 100644 --- a/redisinsight/api/.gitignore +++ b/redisinsight/api/.gitignore @@ -1,7 +1,8 @@ # compiled output /dist /node_modules -/src/static +/static +/defaults # Logs logs diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index 7f33a17f7a..f51f4fde4c 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -6,15 +6,21 @@ const staticDir = process.env.BUILD_TYPE === 'ELECTRON' && process['resourcesPat ? join(process['resourcesPath'], 'static') : join(__dirname, '..', 'static'); +const defaultsDir = process.env.BUILD_TYPE === 'ELECTRON' && process['resourcesPath'] + ? join(process['resourcesPath'], 'defaults') + : join(__dirname, '..', 'defaults'); + export default { dir_path: { homedir, staticDir, + defaultsDir, logs: join(homedir, 'logs'), defaultPlugins: join(staticDir, 'plugins'), customPlugins: join(homedir, 'plugins'), pluginsAssets: join(staticDir, 'resources', 'plugins'), commands: join(homedir, 'commands'), + defaultCommandsDir: join(defaultsDir, 'commands'), caCertificates: join(homedir, 'ca_certificates'), clientCertificates: join(homedir, 'client_certificates'), }, @@ -42,7 +48,7 @@ export default { migrationsRun: process.env.DB_MIGRATIONS ? process.env.DB_MIGRATIONS === 'true' : true, }, redis_cloud: { - url: process.env.REDIS_CLOUD_URL || 'https://qa-api.redislabs.com/v1/', + url: process.env.REDIS_CLOUD_URL || 'https://api.qa.redislabs.com/v1', }, redis_clients: { idleSyncInterval: parseInt(process.env.CLIENTS_IDLE_SYNC_INTERVAL, 10) || 1000 * 60 * 60, // 1hr @@ -72,18 +78,36 @@ export default { omitSensitiveData: process.env.LOGGER_OMIT_DATA ? process.env.LOGGER_OMIT_DATA === 'true' : true, pipelineSummaryLimit: parseInt(process.env.LOGGER_PIPELINE_SUMMARY_LIMIT, 10) || 5, }, - commands: { - mainUrl: process.env.COMMANDS_MAIN_URL - || 'https://raw.githubusercontent.com/redis/redis-doc/master/commands.json', - redisearchUrl: process.env.COMMANDS_REDISEARCH_URL - || 'https://raw.githubusercontent.com/RediSearch/RediSearch/master/commands.json', - redijsonUrl: process.env.COMMANDS_REDIJSON_URL - || 'https://raw.githubusercontent.com/RedisJSON/RedisJSON/master/commands.json', - redistimeseriesUrl: process.env.COMMANDS_REDISTIMESERIES_URL - || 'https://raw.githubusercontent.com/RedisTimeSeries/RedisTimeSeries/master/src/commands.json', - redisaiUrl: process.env.COMMANDS_REDISAI_URL - || 'https://raw.githubusercontent.com/RedisAI/RedisAI/master/commands.json', - redisgraphUrl: process.env.COMMANDS_REDISGRAPH_URL - || 'https://raw.githubusercontent.com/RedisGraph/RedisGraph/master/commands.json', - }, + commands: [ + { + name: 'main', + url: process.env.COMMANDS_MAIN_URL + || 'https://raw.githubusercontent.com/redis/redis-doc/master/commands.json', + }, + { + name: 'redisearch', + url: process.env.COMMANDS_REDISEARCH_URL + || 'https://raw.githubusercontent.com/RediSearch/RediSearch/master/commands.json', + }, + { + name: 'redijson', + url: process.env.COMMANDS_REDIJSON_URL + || 'https://raw.githubusercontent.com/RedisJSON/RedisJSON/master/commands.json', + }, + { + name: 'redistimeseries', + url: process.env.COMMANDS_REDISTIMESERIES_URL + || 'https://raw.githubusercontent.com/RedisTimeSeries/RedisTimeSeries/master/commands.json', + }, + { + name: 'redisai', + url: process.env.COMMANDS_REDISAI_URL + || 'https://raw.githubusercontent.com/RedisAI/RedisAI/master/commands.json', + }, + { + name: 'redisgraph', + url: process.env.COMMANDS_REDISGRAPH_URL + || 'https://raw.githubusercontent.com/RedisGraph/RedisGraph/master/commands.json', + }, + ], }; diff --git a/redisinsight/api/config/production.ts b/redisinsight/api/config/production.ts index eb991c0698..d74018b3ba 100644 --- a/redisinsight/api/config/production.ts +++ b/redisinsight/api/config/production.ts @@ -1,6 +1,6 @@ import { join } from 'path'; -const homedir = join(require('os').homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-v2.0'); +const homedir = join(require('os').homedir(), process.env.APP_FOLDER_NAME || '.redisinsight-preview'); export default { dir_path: { @@ -18,6 +18,6 @@ export default { database: join(homedir, 'redisinsight.db'), }, redis_cloud: { - url: process.env.REDIS_CLOUD_URL || 'https://api.redislabs.com/v1/', + url: process.env.REDIS_CLOUD_URL || 'https://api.redislabs.com/v1', }, }; diff --git a/redisinsight/api/nest-cli.json b/redisinsight/api/nest-cli.json index 316a787c6a..4a840a3e32 100644 --- a/redisinsight/api/nest-cli.json +++ b/redisinsight/api/nest-cli.json @@ -3,7 +3,14 @@ "sourceRoot": "src", "compilerOptions": { "assets": [ - "static/**/*" + { + "include": "../static/**/*", + "outDir": "dist/static" + }, + { + "include": "../defaults/**/*", + "outDir": "dist/defaults" + } ] } } diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 4be212dac8..9b806cbc4d 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -9,6 +9,8 @@ "url": "https://redis.com/redis-enterprise/redis-insight" }, "scripts": { + "build:defaults:commands": "ts-node ./scripts/default-commands.ts", + "build:defaults": "yarn build:defaults:commands", "prebuild": "rimraf dist", "build": "nest build", "build:prod": "rimraf dist && nest build -p ./tsconfig.build.prod.json && cross-env NODE_ENV=production", @@ -46,6 +48,7 @@ "class-transformer": "^0.2.3", "class-validator": "^0.12.2", "express": "^4.17.1", + "fs-extra": "^10.0.0", "ioredis": "^4.27.1", "is-glob": "^4.0.1", "jsonpath": "^1.1.1", diff --git a/redisinsight/api/scripts/default-commands.ts b/redisinsight/api/scripts/default-commands.ts new file mode 100644 index 0000000000..46674239c7 --- /dev/null +++ b/redisinsight/api/scripts/default-commands.ts @@ -0,0 +1,40 @@ +import axios from 'axios'; +import * as fs from 'fs'; +import * as path from 'path'; +import { get } from '../src/utils/config'; + +const PATH_CONFIG = get('dir_path'); +const COMMANDS_CONFIG = get('commands'); + +async function init() { + try { + await Promise.all(COMMANDS_CONFIG.map(async ({ name, url }) => { + try { + console.log(`Trying to get ${name} commands...`); + const { data } = await axios.get(url, { + responseType: 'text', + transformResponse: [(raw) => raw], + }); + + if (!fs.existsSync(PATH_CONFIG.defaultCommandsDir)) { + fs.mkdirSync(PATH_CONFIG.defaultCommandsDir, { recursive: true }); + } + + fs.writeFileSync( + path.join(PATH_CONFIG.defaultCommandsDir, `${name}.json`), + JSON.stringify(JSON.parse(data)), // check that we received proper json object + ); + console.log(`Successfully generated default ${name} commands`); + } catch (error) { + console.error(`Unable to update ${name} commands`, error); + } + })); + + process.exit(0); + } catch (e) { + console.error('Something went wrong trying to get default commands jsons', e); + process.exit(1); + } +} + +init(); diff --git a/redisinsight/api/src/__mocks__/analytics.ts b/redisinsight/api/src/__mocks__/analytics.ts index 659e31e891..85df9d478f 100644 --- a/redisinsight/api/src/__mocks__/analytics.ts +++ b/redisinsight/api/src/__mocks__/analytics.ts @@ -1,4 +1,5 @@ export const mockInstancesAnalyticsService = () => ({ + sendInstanceListReceivedEvent: jest.fn(), sendInstanceAddedEvent: jest.fn(), sendInstanceAddFailedEvent: jest.fn(), sendInstanceEditedEvent: jest.fn(), @@ -23,14 +24,14 @@ export const mockBrowserAnalyticsService = () => ({ }); export const mockCliAnalyticsService = () => ({ - sendCliClientCreatedEvent: jest.fn(), - sendCliClientCreationFailedEvent: jest.fn(), - sendCliClientDeletedEvent: jest.fn(), - sendCliClientRecreatedEvent: jest.fn(), - sendCliCommandExecutedEvent: jest.fn(), - sendCliCommandErrorEvent: jest.fn(), - sendCliClusterCommandExecutedEvent: jest.fn(), - sendCliConnectionErrorEvent: jest.fn(), + sendClientCreatedEvent: jest.fn(), + sendClientCreationFailedEvent: jest.fn(), + sendClientDeletedEvent: jest.fn(), + sendClientRecreatedEvent: jest.fn(), + sendCommandExecutedEvent: jest.fn(), + sendCommandErrorEvent: jest.fn(), + sendClusterCommandExecutedEvent: jest.fn(), + sendConnectionErrorEvent: jest.fn(), }); export const mockSettingsAnalyticsService = () => ({ diff --git a/redisinsight/api/src/__mocks__/commands.ts b/redisinsight/api/src/__mocks__/commands.ts index bf3004064a..237afec8a0 100644 --- a/redisinsight/api/src/__mocks__/commands.ts +++ b/redisinsight/api/src/__mocks__/commands.ts @@ -165,5 +165,7 @@ export const mockRedisgraphCommands = { }; export const mockCommandsJsonProvider = () => ({ + updateLatestJson: jest.fn(), getCommands: jest.fn(), + getDefaultCommands: jest.fn(), }); diff --git a/redisinsight/api/src/constants/agreements-spec.json b/redisinsight/api/src/constants/agreements-spec.json index 6555d5a8fe..feea7da9ab 100644 --- a/redisinsight/api/src/constants/agreements-spec.json +++ b/redisinsight/api/src/constants/agreements-spec.json @@ -49,7 +49,7 @@ "disabled": false, "since": "1.0.4", "title": "Server Side Public License", - "label": "I have read and understood the Server Side Public License", + "label": "I have read and understood the Server Side Public License", "requiredText": "Accept the Server Side Public License" } } diff --git a/redisinsight/api/src/constants/commands/main.json b/redisinsight/api/src/constants/commands/main.json deleted file mode 100644 index 45cb3d0a78..0000000000 --- a/redisinsight/api/src/constants/commands/main.json +++ /dev/null @@ -1,5901 +0,0 @@ -{ - "ACL LOAD": { - "summary": "Reload the ACLs from the configured ACL file", - "complexity": "O(N). Where N is the number of configured users.", - "since": "6.0.0", - "group": "server" - }, - "ACL SAVE": { - "summary": "Save the current ACL rules in the configured ACL file", - "complexity": "O(N). Where N is the number of configured users.", - "since": "6.0.0", - "group": "server" - }, - "ACL LIST": { - "summary": "List the current ACL rules in ACL config file format", - "complexity": "O(N). Where N is the number of configured users.", - "since": "6.0.0", - "group": "server" - }, - "ACL USERS": { - "summary": "List the username of all the configured ACL rules", - "complexity": "O(N). Where N is the number of configured users.", - "since": "6.0.0", - "group": "server" - }, - "ACL GETUSER": { - "summary": "Get the rules for a specific ACL user", - "complexity": "O(N). Where N is the number of password, command and pattern rules that the user has.", - "arguments": [ - { - "name": "username", - "type": "string" - } - ], - "since": "6.0.0", - "group": "server" - }, - "ACL SETUSER": { - "summary": "Modify or create the rules for a specific ACL user", - "complexity": "O(N). Where N is the number of rules provided.", - "arguments": [ - { - "name": "username", - "type": "string" - }, - { - "name": "rule", - "type": "string", - "multiple": true, - "optional": true - } - ], - "since": "6.0.0", - "group": "server" - }, - "ACL DELUSER": { - "summary": "Remove the specified ACL users and the associated rules", - "complexity": "O(1) amortized time considering the typical user.", - "arguments": [ - { - "name": "username", - "type": "string", - "multiple": true - } - ], - "since": "6.0.0", - "group": "server" - }, - "ACL CAT": { - "summary": "List the ACL categories or the commands inside a category", - "complexity": "O(1) since the categories and commands are a fixed set.", - "arguments": [ - { - "name": "categoryname", - "type": "string", - "optional": true - } - ], - "since": "6.0.0", - "group": "server" - }, - "ACL GENPASS": { - "summary": "Generate a pseudorandom secure password to use for ACL users", - "complexity": "O(1)", - "arguments": [ - { - "name": "bits", - "type": "integer", - "optional": true - } - ], - "since": "6.0.0", - "group": "server" - }, - "ACL WHOAMI": { - "summary": "Return the name of the user associated to the current connection", - "complexity": "O(1)", - "since": "6.0.0", - "group": "server" - }, - "ACL LOG": { - "summary": "List latest events denied because of ACLs in place", - "complexity": "O(N) with N being the number of entries shown.", - "arguments": [ - { - "name": "count or RESET", - "type": "string", - "optional": true - } - ], - "since": "6.0.0", - "group": "server" - }, - "ACL HELP": { - "summary": "Show helpful text about the different subcommands", - "complexity": "O(1)", - "since": "6.0.0", - "group": "server" - }, - "APPEND": { - "summary": "Append a value to a key", - "complexity": "O(1). The amortized time complexity is O(1) assuming the appended value is small and the already present value is of any size, since the dynamic string library used by Redis will double the free space available on every reallocation.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "2.0.0", - "group": "string" - }, - "ASKING": { - "summary": "Sent by cluster clients after an -ASK redirect", - "complexity": "O(1)", - "arguments": [], - "since": "3.0.0", - "group": "cluster" - }, - "AUTH": { - "summary": "Authenticate to the server", - "arguments": [ - { - "name": "username", - "type": "string", - "optional": true - }, - { - "name": "password", - "type": "string" - } - ], - "since": "1.0.0", - "group": "connection" - }, - "BGREWRITEAOF": { - "summary": "Asynchronously rewrite the append-only file", - "since": "1.0.0", - "group": "server" - }, - "BGSAVE": { - "summary": "Asynchronously save the dataset to disk", - "arguments": [ - { - "name": "schedule", - "type": "enum", - "enum": [ - "SCHEDULE" - ], - "optional": true - } - ], - "since": "1.0.0", - "group": "server" - }, - "BITCOUNT": { - "summary": "Count set bits in a string", - "complexity": "O(N)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": [ - "start", - "end" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - } - ], - "since": "2.6.0", - "group": "bitmap" - }, - "BITFIELD": { - "summary": "Perform arbitrary bitfield integer operations on strings", - "complexity": "O(1) for each subcommand specified", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "command": "GET", - "name": [ - "type", - "offset" - ], - "type": [ - "type", - "integer" - ], - "optional": true - }, - { - "command": "SET", - "name": [ - "type", - "offset", - "value" - ], - "type": [ - "type", - "integer", - "integer" - ], - "optional": true - }, - { - "command": "INCRBY", - "name": [ - "type", - "offset", - "increment" - ], - "type": [ - "type", - "integer", - "integer" - ], - "optional": true - }, - { - "command": "OVERFLOW", - "type": "enum", - "enum": [ - "WRAP", - "SAT", - "FAIL" - ], - "optional": true - } - ], - "since": "3.2.0", - "group": "bitmap" - }, - "BITFIELD_RO": { - "summary": "Perform arbitrary bitfield integer operations on strings. Read-only variant of BITFIELD", - "complexity": "O(1) for each subcommand specified", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "command": "GET", - "name": [ - "type", - "offset" - ], - "type": [ - "type", - "integer" - ] - } - ], - "since": "6.2.0", - "group": "bitmap" - }, - "BITOP": { - "summary": "Perform bitwise operations between strings", - "complexity": "O(N)", - "arguments": [ - { - "name": "operation", - "type": "string" - }, - { - "name": "destkey", - "type": "key" - }, - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "2.6.0", - "group": "bitmap" - }, - "BITPOS": { - "summary": "Find first bit set or clear in a string", - "complexity": "O(N)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "bit", - "type": "integer" - }, - { - "name": "index", - "type": "block", - "optional": true, - "block": [ - { - "name": "start", - "type": "integer" - }, - { - "name": "end", - "type": "integer", - "optional": true - } - ] - } - ], - "since": "2.8.7", - "group": "bitmap" - }, - "BLPOP": { - "summary": "Remove and get the first element in a list, or block until one is available", - "complexity": "O(N) where N is the number of provided keys.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "timeout", - "type": "double" - } - ], - "since": "2.0.0", - "group": "list" - }, - "BRPOP": { - "summary": "Remove and get the last element in a list, or block until one is available", - "complexity": "O(N) where N is the number of provided keys.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "timeout", - "type": "double" - } - ], - "since": "2.0.0", - "group": "list" - }, - "BRPOPLPUSH": { - "summary": "Pop an element from a list, push it to another list and return it; or block until one is available", - "complexity": "O(1)", - "arguments": [ - { - "name": "source", - "type": "key" - }, - { - "name": "destination", - "type": "key" - }, - { - "name": "timeout", - "type": "double" - } - ], - "since": "2.2.0", - "group": "list" - }, - "BLMOVE": { - "summary": "Pop an element from a list, push it to another list and return it; or block until one is available", - "complexity": "O(1)", - "arguments": [ - { - "name": "source", - "type": "key" - }, - { - "name": "destination", - "type": "key" - }, - { - "name": "wherefrom", - "type": "enum", - "enum": [ - "LEFT", - "RIGHT" - ] - }, - { - "name": "whereto", - "type": "enum", - "enum": [ - "LEFT", - "RIGHT" - ] - }, - { - "name": "timeout", - "type": "double" - } - ], - "since": "6.2.0", - "group": "list" - }, - "LMPOP": { - "summary": "Pop elements from a list", - "complexity": "O(N+M) where N is the number of provided keys and M is the number of elements returned.", - "arguments": [ - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "optional": true, - "multiple": true - }, - { - "name": "where", - "type": "enum", - "enum": [ - "LEFT", - "RIGHT" - ] - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "7.0.0", - "group": "list" - }, - "BLMPOP": { - "summary": "Pop elements from a list, or block until one is available", - "complexity": "O(N+M) where N is the number of provided keys and M is the number of elements returned.", - "arguments": [ - { - "name": "timeout", - "type": "double" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "optional": true, - "multiple": true - }, - { - "name": "where", - "type": "enum", - "enum": [ - "LEFT", - "RIGHT" - ] - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "7.0.0", - "group": "list" - }, - "BZPOPMIN": { - "summary": "Remove and return the member with the lowest score from one or more sorted sets, or block until one is available", - "complexity": "O(log(N)) with N being the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "timeout", - "type": "double" - } - ], - "since": "5.0.0", - "group": "sorted_set" - }, - "BZPOPMAX": { - "summary": "Remove and return the member with the highest score from one or more sorted sets, or block until one is available", - "complexity": "O(log(N)) with N being the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "timeout", - "type": "double" - } - ], - "since": "5.0.0", - "group": "sorted_set" - }, - "CLIENT CACHING": { - "summary": "Instruct the server about tracking or not keys in the next request", - "complexity": "O(1)", - "arguments": [ - { - "name": "mode", - "type": "enum", - "enum": [ - "YES", - "NO" - ] - } - ], - "since": "6.0.0", - "group": "connection" - }, - "CLIENT ID": { - "summary": "Returns the client ID for the current connection", - "complexity": "O(1)", - "since": "5.0.0", - "group": "connection" - }, - "CLIENT INFO": { - "summary": "Returns information about the current client connection.", - "complexity": "O(1)", - "since": "6.2.0", - "group": "connection" - }, - "CLIENT KILL": { - "summary": "Kill the connection of a client", - "complexity": "O(N) where N is the number of client connections", - "arguments": [ - { - "name": "ip:port", - "type": "string", - "optional": true - }, - { - "command": "ID", - "name": "client-id", - "type": "integer", - "optional": true - }, - { - "command": "TYPE", - "type": "enum", - "enum": [ - "normal", - "master", - "slave", - "pubsub" - ], - "optional": true - }, - { - "command": "USER", - "name": "username", - "type": "string", - "optional": true - }, - { - "command": "ADDR", - "name": "ip:port", - "type": "string", - "optional": true - }, - { - "command": "LADDR", - "name": "ip:port", - "type": "string", - "optional": true - }, - { - "command": "SKIPME", - "name": "yes/no", - "type": "string", - "optional": true - } - ], - "since": "2.4.0", - "group": "connection" - }, - "CLIENT LIST": { - "summary": "Get the list of client connections", - "complexity": "O(N) where N is the number of client connections", - "arguments": [ - { - "command": "TYPE", - "type": "enum", - "enum": [ - "normal", - "master", - "replica", - "pubsub" - ], - "optional": true - }, - { - "name": "id", - "type": "block", - "block": [ - { - "command": "ID" - }, - { - "name": "client-id", - "type": "integer", - "multiple": true - } - ], - "optional": true - } - ], - "since": "2.4.0", - "group": "connection" - }, - "CLIENT GETNAME": { - "summary": "Get the current connection name", - "complexity": "O(1)", - "since": "2.6.9", - "group": "connection" - }, - "CLIENT GETREDIR": { - "summary": "Get tracking notifications redirection client ID if any", - "complexity": "O(1)", - "since": "6.0.0", - "group": "connection" - }, - "CLIENT UNPAUSE": { - "summary": "Resume processing of clients that were paused", - "complexity": "O(N) Where N is the number of paused clients", - "since": "6.2.0", - "group": "connection" - }, - "CLIENT PAUSE": { - "summary": "Stop processing commands from clients for some time", - "complexity": "O(1)", - "arguments": [ - { - "name": "timeout", - "type": "integer" - }, - { - "name": "mode", - "type": "enum", - "optional": true, - "enum": [ - "WRITE", - "ALL" - ] - } - ], - "since": "2.9.50", - "group": "connection" - }, - "CLIENT REPLY": { - "summary": "Instruct the server whether to reply to commands", - "complexity": "O(1)", - "arguments": [ - { - "name": "reply-mode", - "type": "enum", - "enum": [ - "ON", - "OFF", - "SKIP" - ] - } - ], - "since": "3.2.0", - "group": "connection" - }, - "CLIENT SETNAME": { - "summary": "Set the current connection name", - "complexity": "O(1)", - "since": "2.6.9", - "arguments": [ - { - "name": "connection-name", - "type": "string" - } - ], - "group": "connection" - }, - "CLIENT TRACKING": { - "summary": "Enable or disable server assisted client side caching support", - "complexity": "O(1). Some options may introduce additional complexity.", - "arguments": [ - { - "name": "status", - "type": "enum", - "enum": [ - "ON", - "OFF" - ] - }, - { - "command": "REDIRECT", - "name": "client-id", - "type": "integer", - "optional": true - }, - { - "command": "PREFIX", - "name": "prefix", - "type": "string", - "optional": true, - "multiple": true - }, - { - "name": "BCAST", - "type": "enum", - "enum": [ - "BCAST" - ], - "optional": true - }, - { - "name": "OPTIN", - "type": "enum", - "enum": [ - "OPTIN" - ], - "optional": true - }, - { - "name": "OPTOUT", - "type": "enum", - "enum": [ - "OPTOUT" - ], - "optional": true - }, - { - "name": "NOLOOP", - "type": "enum", - "enum": [ - "NOLOOP" - ], - "optional": true - } - ], - "since": "6.0.0", - "group": "connection" - }, - "CLIENT TRACKINGINFO": { - "summary": "Return information about server assisted client side caching for the current connection", - "complexity": "O(1)", - "since": "6.2.0", - "group": "connection" - }, - "CLIENT UNBLOCK": { - "summary": "Unblock a client blocked in a blocking command from a different connection", - "complexity": "O(log N) where N is the number of client connections", - "arguments": [ - { - "name": "client-id", - "type": "integer" - }, - { - "name": "unblock-type", - "type": "enum", - "enum": [ - "TIMEOUT", - "ERROR" - ], - "optional": true - } - ], - "since": "5.0.0", - "group": "connection" - }, - "CLIENT NO-EVICT": { - "summary": "Set client eviction mode for the current connection", - "complexity": "O(1)", - "since": "7.0.0", - "arguments": [ - { - "name": "enabled", - "type": "enum", - "enum": [ - "ON", - "OFF" - ] - } - ], - "group": "connection" - }, - "CLUSTER ADDSLOTS": { - "summary": "Assign new hash slots to receiving node", - "complexity": "O(N) where N is the total number of hash slot arguments", - "arguments": [ - { - "name": "slot", - "type": "integer", - "multiple": true - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER BUMPEPOCH": { - "summary": "Advance the cluster config epoch", - "complexity": "O(1)", - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER COUNT-FAILURE-REPORTS": { - "summary": "Return the number of failure reports active for a given node", - "complexity": "O(N) where N is the number of failure reports", - "arguments": [ - { - "name": "node-id", - "type": "string" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER COUNTKEYSINSLOT": { - "summary": "Return the number of local keys in the specified hash slot", - "complexity": "O(1)", - "arguments": [ - { - "name": "slot", - "type": "integer" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER DELSLOTS": { - "summary": "Set hash slots as unbound in receiving node", - "complexity": "O(N) where N is the total number of hash slot arguments", - "arguments": [ - { - "name": "slot", - "type": "integer", - "multiple": true - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER FAILOVER": { - "summary": "Forces a replica to perform a manual failover of its master.", - "complexity": "O(1)", - "arguments": [ - { - "name": "options", - "type": "enum", - "enum": [ - "FORCE", - "TAKEOVER" - ], - "optional": true - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER FLUSHSLOTS": { - "summary": "Delete a node's own slots information", - "complexity": "O(1)", - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER FORGET": { - "summary": "Remove a node from the nodes table", - "complexity": "O(1)", - "arguments": [ - { - "name": "node-id", - "type": "string" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER GETKEYSINSLOT": { - "summary": "Return local key names in the specified hash slot", - "complexity": "O(log(N)) where N is the number of requested keys", - "arguments": [ - { - "name": "slot", - "type": "integer" - }, - { - "name": "count", - "type": "integer" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER INFO": { - "summary": "Provides info about Redis Cluster node state", - "complexity": "O(1)", - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER KEYSLOT": { - "summary": "Returns the hash slot of the specified key", - "complexity": "O(N) where N is the number of bytes in the key", - "arguments": [ - { - "name": "key", - "type": "string" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER MEET": { - "summary": "Force a node cluster to handshake with another node", - "complexity": "O(1)", - "arguments": [ - { - "name": "ip", - "type": "string" - }, - { - "name": "port", - "type": "integer" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER MYID": { - "summary": "Return the node id", - "complexity": "O(1)", - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER NODES": { - "summary": "Get Cluster config for the node", - "complexity": "O(N) where N is the total number of Cluster nodes", - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER REPLICATE": { - "summary": "Reconfigure a node as a replica of the specified master node", - "complexity": "O(1)", - "arguments": [ - { - "name": "node-id", - "type": "string" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER RESET": { - "summary": "Reset a Redis Cluster node", - "complexity": "O(N) where N is the number of known nodes. The command may execute a FLUSHALL as a side effect.", - "arguments": [ - { - "name": "reset-type", - "type": "enum", - "enum": [ - "HARD", - "SOFT" - ], - "optional": true - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER SAVECONFIG": { - "summary": "Forces the node to save cluster state on disk", - "complexity": "O(1)", - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER SET-CONFIG-EPOCH": { - "summary": "Set the configuration epoch in a new node", - "complexity": "O(1)", - "arguments": [ - { - "name": "config-epoch", - "type": "integer" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER SETSLOT": { - "summary": "Bind a hash slot to a specific node", - "complexity": "O(1)", - "arguments": [ - { - "name": "slot", - "type": "integer" - }, - { - "name": "subcommand", - "type": "enum", - "enum": [ - "IMPORTING", - "MIGRATING", - "STABLE", - "NODE" - ] - }, - { - "name": "node-id", - "type": "string", - "optional": true - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER SLAVES": { - "summary": "List replica nodes of the specified master node", - "complexity": "O(1)", - "arguments": [ - { - "name": "node-id", - "type": "string" - } - ], - "since": "3.0.0", - "group": "cluster" - }, - "CLUSTER REPLICAS": { - "summary": "List replica nodes of the specified master node", - "complexity": "O(1)", - "arguments": [ - { - "name": "node-id", - "type": "string" - } - ], - "since": "5.0.0", - "group": "cluster" - }, - "CLUSTER SLOTS": { - "summary": "Get array of Cluster slot to node mappings", - "complexity": "O(N) where N is the total number of Cluster nodes", - "since": "3.0.0", - "group": "cluster" - }, - "COMMAND": { - "summary": "Get array of Redis command details", - "complexity": "O(N) where N is the total number of Redis commands", - "since": "2.8.13", - "group": "server" - }, - "COMMAND COUNT": { - "summary": "Get total number of Redis commands", - "complexity": "O(1)", - "since": "2.8.13", - "group": "server" - }, - "COMMAND GETKEYS": { - "summary": "Extract keys given a full Redis command", - "complexity": "O(N) where N is the number of arguments to the command", - "since": "2.8.13", - "group": "server" - }, - "COMMAND INFO": { - "summary": "Get array of specific Redis command details", - "complexity": "O(N) when N is number of commands to look up", - "since": "2.8.13", - "arguments": [ - { - "name": "command-name", - "type": "string", - "multiple": true - } - ], - "group": "server" - }, - "CONFIG GET": { - "summary": "Get the value of a configuration parameter", - "arguments": [ - { - "name": "parameter", - "type": "string" - } - ], - "since": "2.0.0", - "group": "server" - }, - "CONFIG REWRITE": { - "summary": "Rewrite the configuration file with the in memory configuration", - "since": "2.8.0", - "group": "server" - }, - "CONFIG SET": { - "summary": "Set a configuration parameter to the given value", - "arguments": [ - { - "name": "parameter", - "type": "string" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "2.0.0", - "group": "server" - }, - "CONFIG RESETSTAT": { - "summary": "Reset the stats returned by INFO", - "complexity": "O(1)", - "since": "2.0.0", - "group": "server" - }, - "COPY": { - "summary": "Copy a key", - "complexity": "O(N) worst case for collections, where N is the number of nested items. O(1) for string values.", - "since": "6.2.0", - "arguments": [ - { - "name": "source", - "type": "key" - }, - { - "name": "destination", - "type": "key" - }, - { - "command": "DB", - "name": "destination-db", - "type": "integer", - "optional": true - }, - { - "name": "replace", - "type": "enum", - "enum": [ - "REPLACE" - ], - "optional": true - } - ], - "group": "generic" - }, - "DBSIZE": { - "summary": "Return the number of keys in the selected database", - "since": "1.0.0", - "group": "server" - }, - "DEBUG OBJECT": { - "summary": "Get debugging information about a key", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "server" - }, - "DEBUG SEGFAULT": { - "summary": "Make the server crash", - "since": "1.0.0", - "group": "server" - }, - "DECR": { - "summary": "Decrement the integer value of a key by one", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "string" - }, - "DECRBY": { - "summary": "Decrement the integer value of a key by the given number", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "decrement", - "type": "integer" - } - ], - "since": "1.0.0", - "group": "string" - }, - "DEL": { - "summary": "Delete a key", - "complexity": "O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "generic" - }, - "DISCARD": { - "summary": "Discard all commands issued after MULTI", - "since": "2.0.0", - "group": "transactions" - }, - "DUMP": { - "summary": "Return a serialized version of the value stored at the specified key.", - "complexity": "O(1) to access the key and additional O(N*M) to serialize it, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1).", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.6.0", - "group": "generic" - }, - "ECHO": { - "summary": "Echo the given string", - "arguments": [ - { - "name": "message", - "type": "string" - } - ], - "since": "1.0.0", - "group": "connection" - }, - "EVAL": { - "summary": "Execute a Lua script server side", - "complexity": "Depends on the script that is executed.", - "arguments": [ - { - "name": "script", - "type": "string" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "optional": true, - "multiple": true - }, - { - "name": "arg", - "type": "string", - "optional": true, - "multiple": true - } - ], - "since": "2.6.0", - "group": "scripting" - }, - "EVAL_RO": { - "summary": "Execute a read-only Lua script server side", - "complexity": "Depends on the script that is executed.", - "arguments": [ - { - "name": "script", - "type": "string" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "arg", - "type": "string", - "multiple": true - } - ], - "since": "7.0.0", - "group": "scripting" - }, - "EVALSHA": { - "summary": "Execute a Lua script server side", - "complexity": "Depends on the script that is executed.", - "arguments": [ - { - "name": "sha1", - "type": "string" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "optional": true, - "multiple": true - }, - { - "name": "arg", - "type": "string", - "optional": true, - "multiple": true - } - ], - "since": "2.6.0", - "group": "scripting" - }, - "EVALSHA_RO": { - "summary": "Execute a read-only Lua script server side", - "complexity": "Depends on the script that is executed.", - "arguments": [ - { - "name": "sha1", - "type": "string" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "arg", - "type": "string", - "multiple": true - } - ], - "since": "7.0.0", - "group": "scripting" - }, - "EXEC": { - "summary": "Execute all commands issued after MULTI", - "since": "1.2.0", - "group": "transactions" - }, - "EXISTS": { - "summary": "Determine if a key exists", - "complexity": "O(N) where N is the number of keys to check.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "generic" - }, - "EXPIRE": { - "summary": "Set a key's time to live in seconds", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "seconds", - "type": "integer" - }, - { - "name": "condition", - "type": "enum", - "enum": [ - "NX", - "XX", - "GT", - "LT" - ], - "optional": true - } - ], - "since": "1.0.0", - "group": "generic" - }, - "EXPIREAT": { - "summary": "Set the expiration for a key as a UNIX timestamp", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "timestamp", - "type": "posix time" - }, - { - "name": "condition", - "type": "enum", - "enum": [ - "NX", - "XX", - "GT", - "LT" - ], - "optional": true - } - ], - "since": "1.2.0", - "group": "generic" - }, - "EXPIRETIME": { - "summary": "Get the expiration Unix timestamp for a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "7.0.0", - "group": "generic" - }, - "FAILOVER": { - "summary": "Start a coordinated failover between this server and one of its replicas.", - "arguments": [ - { - "name": "target", - "type": "block", - "optional": true, - "block": [ - { - "command": "TO" - }, - { - "name": "host", - "type": "string" - }, - { - "name": "port", - "type": "integer" - }, - { - "command": "FORCE", - "optional": true - } - ] - }, - { - "command": "ABORT", - "optional": true - }, - { - "command": "TIMEOUT", - "name": "milliseconds", - "type": "integer", - "optional": true - } - ], - "since": "6.2.0", - "group": "server" - }, - "FLUSHALL": { - "summary": "Remove all keys from all databases", - "complexity": "O(N) where N is the total number of keys in all databases", - "arguments": [ - { - "name": "async", - "type": "enum", - "enum": [ - "ASYNC", - "SYNC" - ], - "optional": true - } - ], - "since": "1.0.0", - "group": "server" - }, - "FLUSHDB": { - "summary": "Remove all keys from the current database", - "complexity": "O(N) where N is the number of keys in the selected database", - "arguments": [ - { - "name": "async", - "type": "enum", - "enum": [ - "ASYNC", - "SYNC" - ], - "optional": true - } - ], - "since": "1.0.0", - "group": "server" - }, - "GEOADD": { - "summary": "Add one or more geospatial items in the geospatial index represented using a sorted set", - "complexity": "O(log(N)) for each item added, where N is the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "condition", - "type": "enum", - "enum": [ - "NX", - "XX" - ], - "optional": true - }, - { - "name": "change", - "type": "enum", - "enum": [ - "CH" - ], - "optional": true - }, - { - "name": [ - "longitude", - "latitude", - "member" - ], - "type": [ - "double", - "double", - "string" - ], - "multiple": true - } - ], - "since": "3.2.0", - "group": "geo" - }, - "GEOHASH": { - "summary": "Returns members of a geospatial index as standard geohash strings", - "complexity": "O(log(N)) for each member requested, where N is the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string", - "multiple": true - } - ], - "since": "3.2.0", - "group": "geo" - }, - "GEOPOS": { - "summary": "Returns longitude and latitude of members of a geospatial index", - "complexity": "O(N) where N is the number of members requested.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string", - "multiple": true - } - ], - "since": "3.2.0", - "group": "geo" - }, - "GEODIST": { - "summary": "Returns the distance between two members of a geospatial index", - "complexity": "O(log(N))", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member1", - "type": "string" - }, - { - "name": "member2", - "type": "string" - }, - { - "name": "unit", - "type": "enum", - "enum": [ - "m", - "km", - "ft", - "mi" - ], - "optional": true - } - ], - "since": "3.2.0", - "group": "geo" - }, - "GEORADIUS": { - "summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point", - "complexity": "O(N+log(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "longitude", - "type": "double" - }, - { - "name": "latitude", - "type": "double" - }, - { - "name": "radius", - "type": "double" - }, - { - "name": "unit", - "type": "enum", - "enum": [ - "m", - "km", - "ft", - "mi" - ] - }, - { - "name": "withcoord", - "type": "enum", - "enum": [ - "WITHCOORD" - ], - "optional": true - }, - { - "name": "withdist", - "type": "enum", - "enum": [ - "WITHDIST" - ], - "optional": true - }, - { - "name": "withhash", - "type": "enum", - "enum": [ - "WITHHASH" - ], - "optional": true - }, - { - "type": "block", - "name": "count", - "block": [ - { - "name": "count", - "command": "COUNT", - "type": "integer" - }, - { - "name": "any", - "type": "enum", - "enum": [ - "ANY" - ], - "optional": true - } - ], - "optional": true - }, - { - "name": "order", - "type": "enum", - "enum": [ - "ASC", - "DESC" - ], - "optional": true - }, - { - "command": "STORE", - "name": "key", - "type": "key", - "optional": true - }, - { - "command": "STOREDIST", - "name": "key", - "type": "key", - "optional": true - } - ], - "since": "3.2.0", - "group": "geo" - }, - "GEORADIUSBYMEMBER": { - "summary": "Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member", - "complexity": "O(N+log(M)) where N is the number of elements inside the bounding box of the circular area delimited by center and radius and M is the number of items inside the index.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string" - }, - { - "name": "radius", - "type": "double" - }, - { - "name": "unit", - "type": "enum", - "enum": [ - "m", - "km", - "ft", - "mi" - ] - }, - { - "name": "withcoord", - "type": "enum", - "enum": [ - "WITHCOORD" - ], - "optional": true - }, - { - "name": "withdist", - "type": "enum", - "enum": [ - "WITHDIST" - ], - "optional": true - }, - { - "name": "withhash", - "type": "enum", - "enum": [ - "WITHHASH" - ], - "optional": true - }, - { - "type": "block", - "name": "count", - "block": [ - { - "name": "count", - "command": "COUNT", - "type": "integer" - }, - { - "name": "any", - "type": "enum", - "enum": [ - "ANY" - ], - "optional": true - } - ], - "optional": true - }, - { - "name": "order", - "type": "enum", - "enum": [ - "ASC", - "DESC" - ], - "optional": true - }, - { - "command": "STORE", - "name": "key", - "type": "key", - "optional": true - }, - { - "command": "STOREDIST", - "name": "key", - "type": "key", - "optional": true - } - ], - "since": "3.2.0", - "group": "geo" - }, - "GEOSEARCH": { - "summary": "Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle.", - "complexity": "O(N+log(M)) where N is the number of elements in the grid-aligned bounding box area around the shape provided as the filter and M is the number of items inside the shape", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "command": "FROMMEMBER", - "name": "member", - "type": "string", - "optional": true - }, - { - "command": "FROMLONLAT", - "name": [ - "longitude", - "latitude" - ], - "type": [ - "double", - "double" - ], - "optional": true - }, - { - "type": "block", - "name": "circle", - "block": [ - { - "name": "radius", - "command": "BYRADIUS", - "type": "double" - }, - { - "name": "unit", - "type": "enum", - "enum": [ - "m", - "km", - "ft", - "mi" - ] - } - ], - "optional": true - }, - { - "type": "block", - "name": "box", - "block": [ - { - "name": "width", - "command": "BYBOX", - "type": "double" - }, - { - "name": "height", - "type": "double" - }, - { - "name": "unit", - "type": "enum", - "enum": [ - "m", - "km", - "ft", - "mi" - ] - } - ], - "optional": true - }, - { - "name": "order", - "type": "enum", - "enum": [ - "ASC", - "DESC" - ], - "optional": true - }, - { - "type": "block", - "name": "count", - "block": [ - { - "name": "count", - "command": "COUNT", - "type": "integer" - }, - { - "name": "any", - "type": "enum", - "enum": [ - "ANY" - ], - "optional": true - } - ], - "optional": true - }, - { - "name": "withcoord", - "type": "enum", - "enum": [ - "WITHCOORD" - ], - "optional": true - }, - { - "name": "withdist", - "type": "enum", - "enum": [ - "WITHDIST" - ], - "optional": true - }, - { - "name": "withhash", - "type": "enum", - "enum": [ - "WITHHASH" - ], - "optional": true - } - ], - "since": "6.2", - "group": "geo" - }, - "GEOSEARCHSTORE": { - "summary": "Query a sorted set representing a geospatial index to fetch members inside an area of a box or a circle, and store the result in another key.", - "complexity": "O(N+log(M)) where N is the number of elements in the grid-aligned bounding box area around the shape provided as the filter and M is the number of items inside the shape", - "arguments": [ - { - "name": "destination", - "type": "key" - }, - { - "name": "source", - "type": "key" - }, - { - "command": "FROMMEMBER", - "name": "member", - "type": "string", - "optional": true - }, - { - "command": "FROMLONLAT", - "name": [ - "longitude", - "latitude" - ], - "type": [ - "double", - "double" - ], - "optional": true - }, - { - "type": "block", - "name": "circle", - "block": [ - { - "name": "radius", - "command": "BYRADIUS", - "type": "double" - }, - { - "name": "unit", - "type": "enum", - "enum": [ - "m", - "km", - "ft", - "mi" - ] - } - ], - "optional": true - }, - { - "type": "block", - "name": "box", - "block": [ - { - "name": "width", - "command": "BYBOX", - "type": "double" - }, - { - "name": "height", - "type": "double" - }, - { - "name": "unit", - "type": "enum", - "enum": [ - "m", - "km", - "ft", - "mi" - ] - } - ], - "optional": true - }, - { - "name": "order", - "type": "enum", - "enum": [ - "ASC", - "DESC" - ], - "optional": true - }, - { - "type": "block", - "name": "count", - "block": [ - { - "name": "count", - "command": "COUNT", - "type": "integer" - }, - { - "name": "any", - "type": "enum", - "enum": [ - "ANY" - ], - "optional": true - } - ], - "optional": true - }, - { - "name": "storedist", - "type": "enum", - "enum": [ - "STOREDIST" - ], - "optional": true - } - ], - "since": "6.2", - "group": "geo" - }, - "GET": { - "summary": "Get the value of a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "string" - }, - "GETBIT": { - "summary": "Returns the bit value at offset in the string value stored at key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "offset", - "type": "integer" - } - ], - "since": "2.2.0", - "group": "bitmap" - }, - "GETDEL": { - "summary":"Get the value of a key and delete the key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "6.2.0", - "group": "string" - }, - "GETEX": { - "summary": "Get the value of a key and optionally set its expiration", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "expiration", - "type": "enum", - "enum": [ - "EX seconds", - "PX milliseconds", - "EXAT timestamp", - "PXAT milliseconds-timestamp", - "PERSIST" - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "string" - }, - "GETRANGE": { - "summary": "Get a substring of the string stored at a key", - "complexity": "O(N) where N is the length of the returned string. The complexity is ultimately determined by the returned length, but because creating a substring from an existing string is very cheap, it can be considered O(1) for small strings.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "start", - "type": "integer" - }, - { - "name": "end", - "type": "integer" - } - ], - "since": "2.4.0", - "group": "string" - }, - "GETSET": { - "summary": "Set the string value of a key and return its old value", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "1.0.0", - "group": "string" - }, - "HDEL": { - "summary": "Delete one or more hash fields", - "complexity": "O(N) where N is the number of fields to be removed.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string", - "multiple": true - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HELLO": { - "summary": "Handshake with Redis", - "complexity": "O(1)", - "arguments": [ - { - "name": "arguments", - "type": "block", - "block": [ - { - "name": "protover", - "type": "integer" - }, - { - "command": "AUTH", - "name": [ - "username", - "password" - ], - "type": [ - "string", - "string" - ], - "optional": true - }, - { - "command": "SETNAME", - "name": "clientname", - "type": "string", - "optional": true - } - ], - "optional": true - } - ], - "since": "6.0.0", - "group": "connection" - }, - "HEXISTS": { - "summary": "Determine if a hash field exists", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HGET": { - "summary": "Get the value of a hash field", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HGETALL": { - "summary": "Get all the fields and values in a hash", - "complexity": "O(N) where N is the size of the hash.", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HINCRBY": { - "summary": "Increment the integer value of a hash field by the given number", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string" - }, - { - "name": "increment", - "type": "integer" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HINCRBYFLOAT": { - "summary": "Increment the float value of a hash field by the given amount", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string" - }, - { - "name": "increment", - "type": "double" - } - ], - "since": "2.6.0", - "group": "hash" - }, - "HKEYS": { - "summary": "Get all the fields in a hash", - "complexity": "O(N) where N is the size of the hash.", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HLEN": { - "summary": "Get the number of fields in a hash", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HMGET": { - "summary": "Get the values of all the given hash fields", - "complexity": "O(N) where N is the number of fields being requested.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string", - "multiple": true - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HMSET": { - "summary": "Set multiple hash fields to multiple values", - "complexity": "O(N) where N is the number of fields being set.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": [ - "field", - "value" - ], - "type": [ - "string", - "string" - ], - "multiple": true - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HSET": { - "summary": "Set the string value of a hash field", - "complexity": "O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": [ - "field", - "value" - ], - "type": [ - "string", - "string" - ], - "multiple": true - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HSETNX": { - "summary": "Set the value of a hash field, only if the field does not exist", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "HRANDFIELD": { - "summary": "Get one or multiple random fields from a hash", - "complexity": "O(N) where N is the number of fields returned", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "options", - "type": "block", - "block": [ - { - "name": "count", - "type": "integer" - }, - { - "name": "withvalues", - "type": "enum", - "enum": [ - "WITHVALUES" - ], - "optional": true - } - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "hash" - }, - "HSTRLEN": { - "summary": "Get the length of the value of a hash field", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "field", - "type": "string" - } - ], - "since": "3.2.0", - "group": "hash" - }, - "HVALS": { - "summary": "Get all the values in a hash", - "complexity": "O(N) where N is the size of the hash.", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.0.0", - "group": "hash" - }, - "INCR": { - "summary": "Increment the integer value of a key by one", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "string" - }, - "INCRBY": { - "summary": "Increment the integer value of a key by the given amount", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "increment", - "type": "integer" - } - ], - "since": "1.0.0", - "group": "string" - }, - "INCRBYFLOAT": { - "summary": "Increment the float value of a key by the given amount", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "increment", - "type": "double" - } - ], - "since": "2.6.0", - "group": "string" - }, - "INFO": { - "summary": "Get information and statistics about the server", - "arguments": [ - { - "name": "section", - "type": "string", - "optional": true - } - ], - "since": "1.0.0", - "group": "server" - }, - "LOLWUT": { - "summary": "Display some computer art and the Redis version", - "arguments": [ - { - "command": "VERSION", - "name": "version", - "type": "integer", - "optional": true - } - ], - "since": "5.0.0", - "group": "server" - }, - "KEYS": { - "summary": "Find all keys matching the given pattern", - "complexity": "O(N) with N being the number of keys in the database, under the assumption that the key names in the database and the given pattern have limited length.", - "arguments": [ - { - "name": "pattern", - "type": "pattern" - } - ], - "since": "1.0.0", - "group": "generic" - }, - "LASTSAVE": { - "summary": "Get the UNIX time stamp of the last successful save to disk", - "since": "1.0.0", - "group": "server" - }, - "LINDEX": { - "summary": "Get an element from a list by its index", - "complexity": "O(N) where N is the number of elements to traverse to get to the element at index. This makes asking for the first or the last element of the list O(1).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "index", - "type": "integer" - } - ], - "since": "1.0.0", - "group": "list" - }, - "LINSERT": { - "summary": "Insert an element before or after another element in a list", - "complexity": "O(N) where N is the number of elements to traverse before seeing the value pivot. This means that inserting somewhere on the left end on the list (head) can be considered O(1) and inserting somewhere on the right end (tail) is O(N).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "where", - "type": "enum", - "enum": [ - "BEFORE", - "AFTER" - ] - }, - { - "name": "pivot", - "type": "string" - }, - { - "name": "element", - "type": "string" - } - ], - "since": "2.2.0", - "group": "list" - }, - "LLEN": { - "summary": "Get the length of a list", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "list" - }, - "LPOP": { - "summary": "Remove and get the first elements in a list", - "complexity": "O(N) where N is the number of elements returned", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "1.0.0", - "group": "list" - }, - "LPOS": { - "summary": "Return the index of matching elements on a list", - "complexity": "O(N) where N is the number of elements in the list, for the average case. When searching for elements near the head or the tail of the list, or when the MAXLEN option is provided, the command may run in constant time.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "element", - "type": "string" - }, - { - "command": "RANK", - "name": "rank", - "type": "integer", - "optional": true - }, - { - "command": "COUNT", - "name": "num-matches", - "type": "integer", - "optional": true - }, - { - "command": "MAXLEN", - "name": "len", - "type": "integer", - "optional": true - } - ], - "since": "6.0.6", - "group": "list" - }, - "LPUSH": { - "summary": "Prepend one or multiple elements to a list", - "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "element", - "type": "string", - "multiple": true - } - ], - "since": "1.0.0", - "group": "list" - }, - "LPUSHX": { - "summary": "Prepend an element to a list, only if the list exists", - "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "element", - "type": "string", - "multiple": true - } - ], - "since": "2.2.0", - "group": "list" - }, - "LRANGE": { - "summary": "Get a range of elements from a list", - "complexity": "O(S+N) where S is the distance of start offset from HEAD for small lists, from nearest end (HEAD or TAIL) for large lists; and N is the number of elements in the specified range.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "start", - "type": "integer" - }, - { - "name": "stop", - "type": "integer" - } - ], - "since": "1.0.0", - "group": "list" - }, - "LREM": { - "summary": "Remove elements from a list", - "complexity": "O(N+M) where N is the length of the list and M is the number of elements removed.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "count", - "type": "integer" - }, - { - "name": "element", - "type": "string" - } - ], - "since": "1.0.0", - "group": "list" - }, - "LSET": { - "summary": "Set the value of an element in a list by its index", - "complexity": "O(N) where N is the length of the list. Setting either the first or the last element of the list is O(1).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "index", - "type": "integer" - }, - { - "name": "element", - "type": "string" - } - ], - "since": "1.0.0", - "group": "list" - }, - "LTRIM": { - "summary": "Trim a list to the specified range", - "complexity": "O(N) where N is the number of elements to be removed by the operation.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "start", - "type": "integer" - }, - { - "name": "stop", - "type": "integer" - } - ], - "since": "1.0.0", - "group": "list" - }, - "MEMORY DOCTOR": { - "summary": "Outputs memory problems report", - "since": "4.0.0", - "group": "server" - }, - "MEMORY HELP": { - "summary": "Show helpful text about the different subcommands", - "since": "4.0.0", - "group": "server" - }, - "MEMORY MALLOC-STATS": { - "summary": "Show allocator internal stats", - "since": "4.0.0", - "group": "server" - }, - "MEMORY PURGE": { - "summary": "Ask the allocator to release memory", - "since": "4.0.0", - "group": "server" - }, - "MEMORY STATS": { - "summary": "Show memory usage details", - "since": "4.0.0", - "group": "server" - }, - "MEMORY USAGE": { - "summary": "Estimate the memory usage of a key", - "complexity": "O(N) where N is the number of samples.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "command": "SAMPLES", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "4.0.0", - "group": "server" - }, - "MGET": { - "summary": "Get the values of all the given keys", - "complexity": "O(N) where N is the number of keys to retrieve.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "string" - }, - "MIGRATE": { - "summary": "Atomically transfer a key from a Redis instance to another one.", - "complexity": "This command actually executes a DUMP+DEL in the source instance, and a RESTORE in the target instance. See the pages of these commands for time complexity. Also an O(N) data transfer between the two instances is performed.", - "arguments": [ - { - "name": "host", - "type": "string" - }, - { - "name": "port", - "type": "string" - }, - { - "name": "key", - "type": "enum", - "enum": [ - "key", - "\"\"" - ] - }, - { - "name": "destination-db", - "type": "integer" - }, - { - "name": "timeout", - "type": "integer" - }, - { - "name": "copy", - "type": "enum", - "enum": [ - "COPY" - ], - "optional": true - }, - { - "name": "replace", - "type": "enum", - "enum": [ - "REPLACE" - ], - "optional": true - }, - { - "command": "AUTH", - "name": "password", - "type": "string", - "optional": true - }, - { - "command": "AUTH2", - "name": "username password", - "type": "string", - "optional": true - }, - { - "name": "key", - "command": "KEYS", - "type": "key", - "variadic": true, - "optional": true - } - ], - "since": "2.6.0", - "group": "generic" - }, - "MODULE LIST": { - "summary": "List all modules loaded by the server", - "complexity": "O(N) where N is the number of loaded modules.", - "since": "4.0.0", - "group": "server" - }, - "MODULE LOAD": { - "summary": "Load a module", - "complexity": "O(1)", - "arguments": [ - { - "name": "path", - "type": "string" - }, - { - "name": "arg", - "type": "string", - "variadic": true, - "optional": true - } - ], - "since": "4.0.0", - "group": "server" - }, - "MODULE UNLOAD": { - "summary": "Unload a module", - "complexity": "O(1)", - "arguments": [ - { - "name": "name", - "type": "string" - } - ], - "since": "4.0.0", - "group": "server" - }, - "MONITOR": { - "summary": "Listen for all requests received by the server in real time", - "since": "1.0.0", - "group": "server" - }, - "MOVE": { - "summary": "Move a key to another database", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "db", - "type": "integer" - } - ], - "since": "1.0.0", - "group": "generic" - }, - "MSET": { - "summary": "Set multiple keys to multiple values", - "complexity": "O(N) where N is the number of keys to set.", - "arguments": [ - { - "name": [ - "key", - "value" - ], - "type": [ - "key", - "string" - ], - "multiple": true - } - ], - "since": "1.0.1", - "group": "string" - }, - "MSETNX": { - "summary": "Set multiple keys to multiple values, only if none of the keys exist", - "complexity": "O(N) where N is the number of keys to set.", - "arguments": [ - { - "name": [ - "key", - "value" - ], - "type": [ - "key", - "string" - ], - "multiple": true - } - ], - "since": "1.0.1", - "group": "string" - }, - "MULTI": { - "summary": "Mark the start of a transaction block", - "since": "1.2.0", - "group": "transactions" - }, - "OBJECT": { - "summary": "Inspect the internals of Redis objects", - "complexity": "O(1) for all the currently implemented subcommands.", - "since": "2.2.3", - "group": "generic", - "arguments": [ - { - "name": "subcommand", - "type": "string" - }, - { - "name": "arguments", - "type": "string", - "optional": true, - "multiple": true - } - ] - }, - "PERSIST": { - "summary": "Remove the expiration from a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.2.0", - "group": "generic" - }, - "PEXPIRE": { - "summary": "Set a key's time to live in milliseconds", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "milliseconds", - "type": "integer" - }, - { - "name": "condition", - "type": "enum", - "enum": [ - "NX", - "XX", - "GT", - "LT" - ], - "optional": true - } - ], - "since": "2.6.0", - "group": "generic" - }, - "PEXPIREAT": { - "summary": "Set the expiration for a key as a UNIX timestamp specified in milliseconds", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "milliseconds-timestamp", - "type": "posix time" - }, - { - "name": "condition", - "type": "enum", - "enum": [ - "NX", - "XX", - "GT", - "LT" - ], - "optional": true - } - ], - "since": "2.6.0", - "group": "generic" - }, - "PEXPIRETIME": { - "summary": "Get the expiration Unix timestamp for a key in milliseconds", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "7.0.0", - "group": "generic" - }, - "PFADD": { - "summary": "Adds the specified elements to the specified HyperLogLog.", - "complexity": "O(1) to add every element.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "element", - "type": "string", - "optional": true, - "multiple": true - } - ], - "since": "2.8.9", - "group": "hyperloglog" - }, - "PFCOUNT": { - "summary": "Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s).", - "complexity": "O(1) with a very small average constant time when called with a single key. O(N) with N being the number of keys, and much bigger constant times, when called with multiple keys.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "2.8.9", - "group": "hyperloglog" - }, - "PFMERGE": { - "summary": "Merge N different HyperLogLogs into a single one.", - "complexity": "O(N) to merge N HyperLogLogs, but with high constant times.", - "arguments": [ - { - "name": "destkey", - "type": "key" - }, - { - "name": "sourcekey", - "type": "key", - "multiple": true - } - ], - "since": "2.8.9", - "group": "hyperloglog" - }, - "PING": { - "summary": "Ping the server", - "arguments": [ - { - "name": "message", - "type": "string", - "optional": true - } - ], - "since": "1.0.0", - "group": "connection" - }, - "PSETEX": { - "summary": "Set the value and expiration in milliseconds of a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "milliseconds", - "type": "integer" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "2.6.0", - "group": "string" - }, - "PSUBSCRIBE": { - "summary": "Listen for messages published to channels matching the given patterns", - "complexity": "O(N) where N is the number of patterns the client is already subscribed to.", - "arguments": [ - { - "name": [ - "pattern" - ], - "type": [ - "pattern" - ], - "multiple": true - } - ], - "since": "2.0.0", - "group": "pubsub" - }, - "PUBSUB": { - "summary": "Inspect the state of the Pub/Sub subsystem", - "complexity": "O(N) for the CHANNELS subcommand, where N is the number of active channels, and assuming constant time pattern matching (relatively short channels and patterns). O(N) for the NUMSUB subcommand, where N is the number of requested channels. O(1) for the NUMPAT subcommand.", - "arguments": [ - { - "name": "subcommand", - "type": "string" - }, - { - "name": "argument", - "type": "string", - "optional": true, - "multiple": true - } - ], - "since": "2.8.0", - "group": "pubsub" - }, - "PTTL": { - "summary": "Get the time to live for a key in milliseconds", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.6.0", - "group": "generic" - }, - "PUBLISH": { - "summary": "Post a message to a channel", - "complexity": "O(N+M) where N is the number of clients subscribed to the receiving channel and M is the total number of subscribed patterns (by any client).", - "arguments": [ - { - "name": "channel", - "type": "string" - }, - { - "name": "message", - "type": "string" - } - ], - "since": "2.0.0", - "group": "pubsub" - }, - "PUNSUBSCRIBE": { - "summary": "Stop listening for messages posted to channels matching the given patterns", - "complexity": "O(N+M) where N is the number of patterns the client is already subscribed and M is the number of total patterns subscribed in the system (by any client).", - "arguments": [ - { - "name": "pattern", - "type": "pattern", - "optional": true, - "multiple": true - } - ], - "since": "2.0.0", - "group": "pubsub" - }, - "QUIT": { - "summary": "Close the connection", - "since": "1.0.0", - "group": "connection" - }, - "RANDOMKEY": { - "summary": "Return a random key from the keyspace", - "complexity": "O(1)", - "since": "1.0.0", - "group": "generic" - }, - "READONLY": { - "summary": "Enables read queries for a connection to a cluster replica node", - "complexity": "O(1)", - "since": "3.0.0", - "group": "cluster" - }, - "READWRITE": { - "summary": "Disables read queries for a connection to a cluster replica node", - "complexity": "O(1)", - "since": "3.0.0", - "group": "cluster" - }, - "RENAME": { - "summary": "Rename a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "newkey", - "type": "key" - } - ], - "since": "1.0.0", - "group": "generic" - }, - "RENAMENX": { - "summary": "Rename a key, only if the new key does not exist", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "newkey", - "type": "key" - } - ], - "since": "1.0.0", - "group": "generic" - }, - "RESET": { - "summary": "Reset the connection", - "since": "6.2", - "group": "connection" - }, - "RESTORE": { - "summary": "Create a key using the provided serialized value, previously obtained using DUMP.", - "complexity": "O(1) to create the new key and additional O(N*M) to reconstruct the serialized value, where N is the number of Redis objects composing the value and M their average size. For small string values the time complexity is thus O(1)+O(1*M) where M is small, so simply O(1). However for sorted set values the complexity is O(N*M*log(N)) because inserting values into sorted sets is O(log(N)).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "ttl", - "type": "integer" - }, - { - "name": "serialized-value", - "type": "string" - }, - { - "name": "replace", - "type": "enum", - "enum": [ - "REPLACE" - ], - "optional": true - }, - { - "name": "absttl", - "type": "enum", - "enum": [ - "ABSTTL" - ], - "optional": true - }, - { - "command": "IDLETIME", - "name": "seconds", - "type": "integer", - "optional": true - }, - { - "command": "FREQ", - "name": "frequency", - "type": "integer", - "optional": true - } - ], - "since": "2.6.0", - "group": "generic" - }, - "ROLE": { - "summary": "Return the role of the instance in the context of replication", - "since": "2.8.12", - "group": "server" - }, - "RPOP": { - "summary": "Remove and get the last elements in a list", - "complexity": "O(N) where N is the number of elements returned", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "1.0.0", - "group": "list" - }, - "RPOPLPUSH": { - "summary": "Remove the last element in a list, prepend it to another list and return it", - "complexity": "O(1)", - "arguments": [ - { - "name": "source", - "type": "key" - }, - { - "name": "destination", - "type": "key" - } - ], - "since": "1.2.0", - "group": "list" - }, - "LMOVE": { - "summary": "Pop an element from a list, push it to another list and return it", - "complexity": "O(1)", - "arguments": [ - { - "name": "source", - "type": "key" - }, - { - "name": "destination", - "type": "key" - }, - { - "name": "wherefrom", - "type": "enum", - "enum": [ - "LEFT", - "RIGHT" - ] - }, - { - "name": "whereto", - "type": "enum", - "enum": [ - "LEFT", - "RIGHT" - ] - } - ], - "since": "6.2.0", - "group": "list" - }, - "RPUSH": { - "summary": "Append one or multiple elements to a list", - "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "element", - "type": "string", - "multiple": true - } - ], - "since": "1.0.0", - "group": "list" - }, - "RPUSHX": { - "summary": "Append an element to a list, only if the list exists", - "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "element", - "type": "string", - "multiple": true - } - ], - "since": "2.2.0", - "group": "list" - }, - "SADD": { - "summary": "Add one or more members to a set", - "complexity": "O(1) for each element added, so O(N) to add N elements when the command is called with multiple arguments.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SAVE": { - "summary": "Synchronously save the dataset to disk", - "since": "1.0.0", - "group": "server" - }, - "SCARD": { - "summary": "Get the number of members in a set", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "set" - }, - "SCRIPT DEBUG": { - "summary": "Set the debug mode for executed scripts.", - "complexity": "O(1)", - "arguments": [ - { - "name": "mode", - "type": "enum", - "enum": [ - "YES", - "SYNC", - "NO" - ] - } - ], - "since": "3.2.0", - "group": "scripting" - }, - "SCRIPT EXISTS": { - "summary": "Check existence of scripts in the script cache.", - "complexity": "O(N) with N being the number of scripts to check (so checking a single script is an O(1) operation).", - "arguments": [ - { - "name": "sha1", - "type": "string", - "multiple": true - } - ], - "since": "2.6.0", - "group": "scripting" - }, - "SCRIPT FLUSH": { - "summary": "Remove all the scripts from the script cache.", - "arguments": [ - { - "name": "async", - "type": "enum", - "enum": [ - "ASYNC", - "SYNC" - ], - "optional": true - } - ], - "complexity": "O(N) with N being the number of scripts in cache", - "since": "2.6.0", - "group": "scripting" - }, - "SCRIPT KILL": { - "summary": "Kill the script currently in execution.", - "complexity": "O(1)", - "since": "2.6.0", - "group": "scripting" - }, - "SCRIPT LOAD": { - "summary": "Load the specified Lua script into the script cache.", - "complexity": "O(N) with N being the length in bytes of the script body.", - "arguments": [ - { - "name": "script", - "type": "string" - } - ], - "since": "2.6.0", - "group": "scripting" - }, - "SDIFF": { - "summary": "Subtract multiple sets", - "complexity": "O(N) where N is the total number of elements in all given sets.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SDIFFSTORE": { - "summary": "Subtract multiple sets and store the resulting set in a key", - "complexity": "O(N) where N is the total number of elements in all given sets.", - "arguments": [ - { - "name": "destination", - "type": "key" - }, - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SELECT": { - "summary": "Change the selected database for the current connection", - "arguments": [ - { - "name": "index", - "type": "integer" - } - ], - "since": "1.0.0", - "group": "connection" - }, - "SET": { - "summary": "Set the string value of a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "value", - "type": "string" - }, - { - "name": "expiration", - "type": "enum", - "enum": [ - "EX seconds", - "PX milliseconds", - "EXAT timestamp", - "PXAT milliseconds-timestamp", - "KEEPTTL" - ], - "optional": true - }, - { - "name": "condition", - "type": "enum", - "enum": [ - "NX", - "XX" - ], - "optional": true - }, - { - "name": "get", - "type": "enum", - "enum": [ - "GET" - ], - "optional": true - } - ], - "since": "1.0.0", - "group": "string" - }, - "SETBIT": { - "summary": "Sets or clears the bit at offset in the string value stored at key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "offset", - "type": "integer" - }, - { - "name": "value", - "type": "integer" - } - ], - "since": "2.2.0", - "group": "bitmap" - }, - "SETEX": { - "summary": "Set the value and expiration of a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "seconds", - "type": "integer" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "2.0.0", - "group": "string" - }, - "SETNX": { - "summary": "Set the value of a key, only if the key does not exist", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "1.0.0", - "group": "string" - }, - "SETRANGE": { - "summary": "Overwrite part of a string at key starting at the specified offset", - "complexity": "O(1), not counting the time taken to copy the new string in place. Usually, this string is very small so the amortized complexity is O(1). Otherwise, complexity is O(M) with M being the length of the value argument.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "offset", - "type": "integer" - }, - { - "name": "value", - "type": "string" - } - ], - "since": "2.2.0", - "group": "string" - }, - "SHUTDOWN": { - "summary": "Synchronously save the dataset to disk and then shut down the server", - "arguments": [ - { - "name": "save-mode", - "type": "enum", - "enum": [ - "NOSAVE", - "SAVE" - ], - "optional": true - } - ], - "since": "1.0.0", - "group": "server" - }, - "SINTER": { - "summary": "Intersect multiple sets", - "complexity": "O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SINTERCARD": { - "summary": "Intersect multiple sets and return the cardinality of the result", - "complexity": "O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "7.0.0", - "group": "set" - }, - "SINTERSTORE": { - "summary": "Intersect multiple sets and store the resulting set in a key", - "complexity": "O(N*M) worst case where N is the cardinality of the smallest set and M is the number of sets.", - "arguments": [ - { - "name": "destination", - "type": "key" - }, - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SISMEMBER": { - "summary": "Determine if a given value is a member of a set", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string" - } - ], - "since": "1.0.0", - "group": "set" - }, - "SMISMEMBER": { - "summary": "Returns the membership associated with the given elements for a set", - "complexity": "O(N) where N is the number of elements being checked for membership", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string", - "multiple": true - } - ], - "since": "6.2.0", - "group": "set" - }, - "SLAVEOF": { - "summary": "Make the server a replica of another instance, or promote it as master. Deprecated starting with Redis 5. Use REPLICAOF instead.", - "arguments": [ - { - "name": "host", - "type": "string" - }, - { - "name": "port", - "type": "string" - } - ], - "since": "1.0.0", - "group": "server" - }, - "REPLICAOF": { - "summary": "Make the server a replica of another instance, or promote it as master.", - "arguments": [ - { - "name": "host", - "type": "string" - }, - { - "name": "port", - "type": "string" - } - ], - "since": "5.0.0", - "group": "server" - }, - "SLOWLOG": { - "summary": "Manages the Redis slow queries log", - "arguments": [ - { - "name": "subcommand", - "type": "string" - }, - { - "name": "argument", - "type": "string", - "optional": true - } - ], - "since": "2.2.12", - "group": "server" - }, - "SMEMBERS": { - "summary": "Get all the members in a set", - "complexity": "O(N) where N is the set cardinality.", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "set" - }, - "SMOVE": { - "summary": "Move a member from one set to another", - "complexity": "O(1)", - "arguments": [ - { - "name": "source", - "type": "key" - }, - { - "name": "destination", - "type": "key" - }, - { - "name": "member", - "type": "string" - } - ], - "since": "1.0.0", - "group": "set" - }, - "SORT": { - "summary": "Sort the elements in a list, set or sorted set", - "complexity": "O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is O(N).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "command": "BY", - "name": "pattern", - "type": "pattern", - "optional": true - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - }, - { - "command": "GET", - "name": "pattern", - "type": "string", - "optional": true, - "multiple": true - }, - { - "name": "order", - "type": "enum", - "enum": [ - "ASC", - "DESC" - ], - "optional": true - }, - { - "name": "sorting", - "type": "enum", - "enum": [ - "ALPHA" - ], - "optional": true - }, - { - "command": "STORE", - "name": "destination", - "type": "key", - "optional": true - } - ], - "since": "1.0.0", - "group": "generic" - }, - "SORT_RO": { - "summary": "Sort the elements in a list, set or sorted set. Read-only variant of SORT.", - "complexity": "O(N+M*log(M)) where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is O(N).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "command": "BY", - "name": "pattern", - "type": "pattern", - "optional": true - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - }, - { - "command": "GET", - "name": "pattern", - "type": "string", - "optional": true, - "multiple": true - }, - { - "name": "order", - "type": "enum", - "enum": [ - "ASC", - "DESC" - ], - "optional": true - }, - { - "name": "sorting", - "type": "enum", - "enum": [ - "ALPHA" - ], - "optional": true - } - ], - "since": "7.0.0", - "group": "generic" - }, - "SPOP": { - "summary": "Remove and return one or multiple random members from a set", - "complexity": "Without the count argument O(1), otherwise O(N) where N is the value of the passed count.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SRANDMEMBER": { - "summary": "Get one or multiple random members from a set", - "complexity": "Without the count argument O(1), otherwise O(N) where N is the absolute value of the passed count.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SREM": { - "summary": "Remove one or more members from a set", - "complexity": "O(N) where N is the number of members to be removed.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "STRALGO": { - "summary": "Run algorithms (currently LCS) against strings", - "complexity": "For LCS O(strlen(s1)*strlen(s2))", - "arguments": [ - { - "name": "algorithm", - "type": "enum", - "enum": [ - "LCS" - ] - }, - { - "name": "algo-specific-argument", - "type": "string", - "multiple": true - } - ], - "since": "6.0.0", - "group": "string" - }, - "STRLEN": { - "summary": "Get the length of the value stored in a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "2.2.0", - "group": "string" - }, - "SUBSCRIBE": { - "summary": "Listen for messages published to the given channels", - "complexity": "O(N) where N is the number of channels to subscribe to.", - "arguments": [ - { - "name": "channel", - "type": "string", - "multiple": true - } - ], - "since": "2.0.0", - "group": "pubsub" - }, - "SUNION": { - "summary": "Add multiple sets", - "complexity": "O(N) where N is the total number of elements in all given sets.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SUNIONSTORE": { - "summary": "Add multiple sets and store the resulting set in a key", - "complexity": "O(N) where N is the total number of elements in all given sets.", - "arguments": [ - { - "name": "destination", - "type": "key" - }, - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "1.0.0", - "group": "set" - }, - "SWAPDB": { - "summary": "Swaps two Redis databases", - "complexity": "O(N) where N is the count of clients watching or blocking on keys from both databases.", - "arguments": [ - { - "name": "index1", - "type": "integer" - }, - { - "name": "index2", - "type": "integer" - } - ], - "since": "4.0.0", - "group": "server" - }, - "SYNC": { - "summary": "Internal command used for replication", - "since": "1.0.0", - "group": "server" - }, - "PSYNC": { - "summary": "Internal command used for replication", - "arguments": [ - { - "name": "replicationid", - "type": "integer" - }, - { - "name": "offset", - "type": "integer" - } - ], - "since": "2.8.0", - "group": "server" - }, - "TIME": { - "summary": "Return the current server time", - "complexity": "O(1)", - "since": "2.6.0", - "group": "server" - }, - "TOUCH": { - "summary": "Alters the last access time of a key(s). Returns the number of existing keys specified.", - "complexity": "O(N) where N is the number of keys that will be touched.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "3.2.1", - "group": "generic" - }, - "TTL": { - "summary": "Get the time to live for a key in seconds", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "generic" - }, - "TYPE": { - "summary": "Determine the type stored at key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.0.0", - "group": "generic" - }, - "UNSUBSCRIBE": { - "summary": "Stop listening for messages posted to the given channels", - "complexity": "O(N) where N is the number of clients already subscribed to a channel.", - "arguments": [ - { - "name": "channel", - "type": "string", - "optional": true, - "multiple": true - } - ], - "since": "2.0.0", - "group": "pubsub" - }, - "UNLINK": { - "summary": "Delete a key asynchronously in another thread. Otherwise it is just as DEL, but non blocking.", - "complexity": "O(1) for each key removed regardless of its size. Then the command does O(N) work in a different thread in order to reclaim memory, where N is the number of allocations the deleted objects where composed of.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "4.0.0", - "group": "generic" - }, - "UNWATCH": { - "summary": "Forget about all watched keys", - "complexity": "O(1)", - "since": "2.2.0", - "group": "transactions" - }, - "WAIT": { - "summary": "Wait for the synchronous replication of all the write commands sent in the context of the current connection", - "complexity": "O(1)", - "arguments": [ - { - "name": "numreplicas", - "type": "integer" - }, - { - "name": "timeout", - "type": "integer" - } - ], - "since": "3.0.0", - "group": "generic" - }, - "WATCH": { - "summary": "Watch the given keys to determine execution of the MULTI/EXEC block", - "complexity": "O(1) for every key.", - "arguments": [ - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "2.2.0", - "group": "transactions" - }, - "ZADD": { - "summary": "Add one or more members to a sorted set, or update its score if it already exists", - "complexity": "O(log(N)) for each item added, where N is the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "condition", - "type": "enum", - "enum": [ - "NX", - "XX" - ], - "optional": true - }, - { - "name": "comparison", - "type": "enum", - "enum": [ - "GT", - "LT" - ], - "optional": true - }, - { - "name": "change", - "type": "enum", - "enum": [ - "CH" - ], - "optional": true - }, - { - "name": "increment", - "type": "enum", - "enum": [ - "INCR" - ], - "optional": true - }, - { - "name": [ - "score", - "member" - ], - "type": [ - "double", - "string" - ], - "multiple": true - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZCARD": { - "summary": "Get the number of members in a sorted set", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZCOUNT": { - "summary": "Count the members in a sorted set with scores within the given values", - "complexity": "O(log(N)) with N being the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "min", - "type": "double" - }, - { - "name": "max", - "type": "double" - } - ], - "since": "2.0.0", - "group": "sorted_set" - }, - "ZDIFF": { - "summary": "Subtract multiple sorted sets", - "complexity": "O(L + (N-K)log(N)) worst case where L is the total number of elements in all the sets, N is the size of the first set, and K is the size of the result set.", - "arguments": [ - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "sorted_set" - }, - "ZDIFFSTORE": { - "summary": "Subtract multiple sorted sets and store the resulting sorted set in a new key", - "complexity": "O(L + (N-K)log(N)) worst case where L is the total number of elements in all the sets, N is the size of the first set, and K is the size of the result set.", - "arguments": [ - { - "name": "destination", - "type": "key" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "6.2.0", - "group": "sorted_set" - }, - "ZINCRBY": { - "summary": "Increment the score of a member in a sorted set", - "complexity": "O(log(N)) where N is the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "increment", - "type": "integer" - }, - { - "name": "member", - "type": "string" - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZINTER": { - "summary": "Intersect multiple sorted sets", - "complexity": "O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.", - "arguments": [ - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "command": "WEIGHTS", - "name": "weight", - "type": "integer", - "variadic": true, - "optional": true - }, - { - "command": "AGGREGATE", - "name": "aggregate", - "type": "enum", - "enum": [ - "SUM", - "MIN", - "MAX" - ], - "optional": true - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "sorted_set" - }, - "ZINTERCARD": { - "summary": "Intersect multiple sorted sets and return the cardinality of the result", - "complexity": "O(N*K) worst case with N being the smallest input sorted set, K being the number of input sorted sets.", - "arguments": [ - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - } - ], - "since": "7.0.0", - "group": "sorted_set" - }, - "ZINTERSTORE": { - "summary": "Intersect multiple sorted sets and store the resulting sorted set in a new key", - "complexity": "O(N*K)+O(M*log(M)) worst case with N being the smallest input sorted set, K being the number of input sorted sets and M being the number of elements in the resulting sorted set.", - "arguments": [ - { - "name": "destination", - "type": "key" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "command": "WEIGHTS", - "name": "weight", - "type": "integer", - "variadic": true, - "optional": true - }, - { - "command": "AGGREGATE", - "name": "aggregate", - "type": "enum", - "enum": [ - "SUM", - "MIN", - "MAX" - ], - "optional": true - } - ], - "since": "2.0.0", - "group": "sorted_set" - }, - "ZLEXCOUNT": { - "summary": "Count the number of members in a sorted set between a given lexicographical range", - "complexity": "O(log(N)) with N being the number of elements in the sorted set.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "min", - "type": "string" - }, - { - "name": "max", - "type": "string" - } - ], - "since": "2.8.9", - "group": "sorted_set" - }, - "ZPOPMAX": { - "summary": "Remove and return members with the highest scores in a sorted set", - "complexity": "O(log(N)*M) with N being the number of elements in the sorted set, and M being the number of elements popped.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "5.0.0", - "group": "sorted_set" - }, - "ZPOPMIN": { - "summary": "Remove and return members with the lowest scores in a sorted set", - "complexity": "O(log(N)*M) with N being the number of elements in the sorted set, and M being the number of elements popped.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "5.0.0", - "group": "sorted_set" - }, - "ZRANDMEMBER": { - "summary": "Get one or multiple random elements from a sorted set", - "complexity": "O(N) where N is the number of elements returned", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "options", - "type": "block", - "block": [ - { - "name": "count", - "type": "integer" - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - } - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "sorted_set" - }, - "ZRANGESTORE": { - "summary": "Store a range of members from sorted set into another key", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements stored into the destination key.", - "arguments": [ - { - "name": "dst", - "type": "key" - }, - { - "name": "src", - "type": "key" - }, - { - "name": "min", - "type": "string" - }, - { - "name": "max", - "type": "string" - }, - { - "name": "sortby", - "type": "enum", - "enum": [ - "BYSCORE", - "BYLEX" - ], - "optional": true - }, - { - "name": "rev", - "type": "enum", - "enum": [ - "REV" - ], - "optional": true - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "sorted_set" - }, - "ZRANGE": { - "summary": "Return a range of members in a sorted set", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "min", - "type": "string" - }, - { - "name": "max", - "type": "string" - }, - { - "name": "sortby", - "type": "enum", - "enum": [ - "BYSCORE", - "BYLEX" - ], - "optional": true - }, - { - "name": "rev", - "type": "enum", - "enum": [ - "REV" - ], - "optional": true - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZRANGEBYLEX": { - "summary": "Return a range of members in a sorted set, by lexicographical range", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "min", - "type": "string" - }, - { - "name": "max", - "type": "string" - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - } - ], - "since": "2.8.9", - "group": "sorted_set" - }, - "ZREVRANGEBYLEX": { - "summary": "Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings.", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "max", - "type": "string" - }, - { - "name": "min", - "type": "string" - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - } - ], - "since": "2.8.9", - "group": "sorted_set" - }, - "ZRANGEBYSCORE": { - "summary": "Return a range of members in a sorted set, by score", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "min", - "type": "double" - }, - { - "name": "max", - "type": "double" - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - } - ], - "since": "1.0.5", - "group": "sorted_set" - }, - "ZRANK": { - "summary": "Determine the index of a member in a sorted set", - "complexity": "O(log(N))", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string" - } - ], - "since": "2.0.0", - "group": "sorted_set" - }, - "ZREM": { - "summary": "Remove one or more members from a sorted set", - "complexity": "O(M*log(N)) with N being the number of elements in the sorted set and M the number of elements to be removed.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string", - "multiple": true - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZREMRANGEBYLEX": { - "summary": "Remove all members in a sorted set between the given lexicographical range", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "min", - "type": "string" - }, - { - "name": "max", - "type": "string" - } - ], - "since": "2.8.9", - "group": "sorted_set" - }, - "ZREMRANGEBYRANK": { - "summary": "Remove all members in a sorted set within the given indexes", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "start", - "type": "integer" - }, - { - "name": "stop", - "type": "integer" - } - ], - "since": "2.0.0", - "group": "sorted_set" - }, - "ZREMRANGEBYSCORE": { - "summary": "Remove all members in a sorted set within the given scores", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements removed by the operation.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "min", - "type": "double" - }, - { - "name": "max", - "type": "double" - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZREVRANGE": { - "summary": "Return a range of members in a sorted set, by index, with scores ordered from high to low", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements returned.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "start", - "type": "integer" - }, - { - "name": "stop", - "type": "integer" - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZREVRANGEBYSCORE": { - "summary": "Return a range of members in a sorted set, by score, with scores ordered from high to low", - "complexity": "O(log(N)+M) with N being the number of elements in the sorted set and M the number of elements being returned. If M is constant (e.g. always asking for the first 10 elements with LIMIT), you can consider it O(log(N)).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "max", - "type": "double" - }, - { - "name": "min", - "type": "double" - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - }, - { - "command": "LIMIT", - "name": [ - "offset", - "count" - ], - "type": [ - "integer", - "integer" - ], - "optional": true - } - ], - "since": "2.2.0", - "group": "sorted_set" - }, - "ZREVRANK": { - "summary": "Determine the index of a member in a sorted set, with scores ordered from high to low", - "complexity": "O(log(N))", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string" - } - ], - "since": "2.0.0", - "group": "sorted_set" - }, - "ZSCORE": { - "summary": "Get the score associated with the given member in a sorted set", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string" - } - ], - "since": "1.2.0", - "group": "sorted_set" - }, - "ZUNION": { - "summary": "Add multiple sorted sets", - "complexity": "O(N)+O(M*log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.", - "arguments": [ - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "command": "WEIGHTS", - "name": "weight", - "type": "integer", - "variadic": true, - "optional": true - }, - { - "command": "AGGREGATE", - "name": "aggregate", - "type": "enum", - "enum": [ - "SUM", - "MIN", - "MAX" - ], - "optional": true - }, - { - "name": "withscores", - "type": "enum", - "enum": [ - "WITHSCORES" - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "sorted_set" - }, - "ZMSCORE": { - "summary": "Get the score associated with the given members in a sorted set", - "complexity": "O(N) where N is the number of members being requested.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "member", - "type": "string", - "multiple": true - } - ], - "since": "6.2.0", - "group": "sorted_set" - }, - "ZUNIONSTORE": { - "summary": "Add multiple sorted sets and store the resulting sorted set in a new key", - "complexity": "O(N)+O(M log(M)) with N being the sum of the sizes of the input sorted sets, and M being the number of elements in the resulting sorted set.", - "arguments": [ - { - "name": "destination", - "type": "key" - }, - { - "name": "numkeys", - "type": "integer" - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "command": "WEIGHTS", - "name": "weight", - "type": "integer", - "variadic": true, - "optional": true - }, - { - "command": "AGGREGATE", - "name": "aggregate", - "type": "enum", - "enum": [ - "SUM", - "MIN", - "MAX" - ], - "optional": true - } - ], - "since": "2.0.0", - "group": "sorted_set" - }, - "SCAN": { - "summary": "Incrementally iterate the keys space", - "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection.", - "arguments": [ - { - "name": "cursor", - "type": "integer" - }, - { - "command": "MATCH", - "name": "pattern", - "type": "pattern", - "optional": true - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - }, - { - "command": "TYPE", - "name": "type", - "type": "string", - "optional": true - } - ], - "since": "2.8.0", - "group": "generic" - }, - "SSCAN": { - "summary": "Incrementally iterate Set elements", - "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection..", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "cursor", - "type": "integer" - }, - { - "command": "MATCH", - "name": "pattern", - "type": "pattern", - "optional": true - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "2.8.0", - "group": "set" - }, - "HSCAN": { - "summary": "Incrementally iterate hash fields and associated values", - "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection..", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "cursor", - "type": "integer" - }, - { - "command": "MATCH", - "name": "pattern", - "type": "pattern", - "optional": true - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "2.8.0", - "group": "hash" - }, - "ZSCAN": { - "summary": "Incrementally iterate sorted sets elements and associated scores", - "complexity": "O(1) for every call. O(N) for a complete iteration, including enough command calls for the cursor to return back to 0. N is the number of elements inside the collection..", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "cursor", - "type": "integer" - }, - { - "command": "MATCH", - "name": "pattern", - "type": "pattern", - "optional": true - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "2.8.0", - "group": "sorted_set" - }, - "XINFO": { - "summary": "Get information on streams and consumer groups", - "complexity": "O(N) with N being the number of returned items for the subcommands CONSUMERS and GROUPS. The STREAM subcommand is O(log N) with N being the number of items in the stream.", - "arguments": [ - { - "command": "CONSUMERS", - "name": [ - "key", - "groupname" - ], - "type": [ - "key", - "string" - ], - "optional": true - }, - { - "command": "GROUPS", - "name": "key", - "type": "key", - "optional": true - }, - { - "command": "STREAM", - "name": "key", - "type": "key", - "optional": true - }, - { - "name": "help", - "type": "enum", - "enum": [ - "HELP" - ], - "optional": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XADD": { - "summary": "Appends a new entry to a stream", - "complexity": "O(1) when adding a new entry, O(N) when trimming where N being the number of entires evicted.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "command": "NOMKSTREAM", - "optional": true - }, - { - "name": "trim", - "type": "block", - "optional": true, - "block": [ - { - "name": "strategy", - "type": "enum", - "enum": [ - "MAXLEN", - "MINID" - ] - }, - { - "name": "operator", - "type": "enum", - "enum": [ - "=", - "~" - ], - "optional": true - }, - { - "name": "threshold", - "type": "string" - }, - { - "command": "LIMIT", - "name": "count", - "type": "integer", - "optional": true - } - ] - }, - { - "type": "enum", - "enum": [ - "*", - "ID" - ] - }, - { - "name": [ - "field", - "value" - ], - "type": [ - "string", - "string" - ], - "multiple": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XTRIM": { - "summary": "Trims the stream to (approximately if '~' is passed) a certain size", - "complexity": "O(N), with N being the number of evicted entries. Constant times are very small however, since entries are organized in macro nodes containing multiple entries that can be released with a single deallocation.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "trim", - "type": "block", - "block": [ - { - "name": "strategy", - "type": "enum", - "enum": [ - "MAXLEN", - "MINID" - ] - }, - { - "name": "operator", - "type": "enum", - "enum": [ - "=", - "~" - ], - "optional": true - }, - { - "name": "threshold", - "type": "string" - }, - { - "command": "LIMIT", - "name": "count", - "type": "integer", - "optional": true - } - ] - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XDEL": { - "summary": "Removes the specified entries from the stream. Returns the number of items actually deleted, that may be different from the number of IDs passed in case certain IDs do not exist.", - "complexity": "O(1) for each single item to delete in the stream, regardless of the stream size.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "ID", - "type": "string", - "multiple": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XRANGE": { - "summary": "Return a range of elements in a stream, with IDs matching the specified IDs interval", - "complexity": "O(N) with N being the number of elements being returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "start", - "type": "string" - }, - { - "name": "end", - "type": "string" - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XREVRANGE": { - "summary": "Return a range of elements in a stream, with IDs matching the specified IDs interval, in reverse order (from greater to smaller IDs) compared to XRANGE", - "complexity": "O(N) with N being the number of elements returned. If N is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1).", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "end", - "type": "string" - }, - { - "name": "start", - "type": "string" - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XLEN": { - "summary": "Return the number of entries in a stream", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XREAD": { - "summary": "Return never seen elements in multiple streams, with IDs greater than the ones reported by the caller for each stream. Can block.", - "complexity": "For each stream mentioned: O(N) with N being the number of elements being returned, it means that XREAD-ing with a fixed COUNT is O(1). Note that when the BLOCK option is used, XADD will pay O(M) time in order to serve the M clients blocked on the stream getting new data.", - "arguments": [ - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - }, - { - "command": "BLOCK", - "name": "milliseconds", - "type": "integer", - "optional": true - }, - { - "name": "streams", - "type": "enum", - "enum": [ - "STREAMS" - ] - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "ID", - "type": "string", - "multiple": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XGROUP": { - "summary": "Create, destroy, and manage consumer groups.", - "complexity": "O(1) for all the subcommands, with the exception of the DESTROY subcommand which takes an additional O(M) time in order to delete the M entries inside the consumer group pending entries list (PEL).", - "arguments": [ - { - "name": "create", - "type": "block", - "block": [ - { - "command": "CREATE", - "name": [ - "key", - "groupname" - ], - "type": [ - "key", - "string" - ] - }, - { - "name": "id", - "type": "enum", - "enum": [ - "ID", - "$" - ] - }, - { - "command": "MKSTREAM", - "optional": true - } - ], - "optional": true - }, - { - "name": "setid", - "type": "block", - "block": [ - { - "command": "SETID", - "name": [ - "key", - "groupname" - ], - "type": [ - "key", - "string" - ] - }, - { - "name": "id", - "type": "enum", - "enum": [ - "ID", - "$" - ] - } - ], - "optional": true - }, - { - "command": "DESTROY", - "name": [ - "key", - "groupname" - ], - "type": [ - "key", - "string" - ], - "optional": true - }, - { - "command": "CREATECONSUMER", - "name": [ - "key", - "groupname", - "consumername" - ], - "type": [ - "key", - "string", - "string" - ], - "optional": true - }, - { - "command": "DELCONSUMER", - "name": [ - "key", - "groupname", - "consumername" - ], - "type": [ - "key", - "string", - "string" - ], - "optional": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XREADGROUP": { - "summary": "Return new entries from a stream using a consumer group, or access the history of the pending entries for a given consumer. Can block.", - "complexity": "For each stream mentioned: O(M) with M being the number of elements returned. If M is constant (e.g. always asking for the first 10 elements with COUNT), you can consider it O(1). On the other side when XREADGROUP blocks, XADD will pay the O(N) time in order to serve the N clients blocked on the stream getting new data.", - "arguments": [ - { - "command": "GROUP", - "name": [ - "group", - "consumer" - ], - "type": [ - "string", - "string" - ] - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - }, - { - "command": "BLOCK", - "name": "milliseconds", - "type": "integer", - "optional": true - }, - { - "name": "noack", - "type": "enum", - "enum": [ - "NOACK" - ], - "optional": true - }, - { - "name": "streams", - "type": "enum", - "enum": [ - "STREAMS" - ] - }, - { - "name": "key", - "type": "key", - "multiple": true - }, - { - "name": "ID", - "type": "string", - "multiple": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XACK": { - "summary": "Marks a pending message as correctly processed, effectively removing it from the pending entries list of the consumer group. Return value of the command is the number of messages successfully acknowledged, that is, the IDs we were actually able to resolve in the PEL.", - "complexity": "O(1) for each message ID processed.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "group", - "type": "string" - }, - { - "name": "ID", - "type": "string", - "multiple": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XCLAIM": { - "summary": "Changes (or acquires) ownership of a message in a consumer group, as if the message was delivered to the specified consumer.", - "complexity": "O(log N) with N being the number of messages in the PEL of the consumer group.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "group", - "type": "string" - }, - { - "name": "consumer", - "type": "string" - }, - { - "name": "min-idle-time", - "type": "string" - }, - { - "name": "ID", - "type": "string", - "multiple": true - }, - { - "command": "IDLE", - "name": "ms", - "type": "integer", - "optional": true - }, - { - "command": "TIME", - "name": "ms-unix-time", - "type": "integer", - "optional": true - }, - { - "command": "RETRYCOUNT", - "name": "count", - "type": "integer", - "optional": true - }, - { - "name": "force", - "enum": [ - "FORCE" - ], - "optional": true - }, - { - "name": "justid", - "enum": [ - "JUSTID" - ], - "optional": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "XAUTOCLAIM": { - "summary": "Changes (or acquires) ownership of messages in a consumer group, as if the messages were delivered to the specified consumer.", - "complexity": "O(1) if COUNT is small.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "group", - "type": "string" - }, - { - "name": "consumer", - "type": "string" - }, - { - "name": "min-idle-time", - "type": "string" - }, - { - "name": "start", - "type": "string" - }, - { - "command": "COUNT", - "name": "count", - "type": "integer", - "optional": true - }, - { - "name": "justid", - "enum": [ - "JUSTID" - ], - "optional": true - } - ], - "since": "6.2.0", - "group": "stream" - }, - "XPENDING": { - "summary": "Return information and entries from a stream consumer group pending entries list, that are messages fetched but never acknowledged.", - "complexity": "O(N) with N being the number of elements returned, so asking for a small fixed number of entries per call is O(1). O(M), where M is the total number of entries scanned when used with the IDLE filter. When the command returns just the summary and the list of consumers is small, it runs in O(1) time; otherwise, an additional O(N) time for iterating every consumer.", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "group", - "type": "string" - }, - { - "type": "block", - "name": "filters", - "block": [ - { - "command": "IDLE", - "name": "min-idle-time", - "type": "integer", - "optional": true - }, - { - "name": "start", - "type": "string" - }, - { - "name": "end", - "type": "string" - }, - { - "name": "count", - "type": "integer" - }, - { - "name": "consumer", - "type": "string", - "optional": true - } - ], - "optional": true - } - ], - "since": "5.0.0", - "group": "stream" - }, - "LATENCY DOCTOR": { - "summary": "Return a human readable latency analysis report.", - "since": "2.8.13", - "group": "server" - }, - "LATENCY GRAPH": { - "summary": "Return a latency graph for the event.", - "arguments": [ - { - "name": "event", - "type": "string" - } - ], - "since": "2.8.13", - "group": "server" - }, - "LATENCY HISTORY": { - "summary": "Return timestamp-latency samples for the event.", - "arguments": [ - { - "name": "event", - "type": "string" - } - ], - "since": "2.8.13", - "group": "server" - }, - "LATENCY LATEST": { - "summary": "Return the latest latency samples for all events.", - "since": "2.8.13", - "group": "server" - }, - "LATENCY RESET": { - "summary": "Reset latency data for one or more events.", - "arguments": [ - { - "name": "event", - "type": "string", - "optional": true, - "multiple": true - } - ], - "since": "2.8.13", - "group": "server" - }, - "LATENCY HELP": { - "summary": "Show helpful text about the different subcommands.", - "since": "2.8.13", - "group": "server" - } -} diff --git a/redisinsight/api/src/constants/commands/redijson.json b/redisinsight/api/src/constants/commands/redijson.json deleted file mode 100644 index 6d3d8466cc..0000000000 --- a/redisinsight/api/src/constants/commands/redijson.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "JSON.DEL": { - "summary": "Deletes a value", - "complexity": "O(N), where N is the size of the deleted value", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "path", - "type": "json path string", - "optional": true - } - ], - "since": "1.0.0", - "group": "json" - }, - "JSON.GET": { - "summary": "Gets the value at one or more paths in JSON serialized form", - "complexity": "O(N), where N is the size of the value", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "indent", - "type": "string", - "optional": true - }, - { - "name": "newline", - "type": "string", - "optional": true - }, - { - "name": "space", - "type": "string", - "optional": true - }, - { - "name": "escape", - "type": "enum", - "enum": [ - "NOESCAPE" - ], - "optional": true - }, - { - "name": "paths", - "type": "json path string", - "optional": true - } - ], - "since": "1.0.0", - "group": "json" - } -} diff --git a/redisinsight/api/src/constants/commands/redisai.json b/redisinsight/api/src/constants/commands/redisai.json deleted file mode 100644 index 8eb9eeafc7..0000000000 --- a/redisinsight/api/src/constants/commands/redisai.json +++ /dev/null @@ -1,420 +0,0 @@ -{ - "AI.TENSORSET": - { - "summary": "stores a tensor as the value of a key.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "type", - "type": "enum", - "enum": [ - "FLOAT" , "DOUBLE" , "INT8" , "INT16" , "INT32" , "INT64" , "UINT8", "UINT16", "STRING", "BOOL" - ] - }, - { - "name": "shape", - "type": "integer", - "multiple": true - }, - { - "name": "blob", - "command": "BLOB", - "type": "string", - "optional": true - }, - { - "name": "value", - "command": "VALUES", - "type": "string", - "multiple": true, - "optional": true - } - - ], - "since": "1.2.5", - "group": "tensor" - }, - "AI.TENSORGET": - { - "summary": "returns a tensor stored as key's value.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "meta", - "type": "enum", - "enum": [ - "META" - ] - }, - { - "name": "format", - "type": "enum", - "enum": [ - "BLOB", "VALUES" - ], - "optional": true - } - - ], - "since": "1.2.5", - "group": "tensor" - }, - "AI.MODELSETORE": - { - "summary": "stores a model as the value of a key", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "backend", - "type": "enum", - "enum":["TF", "TORCH", "ONNX"] - }, - { - "name": "device", - "type": "enum", - "enum":["CPU", "GPU"] - }, - { - "name": "tag", - "command": "TAG", - "type": "string", - "optional": true - }, - { - "name": "batchsize", - "command": "BATCHSIZE ", - "type": "integer", - "optional": true - }, - { - "name": "minbatchsize", - "command": "BATCHSIZE ", - "type": "integer", - "optional": true - }, - { - "name": "minbatchtimeout", - "command": "MINBATCHTIMEOUT ", - "type": "integer", - "optional": true - }, - { - "type": "block", - "optional": true, - "block": [ - { - "name": "input_count", - "type": "integer", - "command":"INPUTS" - }, - { - "name": "input", - "type": "string", - "multiple": true - } - - ] - }, - { - "type": "block", - "optional": true, - "block": [ - { - "name": "output_count", - "type": "integer", - "command":"OUTPUTS" - }, - { - "name": "output", - "type": "string", - "multiple": true - } - - ] - }, - { - "name": "blob", - "command": "BLOB", - "type": "string", - "optional": true - } - - ], - "since": "1.2.5", - "group": "model" - }, - "AI.MODELGET": { - "summary": "returns a model's metadata and blob stored as a key's value.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "meta", - "type": "enum", - "enum": [ - "META" - ], - "optional": true - }, - { - "name": "blob", - "type": "enum", - "enum": [ - "BLOB" - ], - "optional": true - } - ], - "since": "1.2.5", - "group": "model" - }, - "AI.MODELDEL": - { - "summary": "deletes a model stored as a key's value.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.2.5", - "group": "model" - }, - "AI.MODELEXECUTE": - { - "summary": "runs a model stored as a key's value using its specified backend and device. It accepts one or more input tensors and store output tensors.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "type": "block", - "block": [ - { - "name": "input_count", - "type": "integer", - "command":"INPUTS" - }, - { - "name": "input", - "type": "string", - "multiple": true - } - - ] - }, - { - "type": "block", - "block": [ - { - "name": "output_count", - "type": "integer", - "command":"OUTPUTS" - }, - { - "name": "output", - "type": "string", - "multiple": true - } - - ] - }, - { - "name": "timeout", - "command": "TIMEOUT", - "type": "integer", - "optional": true - } - ], - "since": "1.2.5", - "group": "inference" - }, - "AI.SCRIPTSTORE": { - "summary": "stores a TorchScript as the value of a key.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "device", - "type": "enum", - "enum":["CPU", "GPU"] - }, - { - "name": "tag", - "command": "TAG", - "type": "string", - "optional": true - }, - { - "type": "block", - "block": [ - { - "name": "entry_point_count", - "type": "integer", - "command":"ENTRY_POINTS" - }, - { - "name": "entry_point", - "type": "string", - "multiple": true - } - - ] - } - ], - "since": "1.2.5", - "group": "script" - }, - "AI.SCRIPTGET": { - "summary": "returns the TorchScript stored as a key's value.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "meta", - "type": "enum", - "enum": [ - "META" - ], - "optional": true - }, - { - "name": "source", - "type": "enum", - "enum": [ - "SOURCE" - ], - "optional": true - } - ], - "since": "1.2.5", - "group": "script" - }, - "AI.SCRIPTDEL": { - "summary": "deletes a script stored as a key's value.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - } - ], - "since": "1.2.5", - "group": "script" - }, - "AI.SCRIPTEXECUTE": - { - "summary": "command runs a script stored as a key's value on its specified device.", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "name": "function", - "type": "string" - }, - { - "type": "block", - "optional": true, - "block": [ - { - "name": "key_count", - "type": "integer", - "command":"KEYS" - }, - { - "name": "key", - "type": "string", - "multiple": true - } - - ] - }, - { - "type": "block", - "optional": true, - "block": [ - { - "name": "input_count", - "type": "integer", - "command":"INPUTS" - }, - { - "name": "input", - "type": "string", - "multiple": true - } - - ] - }, - { - "type": "block", - "optional": true, - "block": [ - { - "name": "arg_count", - "type": "integer", - "command":"ARGS" - }, - { - "name": "arg", - "type": "string", - "multiple": true - } - - ] - }, - { - "type": "block", - "optional": true, - "block": [ - { - "name": "output_count", - "type": "integer", - "command":"OUTPUTS" - }, - { - "name": "output", - "type": "string", - "multiple": true - } - - ] - }, - { - "name": "timeout", - "command": "TIMEOUT", - "type": "integer", - "optional": true - } - ], - "since": "1.2.5", - "group": "inference" - } -} diff --git a/redisinsight/api/src/constants/commands/redisearch.json b/redisinsight/api/src/constants/commands/redisearch.json deleted file mode 100644 index 9f0c53e26c..0000000000 --- a/redisinsight/api/src/constants/commands/redisearch.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "FT.CREATE": { - "summary": "Creates an index with the given spec", - "complexity": "O(1)", - "arguments": [ - { - "name": "index", - "type": "key" - } - ], - "since": "1.0.0", - "group": "search" - }, - "FT.DROPINDEX": { - "summary": "Deletes the index", - "complexity": "O(N)", - "arguments": [ - { - "name": "index", - "type": "key" - }, - { - "name": "deletedocs", - "type": "enum", - "enum": [ - "DD" - ], - "optional": true - } - ], - "since": "2.0.0", - "group": "search" - } -} diff --git a/redisinsight/api/src/constants/commands/redisgraph.json b/redisinsight/api/src/constants/commands/redisgraph.json deleted file mode 100644 index d684adce66..0000000000 --- a/redisinsight/api/src/constants/commands/redisgraph.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "GRAPH.QUERY": { - "summary": "Queries the graph", - "arguments": [ - { - "name": "graph", - "type": "key" - }, - { - "name": "query", - "type": "string" - } - ], - "since": "1.0.0", - "group": "graph" - }, - "GRAPH.EXPLAIN": { - "summary": "Produce execution plan for query", - "arguments": [ - { - "name": "graph", - "type": "key" - }, - { - "name": "query", - "type": "string" - } - ], - "since": "2.0.0", - "group": "graph" - } -} diff --git a/redisinsight/api/src/constants/commands/redistimeseries.json b/redisinsight/api/src/constants/commands/redistimeseries.json deleted file mode 100644 index cfe37dc8ae..0000000000 --- a/redisinsight/api/src/constants/commands/redistimeseries.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "TS.CREATE": { - "summary": "Create a new time-series", - "complexity": "O(1)", - "arguments": [ - { - "name": "key", - "type": "key" - }, - { - "type": "integer", - "command": "RETENTION", - "name": "retentionTime", - "optional": true - }, - { - "type": "enum", - "command": "ENCODING", - "enum": [ - "UNCOMPRESSED", - "COMPRESSED" - ], - "optional": true - }, - { - "type": "integer", - "command": "CHUNK_SIZE", - "name": "size", - "optional": true - }, - { - "type": "enum", - "command": "DUPLICATE_POLICY", - "name": "policy", - "enum": [ - "BLOCK", - "FIRST", - "LAST", - "MIN", - "MAX", - "SUM" - ], - "optional": true - }, - { - "command": "LABELS", - "name": [ - "label", - "value" - ], - "type": [ - "string", - "string" - ], - "multiple": true, - "optional": true - } - ], - "since": "1.0.0", - "group": "timeseries" - }, - "TS.ADD": { - "summary": "Append a new sample to the series. If the series has not been created yet with TS.CREATE it will be automatically created.", - "complexity": "O(M) when M is the amount of compaction rules or O(1) with no compaction", - "arguments": [{ - "name": "key", - "type": "key" - }, - { - "name": "timestamp", - "type": "integer" - }, - { - "name": "value", - "type": "double" - }, - { - "type": "integer", - "command": "RETENTION", - "name": "retentionTime", - "optional": true - }, - { - "type": "enum", - "command": "ENCODING", - "enum": [ - "UNCOMPRESSED", - "COMPRESSED" - ], - "optional": true - }, - { - "type": "integer", - "command": "CHUNK_SIZE", - "name": "size", - "optional": true - }, - { - "type": "enum", - "command": "ON_DUPLICATE", - "name": "policy", - "enum": [ - "BLOCK", - "FIRST", - "LAST", - "MIN", - "MAX", - "SUM" - ], - "optional": true - }, - { - "command": "LABELS", - "name": [ - "label", - "value" - ], - "type": [ - "string", - "string" - ], - "multiple": true, - "optional": true - } - ] - } -} diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index b2666b2b1a..266d233aa9 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -11,6 +11,7 @@ export enum TelemetryEvents { RedisInstanceDeleted = 'CONFIG_DATABASES_DATABASE_DELETED', RedisInstanceEditedByUser = 'CONFIG_DATABASES_DATABASE_EDITED_BY_USER', RedisInstanceConnectionFailed = 'DATABASE_CONNECTION_FAILED', + RedisInstanceListReceived = 'CONFIG_DATABASES_DATABASE_LIST_DISPLAYED', // Events for autodiscovery flows REClusterDiscoverySucceed = 'CONFIG_DATABASES_RE_CLUSTER_AUTODISCOVERY_SUCCEEDED', @@ -38,12 +39,12 @@ export enum TelemetryEvents { BrowserJSONPropertyDeleted = 'BROWSER_JSON_PROPERTY_DELETED', // Events for cli tool - CliClientCreated = 'CLI_CLIENT_CREATED', - CliClientCreationFailed = 'CLI_CLIENT_CREATION_FAILED', - CliClientConnectionError = 'CLI_CLIENT_CONNECTION_ERROR', - CliClientDeleted = 'CLI_CLIENT_DELETED', - CliClientRecreated = 'CLI_CLIENT_RECREATED', - CliCommandExecuted = 'CLI_COMMAND_EXECUTED', - CliClusterNodeCommandExecuted = 'CLI_CLUSTER_COMMAND_EXECUTED', - CliCommandErrorReceived = 'CLI_COMMAND_ERROR_RECEIVED', + ClientCreated = 'CLIENT_CREATED', + ClientCreationFailed = 'CLIENT_CREATION_FAILED', + ClientConnectionError = 'CLIENT_CONNECTION_ERROR', + ClientDeleted = 'CLIENT_DELETED', + ClientRecreated = 'CLIENT_RECREATED', + CommandExecuted = 'COMMAND_EXECUTED', + ClusterNodeCommandExecuted = 'CLUSTER_COMMAND_EXECUTED', + CommandErrorReceived = 'COMMAND_ERROR_RECEIVED', } diff --git a/redisinsight/api/src/dto/server.dto.ts b/redisinsight/api/src/dto/server.dto.ts index cf7d71fb01..cd297f3b1f 100644 --- a/redisinsight/api/src/dto/server.dto.ts +++ b/redisinsight/api/src/dto/server.dto.ts @@ -42,4 +42,10 @@ export class GetServerInfoResponse { example: ['PLAIN', 'KEYTAR'], }) encryptionStrategies: string[]; + + @ApiProperty({ + description: 'Server session id.', + type: Number, + }) + sessionId: number; } diff --git a/redisinsight/api/src/modules/browser/dto/keys.dto.ts b/redisinsight/api/src/modules/browser/dto/keys.dto.ts index 256c1a7f43..1eec253706 100644 --- a/redisinsight/api/src/modules/browser/dto/keys.dto.ts +++ b/redisinsight/api/src/modules/browser/dto/keys.dto.ts @@ -201,7 +201,7 @@ export class UpdateKeyTtlDto { @ApiProperty({ type: Number, description: - 'Set a timeout on key in seconds. After the timeout has expired, the key will automatically be deleted.' + 'Set a timeout on key in seconds. After the timeout has expired, the key will automatically be deleted. ' + 'If the property has value of -1, then the key timeout will be removed.', maximum: MAX_TTL_NUMBER, }) @@ -216,7 +216,7 @@ export class KeyTtlResponse { type: Number, description: 'The remaining time to live of a key that has a timeout. ' - + 'If value equals -2 then the key does not exist or has deleted.' + + 'If value equals -2 then the key does not exist or has deleted. ' + 'If value equals -1 then the key has no associated expire (No limit).', maximum: MAX_TTL_NUMBER, }) diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts index ec568c0b93..0e57810885 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts @@ -12,15 +12,11 @@ import { GetKeysWithDetailsResponse, RedisDataType, } from 'src/modules/browser/dto'; -import ERROR_MESSAGES from 'src/constants/error-messages'; +import { parseClusterCursor } from 'src/modules/browser/utils/clusterCursor'; import { ISettingsProvider } from 'src/modules/core/models/settings-provider.interface'; import { AbstractStrategy } from './abstract.strategy'; import { IGetNodeKeysResult } from '../scanner.interface'; -const NODES_SEPARATOR = '||'; -const CURSOR_SEPARATOR = '@'; -// Correct format 172.17.0.1:7001@-1||172.17.0.1:7002@33 -const CLUSTER_CURSOR_REGEX = /^(([a-z0-9.])+:[0-9]+(@-?\d+))+((\|\|)?([a-z0-9.])+:[0-9]+(@-?\d+))*$/; const REDIS_SCAN_CONFIG = config.get('redis_scan'); export class ClusterStrategy extends AbstractStrategy { @@ -100,7 +96,7 @@ export class ClusterStrategy extends AbstractStrategy { initialCursor: string, ): Promise { if (Number.isNaN(toNumber(initialCursor))) { - return this.getNodesFromClusterCursor(initialCursor); + return parseClusterCursor(initialCursor); } const clusterNodes = await this.redisManager.getNodes( @@ -118,35 +114,6 @@ export class ClusterStrategy extends AbstractStrategy { })); } - /** - * Parses composed custom cursor from FE and returns nodes - * Format: 172.17.0.1:7001@22||172.17.0.1:7002@33 - */ - private getNodesFromClusterCursor(cursor: string): IGetNodeKeysResult[] { - const isCorrectFormat = CLUSTER_CURSOR_REGEX.test(cursor); - if (!isCorrectFormat) { - throw new Error(ERROR_MESSAGES.INCORRECT_CLUSTER_CURSOR_FORMAT); - } - const nodeStrings = cursor.split(NODES_SEPARATOR); - const nodes = []; - - nodeStrings.forEach((item: string) => { - const [address, nextCursor] = item.split(CURSOR_SEPARATOR); - const [host, port] = address.split(':'); - if (parseInt(nextCursor, 10) >= 0) { - nodes.push({ - total: 0, - scanned: 0, - host, - port: parseInt(port, 10), - cursor: parseInt(nextCursor, 10), - keys: [], - }); - } - }); - return nodes; - } - private async calculateNodesTotalKeys( clientOptions, nodes: IGetNodeKeysResult[], diff --git a/redisinsight/api/src/modules/browser/utils/clusterCursor.spec.ts b/redisinsight/api/src/modules/browser/utils/clusterCursor.spec.ts new file mode 100644 index 0000000000..2de97689dc --- /dev/null +++ b/redisinsight/api/src/modules/browser/utils/clusterCursor.spec.ts @@ -0,0 +1,85 @@ +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { isClusterCursorValid, parseClusterCursor } from './clusterCursor'; + +const isClusterCursorValidTests = [ + { input: '172.17.0.1:7001@22||172.17.0.1:7002@33', expected: true }, + { input: '172.17.0.1:7001@-1||172.17.0.1:7002@-1', expected: true }, + { + input: '172.17.0.1:7001@10' + + '||172.17.0.1:7002@10' + + '||172.17.0.1:7003@10' + + '||172.17.0.1:7004@10' + + '||172.17.0.1:7005@10' + + '||172.17.0.1:7006@10', + expected: true, + }, + { input: '172.17.0.1:7001@-1', expected: true }, + { input: 'domain.com:7001@-1', expected: true }, + { input: '172.17.0.1:7001@1228822', expected: true }, + { input: '172.17.0.1:7001@', expected: false }, + { input: '172.17.0.1:7001@text', expected: false }, + { input: '172,17,0,1:7001@-1', expected: false }, + { input: 'plain text', expected: false }, + { input: 'text@text||text@text', expected: false }, + { input: 'text@text', expected: false }, + { input: '', expected: false }, +]; + +describe('isClusterCursorValid', () => { + it.each(isClusterCursorValidTests)('%j', ({ input, expected }) => { + expect(isClusterCursorValid(input)).toBe(expected); + }); +}); + +const defaultNodeScanResult = { + total: 0, scanned: 0, host: '172.17.0.1', port: 0, cursor: 0, keys: [], +}; +const parsingError = new Error(ERROR_MESSAGES.INCORRECT_CLUSTER_CURSOR_FORMAT); +const parseClusterCursorTests = [ + { + input: '172.17.0.1:7001@22||172.17.0.1:7002@33', + expected: [ + { ...defaultNodeScanResult, port: 7001, cursor: 22 }, + { ...defaultNodeScanResult, port: 7002, cursor: 33 }, + ], + }, + { + input: '172.17.0.1:7001@-1' + + '||172.17.0.1:7002@10' + + '||172.17.0.1:7003@-1' + + '||172.17.0.1:7004@10' + + '||172.17.0.1:7005@-1' + + '||172.17.0.1:7006@10', + expected: [ + { ...defaultNodeScanResult, port: 7002, cursor: 10 }, + { ...defaultNodeScanResult, port: 7004, cursor: 10 }, + { ...defaultNodeScanResult, port: 7006, cursor: 10 }, + ], + }, + { + input: '172.17.0.1:7001@-1||172.17.0.1:7002@-1', + expected: [], + }, + { input: '172.17.0.1:7001@', expected: parsingError }, + { input: '172.17.0.1:7001@text', expected: parsingError }, + { input: '172,17,0,1:7001@-1', expected: parsingError }, + { input: 'plain text', expected: parsingError }, + { input: 'text@text||text@text', expected: parsingError }, + { input: 'text@text', expected: parsingError }, + { input: '', expected: parsingError }, + { input: '', expected: parsingError }, +]; + +describe('parseClusterCursor', () => { + it.each(parseClusterCursorTests)('%j', ({ input, expected }) => { + if (expected instanceof Error) { + try { + parseClusterCursor(input); + } catch (e) { + expect(e.message).toEqual(expected.message); + } + } else { + expect(parseClusterCursor(input)).toEqual(expected); + } + }); +}); diff --git a/redisinsight/api/src/modules/browser/utils/clusterCursor.ts b/redisinsight/api/src/modules/browser/utils/clusterCursor.ts new file mode 100644 index 0000000000..14ab10b620 --- /dev/null +++ b/redisinsight/api/src/modules/browser/utils/clusterCursor.ts @@ -0,0 +1,39 @@ +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { IGetNodeKeysResult } from 'src/modules/browser/services/keys-business/scanner/scanner.interface'; + +const NODES_SEPARATOR = '||'; +const CURSOR_SEPARATOR = '@'; +// Correct format 172.17.0.1:7001@-1||172.17.0.1:7002@33 +const CLUSTER_CURSOR_REGEX = /^(([a-z0-9.])+:[0-9]+(@-?\d+)(?:\|{2}(?!$)|$))+$/; + +export const isClusterCursorValid = (cursor) => CLUSTER_CURSOR_REGEX.test(cursor); + +/** + * Parses composed custom cursor from FE and returns nodes + * Format: 172.17.0.1:7001@22||172.17.0.1:7002@33 + */ +export const parseClusterCursor = (cursor: string): IGetNodeKeysResult[] => { + if (!isClusterCursorValid(cursor)) { + throw new Error(ERROR_MESSAGES.INCORRECT_CLUSTER_CURSOR_FORMAT); + } + const nodeStrings = cursor.split(NODES_SEPARATOR); + const nodes = []; + + nodeStrings.forEach((item: string) => { + const [address, nextCursor] = item.split(CURSOR_SEPARATOR); + const [host, port] = address.split(':'); + + // ignore nodes with cursor -1 (fully scanned) + if (parseInt(nextCursor, 10) >= 0) { + nodes.push({ + total: 0, + scanned: 0, + host, + port: parseInt(port, 10), + cursor: parseInt(nextCursor, 10), + keys: [], + }); + } + }); + return nodes; +}; diff --git a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts index 974006cdf9..d99f6feeb0 100644 --- a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts +++ b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts @@ -132,7 +132,8 @@ export class CliController { async reCreateClient( @Param('dbInstance') dbInstance: string, @Param('uuid') uuid: string, + @Body() dto: CreateCliClientDto, ): Promise { - return this.service.reCreateClient(dbInstance, uuid); + return this.service.reCreateClient(dbInstance, uuid, dto.namespace); } } diff --git a/redisinsight/api/src/modules/cli/dto/cli.dto.ts b/redisinsight/api/src/modules/cli/dto/cli.dto.ts index 0b66cde74c..14150a04ef 100644 --- a/redisinsight/api/src/modules/cli/dto/cli.dto.ts +++ b/redisinsight/api/src/modules/cli/dto/cli.dto.ts @@ -27,6 +27,15 @@ export enum ClusterNodeRole { Slave = 'SLAVE', } +class ClusterNode extends EndpointDto { + @ApiPropertyOptional({ + description: 'Cluster node slot.', + type: Number, + example: 0, + }) + slot?: number; +} + export class CreateCliClientDto { @ApiPropertyOptional({ type: String, @@ -51,7 +60,7 @@ export class SendCommandDto { @ApiPropertyOptional({ description: 'Define output format', - default: CliOutputFormatterTypes.Text, + default: CliOutputFormatterTypes.Raw, enum: CliOutputFormatterTypes, }) @IsOptional() @@ -123,10 +132,10 @@ export class SendClusterCommandResponse { response: any; @ApiPropertyOptional({ - type: () => EndpointDto, + type: () => ClusterNode, description: 'Redis Cluster Node info', }) - node?: EndpointDto; + node?: ClusterNode; @ApiProperty({ description: 'Redis CLI command execution status', diff --git a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts index 2aec654282..def69fea95 100644 --- a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.spec.ts @@ -3,7 +3,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter'; import { InternalServerErrorException } from '@nestjs/common'; import { mockRedisWrongTypeError, mockStandaloneDatabaseEntity } from 'src/__mocks__'; import { TelemetryEvents } from 'src/constants'; -import { ReplyError } from 'src/models'; +import { AppTool, ReplyError } from 'src/models'; import { CliParsingError } from 'src/modules/cli/constants/errors'; import { ICliExecResultFromNode } from 'src/modules/cli/services/cli-tool/cli-tool.service'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; @@ -42,10 +42,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientCreatedEvent', () => { it('should emit CliClientCreated event', () => { - service.sendCliClientCreatedEvent(instanceId, { data: 'Some data' }); + service.sendClientCreatedEvent(instanceId, AppTool.CLI, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreated, + `CLI_${TelemetryEvents.ClientCreated}`, { databaseId: instanceId, data: 'Some data', @@ -53,10 +53,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientCreated event without additional data', () => { - service.sendCliClientCreatedEvent(instanceId); + service.sendClientCreatedEvent(instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreated, + `CLI_${TelemetryEvents.ClientCreated}`, { databaseId: instanceId, }, @@ -66,10 +66,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientCreationFailedEvent', () => { it('should emit CliClientCreationFailed event', () => { - service.sendCliClientCreationFailedEvent(instanceId, httpException, { data: 'Some data' }); + service.sendClientCreationFailedEvent(instanceId, AppTool.CLI, httpException, { data: 'Some data' }); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreationFailed, + `CLI_${TelemetryEvents.ClientCreationFailed}`, httpException, { databaseId: instanceId, @@ -78,10 +78,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientCreationFailed event without additional data', () => { - service.sendCliClientCreationFailedEvent(instanceId, httpException); + service.sendClientCreationFailedEvent(instanceId, AppTool.CLI, httpException); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientCreationFailed, + `CLI_${TelemetryEvents.ClientCreationFailed}`, httpException, { databaseId: instanceId, @@ -92,10 +92,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientRecreatedEvent', () => { it('should emit CliClientRecreated event', () => { - service.sendCliClientRecreatedEvent(instanceId, { data: 'Some data' }); + service.sendClientRecreatedEvent(instanceId, AppTool.CLI, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientRecreated, + `CLI_${TelemetryEvents.ClientRecreated}`, { databaseId: instanceId, data: 'Some data', @@ -103,10 +103,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientRecreated event without additional data', () => { - service.sendCliClientRecreatedEvent(instanceId); + service.sendClientRecreatedEvent(instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientRecreated, + `CLI_${TelemetryEvents.ClientRecreated}`, { databaseId: instanceId, }, @@ -116,10 +116,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientDeletedEvent', () => { it('should emit CliClientDeleted event', () => { - service.sendCliClientDeletedEvent(1, instanceId, { data: 'Some data' }); + service.sendClientDeletedEvent(1, instanceId, AppTool.CLI, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientDeleted, + `CLI_${TelemetryEvents.ClientDeleted}`, { databaseId: instanceId, data: 'Some data', @@ -127,35 +127,35 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliClientDeleted event without additional data', () => { - service.sendCliClientDeletedEvent(1, instanceId); + service.sendClientDeletedEvent(1, instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientDeleted, + `CLI_${TelemetryEvents.ClientDeleted}`, { databaseId: instanceId, }, ); }); it('should not emit event', () => { - service.sendCliClientDeletedEvent(0, instanceId); + service.sendClientDeletedEvent(0, instanceId, AppTool.CLI); expect(sendEventMethod).not.toHaveBeenCalled(); }); it('should not emit event on invalid input values', () => { const input: any = {}; - service.sendCliClientDeletedEvent(input, instanceId); + service.sendClientDeletedEvent(input, instanceId, AppTool.CLI); - expect(() => service.sendCliClientDeletedEvent(input, instanceId)).not.toThrow(); + expect(() => service.sendClientDeletedEvent(input, instanceId, AppTool.CLI)).not.toThrow(); expect(sendEventMethod).not.toHaveBeenCalled(); }); }); describe('sendCliCommandExecutedEvent', () => { it('should emit CliCommandExecuted event', () => { - service.sendCliCommandExecutedEvent(instanceId, { command: 'info' }); + service.sendCommandExecutedEvent(instanceId, AppTool.CLI, { command: 'info' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandExecuted, + `CLI_${TelemetryEvents.CommandExecuted}`, { databaseId: instanceId, command: 'info', @@ -163,10 +163,42 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliCommandExecuted event without additional data', () => { - service.sendCliCommandExecutedEvent(instanceId); + service.sendCommandExecutedEvent(instanceId, AppTool.CLI); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandExecuted, + `CLI_${TelemetryEvents.CommandExecuted}`, + { + databaseId: instanceId, + }, + ); + }); + it('should emit CliCommandExecuted for undefined namespace', () => { + service.sendCommandExecutedEvent(instanceId, undefined, { command: 'info' }); + + expect(sendEventMethod).toHaveBeenCalledWith( + `CLI_${TelemetryEvents.CommandExecuted}`, + { + databaseId: instanceId, + command: 'info', + }, + ); + }); + it('should emit WorkbenchCommandExecuted event', () => { + service.sendCommandExecutedEvent(instanceId, 'workbench', { command: 'info' }); + + expect(sendEventMethod).toHaveBeenCalledWith( + `WORKBENCH_${TelemetryEvents.CommandExecuted}`, + { + databaseId: instanceId, + command: 'info', + }, + ); + }); + it('should emit WorkbenchCommandExecuted event without additional data', () => { + service.sendCommandExecutedEvent(instanceId, 'workbench'); + + expect(sendEventMethod).toHaveBeenCalledWith( + `WORKBENCH_${TelemetryEvents.CommandExecuted}`, { databaseId: instanceId, }, @@ -176,10 +208,10 @@ describe('CliAnalyticsService', () => { describe('sendCliCommandErrorEvent', () => { it('should emit CliCommandError event', () => { - service.sendCliCommandErrorEvent(instanceId, redisReplyError, { data: 'Some data' }); + service.sendCommandErrorEvent(instanceId, AppTool.CLI, redisReplyError, { data: 'Some data' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: ReplyError.name, @@ -189,10 +221,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliCommandError event without additional data', () => { - service.sendCliCommandErrorEvent(instanceId, redisReplyError); + service.sendCommandErrorEvent(instanceId, AppTool.CLI, redisReplyError); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: ReplyError.name, @@ -202,10 +234,10 @@ describe('CliAnalyticsService', () => { }); it('should emit event for custom error', () => { const error: any = CliParsingError; - service.sendCliCommandErrorEvent(instanceId, error); + service.sendCommandErrorEvent(instanceId, AppTool.CLI, error); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: CliParsingError.name, @@ -216,10 +248,10 @@ describe('CliAnalyticsService', () => { describe('sendCliClientCreationFailedEvent', () => { it('should emit CliConnectionError event', () => { - service.sendCliConnectionErrorEvent(instanceId, httpException, { data: 'Some data' }); + service.sendConnectionErrorEvent(instanceId, AppTool.CLI, httpException, { data: 'Some data' }); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientConnectionError, + `CLI_${TelemetryEvents.ClientConnectionError}`, httpException, { databaseId: instanceId, @@ -228,10 +260,10 @@ describe('CliAnalyticsService', () => { ); }); it('should emit CliConnectionError event without additional data', () => { - service.sendCliConnectionErrorEvent(instanceId, httpException); + service.sendConnectionErrorEvent(instanceId, AppTool.CLI, httpException); expect(sendFailedEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClientConnectionError, + `CLI_${TelemetryEvents.ClientConnectionError}`, httpException, { databaseId: instanceId, @@ -249,10 +281,10 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Success, }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult, { command: 'sadd' }); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult, { command: 'sadd' }); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliClusterNodeCommandExecuted, + `CLI_${TelemetryEvents.ClusterNodeCommandExecuted}`, { databaseId: instanceId, command: 'sadd', @@ -268,10 +300,10 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Fail, }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: redisReplyError.name, @@ -288,10 +320,10 @@ describe('CliAnalyticsService', () => { status: CommandExecutionStatus.Fail, }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); expect(sendEventMethod).toHaveBeenCalledWith( - TelemetryEvents.CliCommandErrorReceived, + `CLI_${TelemetryEvents.CommandErrorReceived}`, { databaseId: instanceId, error: CliParsingError.name, @@ -305,7 +337,7 @@ describe('CliAnalyticsService', () => { port: 7002, status: 'undefined status', }; - service.sendCliClusterCommandExecutedEvent(instanceId, nodExecResult); + service.sendClusterCommandExecutedEvent(instanceId, AppTool.CLI, nodExecResult); expect(sendEventMethod).not.toHaveBeenCalled(); }); diff --git a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts index a0c67651c9..a3d4ad00fd 100644 --- a/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-analytics/cli-analytics.service.ts @@ -2,7 +2,7 @@ import { HttpException, Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { TelemetryEvents } from 'src/constants'; import { TelemetryBaseService } from 'src/modules/shared/services/base/telemetry.base.service'; -import { ReplyError } from 'src/models'; +import { AppTool, ReplyError } from 'src/models'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { ICliExecResultFromNode } from 'src/modules/cli/services/cli-tool/cli-tool.service'; @@ -12,9 +12,13 @@ export class CliAnalyticsService extends TelemetryBaseService { super(eventEmitter); } - sendCliClientCreatedEvent(instanceId: string, additionalData: object = {}): void { + sendClientCreatedEvent( + instanceId: string, + namespace: string, + additionalData: object = {}, + ): void { this.sendEvent( - TelemetryEvents.CliClientCreated, + this.getNamespaceEvent(TelemetryEvents.ClientCreated, namespace), { databaseId: instanceId, ...additionalData, @@ -22,13 +26,14 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClientCreationFailedEvent( + sendClientCreationFailedEvent( instanceId: string, + namespace: string, exception: HttpException, additionalData: object = {}, ): void { this.sendFailedEvent( - TelemetryEvents.CliClientCreationFailed, + this.getNamespaceEvent(TelemetryEvents.ClientCreationFailed, namespace), exception, { databaseId: instanceId, @@ -37,9 +42,13 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClientRecreatedEvent(instanceId: string, additionalData: object = {}): void { + sendClientRecreatedEvent( + instanceId: string, + namespace: string, + additionalData: object = {}, + ): void { this.sendEvent( - TelemetryEvents.CliClientRecreated, + this.getNamespaceEvent(TelemetryEvents.ClientRecreated, namespace), { databaseId: instanceId, ...additionalData, @@ -47,15 +56,16 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClientDeletedEvent( + sendClientDeletedEvent( affected: number, instanceId: string, + namespace: string, additionalData: object = {}, ): void { try { if (affected > 0) { this.sendEvent( - TelemetryEvents.CliClientDeleted, + this.getNamespaceEvent(TelemetryEvents.ClientDeleted, namespace), { databaseId: instanceId, ...additionalData, @@ -67,9 +77,13 @@ export class CliAnalyticsService extends TelemetryBaseService { } } - sendCliCommandExecutedEvent(instanceId: string, additionalData: object = {}): void { + sendCommandExecutedEvent( + instanceId: string, + namespace: string, + additionalData: object = {}, + ): void { this.sendEvent( - TelemetryEvents.CliCommandExecuted, + this.getNamespaceEvent(TelemetryEvents.CommandExecuted, namespace), { databaseId: instanceId, ...additionalData, @@ -77,8 +91,30 @@ export class CliAnalyticsService extends TelemetryBaseService { ); } - sendCliClusterCommandExecutedEvent( + sendCommandErrorEvent( instanceId: string, + namespace: string, + error: ReplyError, + additionalData: object = {}, + ): void { + try { + this.sendEvent( + this.getNamespaceEvent(TelemetryEvents.CommandErrorReceived, namespace), + { + databaseId: instanceId, + error: error?.name, + command: error?.command?.name, + ...additionalData, + }, + ); + } catch (e) { + // continue regardless of error + } + } + + sendClusterCommandExecutedEvent( + instanceId: string, + namespace: string, result: ICliExecResultFromNode, additionalData: object = {}, ): void { @@ -86,7 +122,7 @@ export class CliAnalyticsService extends TelemetryBaseService { try { if (status === CommandExecutionStatus.Success) { this.sendEvent( - TelemetryEvents.CliClusterNodeCommandExecuted, + this.getNamespaceEvent(TelemetryEvents.ClusterNodeCommandExecuted, namespace), { databaseId: instanceId, ...additionalData, @@ -95,7 +131,7 @@ export class CliAnalyticsService extends TelemetryBaseService { } if (status === CommandExecutionStatus.Fail) { this.sendEvent( - TelemetryEvents.CliCommandErrorReceived, + this.getNamespaceEvent(TelemetryEvents.CommandErrorReceived, namespace), { databaseId: instanceId, error: error.name, @@ -108,33 +144,14 @@ export class CliAnalyticsService extends TelemetryBaseService { } } - sendCliCommandErrorEvent( - instanceId: string, - error: ReplyError, - additionalData: object = {}, - ): void { - try { - this.sendEvent( - TelemetryEvents.CliCommandErrorReceived, - { - databaseId: instanceId, - error: error?.name, - command: error?.command?.name, - ...additionalData, - }, - ); - } catch (e) { - // continue regardless of error - } - } - - sendCliConnectionErrorEvent( + sendConnectionErrorEvent( instanceId: string, + namespace: string, exception: HttpException, additionalData: object = {}, ): void { this.sendFailedEvent( - TelemetryEvents.CliClientConnectionError, + this.getNamespaceEvent(TelemetryEvents.ClientConnectionError, namespace), exception, { databaseId: instanceId, @@ -142,4 +159,8 @@ export class CliAnalyticsService extends TelemetryBaseService { }, ); } + + private getNamespaceEvent(event: TelemetryEvents, namespace: string = AppTool.CLI): string { + return namespace.toLowerCase() === 'workbench' ? `WORKBENCH_${event}` : `CLI_${event}`; + } } diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts index 0d181597f2..4aaacd1031 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts @@ -48,13 +48,14 @@ const mockRedisConsumer = () => ({ createNewToolClient: jest.fn(), reCreateToolClient: jest.fn(), deleteToolClient: jest.fn(), + getRedisClientNamespace: jest.fn(), }); const mockENotFoundMessage = 'ENOTFOUND some message'; const mockMemoryUsageCommand = 'memory usage key'; const mockGetEscapedKeyCommand = 'get "\\\\key'; const mockServerInfoCommand = 'info server'; -const mockIntegerResponse = '(integer) 5'; +const mockIntegerResponse = 5; describe('CliBusinessService', () => { let service: CliBusinessService; @@ -196,9 +197,9 @@ describe('CliBusinessService', () => { }); describe('sendCommand', () => { - it('should successfully execute command and return text response', async () => { + it('should successfully execute command (RAW format)', async () => { const dto: SendCommandDto = { command: mockMemoryUsageCommand }; - const formatSpy = jest.spyOn(textFormatter, 'format'); + const formatSpy = jest.spyOn(rawFormatter, 'format'); const mockResult: SendCommandResponse = { response: mockIntegerResponse, status: CommandExecutionStatus.Success, @@ -550,11 +551,42 @@ describe('CliBusinessService', () => { ); }); - it('should successfully execute command for single node with redirection', async () => { + it('should successfully execute command for single node with redirection (RAW format)', async () => { + const command = 'set foo bar'; + const mockResult: SendClusterCommandResponse = { + response: 'OK', + node: { ...mockNode, port: 7002, slot: 7008 }, + status: CommandExecutionStatus.Success, + }; + cliTool.execCommandForNode + .mockResolvedValueOnce({ + response: mockRedisMovedError.message, + error: mockRedisMovedError, + status: CommandExecutionStatus.Fail, + }) + .mockResolvedValueOnce({ + response: 'OK', + host: '127.0.0.1', + port: 7002, + status: CommandExecutionStatus.Success, + }); + + const result = await service.sendCommandForSingleNode( + mockClientOptions, + command, + ClusterNodeRole.All, + nodeOptions, + CliOutputFormatterTypes.Raw, + ); + + expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(2); + expect(result).toEqual(mockResult); + }); + it('should successfully execute command for single node with redirection (Text format)', async () => { const command = 'set foo bar'; const mockResult: SendClusterCommandResponse = { response: '-> Redirected to slot [7008] located at 127.0.0.1:7002\nOK', - node: { ...mockNode, port: 7002 }, + node: { ...mockNode, port: 7002, slot: 7008 }, status: CommandExecutionStatus.Success, }; cliTool.execCommandForNode @@ -575,6 +607,7 @@ describe('CliBusinessService', () => { command, ClusterNodeRole.All, nodeOptions, + CliOutputFormatterTypes.Text, ); expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(2); diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts index 9eb1370c5e..66c0ed3b32 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts @@ -73,11 +73,11 @@ export class CliBusinessService { try { const uuid = await this.cliTool.createNewToolClient(instanceId, namespace); this.logger.log('Succeed to create Redis client for CLI.'); - this.cliAnalyticsService.sendCliClientCreatedEvent(instanceId); + this.cliAnalyticsService.sendClientCreatedEvent(instanceId, namespace); return { uuid }; } catch (error) { this.logger.error('Failed to create redis client for CLI.', error); - this.cliAnalyticsService.sendCliClientCreationFailedEvent(instanceId, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, namespace, error); throw error; } } @@ -86,23 +86,22 @@ export class CliBusinessService { * Method to close exist client and create a new one * @param instanceId * @param uuid + * @param namespace */ public async reCreateClient( instanceId: string, uuid: string, + namespace: string = AppTool.CLI, ): Promise { this.logger.log('re-create Redis client for CLI.'); try { - const clientUuid = await this.cliTool.reCreateToolClient( - instanceId, - uuid, - ); + const clientUuid = await this.cliTool.reCreateToolClient(instanceId, uuid, namespace); this.logger.log('Succeed to re-create Redis client for CLI.'); - this.cliAnalyticsService.sendCliClientRecreatedEvent(instanceId); + this.cliAnalyticsService.sendClientRecreatedEvent(instanceId, namespace); return { uuid: clientUuid }; } catch (error) { this.logger.error('Failed to re-create redis client for CLI.', error); - this.cliAnalyticsService.sendCliClientCreationFailedEvent(instanceId, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, namespace, error); throw error; } } @@ -118,9 +117,12 @@ export class CliBusinessService { ): Promise { this.logger.log('Deleting Redis client for CLI.'); try { + const namespace = this.cliTool.getRedisClientNamespace({ instanceId, uuid }); const affected = await this.cliTool.deleteToolClient(instanceId, uuid); this.logger.log('Succeed to delete Redis client for CLI.'); - this.cliAnalyticsService.sendCliClientDeletedEvent(affected, instanceId); + if (affected) { + this.cliAnalyticsService.sendClientDeletedEvent(affected, instanceId, namespace); + } return { affected }; } catch (error) { this.logger.error('Failed to delete Redis client for CLI.', error); @@ -139,18 +141,21 @@ export class CliBusinessService { ): Promise { this.logger.log('Executing redis CLI command.'); const { command: commandLine } = dto; - const outputFormat = dto.outputFormat || CliOutputFormatterTypes.Text; + let namespace = AppTool.CLI.toString(); + const outputFormat = dto.outputFormat || CliOutputFormatterTypes.Raw; try { const formatter = this.outputFormatterManager.getStrategy(outputFormat); const [command, ...args] = splitCliCommandLine(commandLine); const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; this.checkUnsupportedCommands(`${command} ${args[0]}`); + namespace = this.cliTool.getRedisClientNamespace(clientOptions); const reply = await this.cliTool.execCommand(clientOptions, command, args, replyEncoding); this.logger.log('Succeed to execute redis CLI command.'); - this.cliAnalyticsService.sendCliCommandExecutedEvent( + this.cliAnalyticsService.sendCommandExecutedEvent( clientOptions.instanceId, + namespace, { command, outputFormat, @@ -168,10 +173,10 @@ export class CliBusinessService { || error instanceof CliCommandNotSupportedError || error?.name === 'ReplyError' ) { - this.cliAnalyticsService.sendCliCommandErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, namespace, error); return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendCliConnectionErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, namespace, error); if (error instanceof EncryptionServiceErrorException) { throw error; @@ -211,14 +216,17 @@ export class CliBusinessService { clientOptions: IFindRedisClientInstanceByOptions, commandLine: string, role: ClusterNodeRole, - outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Text, + outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Raw, ): Promise { + let namespace = AppTool.CLI.toString(); this.logger.log(`Executing redis.cluster CLI command for [${role}] nodes.`); try { const formatter = this.outputFormatterManager.getStrategy(outputFormat); const [command, ...args] = splitCliCommandLine(commandLine); const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; this.checkUnsupportedCommands(`${command} ${args[0]}`); + namespace = this.cliTool.getRedisClientNamespace(clientOptions); + const result = await this.cliTool.execCommandForNodes( clientOptions, command, @@ -226,9 +234,11 @@ export class CliBusinessService { role, replyEncoding, ); + return result.map((nodeExecReply) => { - this.cliAnalyticsService.sendCliClusterCommandExecutedEvent( + this.cliAnalyticsService.sendClusterCommandExecutedEvent( clientOptions.instanceId, + namespace, nodeExecReply, { command, outputFormat }, ); @@ -245,13 +255,13 @@ export class CliBusinessService { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CliParsingError || error instanceof CliCommandNotSupportedError) { - this.cliAnalyticsService.sendCliCommandErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, namespace, error); return [ { response: error.message, status: CommandExecutionStatus.Fail }, ]; } - this.cliAnalyticsService.sendCliConnectionErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, namespace, error); if (error instanceof EncryptionServiceErrorException) { throw error; @@ -269,7 +279,7 @@ export class CliBusinessService { commandLine: string, role: ClusterNodeRole, nodeOptions: ClusterSingleNodeOptions, - outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Text, + outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Raw, ): Promise { this.logger.log(`Executing redis.cluster CLI command for single node ${JSON.stringify(nodeOptions)}`); try { @@ -297,27 +307,29 @@ export class CliBusinessService { replyEncoding, ); result.response = formatter.format(result.response, { slot, address }); + result.slot = parseInt(slot, 10); } else { result.response = formatter.format(result.response); } - this.cliAnalyticsService.sendCliClusterCommandExecutedEvent( + this.cliAnalyticsService.sendClusterCommandExecutedEvent( clientOptions.instanceId, + 'cli', result, { command, outputFormat }, ); const { - host, port, error, ...rest + host, port, error, slot, ...rest } = result; - return { ...rest, node: { host, port } }; + return { ...rest, node: { host, port, slot } }; } catch (error) { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CliParsingError || error instanceof CliCommandNotSupportedError) { - this.cliAnalyticsService.sendCliCommandErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, 'cli', error); return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendCliConnectionErrorEvent(clientOptions.instanceId, error); + this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, 'cli', error); if (error instanceof EncryptionServiceErrorException) { throw error; diff --git a/redisinsight/api/src/modules/cli/services/cli-business/output-formatter/output-formatter-manager.spec.ts b/redisinsight/api/src/modules/cli/services/cli-business/output-formatter/output-formatter-manager.spec.ts index 6cda923cc3..3cb30701e0 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/output-formatter/output-formatter-manager.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/output-formatter/output-formatter-manager.spec.ts @@ -11,7 +11,7 @@ class TestFormatterStrategy implements IOutputFormatterStrategy { return ''; } } -const strategyName = CliOutputFormatterTypes.Text; +const strategyName = CliOutputFormatterTypes.Raw; const testStrategy = new TestFormatterStrategy(); describe('OutputFormatterManager', () => { diff --git a/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts b/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts index 6fb98ade4f..509c981040 100644 --- a/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-tool/cli-tool.service.ts @@ -25,6 +25,7 @@ export interface ICliExecResultFromNode { port: number; response: any; status: CommandExecutionStatus; + slot?: number; error?: any, } @@ -159,13 +160,13 @@ export class CliToolService extends RedisConsumerAbstractService { return uuid; } - async reCreateToolClient(instanceId: string, uuid: string): Promise { + async reCreateToolClient(instanceId: string, uuid: string, namespace: string): Promise { this.redisService.removeClientInstance({ instanceId, uuid, tool: this.consumer, }); - await this.createNewClient(instanceId, uuid); + await this.createNewClient(instanceId, uuid, namespace); return uuid; } diff --git a/redisinsight/api/src/modules/commands/commands-json.provider.spec.ts b/redisinsight/api/src/modules/commands/commands-json.provider.spec.ts index c861fe54a5..28600cb35e 100644 --- a/redisinsight/api/src/modules/commands/commands-json.provider.spec.ts +++ b/redisinsight/api/src/modules/commands/commands-json.provider.spec.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import * as fs from 'fs'; +import * as fs from 'fs-extra'; import { Test, TestingModule } from '@nestjs/testing'; import { mockMainCommands, @@ -10,76 +10,73 @@ import { CommandsJsonProvider } from 'src/modules/commands/commands-json.provide jest.mock('axios'); const mockedAxios = axios as jest.Mocked; -jest.mock('fs'); +jest.mock('fs-extra'); const mockedFs = fs as jest.Mocked; describe('CommandsJsonProvider', () => { let service: CommandsJsonProvider; - let updateLatestJsonSpy; beforeEach(async () => { - jest.mock('fs', () => mockedFs); + jest.mock('fs-extra', () => mockedFs); - mockedFs.existsSync.mockReturnValue(true); - mockedFs.mkdirSync.mockReturnValue(''); - mockedFs.writeFileSync.mockReturnValue(undefined); mockedAxios.get.mockResolvedValue({ data: JSON.stringify(mockMainCommands) }); const module: TestingModule = await Test.createTestingModule({ providers: [ { provide: 'service', - useFactory: () => new CommandsJsonProvider('name', 'someurl', mockMainCommands), + useFactory: () => new CommandsJsonProvider('name', 'someurl'), }, ], }).compile(); service = module.get('service'); - updateLatestJsonSpy = jest.spyOn(service, 'updateLatestJson'); - }); - - describe('onModuleInit', () => { - it('should trigger updateLatestJson function', async () => { - await service.onModuleInit(); - - expect(updateLatestJsonSpy).toHaveBeenCalled(); - }); }); describe('updateLatestJson', () => { - it('Should create dir and save proper json', async () => { - mockedFs.existsSync.mockReturnValueOnce(false); - - await service.onModuleInit(); - - // todo: uncomment after enable esModuleInterop in the tsconfig - // expect(mockedFs.mkdirSync).toHaveBeenCalled(); - // expect(mockedFs.writeFileSync).toHaveBeenCalled(); - }); it('should not fail when incorrect data retrieved', async () => { - mockedAxios.get.mockResolvedValueOnce('incorrect json'); - await service.onModuleInit(); + mockedAxios.get.mockResolvedValueOnce('json'); + await service.updateLatestJson(); - // todo: uncomment after enable esModuleInterop in the tsconfig - // expect(mockedFs.writeFileSync).not.toHaveBeenCalled(); + expect(mockedFs.writeFile).not.toHaveBeenCalled(); }); }); describe('getCommands', () => { it('should return default config when file was not found', async () => { - mockedFs.readFileSync.mockImplementationOnce(() => { throw new Error('No file'); }); + mockedFs.readFile.mockRejectedValueOnce(new Error('No file')); + mockedFs.readFile.mockResolvedValueOnce(Buffer.from(JSON.stringify(mockMainCommands))); expect(await service.getCommands()).toEqual(mockMainCommands); }); it('should return default config when incorrect json received from file', async () => { - mockedFs.readFileSync.mockReturnValue('incorrect json'); + mockedFs.readFile.mockResolvedValueOnce(Buffer.from('incorrect json')); + mockedFs.readFile.mockResolvedValueOnce(Buffer.from(JSON.stringify(mockMainCommands))); expect(await service.getCommands()).toEqual(mockMainCommands); }); it('should return latest commands', async () => { - mockedFs.readFileSync.mockReturnValue(JSON.stringify(mockRedijsonCommands)); + mockedFs.readFile.mockResolvedValue(Buffer.from(JSON.stringify(mockRedijsonCommands))); expect(await service.getCommands()).toEqual(mockRedijsonCommands); }); }); + + describe('getDefaultCommands', () => { + it('should return empty object when file was not found', async () => { + mockedFs.readFile.mockRejectedValue(new Error('No file')); + + expect(await service.getDefaultCommands()).toEqual({}); + }); + it('should return empty object when incorrect json received from file', async () => { + mockedFs.readFile.mockResolvedValue(Buffer.from('incorrect json')); + + expect(await service.getDefaultCommands()).toEqual({}); + }); + it('should return default commands', async () => { + mockedFs.readFile.mockResolvedValue(Buffer.from(JSON.stringify(mockRedijsonCommands))); + + expect(await service.getDefaultCommands()).toEqual(mockRedijsonCommands); + }); + }); }); diff --git a/redisinsight/api/src/modules/commands/commands-json.provider.ts b/redisinsight/api/src/modules/commands/commands-json.provider.ts index cc5381c892..9babb04289 100644 --- a/redisinsight/api/src/modules/commands/commands-json.provider.ts +++ b/redisinsight/api/src/modules/commands/commands-json.provider.ts @@ -1,41 +1,29 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import axios from 'axios'; -import * as fs from 'fs'; +import * as fs from 'fs-extra'; import * as path from 'path'; import config from 'src/utils/config'; const PATH_CONFIG = config.get('dir_path'); @Injectable() -export class CommandsJsonProvider implements OnModuleInit { +export class CommandsJsonProvider { private readonly logger: Logger; private readonly name: string; private readonly url: string; - private readonly defaultCommands: Record; - - constructor(name, url, defaultCommands) { + constructor(name, url) { this.name = name; this.url = url; - this.defaultCommands = defaultCommands; - this.logger = new Logger(this.name); - } - - /** - * Updates latest json on startup - */ - async onModuleInit() { - // async operation to not wait for it and not block user in case when no internet connection - this.updateLatestJson(); + this.logger = new Logger(`CommandsJsonProvider:${this.name}`); } /** * Get latest json from external resource and save it locally - * @private */ - private async updateLatestJson() { + async updateLatestJson() { try { this.logger.log(`Trying to update ${this.name} commands...`); const { data } = await axios.get(this.url, { @@ -43,11 +31,9 @@ export class CommandsJsonProvider implements OnModuleInit { transformResponse: [(raw) => raw], }); - if (!fs.existsSync(PATH_CONFIG.commands)) { - fs.mkdirSync(PATH_CONFIG.commands); - } + await fs.ensureDir(PATH_CONFIG.commands); - fs.writeFileSync( + await fs.writeFile( path.join(PATH_CONFIG.commands, `${this.name}.json`), JSON.stringify(JSON.parse(data)), // check that we received proper json object ); @@ -63,13 +49,29 @@ export class CommandsJsonProvider implements OnModuleInit { */ async getCommands() { try { - return JSON.parse(fs.readFileSync( + return JSON.parse(await fs.readFile( path.join(PATH_CONFIG.commands, `${this.name}.json`), 'utf8', )); } catch (error) { - this.logger.error(`Unable to get latest ${this.name} commands. Return default.`, error); - return this.defaultCommands; + this.logger.warn(`Unable to get latest ${this.name} commands. Return default.`, error); + return this.getDefaultCommands(); + } + } + + /** + * Try to get default json that was delivered with build + * In case when no default data we will return empty object to not fail api call + */ + async getDefaultCommands() { + try { + return JSON.parse(await fs.readFile( + path.join(PATH_CONFIG.defaultCommandsDir, `${this.name}.json`), + 'utf8', + )); + } catch (error) { + this.logger.error(`Unable to get default ${this.name} commands.`, error); + return {}; } } } diff --git a/redisinsight/api/src/modules/commands/commands.module.ts b/redisinsight/api/src/modules/commands/commands.module.ts index a2cd32cc37..7d63648306 100644 --- a/redisinsight/api/src/modules/commands/commands.module.ts +++ b/redisinsight/api/src/modules/commands/commands.module.ts @@ -3,65 +3,16 @@ import { CommandsController } from 'src/modules/commands/commands.controller'; import { CommandsService } from 'src/modules/commands/commands.service'; import { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider'; import config from 'src/utils/config'; -import * as defaultMainCommands from 'src/constants/commands/main.json'; -import * as defaultRedisearchCommands from 'src/constants/commands/redisearch.json'; -import * as defaultRedijsonCommands from 'src/constants/commands/redijson.json'; -import * as defaultRedistimeseriesCommands from 'src/constants/commands/redistimeseries.json'; -import * as defaultRedisaiCommands from 'src/constants/commands/redisai.json'; -import * as defaultRedisgraphCommands from 'src/constants/commands/redisgraph.json'; -const COMMANDS_CONFIG = config.get('commands'); +const COMMANDS_CONFIGS = config.get('commands'); @Module({ controllers: [CommandsController], providers: [ - CommandsService, { - provide: 'mainCommandsProvider', - useFactory: () => new CommandsJsonProvider( - 'main', - COMMANDS_CONFIG.mainUrl, - defaultMainCommands, - ), - }, - { - provide: 'redisearchCommandsProvider', - useFactory: () => new CommandsJsonProvider( - 'redisearch', - COMMANDS_CONFIG.redisearchUrl, - defaultRedisearchCommands, - ), - }, - { - provide: 'redijsonCommandsProvider', - useFactory: () => new CommandsJsonProvider( - 'redijson', - COMMANDS_CONFIG.redijsonUrl, - defaultRedijsonCommands, - ), - }, - { - provide: 'redistimeseriesCommandsProvider', - useFactory: () => new CommandsJsonProvider( - 'redistimeseries', - COMMANDS_CONFIG.redistimeseriesUrl, - defaultRedistimeseriesCommands, - ), - }, - { - provide: 'redisaiCommandsProvider', - useFactory: () => new CommandsJsonProvider( - 'redisai', - COMMANDS_CONFIG.redisaiUrl, - defaultRedisaiCommands, - ), - }, - { - provide: 'redisgraphCommandsProvider', - useFactory: () => new CommandsJsonProvider( - 'redisgraph', - COMMANDS_CONFIG.redisgraphUrl, - defaultRedisgraphCommands, + provide: CommandsService, + useFactory: () => new CommandsService( + COMMANDS_CONFIGS.map(({ name, url }) => new CommandsJsonProvider(name, url)), ), }, ], diff --git a/redisinsight/api/src/modules/commands/commands.service.spec.ts b/redisinsight/api/src/modules/commands/commands.service.spec.ts index 115c2b5ab9..2a03f20531 100644 --- a/redisinsight/api/src/modules/commands/commands.service.spec.ts +++ b/redisinsight/api/src/modules/commands/commands.service.spec.ts @@ -14,52 +14,37 @@ import { CommandsJsonProvider } from 'src/modules/commands/commands-json.provide describe('CommandsService', () => { let service: CommandsService; - let mainCommandsProvider: MockType; - let redisearchCommandsProvider: MockType; - let redijsonCommandsProvider: MockType; - let redistimeseriesCommandsProvider: MockType; - let redisaiCommandsProvider: MockType; - let redisgraphCommandsProvider: MockType; + + const mainCommandsProvider: MockType = mockCommandsJsonProvider(); + const redisearchCommandsProvider: MockType = mockCommandsJsonProvider(); + const redijsonCommandsProvider: MockType = mockCommandsJsonProvider(); + const redistimeseriesCommandsProvider: MockType = mockCommandsJsonProvider(); + const redisaiCommandsProvider: MockType = mockCommandsJsonProvider(); + const redisgraphCommandsProvider: MockType = mockCommandsJsonProvider(); beforeEach(async () => { jest.clearAllMocks(); + + const commandsProviders = [ + mainCommandsProvider, + redisearchCommandsProvider, + redijsonCommandsProvider, + redistimeseriesCommandsProvider, + redisaiCommandsProvider, + redisgraphCommandsProvider, + ]; + const module: TestingModule = await Test.createTestingModule({ providers: [ - CommandsService, - { - provide: 'mainCommandsProvider', - useFactory: mockCommandsJsonProvider, - }, - { - provide: 'redisearchCommandsProvider', - useFactory: mockCommandsJsonProvider, - }, - { - provide: 'redijsonCommandsProvider', - useFactory: mockCommandsJsonProvider, - }, { - provide: 'redistimeseriesCommandsProvider', - useFactory: mockCommandsJsonProvider, - }, - { - provide: 'redisaiCommandsProvider', - useFactory: mockCommandsJsonProvider, - }, - { - provide: 'redisgraphCommandsProvider', - useFactory: mockCommandsJsonProvider, + provide: CommandsService, + // @ts-ignore + useFactory: () => new CommandsService(commandsProviders), }, ], }).compile(); service = module.get(CommandsService); - mainCommandsProvider = module.get('mainCommandsProvider'); - redisearchCommandsProvider = module.get('redisearchCommandsProvider'); - redijsonCommandsProvider = module.get('redijsonCommandsProvider'); - redistimeseriesCommandsProvider = module.get('redistimeseriesCommandsProvider'); - redisaiCommandsProvider = module.get('redisaiCommandsProvider'); - redisgraphCommandsProvider = module.get('redisgraphCommandsProvider'); mainCommandsProvider.getCommands.mockResolvedValue(mockMainCommands); redisearchCommandsProvider.getCommands.mockResolvedValue(mockRedisearchCommands); @@ -69,6 +54,19 @@ describe('CommandsService', () => { redisgraphCommandsProvider.getCommands.mockResolvedValue(mockRedisgraphCommands); }); + describe('onModuleInit', () => { + it('should trigger updateLatestJson function', async () => { + await service.onModuleInit(); + + expect(mainCommandsProvider.updateLatestJson).toHaveBeenCalled(); + expect(redisearchCommandsProvider.updateLatestJson).toHaveBeenCalled(); + expect(redijsonCommandsProvider.updateLatestJson).toHaveBeenCalled(); + expect(redistimeseriesCommandsProvider.updateLatestJson).toHaveBeenCalled(); + expect(redisaiCommandsProvider.updateLatestJson).toHaveBeenCalled(); + expect(redisgraphCommandsProvider.updateLatestJson).toHaveBeenCalled(); + }); + }); + describe('getAll', () => { it('Should return merged commands into one', async () => { expect(await service.getAll()).toEqual({ diff --git a/redisinsight/api/src/modules/commands/commands.service.ts b/redisinsight/api/src/modules/commands/commands.service.ts index 787b5281f8..a403a9f3c5 100644 --- a/redisinsight/api/src/modules/commands/commands.service.ts +++ b/redisinsight/api/src/modules/commands/commands.service.ts @@ -1,34 +1,30 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { assign } from 'lodash'; +import { Injectable, OnModuleInit } from '@nestjs/common'; import { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider'; @Injectable() -export class CommandsService { - constructor( - @Inject('redisearchCommandsProvider') - private redisearchCommandsProvider: CommandsJsonProvider, - @Inject('redijsonCommandsProvider') - private redijsonCommandsProvider: CommandsJsonProvider, - @Inject('redistimeseriesCommandsProvider') - private redistimeseriesCommandsProvider: CommandsJsonProvider, - @Inject('redisaiCommandsProvider') - private redisaiCommandsProvider: CommandsJsonProvider, - @Inject('redisgraphCommandsProvider') - private redisgraphCommandsProvider: CommandsJsonProvider, - @Inject('mainCommandsProvider') - private mainCommandsProvider: CommandsJsonProvider, - ) {} +export class CommandsService implements OnModuleInit { + private commandsProviders; + + constructor(commandsProviders: CommandsJsonProvider[] = []) { + this.commandsProviders = commandsProviders; + } + + /** + * Updates latest jsons on startup + */ + async onModuleInit() { + // async operation to not wait for it and not block user in case when no internet connection + Promise.all(this.commandsProviders.map((provider) => provider.updateLatestJson())); + } /** * Get all commands merged into single object */ async getAll(): Promise> { - return { - ...(await this.redisearchCommandsProvider.getCommands()), - ...(await this.redijsonCommandsProvider.getCommands()), - ...(await this.redistimeseriesCommandsProvider.getCommands()), - ...(await this.redisaiCommandsProvider.getCommands()), - ...(await this.redisgraphCommandsProvider.getCommands()), - ...(await this.mainCommandsProvider.getCommands()), - }; + return assign( + {}, + ...(await Promise.all(this.commandsProviders.map((provider) => provider.getCommands()))), + ); } } diff --git a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts index e77b7bee33..26ff9a3c40 100644 --- a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts +++ b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.spec.ts @@ -39,6 +39,7 @@ describe('ServerOnPremiseService', () => { let serverRepository: MockType>; let eventEmitter: EventEmitter2; let encryptionService: MockType; + const sessionId = new Date().getTime(); beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -88,12 +89,12 @@ describe('ServerOnPremiseService', () => { serverRepository.findOne.mockResolvedValue(null); serverRepository.create.mockReturnValue(mockServerEntity); - await service.onApplicationBootstrap(); + await service.onApplicationBootstrap(sessionId); expect(eventEmitter.emit).toHaveBeenNthCalledWith( 1, AppAnalyticsEvents.Initialize, - mockServerEntity.id, + { anonymousId: mockServerEntity.id, sessionId }, ); expect(eventEmitter.emit).toHaveBeenNthCalledWith( 2, @@ -107,12 +108,12 @@ describe('ServerOnPremiseService', () => { it('should emit APPLICATION_STARTED on second application launch', async () => { serverRepository.findOne.mockResolvedValue(mockServerEntity); - await service.onApplicationBootstrap(); + await service.onApplicationBootstrap(sessionId); expect(eventEmitter.emit).toHaveBeenNthCalledWith( 1, AppAnalyticsEvents.Initialize, - mockServerEntity.id, + { anonymousId: mockServerEntity.id, sessionId }, ); expect(eventEmitter.emit).toHaveBeenNthCalledWith( 2, diff --git a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts index 5a03b20ac2..8d98f541ba 100644 --- a/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts +++ b/redisinsight/api/src/modules/core/providers/server-on-premise/server-on-premise.service.ts @@ -27,13 +27,16 @@ implements OnApplicationBootstrap, IServerProvider { private encryptionService: EncryptionService; + private sessionId: number; + constructor(repository, eventEmitter, encryptionService) { this.repository = repository; this.eventEmitter = eventEmitter; this.encryptionService = encryptionService; } - async onApplicationBootstrap() { + async onApplicationBootstrap(sessionId: number = new Date().getTime()) { + this.sessionId = sessionId; await this.upsertServerInfo(); } @@ -45,7 +48,7 @@ implements OnApplicationBootstrap, IServerProvider { // Create default server info on first application launch serverInfo = this.repository.create({}); await this.repository.save(serverInfo); - this.eventEmitter.emit(AppAnalyticsEvents.Initialize, serverInfo.id); + this.eventEmitter.emit(AppAnalyticsEvents.Initialize, { anonymousId: serverInfo.id, sessionId: this.sessionId }); this.eventEmitter.emit(AppAnalyticsEvents.Track, { event: TelemetryEvents.ApplicationFirstStart, eventData: { @@ -57,7 +60,7 @@ implements OnApplicationBootstrap, IServerProvider { }); } else { this.logger.log('Application started.'); - this.eventEmitter.emit(AppAnalyticsEvents.Initialize, serverInfo.id); + this.eventEmitter.emit(AppAnalyticsEvents.Initialize, { anonymousId: serverInfo.id, sessionId: this.sessionId }); this.eventEmitter.emit(AppAnalyticsEvents.Track, { event: TelemetryEvents.ApplicationStarted, eventData: { @@ -82,6 +85,7 @@ implements OnApplicationBootstrap, IServerProvider { } const result = { ...info, + sessionId: this.sessionId, appVersion: SERVER_CONFIG.appVersion, osPlatform: process.platform, buildType: SERVER_CONFIG.buildType, diff --git a/redisinsight/api/src/modules/core/services/analytics/analytics.service.spec.ts b/redisinsight/api/src/modules/core/services/analytics/analytics.service.spec.ts index 0c0d2fbed8..7ef2986edd 100644 --- a/redisinsight/api/src/modules/core/services/analytics/analytics.service.spec.ts +++ b/redisinsight/api/src/modules/core/services/analytics/analytics.service.spec.ts @@ -33,6 +33,7 @@ const mockSettingsWithoutPermission = { describe('AnalyticsService', () => { let service: AnalyticsService; let settingsService: ISettingsProvider; + const sessionId = new Date().getTime(); beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -58,7 +59,7 @@ describe('AnalyticsService', () => { describe('initialize', () => { it('should set anonymousId', () => { - service.initialize(mockAnonymousId); + service.initialize({ anonymousId: mockAnonymousId, sessionId }); const anonymousId = service.getAnonymousId(); @@ -69,7 +70,7 @@ describe('AnalyticsService', () => { describe('sendEvent', () => { beforeEach(() => { mockAnalyticsTrack = jest.fn(); - service.initialize(mockAnonymousId); + service.initialize({ anonymousId: mockAnonymousId, sessionId }); }); it('should send event with anonymousId if permission are granted', async () => { settingsService.getSettings = jest @@ -84,6 +85,7 @@ describe('AnalyticsService', () => { expect(mockAnalyticsTrack).toHaveBeenCalledWith({ anonymousId: mockAnonymousId, + integrations: { Amplitude: { session_id: sessionId } }, event: TelemetryEvents.ApplicationStarted, properties: {}, }); @@ -114,6 +116,7 @@ describe('AnalyticsService', () => { expect(mockAnalyticsTrack).toHaveBeenCalledWith({ anonymousId: NON_TRACKING_ANONYMOUS_ID, + integrations: { Amplitude: { session_id: sessionId } }, event: TelemetryEvents.ApplicationStarted, properties: {}, }); diff --git a/redisinsight/api/src/modules/core/services/analytics/analytics.service.ts b/redisinsight/api/src/modules/core/services/analytics/analytics.service.ts index 5171196740..4fd2ca49ac 100644 --- a/redisinsight/api/src/modules/core/services/analytics/analytics.service.ts +++ b/redisinsight/api/src/modules/core/services/analytics/analytics.service.ts @@ -15,10 +15,17 @@ export interface ITelemetryEvent { nonTracking: boolean; } +export interface ITelemetryInitEvent { + anonymousId: string; + sessionId: number; +} + @Injectable() export class AnalyticsService { private anonymousId: string = NON_TRACKING_ANONYMOUS_ID; + private sessionId: number = -1; + private analytics; constructor( @@ -31,7 +38,9 @@ export class AnalyticsService { } @OnEvent(AppAnalyticsEvents.Initialize) - public initialize(anonymousId: string) { + public initialize(payload: ITelemetryInitEvent) { + const { anonymousId, sessionId } = payload; + this.sessionId = sessionId; this.anonymousId = anonymousId; this.analytics = new Analytics(ANALYTICS_CONFIG.writeKey); } @@ -55,6 +64,7 @@ export class AnalyticsService { if (isAnalyticsGranted) { this.analytics.track({ anonymousId: this.anonymousId, + integrations: { Amplitude: { session_id: this.sessionId } }, event, properties: { ...eventData, @@ -63,6 +73,7 @@ export class AnalyticsService { } else if (nonTracking) { this.analytics.track({ anonymousId: NON_TRACKING_ANONYMOUS_ID, + integrations: { Amplitude: { session_id: this.sessionId } }, event, properties: { ...eventData, diff --git a/redisinsight/api/src/modules/core/services/redis/redis.service.ts b/redisinsight/api/src/modules/core/services/redis/redis.service.ts index 9c9198746b..fece263571 100644 --- a/redisinsight/api/src/modules/core/services/redis/redis.service.ts +++ b/redisinsight/api/src/modules/core/services/redis/redis.service.ts @@ -1,6 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConnectionOptions, SecureContextOptions } from 'tls'; import * as Redis from 'ioredis'; +import { isEmpty } from 'lodash'; import IORedis, { RedisOptions } from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; import { @@ -120,7 +121,7 @@ export class RedisService { }); cluster.on('error', (e): void => { this.logger.error('Failed connection to the redis oss cluster', e); - reject(e); + reject(!isEmpty(e.lastNodeError) ? e.lastNodeError : e); }); cluster.on('ready', (): void => { this.logger.log('Successfully connected to the redis oss cluster.'); diff --git a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts index 8e283cb7f8..c76fb431d8 100644 --- a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts +++ b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.spec.ts @@ -13,6 +13,7 @@ import { import { InstancesBusinessService } from 'src/modules/shared/services/instances-business/instances-business.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { DatabaseInstanceEntity } from 'src/modules/core/models/database-instance.entity'; +import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; const mockClientOptions: IFindRedisClientInstanceByOptions = { instanceId: mockStandaloneDatabaseEntity.id, @@ -203,4 +204,33 @@ describe('RedisConsumerAbstractService', () => { ]); }); }); + + describe('getRedisClientNamespace', () => { + const mockClient = Object.create(Redis.prototype); + mockClient.options = { + ...mockClient.options, + connectionName: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4`, + }; + + it('succeed to get client namespace', async () => { + redisService.getClientInstance.mockReturnValue({ ...mockRedisClientInstance, client: mockClient }); + + const namespace = consumerInstance.getRedisClientNamespace({ + uuid: mockClient.uuid, + instanceId: mockClient.instanceId, + }); + + expect(namespace).toEqual('common'); + }); + it('failed to get client namespace', () => { + redisService.getClientInstance.mockReturnValue(undefined); + + const namespace = consumerInstance.getRedisClientNamespace({ + uuid: mockClient.uuid, + instanceId: mockClient.instanceId, + }); + + expect(namespace).toEqual(''); + }); + }); }); diff --git a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts index f1fd1892eb..f82b987795 100644 --- a/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts +++ b/redisinsight/api/src/modules/shared/services/base/redis-consumer.abstract.service.ts @@ -1,7 +1,11 @@ import IORedis from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; import { AppTool, ReplyError, IRedisConsumer } from 'src/models'; -import { catchRedisConnectionError, generateRedisConnectionName } from 'src/utils'; +import { + catchRedisConnectionError, + generateRedisConnectionName, + getConnectionNamespace, +} from 'src/utils'; import { IFindRedisClientInstanceByOptions, RedisService, @@ -116,6 +120,18 @@ export abstract class RedisConsumerAbstractService implements IRedisConsumer { return redisClientInstance.client; } + getRedisClientNamespace(options: IFindRedisClientInstanceByOptions): string { + try { + const clientInstance = this.redisService.getClientInstance({ + ...options, + tool: this.consumer, + }); + return clientInstance?.client ? getConnectionNamespace(clientInstance.client) : ''; + } catch (e) { + return ''; + } + } + protected async createNewClient( instanceId: string, uuid = uuidv4(), diff --git a/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.spec.ts b/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.spec.ts index 16e9563bc9..d0d829cd36 100644 --- a/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.spec.ts +++ b/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.spec.ts @@ -53,6 +53,51 @@ describe('InstancesAnalytics', () => { ); }); + describe('sendInstanceListReceivedEvent', () => { + const instance = mockDatabaseInstanceDto; + it('should emit event with one db in the list', () => { + service.sendInstanceListReceivedEvent([instance]); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.RedisInstanceListReceived, + { + numberOfDatabases: 1, + }, + ); + }); + it('should emit event with several dbs in the list', () => { + service.sendInstanceListReceivedEvent([instance, instance, instance]); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.RedisInstanceListReceived, + { + numberOfDatabases: 3, + }, + ); + }); + it('should emit event with several empty in the list', () => { + service.sendInstanceListReceivedEvent([]); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.RedisInstanceListReceived, + { + numberOfDatabases: 0, + }, + ); + }); + it('should emit event with additional data', () => { + service.sendInstanceListReceivedEvent([], { data: 'data' }); + + expect(sendEventMethod).toHaveBeenCalledWith( + TelemetryEvents.RedisInstanceListReceived, + { + numberOfDatabases: 0, + data: 'data', + }, + ); + }); + }); + describe('sendInstanceAddedEvent', () => { it('should emit event with enabled tls', () => { const instance = mockDatabaseInstanceDto; @@ -72,6 +117,8 @@ describe('InstancesAnalytics', () => { numberOfKeysRange: '0 - 500 000', totalMemory: mockRedisGeneralInfo.usedMemory, numberedDatabases: mockRedisGeneralInfo.databases, + numberOfModules: 0, + modules: [], }, ); }); @@ -96,11 +143,13 @@ describe('InstancesAnalytics', () => { numberOfKeysRange: '0 - 500 000', totalMemory: mockRedisGeneralInfo.usedMemory, numberedDatabases: mockRedisGeneralInfo.databases, + numberOfModules: 0, + modules: [], }, ); }); it('should emit event without additional info', () => { - const instance = mockDatabaseInstanceDto; + const instance = { ...mockDatabaseInstanceDto, modules: [{ name: 'search', version: 20000 }] }; service.sendInstanceAddedEvent(instance, { version: mockRedisGeneralInfo.version, }); @@ -119,6 +168,8 @@ describe('InstancesAnalytics', () => { numberOfKeysRange: undefined, totalMemory: undefined, numberedDatabases: undefined, + numberOfModules: 1, + modules: [{ name: 'search', version: 20000 }], }, ); }); diff --git a/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.ts b/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.ts index 8c5f646b75..6f77675631 100644 --- a/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.ts +++ b/redisinsight/api/src/modules/shared/services/instances-business/instances-analytics.service.ts @@ -12,6 +12,23 @@ export class InstancesAnalyticsService extends TelemetryBaseService { super(eventEmitter); } + sendInstanceListReceivedEvent( + instances: DatabaseInstanceResponse[], + additionalData: object = {}, + ): void { + try { + this.sendEvent( + TelemetryEvents.RedisInstanceListReceived, + { + numberOfDatabases: instances.length, + ...additionalData, + }, + ); + } catch (e) { + // continue regardless of error + } + } + sendInstanceAddedEvent( instance: DatabaseInstanceResponse, additionalInfo: RedisDatabaseInfoResponse, @@ -35,6 +52,8 @@ export class InstancesAnalyticsService extends TelemetryBaseService { numberOfKeysRange: getRangeForNumber(additionalInfo.totalKeys, TOTAL_KEYS_BREAKPOINTS), totalMemory: additionalInfo.usedMemory, numberedDatabases: additionalInfo.databases, + numberOfModules: instance.modules.length, + modules: instance.modules, }, ); } catch (e) { diff --git a/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts b/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts index c0c5af17f6..86168533df 100644 --- a/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts +++ b/redisinsight/api/src/modules/shared/services/instances-business/instances-business.service.ts @@ -92,7 +92,9 @@ export class InstancesBusinessService { async getAll(): Promise { try { - return (await this.databasesProvider.getAll()).map(convertEntityToDto); + const result = (await this.databasesProvider.getAll()).map(convertEntityToDto); + this.instancesAnalyticsService.sendInstanceListReceivedEvent(result); + return result; } catch (error) { this.logger.error('Failed to get database instance list.', error); throw new InternalServerErrorException(); diff --git a/redisinsight/api/src/modules/shared/services/redis-cloud-business/redis-cloud-business.service.ts b/redisinsight/api/src/modules/shared/services/redis-cloud-business/redis-cloud-business.service.ts index 51c0efd8c7..6628e42e45 100644 --- a/redisinsight/api/src/modules/shared/services/redis-cloud-business/redis-cloud-business.service.ts +++ b/redisinsight/api/src/modules/shared/services/redis-cloud-business/redis-cloud-business.service.ts @@ -50,7 +50,7 @@ export class RedisCloudBusinessService { try { const { data: { account }, - }: AxiosResponse = await this.api.get(`${this.config.url}`, { + }: AxiosResponse = await this.api.get(`${this.config.url}/`, { headers: this.getAuthHeaders(apiKey, apiSecretKey), }); this.logger.log('Succeed to get RE cloud account.'); diff --git a/redisinsight/api/src/utils/hosting-provider-helper.ts b/redisinsight/api/src/utils/hosting-provider-helper.ts index 5df786bc7b..8608a89870 100644 --- a/redisinsight/api/src/utils/hosting-provider-helper.ts +++ b/redisinsight/api/src/utils/hosting-provider-helper.ts @@ -1,6 +1,9 @@ import { HostingProvider } from 'src/modules/core/models/database-instance.entity'; import { IP_ADDRESS_REGEX, PRIVATE_IP_ADDRESS_REGEX } from 'src/constants'; +// Ignore LGTM [js/incomplete-url-substring-sanitization] alert. +// Because we do not bind potentially dangerous logic to this. +// We define a hosting provider for telemetry only. export const getHostingProvider = (host: string): HostingProvider => { // Tries to detect the hosting provider from the hostname. if (host === '0.0.0.0' || host === 'localhost') { @@ -9,12 +12,13 @@ export const getHostingProvider = (host: string): HostingProvider => { if (IP_ADDRESS_REGEX.test(host) && PRIVATE_IP_ADDRESS_REGEX.test(host)) { return HostingProvider.LOCALHOST; } - if (host.endsWith('rlrcp.com') || host.endsWith('redislabs.com')) { + if (host.endsWith('rlrcp.com') || host.endsWith('redislabs.com')) { // lgtm[js/incomplete-url-substring-sanitization] return HostingProvider.RE_CLOUD; } - if (host.endsWith('cache.amazonaws.com')) { + if (host.endsWith('cache.amazonaws.com')) { // lgtm[js/incomplete-url-substring-sanitization] return HostingProvider.AWS; } + // lgtm[js/incomplete-url-substring-sanitization] if (host.endsWith('cache.windows.net')) { return HostingProvider.AZURE; } diff --git a/redisinsight/api/src/utils/redis-connection-helper.spec.ts b/redisinsight/api/src/utils/redis-connection-helper.spec.ts new file mode 100644 index 0000000000..aecf9aab1e --- /dev/null +++ b/redisinsight/api/src/utils/redis-connection-helper.spec.ts @@ -0,0 +1,65 @@ +import * as Redis from 'ioredis'; +import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; +import { + generateRedisConnectionName, + getConnectionName, + getConnectionNamespace, +} from './redis-connection-helper'; + +const CLIENT_ID = '235e72f4-601f-4d01-8399-b5c51b617dc4'; + +const mockClient = Object.create(Redis.prototype); +mockClient.options = { + ...mockClient.options, + host: 'localhost', + port: 6379, + connectionName: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4`, +}; + +const mockCluster = Object.create(Redis.Cluster.prototype); +mockCluster.options = { + redisOptions: mockClient.options, +}; + +const generateRedisConnectionNameTests = [ + { input: ['CLI', CLIENT_ID], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-cli-235e72f4` }, + { input: ['CLI', CLIENT_ID, '_'], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}_cli_235e72f4` }, + { input: ['workbench', CLIENT_ID], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-workbench-235e72f4` }, + { input: ['Browser', CLIENT_ID], expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-browser-235e72f4` }, + { input: ['Browser', undefined], expected: CONNECTION_NAME_GLOBAL_PREFIX }, + { input: [], expected: CONNECTION_NAME_GLOBAL_PREFIX }, +]; + +describe('generateRedisConnectionName', () => { + test.each(generateRedisConnectionNameTests)('%j', ({ input, expected }) => { + // @ts-ignore + const result = generateRedisConnectionName(...input); + expect(result).toEqual(expected); + }); +}); + +const getConnectionNameTests = [ + { input: mockClient, expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4` }, + { input: mockCluster, expected: `${CONNECTION_NAME_GLOBAL_PREFIX}-common-235e72f4` }, + { input: undefined, expected: CONNECTION_NAME_GLOBAL_PREFIX }, +]; + +describe('getConnectionName', () => { + test.each(getConnectionNameTests)('%j', ({ input, expected }) => { + const result = getConnectionName(input); + expect(result).toEqual(expected); + }); +}); + +const getConnectionNamespaceTests = [ + { input: mockClient, expected: 'common' }, + { input: mockCluster, expected: 'common' }, + { input: undefined, expected: '' }, +]; + +describe('getConnectionNamespace', () => { + test.each(getConnectionNamespaceTests)('%j', ({ input, expected }) => { + const result = getConnectionNamespace(input); + expect(result).toEqual(expected); + }); +}); diff --git a/redisinsight/api/src/utils/redis-connection-helper.ts b/redisinsight/api/src/utils/redis-connection-helper.ts index e89bae0d3c..7b49d1c095 100644 --- a/redisinsight/api/src/utils/redis-connection-helper.ts +++ b/redisinsight/api/src/utils/redis-connection-helper.ts @@ -4,13 +4,13 @@ import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; export const generateRedisConnectionName = (namespace: string, id: string, separator = '-') => { try { - return [CONNECTION_NAME_GLOBAL_PREFIX, namespace, id?.substr(0, 8)].join(separator).toLowerCase(); + return [CONNECTION_NAME_GLOBAL_PREFIX, namespace, id.substr(0, 8)].join(separator).toLowerCase(); } catch (e) { return CONNECTION_NAME_GLOBAL_PREFIX; } }; -export const getConnectionName = (client: IORedis.Redis | IORedis.Cluster) => { +export const getConnectionName = (client: IORedis.Redis | IORedis.Cluster): string => { try { if (client instanceof IORedis.Cluster) { return get(client, 'options.redisOptions.connectionName', CONNECTION_NAME_GLOBAL_PREFIX); @@ -20,3 +20,12 @@ export const getConnectionName = (client: IORedis.Redis | IORedis.Cluster) => { return CONNECTION_NAME_GLOBAL_PREFIX; } }; + +export const getConnectionNamespace = (client: IORedis.Redis | IORedis.Cluster, separator = '-'): string => { + try { + const connectionName = getConnectionName(client); + return connectionName.split(separator)[1] || ''; + } catch (e) { + return ''; + } +}; diff --git a/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_cluster_command.test.ts b/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_cluster_command.test.ts index d71e01b813..13c7e1cdc0 100644 --- a/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_cluster_command.test.ts +++ b/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_cluster_command.test.ts @@ -37,6 +37,7 @@ const responseSchema = Joi.array().items(Joi.object().keys({ node: Joi.object().keys({ host: Joi.string().required(), port: Joi.number().integer().required(), + slot: Joi.number().integer(), }) }).required()); @@ -46,6 +47,7 @@ const responseRawSchema = Joi.array().items(Joi.object().keys({ node: Joi.object().keys({ host: Joi.string().required(), port: Joi.number().integer().required(), + slot: Joi.number().integer(), }) }).required()); @@ -85,6 +87,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-cluster-command', () => { name: 'Should create string', data: { command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`, + outputFormat: 'TEXT', role: 'ALL', }, responseSchema, @@ -99,6 +102,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-cluster-command', () => { name: 'Should get string', data: { command: `get ${constants.TEST_STRING_KEY_1}`, + outputFormat: 'TEXT', role: 'ALL', }, responseSchema, @@ -111,6 +115,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-cluster-command', () => { name: 'Should remove string', data: { command: `del ${constants.TEST_STRING_KEY_1}`, + outputFormat: 'TEXT', role: 'ALL', }, responseSchema, @@ -134,6 +139,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-cluster-command', () => { name: 'Should create string', data: { command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`, + outputFormat: 'TEXT', role: 'ALL', nodeOptions }, @@ -152,6 +158,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-cluster-command', () => { name: 'Should get string', data: { command: `get ${constants.TEST_STRING_KEY_1}`, + outputFormat: 'TEXT', role: 'ALL', nodeOptions }, @@ -165,6 +172,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-cluster-command', () => { name: 'Should remove string', data: { command: `del ${constants.TEST_STRING_KEY_1}`, + outputFormat: 'TEXT', role: 'ALL', nodeOptions }, @@ -259,6 +267,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-cluster-command', () => { name: `Should create string with redirection if needed (${node.host}:${node.port})`, data: { command: `set ${constants.TEST_STRING_KEY_1} ${node.host}`, + outputFormat: 'TEXT', role: 'ALL', nodeOptions: { host: node.host, diff --git a/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts b/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts index d004b1cad8..1199e67dc0 100644 --- a/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts +++ b/redisinsight/api/test/api/cli/POST-instance-id-cli-uuid-send_command.test.ts @@ -76,6 +76,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create string', data: { command: `set ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -89,6 +90,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get string', data: { command: `get ${constants.TEST_STRING_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -99,6 +101,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove string', data: { command: `del ${constants.TEST_STRING_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -113,6 +116,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create list', data: { command: `lpush ${constants.TEST_LIST_KEY_1} ${constants.TEST_LIST_ELEMENT_1} ${constants.TEST_LIST_ELEMENT_2}`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -129,6 +133,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get list', data: { command: `lrange ${constants.TEST_LIST_KEY_1} 0 100`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -140,6 +145,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove list', data: { command: `del ${constants.TEST_LIST_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -154,6 +160,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create set', data: { command: `sadd ${constants.TEST_SET_KEY_1} ${constants.TEST_SET_MEMBER_1} ${constants.TEST_SET_MEMBER_2}`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -171,6 +178,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get set', data: { command: `sscan ${constants.TEST_SET_KEY_1} 0 count 100`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -182,6 +190,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove list', data: { command: `del ${constants.TEST_SET_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -196,6 +205,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create zset', data: { command: `zadd ${constants.TEST_ZSET_KEY_1} 1 ${constants.TEST_ZSET_MEMBER_1} 2 ${constants.TEST_ZSET_MEMBER_2}`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -212,6 +222,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get zset', data: { command: `zrange ${constants.TEST_ZSET_KEY_1} 0 100`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -223,6 +234,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove zset', data: { command: `del ${constants.TEST_ZSET_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -237,6 +249,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create hash', data: { command: `hset ${constants.TEST_HASH_KEY_1} ${constants.TEST_HASH_FIELD_1_NAME} ${constants.TEST_HASH_FIELD_1_VALUE}`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -252,6 +265,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get hash', data: { command: `hgetall ${constants.TEST_HASH_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -263,6 +277,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove hash', data: { command: `del ${constants.TEST_HASH_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -278,6 +293,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create json', data: { command: `json.set ${constants.TEST_REJSON_KEY_1} . "{\\"field\\":\\"value\\"}"`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -291,6 +307,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get json', data: { command: `json.get ${constants.TEST_REJSON_KEY_1} .field`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -302,6 +319,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove json', data: { command: `json.del ${constants.TEST_REJSON_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -317,6 +335,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create ts', data: { command: `ts.create ${constants.TEST_TS_KEY_1} ${constants.TEST_TS_VALUE_1} ${constants.TEST_TS_VALUE_2}`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -330,6 +349,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should add to ts', data: { command: `ts.add ${constants.TEST_TS_KEY_1} ${constants.TEST_TS_TIMESTAMP_1} ${constants.TEST_TS_VALUE_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -343,6 +363,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get ts', data: { command: `ts.get ${constants.TEST_TS_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -353,6 +374,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove ts', data: { command: `del ${constants.TEST_TS_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -368,6 +390,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create graph', data: { command: `graph.query ${constants.TEST_GRAPH_KEY_1} "CREATE (n1)"`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -384,6 +407,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get graph', data: { command: `graph.query ${constants.TEST_GRAPH_KEY_1} "MATCH (n1) RETURN n1"`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -394,6 +418,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove graph', data: { command: `del ${constants.TEST_GRAPH_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -411,6 +436,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { data: { command: `ft.create ${constants.TEST_SEARCH_HASH_INDEX_1} ON HASH PREFIX 1 ${constants.TEST_SEARCH_HASH_KEY_PREFIX_1} NOOFFSETS SCHEMA title TEXT WEIGHT 5.0`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -427,6 +453,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should return the list of all existing indexes.', data: { command: `ft._list`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -455,6 +482,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should find documents', data: { command: `ft.search ${constants.TEST_SEARCH_HASH_INDEX_1} "hello world"`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -470,6 +498,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should aggregate documents by uniq @title', data: { command: `ft.aggregate ${constants.TEST_SEARCH_HASH_INDEX_1} * GROUPBY 1 @title`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -480,6 +509,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove index', data: { command: `ft.dropindex ${constants.TEST_SEARCH_HASH_INDEX_1} DD`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -501,6 +531,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { data: { command: `ft.create ${constants.TEST_SEARCH_JSON_INDEX_1} ON JSON NOOFFSETS SCHEMA $.user.name AS name TEXT`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -535,6 +566,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should find documents', data: { command: `ft.search ${constants.TEST_SEARCH_JSON_INDEX_1} "@name:(John)"`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -553,6 +585,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should aggregate documents by uniq @name', data: { command: `ft.aggregate ${constants.TEST_SEARCH_JSON_INDEX_1} * GROUPBY 1 @name`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -563,6 +596,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove index', data: { command: `ft.dropindex ${constants.TEST_SEARCH_JSON_INDEX_1} DD`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -580,6 +614,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create index', data: { command: `ft.create ${constants.TEST_SEARCH_HASH_INDEX_1} NOOFFSETS SCHEMA title TEXT WEIGHT 5.0`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -619,6 +654,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should find documents', data: { command: `ft.search ${constants.TEST_SEARCH_HASH_INDEX_1} "hello world"`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -637,6 +673,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove index', data: { command: `ft.drop ${constants.TEST_SEARCH_HASH_INDEX_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -659,6 +696,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should create stream', data: { command: `xadd ${constants.TEST_STREAM_KEY_1} * ${constants.TEST_STREAM_DATA_1} ${constants.TEST_STREAM_DATA_2}`, + outputFormat: 'TEXT', }, responseSchema, before: async () => { @@ -672,6 +710,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should get stream', data: { command: `xrange ${constants.TEST_STREAM_KEY_1} - +`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -683,6 +722,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove stream', data: { command: `del ${constants.TEST_STREAM_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { @@ -697,6 +737,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should return error if invalid command sent', data: { command: `setx ${constants.TEST_STRING_KEY_1} ${constants.TEST_STRING_VALUE_1}`, + outputFormat: 'TEXT', }, responseSchema, checkFn: ({ body }) => { @@ -778,6 +819,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should use blocking command (unblock by cli command)', data: { command: `blpop ${constants.TEST_LIST_KEY_2} 0`, + outputFormat: 'TEXT', }, responseSchema, before: async function () { @@ -796,6 +838,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should use blocking command (unblock by adding element)', data: { command: `blpop ${constants.TEST_LIST_KEY_2} 0`, + outputFormat: 'TEXT', }, responseSchema, before: async function () { @@ -809,6 +852,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should use blocking command (unblock by removing client through API)', data: { command: `blpop ${constants.TEST_LIST_KEY_2} 0`, + outputFormat: 'TEXT', }, statusCode: 500, // todo: is it as designed? responseBody: { @@ -827,6 +871,7 @@ describe('POST /instance/:instanceId/cli/:uuid/send-command', () => { name: 'Should remove list', data: { command: `del ${constants.TEST_LIST_KEY_1}`, + outputFormat: 'TEXT', }, responseSchema, after: async () => { diff --git a/redisinsight/api/test/api/info/GET-info.test.ts b/redisinsight/api/test/api/info/GET-info.test.ts index b45f26f762..21a6f33e0a 100644 --- a/redisinsight/api/test/api/info/GET-info.test.ts +++ b/redisinsight/api/test/api/info/GET-info.test.ts @@ -18,6 +18,7 @@ const responseSchema = Joi.object().keys({ osPlatform: Joi.string().required(), buildType: Joi.string().valid('ELECTRON', 'DOCKER_ON_PREMISE').required(), encryptionStrategies: Joi.array().items(Joi.string()), + sessionId: Joi.number().required(), }).required(); const mainCheckFn = async (testCase) => { diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 0ff8816356..6f7881e375 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -70,7 +70,7 @@ export const constants = { // cloud TEST_CLOUD_RTE: process.env.TEST_CLOUD_RTE, - TEST_CLOUD_API: process.env.REDIS_CLOUD_URL || process.env.TEST_CLOUD_API || 'https://qa-api.redislabs.com/v1', + TEST_CLOUD_API: process.env.REDIS_CLOUD_URL || process.env.TEST_CLOUD_API || 'https://api.qa.redislabs.com/v1', TEST_CLOUD_API_KEY: process.env.TEST_CLOUD_API_KEY, TEST_CLOUD_API_SECRET_KEY: process.env.TEST_CLOUD_API_SECRET_KEY, TEST_CLOUD_SUBSCRIPTION_NAME: process.env.TEST_CLOUD_SUBSCRIPTION_NAME || 'ITests', diff --git a/redisinsight/api/test/test-runs/re-clu/Dockerfile b/redisinsight/api/test/test-runs/re-clu/Dockerfile index 136d43fbc3..8af7097d05 100644 --- a/redisinsight/api/test/test-runs/re-clu/Dockerfile +++ b/redisinsight/api/test/test-runs/re-clu/Dockerfile @@ -1,18 +1,9 @@ -FROM redislabs/redis:6.0.8-28.bionic +FROM redislabs/redis:6.2.8-50 -# Change user to root to install pip -USER root -RUN set -ex \ - && apt-get update \ - && apt-get install -y python3-pip \ - && pip3 install requests -# Change user back to redislabs -USER redislabs - -# Set the env var to instruct RE to create a cluster on startup +## Set the env var to instruct RE to create a cluster on startup ENV BOOTSTRAP_ACTION create_cluster ENV BOOTSTRAP_CLUSTER_FQDN cluster.local -COPY run_re_and_create_db.sh create_dbs.py cert.pem ./ +COPY entrypoint.sh db.json ./ -ENTRYPOINT [ "bash", "./run_re_and_create_db.sh" ] +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/redisinsight/api/test/test-runs/re-clu/README.md b/redisinsight/api/test/test-runs/re-clu/README.md deleted file mode 100644 index 6b65582c5a..0000000000 --- a/redisinsight/api/test/test-runs/re-clu/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# docker-redisenterprise-testdb -A Docker container that creates test databases on a Redis Enterprise cluster - - -## Databases - -Environment variable control which dbs are created. By default, no db is created. -- `CREATE_SIMPLE_DB`: Single-shard simple database on port 12000 -- `CREATE_CLUSTER_DB`: Database-clustering enabled, with 3 shards on port 12010 -- `CREATE_TLS_DB`: Single-shard TLS database on port 12443 -- `CREATE_TLS_MUTUAL_AUTH_DB`: Single-shard TLS client authentication enabled database on port 12465 -- `CREATE_MODULES_DB`: Single-shard db with modules: RedisGraph, RediSearch and RedisTimeSeries on port 12003 -- `CREATE_CRDB`: CRDT database on port 12005. `CRDB_INSTANCES` env var should also be set to a space-separated list of the FQDNs of participating clusters. - - -## References - -- [Redis Enterprise REST API Docs](https://storage.googleapis.com/rlecrestapi/rest-html/http_rest_api.html) diff --git a/redisinsight/api/test/test-runs/re-clu/cert.pem b/redisinsight/api/test/test-runs/re-clu/cert.pem deleted file mode 100644 index 3dfb16abf7..0000000000 --- a/redisinsight/api/test/test-runs/re-clu/cert.pem +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFfzCCA2egAwIBAgIURYhz7wsPwNGHxFoINaEB6ysJyEYwDQYJKoZIhvcNAQEL -BQAwTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRgwFgYDVQQKDA9SZWRpc0xh -YnMsIEluYy4xGTAXBgNVBAMMEHJlZGlzaW5zaWdodC5jb20wHhcNMjAxMjI4MTA1 -NzU1WhcNMjExMjI4MTA1NzU1WjBPMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0Ex -GDAWBgNVBAoMD1JlZGlzTGFicywgSW5jLjEZMBcGA1UEAwwQcmVkaXNpbnNpZ2h0 -LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANS1OFxNjFn1JQJO -HounllmdHA0hMCoZ5DGO1rur+ppZBZFh9Z4NT01neEVMi9vZkvsmeGQ6xwOibbGv -EClNXrFS/pqBg91AdGJzz6lH08VHgAtu+2P6kDewTXQyN+Gu+3qlss4t3di9jQAd -oYXkbPT5ZPnaJtkWq+rrw8P7hCC0OARETzysg/IVukyYYIGjgpeCOWOWeF4oGurO -EQtZnuVncMC4ZvobGlGtXk0Rk46j1uwLDNgovLkajnMHViGxS/kCOSZB2UJvhve/ -YmKK14kxc/mFoNu1+ING5bGEcprVUe8wKXb+TuTRp2YxIB1GIG8+vQwcYFS5l0kH -BsRGwBKS2ESaSQ1eSyIVd2wdXVHqlILlmy2Zvi9DM/kMX/OtoBjIDhWx9mStTxtz -DjHIooT/FeFQzC2ah1bP+/KYabCHScEXpXxubpK9saXLtj4Vk/RcfZ42+0eeVxBf -Dttln7MHP79VyyCZpT9OSu8q4qU6dVDlz3fczC6fkE6b2kPVQnSLz9Wmr47syawg -Argv96d6wcNiiNzOyHZNCaxHwsVFx0zJOuRiyMwJp4JvrAb2glkKgCzVNjCMO+8v -HuXv6TTUHKvLwqnuqe04VRQazIDUPzQma+whdgIMkAJBanm7U+fmZ/LX0d1XLCMv -E5k/zzu9uOXUNM46Pvz20Rko8W4fAgMBAAGjUzBRMB0GA1UdDgQWBBSbd9FdsIfw -AUWS9kE93p5MGmv4WTAfBgNVHSMEGDAWgBSbd9FdsIfwAUWS9kE93p5MGmv4WTAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCvXOzCaEGf6xoA8A7S -U5GXv5rGof1TW9+FogiUc1GyvEJJa4spo2MEASBCkqN0e5ac4tE3XiMsPXgoMCNt -XFRa71rI1a61an+02+i5hgG6fBTftTLRaMOWKXswiF/GN+Jc2aYPNy9KZtwxowre -g28a47vlAUT3O4ZdpdR4TVD6Zvy/EtA/TfuP9XPBA1TaAp6jEUArZFU3H0VM6nn3 -1GKH8rxqv54jLovA7ASs6CKU8PtgU0RDypB69lYVWjbVdrSA5Jh6o54N60tNizCk -LzaEbSGijI6qeKwouFoqKqx+Kr9aYPnGnA1rJXWVvI1Z8XIsWmb9fFnl9nPspqxF -A6vuvc2W9TIFLvwZIe/QC4kKq07rA7zCiF/dsjqU1VdeoJZXp0109t7Ua0tlUO/J -XIIY8sQHyJ2kZAiL9ghCHvgO5ciezVvu6ru1E6M8FD0OaopPa2jM4V562sZ/Ztdi -7IPlQ160zsHuYq3Q4uYNalWJ7gLaxbXFaok/fZo39GRVzjk/FN8zuXuGjMchhoaB -XrWDF0H6NM92cvH/8cgSg7JXvBlCdGqV9XPnqYm312MqeFZ9Lg2H8wTfHuAzNrOZ -eV1Cp5mBepvYtsFX3ATdDg7+QgAVhLdnAyUmMNY2cW1GTdfQV4L3fIm0S7RKayC6 -eeFbQeTv19tELZiXhalV3YuJMQ== ------END CERTIFICATE----- diff --git a/redisinsight/api/test/test-runs/re-clu/create_dbs.py b/redisinsight/api/test/test-runs/re-clu/create_dbs.py deleted file mode 100644 index 71b306ff86..0000000000 --- a/redisinsight/api/test/test-runs/re-clu/create_dbs.py +++ /dev/null @@ -1,218 +0,0 @@ -import os -import pprint -import subprocess - -import requests - - -# Suppress "Unverified HTTPS request" warnings -# See https://github.com/influxdata/influxdb-python/issues/240#issuecomment-140003499 -# pylint: disable=no-member -requests.packages.urllib3.disable_warnings() - - -CREATE_SIMPLE_DB = bool(os.environ.get("CREATE_SIMPLE_DB", "")) -CREATE_CLUSTER_DB = bool(os.environ.get("CREATE_CLUSTER_DB", "")) -CREATE_TLS_DB = bool(os.environ.get("CREATE_TLS_DB", "")) -CREATE_TLS_MUTUAL_AUTH_DB = bool(os.environ.get("CREATE_TLS_MUTUAL_AUTH_DB", "")) -CREATE_MODULES_DB = bool(os.environ.get("CREATE_MODULES_DB", "")) -CREATE_CRDB = bool(os.environ.get("CREATE_CRDB", "")) -CRDB_INSTANCES = os.environ.get("CRDB_INSTANCES", "") - - -USERNAME = 'demo@redislabs.com' -PASSWORD = '123456' - - -RLEC_API_BASE_URL = 'https://localhost:9443/v1' - - -COMMON_REQ_PARAMS = dict(auth=(USERNAME, PASSWORD), - verify=False,) - - -def get_module_data() -> dict: - """ - Returns a dict of module name to a dict containing: - - module_id - - module_name - - module_args - - semantic_version - """ - resp = requests.get(url=f'{RLEC_API_BASE_URL}/modules', **COMMON_REQ_PARAMS) - if not resp.ok: - raise Exception(f"Failed to get modules info: {resp.status_code}: {resp.text}") - data = resp.json() - module_dict = {} - for m in data: - module_dict[m['module_name']] = { - "module_id": m["uid"], - "module_name": m["module_name"], - "module_args": m["command_line_args"], - "semantic_version": m["semantic_version"], - } - return module_dict - - -def create_db(body: dict) -> dict: - """ - Create a bdb and return the response from the API. - """ - resp = requests.post(url=f'{RLEC_API_BASE_URL}/bdbs', - json=body, - **COMMON_REQ_PARAMS) - if not resp.ok: - raise Exception(f"Failed to create db: {resp.status_code}: {resp.text}") - data = resp.json() - return data - - -def create_simple_db() -> dict: - body = { - "name": "testdb", - "type": "redis", - "memory_size": 100000, - "port": 12000 - } - return create_db(body) - - -def create_cluster_db() -> dict: - body = { - "name": "testdb", - "type": "redis", - "memory_size": 1024* 1024 * 1024, # 1GB - "port": 12010, - "sharding": True, - "shards_count": 3, - "proxy_policy": "all-master-shards", - "oss_cluster": True, -# "oss_sharding": True, - # Default OSS Redis Cluster-like hashing policy. - # These regexes are taken from the RLEC REST API docs: - # https://storage.googleapis.com/rlecrestapi/rest-html/http_rest_api.html#bdb (see the 'shard_key_regex' attribute) - "shard_key_regex": [ - {"regex": ".*\\{(?.*)\\}.*" }, - {"regex": "(?.*)" } - ], - } - return create_db(body) - - -def create_tls_db() -> dict: - body = { - "name": "testtlsdb", - "type": "redis", - "memory_size": 100000, - "port": 12443, - "tls_mode": "enabled", - "enforce_client_authentication": "disabled" - } - return create_db(body) - - -def create_tls_mutual_auth_db() -> dict: - with open('./cert.pem') as f: - cert_str = f.read() - body = { - "name": "testtlsclientauthdb", - "type": "redis", - "memory_size": 100000, - "port": 12465, - "tls_mode": "enabled", - "enforce_client_authentication": "enabled", - "authentication_ssl_client_certs": [{ - "client_cert": cert_str, - }] - } - return create_db(body) - - -def create_modules_db(module_info: dict) -> dict: - body = { - "name": "modulesdb", - "type": "redis", - "memory_size": 100000, - "port": 12003, - "module_list": [ - module_info['ft'], - module_info['graph'], - module_info['timeseries'], - ] - } - return create_db(body) - - -def create_crdb() -> dict: - cluster_fqdns = CRDB_INSTANCES.split() - assert len(cluster_fqdns) >= 2, f"At least two clusters are needed for a CRDB, got {cluster_fqdns}" - crdb_cli_instances_args = (f"--instance fqdn={fqdn},username={USERNAME},password={PASSWORD}" - for fqdn in cluster_fqdns) - crdb_cli_instances_args = " ".join(crdb_cli_instances_args) - crdb_cli_command = f"/opt/redislabs/bin/crdb-cli crdb create --name mycrdb --memory-size 10mb --port 12005 --replication false --shards-count 1 {crdb_cli_instances_args}" - print("Running the following command:") - print(crdb_cli_command) - subprocess.run(crdb_cli_command.split()) - - -def main(): - - if CREATE_SIMPLE_DB: - print("Creating simple db...") - bdb = create_simple_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping simple db") - - if CREATE_CLUSTER_DB: - print("Creating cluster db...") - bdb = create_cluster_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping cluster db") - - if CREATE_TLS_DB: - print("Creating TLS db...") - bdb = create_tls_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping TLS db") - - if CREATE_TLS_MUTUAL_AUTH_DB: - print("Creating TLS mutual auth db...") - bdb = create_tls_mutual_auth_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping TLS mutual auth db") - - if CREATE_MODULES_DB: - print("Getting modules info...") - module_info = get_module_data() - print('done') - print("Creating modules db...") - bdb = create_modules_db(module_info) - print('done') - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping modules db") - - if CREATE_CRDB: - print("Creating CRDB...") - create_crdb() - print('done') - print("\n\n") - else: - print("Skipping CRDB") - - -if __name__ == '__main__': - main() diff --git a/redisinsight/api/test/test-runs/re-clu/db.json b/redisinsight/api/test/test-runs/re-clu/db.json new file mode 100644 index 0000000000..0ceba7ee65 --- /dev/null +++ b/redisinsight/api/test/test-runs/re-clu/db.json @@ -0,0 +1,18 @@ +{ + "name": "testdb", + "type": "redis", + "memory_size": 1073741824, + "port": 12010, + "sharding": true, + "shards_count": 3, + "proxy_policy": "all-master-shards", + "oss_cluster": true, + "shard_key_regex": [ + { + "regex": ".*\\{(?.*)\\}.*" + }, + { + "regex": "(?.*)" + } + ] +} diff --git a/redisinsight/api/test/test-runs/re-clu/docker-compose.yml b/redisinsight/api/test/test-runs/re-clu/docker-compose.yml index b0e04aeb61..b7be56fc94 100644 --- a/redisinsight/api/test/test-runs/re-clu/docker-compose.yml +++ b/redisinsight/api/test/test-runs/re-clu/docker-compose.yml @@ -9,9 +9,5 @@ services: build: ./re-clu cap_add: - sys_resource - environment: - - CREATE_CLUSTER_DB=1 -# ports: -# - 12010:12010 -# - 8443:8443 -# - 9443:9443 + env_file: + - ./re-clu/.env diff --git a/redisinsight/api/test/test-runs/re-st/run_re_and_create_db.sh b/redisinsight/api/test/test-runs/re-clu/entrypoint.sh similarity index 70% rename from redisinsight/api/test/test-runs/re-st/run_re_and_create_db.sh rename to redisinsight/api/test/test-runs/re-clu/entrypoint.sh index 1f8c28f883..70b5c44bdb 100644 --- a/redisinsight/api/test/test-runs/re-st/run_re_and_create_db.sh +++ b/redisinsight/api/test/test-runs/re-clu/entrypoint.sh @@ -1,5 +1,8 @@ #! /bin/bash +TEST_RE_USER=${TEST_RE_USER:-"demo@redislabs.com"} +TEST_RE_PASS=${TEST_RE_PASS:-"123456"} + set -e # enable job control @@ -8,7 +11,7 @@ set -m /opt/start.sh & # This command queries the REST API and outputs the status code -CURL_CMD="curl --silent --fail --output /dev/null -i -w %{http_code} -u demo@redislabs.com:123456 -k https://localhost:9443/v1/nodes" +CURL_CMD="curl --silent --fail --output /dev/null -i -w %{http_code} -u $TEST_RE_USER:$TEST_RE_PASS -k https://localhost:9443/v1/nodes" # Wait to get 2 consecutive 200 responses from the REST API while true @@ -30,9 +33,9 @@ do fi done -echo "Running Python script to create databases..." -python3 create_dbs.py +echo "Creating databases..." +curl -k -u "$TEST_RE_USER:$TEST_RE_PASS" --request POST --url "https://localhost:9443/v1/bdbs" --header 'content-type: application/json' --data-binary "@db.json" # now we bring the primary process back into the foreground # and leave it there diff --git a/redisinsight/api/test/test-runs/re-st/Dockerfile b/redisinsight/api/test/test-runs/re-st/Dockerfile index 136d43fbc3..8af7097d05 100644 --- a/redisinsight/api/test/test-runs/re-st/Dockerfile +++ b/redisinsight/api/test/test-runs/re-st/Dockerfile @@ -1,18 +1,9 @@ -FROM redislabs/redis:6.0.8-28.bionic +FROM redislabs/redis:6.2.8-50 -# Change user to root to install pip -USER root -RUN set -ex \ - && apt-get update \ - && apt-get install -y python3-pip \ - && pip3 install requests -# Change user back to redislabs -USER redislabs - -# Set the env var to instruct RE to create a cluster on startup +## Set the env var to instruct RE to create a cluster on startup ENV BOOTSTRAP_ACTION create_cluster ENV BOOTSTRAP_CLUSTER_FQDN cluster.local -COPY run_re_and_create_db.sh create_dbs.py cert.pem ./ +COPY entrypoint.sh db.json ./ -ENTRYPOINT [ "bash", "./run_re_and_create_db.sh" ] +ENTRYPOINT [ "bash", "./entrypoint.sh" ] diff --git a/redisinsight/api/test/test-runs/re-st/README.md b/redisinsight/api/test/test-runs/re-st/README.md deleted file mode 100644 index 6b65582c5a..0000000000 --- a/redisinsight/api/test/test-runs/re-st/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# docker-redisenterprise-testdb -A Docker container that creates test databases on a Redis Enterprise cluster - - -## Databases - -Environment variable control which dbs are created. By default, no db is created. -- `CREATE_SIMPLE_DB`: Single-shard simple database on port 12000 -- `CREATE_CLUSTER_DB`: Database-clustering enabled, with 3 shards on port 12010 -- `CREATE_TLS_DB`: Single-shard TLS database on port 12443 -- `CREATE_TLS_MUTUAL_AUTH_DB`: Single-shard TLS client authentication enabled database on port 12465 -- `CREATE_MODULES_DB`: Single-shard db with modules: RedisGraph, RediSearch and RedisTimeSeries on port 12003 -- `CREATE_CRDB`: CRDT database on port 12005. `CRDB_INSTANCES` env var should also be set to a space-separated list of the FQDNs of participating clusters. - - -## References - -- [Redis Enterprise REST API Docs](https://storage.googleapis.com/rlecrestapi/rest-html/http_rest_api.html) diff --git a/redisinsight/api/test/test-runs/re-st/cert.pem b/redisinsight/api/test/test-runs/re-st/cert.pem deleted file mode 100644 index 3dfb16abf7..0000000000 --- a/redisinsight/api/test/test-runs/re-st/cert.pem +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFfzCCA2egAwIBAgIURYhz7wsPwNGHxFoINaEB6ysJyEYwDQYJKoZIhvcNAQEL -BQAwTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRgwFgYDVQQKDA9SZWRpc0xh -YnMsIEluYy4xGTAXBgNVBAMMEHJlZGlzaW5zaWdodC5jb20wHhcNMjAxMjI4MTA1 -NzU1WhcNMjExMjI4MTA1NzU1WjBPMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0Ex -GDAWBgNVBAoMD1JlZGlzTGFicywgSW5jLjEZMBcGA1UEAwwQcmVkaXNpbnNpZ2h0 -LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANS1OFxNjFn1JQJO -HounllmdHA0hMCoZ5DGO1rur+ppZBZFh9Z4NT01neEVMi9vZkvsmeGQ6xwOibbGv -EClNXrFS/pqBg91AdGJzz6lH08VHgAtu+2P6kDewTXQyN+Gu+3qlss4t3di9jQAd -oYXkbPT5ZPnaJtkWq+rrw8P7hCC0OARETzysg/IVukyYYIGjgpeCOWOWeF4oGurO -EQtZnuVncMC4ZvobGlGtXk0Rk46j1uwLDNgovLkajnMHViGxS/kCOSZB2UJvhve/ -YmKK14kxc/mFoNu1+ING5bGEcprVUe8wKXb+TuTRp2YxIB1GIG8+vQwcYFS5l0kH -BsRGwBKS2ESaSQ1eSyIVd2wdXVHqlILlmy2Zvi9DM/kMX/OtoBjIDhWx9mStTxtz -DjHIooT/FeFQzC2ah1bP+/KYabCHScEXpXxubpK9saXLtj4Vk/RcfZ42+0eeVxBf -Dttln7MHP79VyyCZpT9OSu8q4qU6dVDlz3fczC6fkE6b2kPVQnSLz9Wmr47syawg -Argv96d6wcNiiNzOyHZNCaxHwsVFx0zJOuRiyMwJp4JvrAb2glkKgCzVNjCMO+8v -HuXv6TTUHKvLwqnuqe04VRQazIDUPzQma+whdgIMkAJBanm7U+fmZ/LX0d1XLCMv -E5k/zzu9uOXUNM46Pvz20Rko8W4fAgMBAAGjUzBRMB0GA1UdDgQWBBSbd9FdsIfw -AUWS9kE93p5MGmv4WTAfBgNVHSMEGDAWgBSbd9FdsIfwAUWS9kE93p5MGmv4WTAP -BgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCvXOzCaEGf6xoA8A7S -U5GXv5rGof1TW9+FogiUc1GyvEJJa4spo2MEASBCkqN0e5ac4tE3XiMsPXgoMCNt -XFRa71rI1a61an+02+i5hgG6fBTftTLRaMOWKXswiF/GN+Jc2aYPNy9KZtwxowre -g28a47vlAUT3O4ZdpdR4TVD6Zvy/EtA/TfuP9XPBA1TaAp6jEUArZFU3H0VM6nn3 -1GKH8rxqv54jLovA7ASs6CKU8PtgU0RDypB69lYVWjbVdrSA5Jh6o54N60tNizCk -LzaEbSGijI6qeKwouFoqKqx+Kr9aYPnGnA1rJXWVvI1Z8XIsWmb9fFnl9nPspqxF -A6vuvc2W9TIFLvwZIe/QC4kKq07rA7zCiF/dsjqU1VdeoJZXp0109t7Ua0tlUO/J -XIIY8sQHyJ2kZAiL9ghCHvgO5ciezVvu6ru1E6M8FD0OaopPa2jM4V562sZ/Ztdi -7IPlQ160zsHuYq3Q4uYNalWJ7gLaxbXFaok/fZo39GRVzjk/FN8zuXuGjMchhoaB -XrWDF0H6NM92cvH/8cgSg7JXvBlCdGqV9XPnqYm312MqeFZ9Lg2H8wTfHuAzNrOZ -eV1Cp5mBepvYtsFX3ATdDg7+QgAVhLdnAyUmMNY2cW1GTdfQV4L3fIm0S7RKayC6 -eeFbQeTv19tELZiXhalV3YuJMQ== ------END CERTIFICATE----- diff --git a/redisinsight/api/test/test-runs/re-st/create_dbs.py b/redisinsight/api/test/test-runs/re-st/create_dbs.py deleted file mode 100644 index 422f57b832..0000000000 --- a/redisinsight/api/test/test-runs/re-st/create_dbs.py +++ /dev/null @@ -1,215 +0,0 @@ -import os -import pprint -import subprocess - -import requests - - -# Suppress "Unverified HTTPS request" warnings -# See https://github.com/influxdata/influxdb-python/issues/240#issuecomment-140003499 -# pylint: disable=no-member -requests.packages.urllib3.disable_warnings() - - -CREATE_SIMPLE_DB = bool(os.environ.get("CREATE_SIMPLE_DB", "")) -CREATE_CLUSTER_DB = bool(os.environ.get("CREATE_CLUSTER_DB", "")) -CREATE_TLS_DB = bool(os.environ.get("CREATE_TLS_DB", "")) -CREATE_TLS_MUTUAL_AUTH_DB = bool(os.environ.get("CREATE_TLS_MUTUAL_AUTH_DB", "")) -CREATE_MODULES_DB = bool(os.environ.get("CREATE_MODULES_DB", "")) -CREATE_CRDB = bool(os.environ.get("CREATE_CRDB", "")) -CRDB_INSTANCES = os.environ.get("CRDB_INSTANCES", "") - - -USERNAME = 'demo@redislabs.com' -PASSWORD = '123456' - - -RLEC_API_BASE_URL = 'https://localhost:9443/v1' - - -COMMON_REQ_PARAMS = dict(auth=(USERNAME, PASSWORD), - verify=False,) - - -def get_module_data() -> dict: - """ - Returns a dict of module name to a dict containing: - - module_id - - module_name - - module_args - - semantic_version - """ - resp = requests.get(url=f'{RLEC_API_BASE_URL}/modules', **COMMON_REQ_PARAMS) - if not resp.ok: - raise Exception(f"Failed to get modules info: {resp.status_code}: {resp.text}") - data = resp.json() - module_dict = {} - for m in data: - module_dict[m['module_name']] = { - "module_id": m["uid"], - "module_name": m["module_name"], - "module_args": m["command_line_args"], - "semantic_version": m["semantic_version"], - } - return module_dict - - -def create_db(body: dict) -> dict: - """ - Create a bdb and return the response from the API. - """ - resp = requests.post(url=f'{RLEC_API_BASE_URL}/bdbs', - json=body, - **COMMON_REQ_PARAMS) - if not resp.ok: - raise Exception(f"Failed to create db: {resp.status_code}: {resp.text}") - data = resp.json() - return data - - -def create_simple_db() -> dict: - body = { - "name": "testdb", - "type": "redis", - "memory_size": 1024 * 1024 * 1024, # 1GB - "port": 12000 - } - return create_db(body) - - -def create_cluster_db() -> dict: - body = { - "name": "testdb", - "type": "redis", - "memory_size": 1024 * 1024 * 1024, # 1GB - "port": 12010, - "sharding": True, - "shards_count": 3, - # Default OSS Redis Cluster-like hashing policy. - # These regexes are taken from the RLEC REST API docs: - # https://storage.googleapis.com/rlecrestapi/rest-html/http_rest_api.html#bdb (see the 'shard_key_regex' attribute) - "shard_key_regex": [ - {"regex": ".*\\{(?.*)\\}.*" }, - {"regex": "(?.*)" } - ], - } - return create_db(body) - - -def create_tls_db() -> dict: - body = { - "name": "testtlsdb", - "type": "redis", - "memory_size": 100000, - "port": 12443, - "tls_mode": "enabled", - "enforce_client_authentication": "disabled" - } - return create_db(body) - - -def create_tls_mutual_auth_db() -> dict: - with open('./cert.pem') as f: - cert_str = f.read() - body = { - "name": "testtlsclientauthdb", - "type": "redis", - "memory_size": 100000, - "port": 12465, - "tls_mode": "enabled", - "enforce_client_authentication": "enabled", - "authentication_ssl_client_certs": [{ - "client_cert": cert_str, - }] - } - return create_db(body) - - -def create_modules_db(module_info: dict) -> dict: - body = { - "name": "modulesdb", - "type": "redis", - "memory_size": 100000, - "port": 12003, - "module_list": [ - module_info['ft'], - module_info['graph'], - module_info['timeseries'], - ] - } - return create_db(body) - - -def create_crdb() -> dict: - cluster_fqdns = CRDB_INSTANCES.split() - assert len(cluster_fqdns) >= 2, f"At least two clusters are needed for a CRDB, got {cluster_fqdns}" - crdb_cli_instances_args = (f"--instance fqdn={fqdn},username={USERNAME},password={PASSWORD}" - for fqdn in cluster_fqdns) - crdb_cli_instances_args = " ".join(crdb_cli_instances_args) - crdb_cli_command = f"/opt/redislabs/bin/crdb-cli crdb create --name mycrdb --memory-size 10mb --port 12005 --replication false --shards-count 1 {crdb_cli_instances_args}" - print("Running the following command:") - print(crdb_cli_command) - subprocess.run(crdb_cli_command.split()) - - -def main(): - - if CREATE_SIMPLE_DB: - print("Creating simple db...") - bdb = create_simple_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping simple db") - - if CREATE_CLUSTER_DB: - print("Creating cluster db...") - bdb = create_cluster_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping cluster db") - - if CREATE_TLS_DB: - print("Creating TLS db...") - bdb = create_tls_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping TLS db") - - if CREATE_TLS_MUTUAL_AUTH_DB: - print("Creating TLS mutual auth db...") - bdb = create_tls_mutual_auth_db() - print("done") - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping TLS mutual auth db") - - if CREATE_MODULES_DB: - print("Getting modules info...") - module_info = get_module_data() - print('done') - print("Creating modules db...") - bdb = create_modules_db(module_info) - print('done') - pprint.pprint(bdb) - print("\n\n") - else: - print("Skipping modules db") - - if CREATE_CRDB: - print("Creating CRDB...") - create_crdb() - print('done') - print("\n\n") - else: - print("Skipping CRDB") - - -if __name__ == '__main__': - main() diff --git a/redisinsight/api/test/test-runs/re-st/db.json b/redisinsight/api/test/test-runs/re-st/db.json new file mode 100644 index 0000000000..750c740eb5 --- /dev/null +++ b/redisinsight/api/test/test-runs/re-st/db.json @@ -0,0 +1,6 @@ +{ + "name": "testdb", + "type": "redis", + "memory_size": 1073741824, + "port": 12000 +} diff --git a/redisinsight/api/test/test-runs/re-st/docker-compose.yml b/redisinsight/api/test/test-runs/re-st/docker-compose.yml index e1f508e749..150b4ba1fd 100644 --- a/redisinsight/api/test/test-runs/re-st/docker-compose.yml +++ b/redisinsight/api/test/test-runs/re-st/docker-compose.yml @@ -9,9 +9,5 @@ services: build: ./re-st cap_add: - sys_resource - environment: - - CREATE_SIMPLE_DB=true -# ports: -# - 12000:12000 -# - 8443:8443 -# - 9443:9443 + env_file: + - ./re-st/.env diff --git a/tests/e2e/rte/redis-enterprise/run_re_and_create_db.sh b/redisinsight/api/test/test-runs/re-st/entrypoint.sh similarity index 70% rename from tests/e2e/rte/redis-enterprise/run_re_and_create_db.sh rename to redisinsight/api/test/test-runs/re-st/entrypoint.sh index bc72dca13d..70b5c44bdb 100644 --- a/tests/e2e/rte/redis-enterprise/run_re_and_create_db.sh +++ b/redisinsight/api/test/test-runs/re-st/entrypoint.sh @@ -1,5 +1,8 @@ #! /bin/bash +TEST_RE_USER=${TEST_RE_USER:-"demo@redislabs.com"} +TEST_RE_PASS=${TEST_RE_PASS:-"123456"} + set -e # enable job control @@ -8,7 +11,7 @@ set -m /opt/start.sh & # This command queries the REST API and outputs the status code -CURL_CMD="curl --silent --fail --output /dev/null -i -w %{http_code} -u demo@redislabs.com:123456 -k https://redis-enterprise:9443/v1/nodes" +CURL_CMD="curl --silent --fail --output /dev/null -i -w %{http_code} -u $TEST_RE_USER:$TEST_RE_PASS -k https://localhost:9443/v1/nodes" # Wait to get 2 consecutive 200 responses from the REST API while true @@ -30,9 +33,9 @@ do fi done -echo "Running Python script to create databases..." -python3 create_dbs.py +echo "Creating databases..." +curl -k -u "$TEST_RE_USER:$TEST_RE_PASS" --request POST --url "https://localhost:9443/v1/bdbs" --header 'content-type: application/json' --data-binary "@db.json" # now we bring the primary process back into the foreground # and leave it there diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index 9ebe25d550..846879c23a 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -3686,6 +3686,15 @@ fs-extra@9.1.0, fs-extra@^9.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-minipass@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" diff --git a/redisinsight/main.dev.ts b/redisinsight/main.dev.ts index a4564209a6..1195c401ef 100644 --- a/redisinsight/main.dev.ts +++ b/redisinsight/main.dev.ts @@ -44,14 +44,18 @@ if (process.env.NODE_ENV !== 'production') { log.info('App starting.....'); export default class AppUpdater { - constructor() { + constructor(url: string = '') { log.info('AppUpdater initialization'); log.transports.file.level = 'info'; - autoUpdater.setFeedURL({ - provider: 'generic', - url: process.env.MANUAL_UPGRADES_LINK || process.env.UPGRADES_LINK, - }); + try { + autoUpdater.setFeedURL({ + provider: 'generic', + url, + }); + } catch (error) { + log.error(error); + } autoUpdater.checkForUpdatesAndNotify(); autoUpdater.autoDownload = true; @@ -115,8 +119,10 @@ const bootstrap = async () => { trayInstance = tray.buildTray(); } - if (process.env.NODE_ENV === 'production') { - new AppUpdater(); + const upgradeUrl = process.env.MANUAL_UPGRADES_LINK || process.env.UPGRADES_LINK; + + if (upgradeUrl) { + new AppUpdater(upgradeUrl); } app.setName('RedisInsight'); @@ -132,7 +138,7 @@ const bootstrap = async () => { export const windows = new Set(); -const titleSplash = 'splash'; +const titleSplash = 'RedisInsight'; export const createSplashScreen = async () => { const splash = new BrowserWindow({ width: 500, @@ -149,7 +155,7 @@ export const createSplashScreen = async () => { return splash; }; -export const createWindow = async (splash: BrowserWindow | null) => { +export const createWindow = async (splash: BrowserWindow | null = null) => { const RESOURCES_PATH = app.isPackaged ? path.join(process.resourcesPath, 'resources') : path.join(__dirname, '../resources'); @@ -179,11 +185,10 @@ export const createWindow = async (splash: BrowserWindow | null) => { webPreferences: { nodeIntegration: true, nodeIntegrationInWorker: true, - webSecurity: false, + webSecurity: true, contextIsolation: false, spellcheck: true, - allowRunningInsecureContent: true, - enableRemoteModule: true, + allowRunningInsecureContent: false, scrollBounce: true, }, }); diff --git a/redisinsight/menu.ts b/redisinsight/menu.ts index 2522cff033..d7e336b4fd 100644 --- a/redisinsight/menu.ts +++ b/redisinsight/menu.ts @@ -156,7 +156,7 @@ export default class MenuBuilder { { label: 'License Terms', click() { - shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/master/LICENSE'); + shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/main/LICENSE'); }, }, { @@ -256,7 +256,7 @@ export default class MenuBuilder { { label: 'License Terms', click() { - shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/master/LICENSE'); + shell.openExternal('https://github.com/RedisInsight/RedisInsight/blob/main/LICENSE'); }, }, { diff --git a/redisinsight/package.json b/redisinsight/package.json index d3cbfca0e5..d7ddbcae9b 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "productName": "RedisInsight", "private": true, - "version": "2.0.2-preview", + "version": "2.0.3-preview", "description": "RedisInsight", "main": "./main.prod.js", "author": { diff --git a/redisinsight/splash.html b/redisinsight/splash.html index 5763e7345a..b25a28ad9b 100644 --- a/redisinsight/splash.html +++ b/redisinsight/splash.html @@ -10,39 +10,23 @@ #app { width: 100%; height: 100%; - } - - .DARK { - background-color: #202020; + background-size: cover; color: #ffffff; - } - - .LIGHT { - background-color: #FFFFFF; - color: #202020; - } - - .DARK #Group_4 { - fill: #FFFFFF; - } - - .LIGHT #Group_4 { - fill: #202020; - } - - .DARK .copyright { - color: #B4B4B4 - } - - .LIGHT .copyright { - color: #69707D + background-image: url(data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/7AARRHVja3kAAQAEAAAAPAAA/+EDL2h0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZWRhMmIzZmFjLCAyMDIxLzExLzE3LTE3OjIzOjE5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjMuMSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QkQyQTg3MDY2M0RGMTFFQ0IzMDE5MTczRjQ1RjM4RTYiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QkQyQTg3MDc2M0RGMTFFQ0IzMDE5MTczRjQ1RjM4RTYiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpCRDJBODcwNDYzREYxMUVDQjMwMTkxNzNGNDVGMzhFNiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpCRDJBODcwNTYzREYxMUVDQjMwMTkxNzNGNDVGMzhFNiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMABAMDBAMDBAQEBAUFBAUHCwcHBgYHDgoKCAsQDhEREA4QDxIUGhYS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); } .container { width: 400px; text-align: center; margin: 0 auto; + } + + #Group_4 { + fill: #FFFFFF; + } + .copyright { + color: #ffffff } .logo { @@ -90,7 +74,7 @@ - RedisInsight 2.0.2-preview © 2021 Redis Ltd. + RedisInsight 2.0.3-preview © 2021 Redis Ltd. diff --git a/redisinsight/ui/src/App.tsx b/redisinsight/ui/src/App.tsx index 63cc50a085..61b458b345 100644 --- a/redisinsight/ui/src/App.tsx +++ b/redisinsight/ui/src/App.tsx @@ -7,7 +7,7 @@ import Router from './Router' import store from './slices/store' import { Theme } from './constants' import { themeService } from './services' -import { NavigationMenu, Notifications, Config } from './components' +import { NavigationMenu, Notifications, Config, ShortcutsFlyout } from './components' import { ThemeProvider } from './contexts/themeContext' import MainComponent from './components/main/MainComponent' @@ -32,6 +32,7 @@ const App = ({ children }: { children?: ReactElement }) => ( + diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Light.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Light.otf deleted file mode 100644 index 89b54757af..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Light.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Light.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Light.woff2 new file mode 100644 index 0000000000..12cd6e44f8 Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Light.woff2 differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-LightItalic.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-LightItalic.otf deleted file mode 100644 index 07778948b4..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-LightItalic.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-LightItalic.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-LightItalic.woff2 new file mode 100644 index 0000000000..3d6136a2db Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-LightItalic.woff2 differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Medium.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Medium.otf deleted file mode 100644 index 5d510bb174..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Medium.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Medium.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Medium.woff2 new file mode 100644 index 0000000000..6214da69c8 Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Medium.woff2 differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-MediumItalic.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-MediumItalic.otf deleted file mode 100644 index 5847de0111..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-MediumItalic.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-MediumItalic.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-MediumItalic.woff2 new file mode 100644 index 0000000000..983076566c Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-MediumItalic.woff2 differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Regular.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Regular.otf deleted file mode 100644 index 16985093d9..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Regular.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Regular.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Regular.woff2 new file mode 100644 index 0000000000..b1fd3d4b69 Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Regular.woff2 differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-RegularItalic.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-RegularItalic.otf deleted file mode 100644 index 231c2b6774..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-RegularItalic.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-RegularItalic.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-RegularItalic.woff2 new file mode 100644 index 0000000000..b87af84268 Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-RegularItalic.woff2 differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Semibold.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Semibold.otf deleted file mode 100644 index 1979fd830a..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Semibold.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-Semibold.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Semibold.woff2 new file mode 100644 index 0000000000..4ba509b5d2 Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-Semibold.woff2 differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-SemiboldItalic.otf b/redisinsight/ui/src/assets/fonts/graphik/Graphik-SemiboldItalic.otf deleted file mode 100644 index 25b3933538..0000000000 Binary files a/redisinsight/ui/src/assets/fonts/graphik/Graphik-SemiboldItalic.otf and /dev/null differ diff --git a/redisinsight/ui/src/assets/fonts/graphik/Graphik-SemiboldItalic.woff2 b/redisinsight/ui/src/assets/fonts/graphik/Graphik-SemiboldItalic.woff2 new file mode 100644 index 0000000000..65a9c357dc Binary files /dev/null and b/redisinsight/ui/src/assets/fonts/graphik/Graphik-SemiboldItalic.woff2 differ diff --git a/redisinsight/ui/src/assets/img/NYbg.jpg b/redisinsight/ui/src/assets/img/NYbg.jpg new file mode 100644 index 0000000000..04b7299e35 Binary files /dev/null and b/redisinsight/ui/src/assets/img/NYbg.jpg differ diff --git a/redisinsight/ui/src/assets/img/active_auto.svg b/redisinsight/ui/src/assets/img/active_auto.svg index bc73d0dc30..54fffbbf6a 100644 --- a/redisinsight/ui/src/assets/img/active_auto.svg +++ b/redisinsight/ui/src/assets/img/active_auto.svg @@ -2,7 +2,7 @@ - - - - - diff --git a/redisinsight/ui/src/assets/img/active_manual.svg b/redisinsight/ui/src/assets/img/active_manual.svg index e135078f2b..b4b393c38f 100644 --- a/redisinsight/ui/src/assets/img/active_manual.svg +++ b/redisinsight/ui/src/assets/img/active_manual.svg @@ -16,16 +16,16 @@ c0.466,0,0.845,0.378,0.845,0.845c0,0.466-0.378,0.844-0.845,0.844H7.238C6.793,44.222,6.44,43.871,6.389,43.426z"/> - - - - - - - - - - diff --git a/redisinsight/ui/src/assets/img/light_theme/active_manual.svg b/redisinsight/ui/src/assets/img/light_theme/active_manual.svg index 8e664b831d..1ad7a3251c 100644 --- a/redisinsight/ui/src/assets/img/light_theme/active_manual.svg +++ b/redisinsight/ui/src/assets/img/light_theme/active_manual.svg @@ -16,16 +16,16 @@ c0.466,0,0.845,0.378,0.845,0.845c0,0.466-0.378,0.844-0.845,0.844H7.238C6.793,44.222,6.44,43.871,6.389,43.426z"/> - - - - - + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/multi_play_icon_light.svg b/redisinsight/ui/src/assets/img/multi_play_icon_light.svg new file mode 100644 index 0000000000..bc8ebf9f9c --- /dev/null +++ b/redisinsight/ui/src/assets/img/multi_play_icon_light.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/redisinsight/ui/src/assets/img/not_active_auto.svg b/redisinsight/ui/src/assets/img/not_active_auto.svg index 7c4d4af8f3..bd1ea53739 100644 --- a/redisinsight/ui/src/assets/img/not_active_auto.svg +++ b/redisinsight/ui/src/assets/img/not_active_auto.svg @@ -2,7 +2,7 @@ - - - - - diff --git a/redisinsight/ui/src/assets/img/not_active_manual.svg b/redisinsight/ui/src/assets/img/not_active_manual.svg index 4875892874..a996370837 100644 --- a/redisinsight/ui/src/assets/img/not_active_manual.svg +++ b/redisinsight/ui/src/assets/img/not_active_manual.svg @@ -16,16 +16,16 @@ c0.466,0,0.845,0.378,0.845,0.845c0,0.466-0.378,0.844-0.845,0.844H7.238C6.793,44.222,6.44,43.871,6.389,43.426z"/> - - - - -