├── .editorconfig ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── examples ├── example01.js ├── example02.js └── example03.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── Query.js ├── QuerySchema.js ├── RedisTimeSeries.js └── commands │ ├── add.js │ ├── alter.js │ ├── create.js │ ├── createRule.js │ ├── decrBy.js │ ├── deleteRule.js │ ├── fragments │ ├── aggregation.js │ ├── count.js │ ├── duplicatePolicy.js │ ├── filter.js │ ├── index.js │ ├── labels.js │ ├── onDuplicate.js │ ├── retention.js │ ├── timestamp.js │ ├── uncompressed.js │ └── withLabels.js │ ├── get.js │ ├── incrBy.js │ ├── index.js │ ├── info.js │ ├── madd.js │ ├── mget.js │ ├── mrange.js │ ├── mrevrange.js │ ├── queryIndex.js │ ├── range.js │ ├── revrange.js │ └── utils │ ├── Filter.js │ ├── Validator.js │ └── index.js └── tests ├── __mocks__ └── redis.js ├── components ├── Query.test.js ├── QuerySchema.test.js ├── RedisTimeSeries.test.js └── filter.test.js ├── constants.js └── methods ├── add.test.js ├── alter.test.js ├── create.test.js ├── createRule.test.js ├── decrby.test.js ├── deleteRule.test.js ├── get.test.js ├── incrby.test.js ├── info.test.js ├── madd.test.js ├── mget.test.js ├── mrange.test.js ├── mrevrange.test.js ├── queryIndex.test.js ├── range.test.js └── revrange.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = null 12 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x, 14.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | npm run test-coverage 25 | env: 26 | CI: true 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v1 29 | with: 30 | token: ${{ secrets.CODECOV_TOKEN }} #required 31 | file: ./coverage-report/clover.xml 32 | name: redistimeseries-js-node-v${{ matrix.node-version }} 33 | fail_ci_if_error: true 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Jest coverage report 107 | coverage-report 108 | 109 | .idea 110 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Milos Nikolovski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Tests](https://github.com/nikolovskimilos/redistimeseries-js/workflows/Tests/badge.svg) 2 | [![npm version](https://badge.fury.io/js/redistimeseries-js.svg)](https://badge.fury.io/js/redistimeseries-js) 3 | 4 | 5 | # redistimeseries-js 6 | 7 | JavaScript client for [RedisTimeSeries](https://github.com/RedisLabsModules/redis-timeseries) 8 | 9 | ``` 10 | npm install redistimeseries-js 11 | ``` 12 | 13 | 14 | #### Example 15 | 16 | ```javascript 17 | const RedisTimeSeries = require('redistimeseries-js'); 18 | 19 | 20 | // check base redis client for options 21 | // https://github.com/NodeRedis/node_redis 22 | const options = { 23 | host: 'localhost', 24 | port: 6379 25 | } 26 | 27 | const rtsClient = new RedisTimeSeries(options); 28 | const key = 'temperature'; 29 | 30 | const updateTemperature = async () => { 31 | await rtsClient.add(key, Date.now(), Math.floor(Math.random()*30)).send(); 32 | } 33 | 34 | const start = async () => { 35 | await rtsClient.connect(); 36 | await rtsClient.create(key).retention(60000).send(); 37 | setInterval(updateTemperature, 1000); 38 | } 39 | 40 | start(); 41 | 42 | ``` 43 | 44 | 45 | ## Filtering 46 | 47 | Filters are represented as array of conditions 48 | 49 | ```javascript 50 | const { Filter } = require('redistimeseries-js'); 51 | 52 | const myFilter = [ 53 | Filter.equal('area_id', 32), 54 | Filter.notEqual('sensor_id', 1), 55 | Filter.exists('sub_area_id'), 56 | Filter.notExists('outdoor'), 57 | Filter.in('secitons', [2, 3, 4]), 58 | Filter.notIn('secitons', [5, 6]) 59 | ]; 60 | 61 | ``` 62 | 63 | ## Aggregation 64 | 65 | Possible aggregation values 66 | 67 | ```javascript 68 | const { Aggregation } = require('redistimeseries-js'); 69 | 70 | // Aggregation.AVG 71 | // Aggregation.SUM 72 | // Aggregation.MIN 73 | // Aggregation.MAX 74 | // Aggregation.RANGE 75 | // Aggregation.COUNT 76 | // Aggregation.FIRST 77 | // Aggregation.LAST 78 | // Aggregation.STDP 79 | // Aggregation.STDS 80 | // Aggregation.VARP 81 | // Aggregation.VARS 82 | 83 | ``` 84 | 85 | 86 | ## Duplicate Policy 87 | 88 | Possible duplicate policy values 89 | 90 | ```javascript 91 | const { DuplicatePolicy } = require('redistimeseries-js'); 92 | 93 | // DuplicatePolicy.BLOCK 94 | // DuplicatePolicy.FIRST 95 | // DuplicatePolicy.LAST 96 | // DuplicatePolicy.MIN 97 | // DuplicatePolicy.MAX 98 | // DuplicatePolicy.SUM 99 | 100 | ``` 101 | 102 | 103 | 104 | 105 | ## Methods 106 | 107 | Examples for each method are shown below, notice that optional parameters are always represented as object which is the last argument in the methods. 108 | 109 | #### create 110 | ```javascript 111 | // TS.CREATE temperature:2:32 RETENTION 60000 LABELS sensor_id 2 area_id 32 UNCOMPRESSED 112 | 113 | const RedisTimeSeries = require('redistimeseries-js'); 114 | const { DuplicatePolicy } = RedisTimeSeries; 115 | 116 | client 117 | .create('temperature:2:32') 118 | .retention(60000) 119 | .labels({ sensor_id: 2, area_id: 32 }) 120 | .duplicatePolicy(DuplicatePolicy.LAST) 121 | .uncompressed() 122 | .send(); 123 | ``` 124 | 125 | #### alter 126 | ```javascript 127 | // TS.ALTER temperature:2:32 LABELS sensor_id 2 area_id 32 sub_area_id 15 128 | 129 | client 130 | .alter('temperature:2:32') 131 | .labels({ sensor_id: 2, area_id: 32, sub_area_id: 15 }) 132 | .send(); 133 | ``` 134 | 135 | #### add 136 | ```javascript 137 | // TS.ADD temperature:2:32 1548149180000 26 LABELS sensor_id 2 area_id 32 138 | 139 | const RedisTimeSeries = require('redistimeseries-js'); 140 | const { DuplicatePolicy } = RedisTimeSeries; 141 | 142 | client 143 | .add('temperature:2:32', 1548149180000, 26) 144 | .onDuplicate(DuplicatePolicy.SUM) 145 | .labels({ sensor_id: 2, area_id: 32 }) 146 | .send(); 147 | ``` 148 | ```javascript 149 | // TS.ADD temperature:3:11 1548149183000 27 RETENTION 3600 150 | 151 | client 152 | .add('temperature:2:32', 1548149180000, 26) 153 | .retention(3600) 154 | .send(); 155 | ``` 156 | 157 | #### madd 158 | ```javascript 159 | // TS.MADD temperature:2:32 1548149180000 26 cpu:2:32 1548149183000 54 160 | 161 | client 162 | .madd([ 163 | { key: 'temperature:2:32', timestamp: 1548149180000, value: 26 }, 164 | { key: 'cpu:2:32', timestamp: 1548149183000, value: 54 } 165 | ]); 166 | .send(); 167 | ``` 168 | 169 | #### incrBy 170 | ```javascript 171 | // TS.INCRBY temperature:2:32 3 RETENTION 30000 172 | 173 | client 174 | .incrBy('temperature:2:32', 3) 175 | .retention(30000) 176 | .send(); 177 | ``` 178 | 179 | #### decrBy 180 | ```javascript 181 | // TS.DECRBY temperature:2:32 5 RETENTION 30000 UNCOMPRESSED 182 | 183 | client 184 | .decrBy('temperature:2:32', 5) 185 | .retention(30000) 186 | .uncompressed() 187 | .send(); 188 | ``` 189 | 190 | #### createRule 191 | ```javascript 192 | // TS.CREATERULE temperature:2:32 temperature:avg AGGREGATION avg 60000 193 | 194 | const RedisTimeSeries = require('redistimeseries-js'); 195 | const { Aggregation } = RedisTimeSeries; 196 | 197 | // ... 198 | 199 | client 200 | .createRule('temperature:2:32', 'temperature:avg') 201 | .aggregation(Aggregation.AVG, 60000) 202 | .send(); 203 | ``` 204 | 205 | #### deleteRule 206 | ```javascript 207 | // TS.DELETE temperature:2:32 temperature:avg 208 | 209 | client.deleteRule('temperature:2:32', 'temperature:avg').send(); 210 | ``` 211 | 212 | #### range 213 | ```javascript 214 | // TS.RANGE temperature:3:32 1548149180000 1548149210000 AGGREGATION avg 5000 215 | // TS.REVRANGE temperature:3:32 1548149180000 1548149210000 AGGREGATION avg 5000 216 | 217 | const RedisTimeSeries = require('redistimeseries-js'); 218 | const { Aggregation } = RedisTimeSeries; 219 | 220 | // ... 221 | 222 | client 223 | .range('temperature:2:32', 1548149180000, 1548149210000) 224 | .aggregation(Aggregation.AVG, 5000) 225 | .send(); 226 | ``` 227 | 228 | #### mrange 229 | ```javascript 230 | // TS.MRANGE 1548149180000 1548149210000 AGGREGATION avg 5000 FILTER area_id=32 sensor_id!=1 231 | // TS.MREVRANGE 1548149180000 1548149210000 AGGREGATION avg 5000 FILTER area_id=32 sensor_id!=1 232 | 233 | const RedisTimeSeries = require('redistimeseries-js'); 234 | const { Aggregation, Filter } = RedisTimeSeries; 235 | 236 | // ... 237 | 238 | client 239 | .mrange('temperature:2:32', 1548149180000, 1548149210000) 240 | .filter([ 241 | Filter.equal('area_id', 32), 242 | Filter.notEqual('sensor_id', 1) 243 | ]) 244 | .aggregation(Aggregation.AVG, 5000) 245 | .send(); 246 | ``` 247 | 248 | ```javascript 249 | // TS.MRANGE 1548149180000 1548149210000 AGGREGATION avg 5000 WITHLABELS FILTER area_id=32 sensor_id!=1 250 | // TS.MREVRANGE 1548149180000 1548149210000 AGGREGATION avg 5000 WITHLABELS FILTER area_id=32 sensor_id!=1 251 | 252 | const RedisTimeSeries = require('redistimeseries-js'); 253 | const { Aggregation, Filter } = RedisTimeSeries; 254 | 255 | // ... 256 | 257 | client 258 | .mrevrange('temperature:2:32', 1548149180000, 1548149210000) 259 | .aggregation(Aggregation.AVG, 5000) 260 | .withLabels() 261 | .filter([ 262 | Filter.equal('area_id', 32), 263 | Filter.notEqual('sensor_id', 1) 264 | ]) 265 | .send(); 266 | ``` 267 | 268 | 269 | #### get 270 | ```javascript 271 | // TS.GET temperature:2:32 272 | 273 | client 274 | .get('temperature:2:32') 275 | .send(); 276 | ``` 277 | 278 | #### mget 279 | ```javascript 280 | // TS.MGET FILTER area_id=32 281 | 282 | const RedisTimeSeries = require('redistimeseries-js'); 283 | const { Filter } = RedisTimeSeries; 284 | 285 | // ... 286 | 287 | client 288 | .mget() 289 | .filter([ Filter.equal('area_id', 32) ]) 290 | .send(); 291 | ``` 292 | 293 | #### info 294 | ```javascript 295 | // TS.INFO temperature:2:32 296 | 297 | client 298 | .info('temperature:2:32') 299 | .send(); 300 | ``` 301 | 302 | #### queryIndex 303 | ```javascript 304 | // TS.QUERYINDEX sensor_id=2 305 | 306 | client 307 | .queryIndex([ Filter.equal('sensor_id', 2) ]) 308 | .send(); 309 | ``` 310 | -------------------------------------------------------------------------------- /examples/example01.js: -------------------------------------------------------------------------------- 1 | const RedisTimeSeries = require('../index'); 2 | 3 | const rtsClient = new RedisTimeSeries(); 4 | 5 | const key = 'temperature'; 6 | const retention = 60000; 7 | 8 | const updateTemperature = async () => { 9 | await rtsClient.add(key, Date.now(), Math.floor(Math.random() * 30)).send(); 10 | }; 11 | 12 | const start = async () => { 13 | await rtsClient.connect(); 14 | await rtsClient.create(key).retention(retention).uncompressed().send(); 15 | setInterval(updateTemperature, 1000); 16 | }; 17 | 18 | start(); 19 | -------------------------------------------------------------------------------- /examples/example02.js: -------------------------------------------------------------------------------- 1 | const RedisTimeSeries = require('../index'); 2 | 3 | const { Aggregation } = RedisTimeSeries; 4 | 5 | const rtsClient = new RedisTimeSeries(); 6 | 7 | const key = 'temperature'; 8 | const keyAvgTemp = 'temperature:avg'; 9 | const keyMaxTemp = 'temperature:max'; 10 | const keyMinTemp = 'temperature:min'; 11 | 12 | const updateTemperature = async () => { 13 | const timestamp = Date.now(); 14 | const value = Math.floor(Math.random() * 30); 15 | const section = Math.floor(Math.random() * 5); 16 | 17 | await rtsClient 18 | .add(key, timestamp, value) 19 | .labels({ section }) 20 | .send(); 21 | }; 22 | 23 | const readTemperature = async () => { 24 | const avg = await rtsClient.get(keyAvgTemp).send(); 25 | const max = await rtsClient.get(keyMaxTemp).send(); 26 | const min = await rtsClient.get(keyMinTemp).send(); 27 | 28 | console.log('average', avg); 29 | console.log('maximum', max); 30 | console.log('minimum', min); 31 | }; 32 | 33 | const start = async () => { 34 | await rtsClient.connect(); 35 | await rtsClient.create(key).send(); 36 | await rtsClient.create(keyAvgTemp).send(); 37 | await rtsClient.create(keyMaxTemp).send(); 38 | await rtsClient.create(keyMinTemp).send(); 39 | 40 | await rtsClient 41 | .createRule(key, keyAvgTemp) 42 | .aggregation(Aggregation.AVG, 60000) 43 | .send(); 44 | 45 | await rtsClient 46 | .createRule(key, keyMaxTemp) 47 | .aggregation(Aggregation.MAX, 60000) 48 | .send(); 49 | 50 | await rtsClient 51 | .createRule(key, keyMinTemp) 52 | .aggregation(Aggregation.MIN, 60000) 53 | .send(); 54 | 55 | // update temperature every second 56 | setInterval(updateTemperature, 1000); 57 | 58 | // read temperature avg, max and min values every 5 seconds 59 | setInterval(readTemperature, 5000); 60 | }; 61 | 62 | start(); 63 | -------------------------------------------------------------------------------- /examples/example03.js: -------------------------------------------------------------------------------- 1 | const RedisTimeSeries = require('../index'); 2 | 3 | const rtsClient = new RedisTimeSeries(); 4 | 5 | const key = 'temperatures'; 6 | const duplicatePolicy = 'LAST'; 7 | 8 | const updateTemperature = async () => { 9 | await rtsClient.add(key, Date.now(), Math.floor(Math.random() * 30)).send(); 10 | }; 11 | 12 | const start = async () => { 13 | await rtsClient.connect(); 14 | await rtsClient.create(key).duplicatePolicy(duplicatePolicy).uncompressed().send(); 15 | setInterval(updateTemperature, 1000); 16 | }; 17 | 18 | start(); 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | const RedisTimeSeries = require('./src/RedisTimeSeries'); 3 | const commands = require('./src/commands'); 4 | 5 | module.exports = RedisTimeSeries; 6 | 7 | const setExports = (schemas) => { 8 | Object.values(schemas).forEach((querySchema) => { 9 | const schemaExports = querySchema.getExports(); 10 | if (schemaExports) { 11 | Object.keys(schemaExports).forEach(name => 12 | module.exports[name] = schemaExports[name] 13 | ); 14 | } 15 | }); 16 | } 17 | 18 | setExports(commands); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redistimeseries-js", 3 | "version": "2.3.1", 4 | "description": "JavaScript client for RedisTimeSeries", 5 | "author": "Milos Nikolovski", 6 | "license": "MIT", 7 | "homepage": "https://github.com/nikolovskimilos/redistimeseries-js#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/nikolovskimilos/redistimeseries-js.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/nikolovskimilos/redistimeseries-js/issues" 14 | }, 15 | "keywords": [ 16 | "redis", 17 | "redis-client", 18 | "timeseries", 19 | "javascript", 20 | "nodejs" 21 | ], 22 | "files": [ 23 | "src/" 24 | ], 25 | "main": "index.js", 26 | "scripts": { 27 | "lint": "eslint --color ./src ./tests ./examples", 28 | "lint-fix": "eslint --fix --color ./src ./tests ./examples", 29 | "lint-watch": "eslint --fix -w --color ./src ./tests ./examples", 30 | "test": "NODE_PATH=src/ jest", 31 | "test-coverage": "NODE_PATH=src/ jest --coverage", 32 | "test-watch": "NODE_PATH=src/ jest --watchAll --bail" 33 | }, 34 | "dependencies": { 35 | "redis": "^3.1.2" 36 | }, 37 | "devDependencies": { 38 | "eslint": "^7.28.0", 39 | "eslint-config-airbnb-base": "^14.2.1", 40 | "eslint-plugin-import": "^2.23.4", 41 | "jest": "^27.0.4" 42 | }, 43 | "eslintConfig": { 44 | "extends": "airbnb-base", 45 | "root": true, 46 | "env": { 47 | "node": true, 48 | "jest": true 49 | }, 50 | "plugins": [ 51 | "import" 52 | ], 53 | "parserOptions": { 54 | "ecmaVersion": 8 55 | }, 56 | "rules": { 57 | "comma-dangle": [ 58 | "error", 59 | "never" 60 | ], 61 | "max-len": [ 62 | "error", 63 | { 64 | "code": 125, 65 | "ignoreComments": true 66 | } 67 | ], 68 | "no-debugger": 1, 69 | "no-underscore-dangle": 0, 70 | "object-curly-spacing": 0, 71 | "object-curly-newline": 0, 72 | "no-plusplus": 0, 73 | "class-methods-use-this": 0 74 | } 75 | }, 76 | "jest": { 77 | "collectCoverageFrom": [ 78 | "src/**/*.js" 79 | ], 80 | "coverageDirectory": "./coverage-report", 81 | "testMatch": [ 82 | "/tests/**/?(*.)(spec|test).js" 83 | ], 84 | "testEnvironment": "node", 85 | "moduleDirectories": [ 86 | "node_modules", 87 | "./src/", 88 | "./tests/" 89 | ], 90 | "moduleFileExtensions": [ 91 | "js" 92 | ], 93 | "testPathIgnorePatterns": [ 94 | "/tests/__mocks__/" 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Query.js: -------------------------------------------------------------------------------- 1 | const SIGN_SPACE = ' '; 2 | const QuerySchema = require('./QuerySchema'); 3 | 4 | class Query { 5 | constructor(schema) { 6 | if (!schema || !(schema instanceof QuerySchema)) { 7 | throw new Error('QuerySchema is required'); 8 | } 9 | 10 | this._schema = schema; 11 | this._params = []; 12 | this._queries = {}; 13 | this._queriesOrder = []; 14 | 15 | this._sendHandler = null; 16 | this._init(); 17 | } 18 | 19 | _init() { 20 | this._schema.getSubqueries().forEach(({ query: subquerySchema }) => { 21 | const methodName = subquerySchema.getMethodName(); 22 | this._queriesOrder.push(methodName); 23 | this[methodName] = (...params) => { 24 | const subquery = Query.create(subquerySchema).params(params); 25 | this._queries[methodName] = subquery; 26 | return this; 27 | }; 28 | }); 29 | } 30 | 31 | static create(schema) { 32 | return new Query(schema); 33 | } 34 | 35 | params(values) { 36 | if (!values || !Array.isArray(values)) { 37 | throw new Error('Param values should be an array of values'); 38 | } 39 | 40 | this._params = values; 41 | return this; 42 | } 43 | 44 | validate() { 45 | this._schema.getParams().forEach(({ name, validation }, index) => { 46 | const value = this._params[index]; 47 | if (validation && !validation(value)) { 48 | throw new Error(`Invalid value '${value}' for parameter '${name}' in ${this._schema.getMethodName()} query`); 49 | } 50 | }); 51 | 52 | this._schema.getSubqueries().forEach(({ query: subquerySchema, required }) => { 53 | const methodName = subquerySchema.getMethodName(); 54 | const subquery = this._queries[methodName]; 55 | if (subquery) { 56 | subquery.validate(); 57 | } else if (required) { 58 | throw new Error(`${methodName} is required for ${this._schema.getMethodName()} query`); 59 | } 60 | }); 61 | } 62 | 63 | serialize() { 64 | const serialize = this._schema.getSerializator().bind(this); 65 | const queryArray = []; 66 | queryArray.push(serialize(this._schema.getCommand(), ...this._params)); 67 | 68 | this._queriesOrder.forEach((subqueryName) => { 69 | const subquery = this._queries[subqueryName]; 70 | if (subquery) { 71 | queryArray.push(subquery.serialize()); 72 | } 73 | }); 74 | 75 | return queryArray.join(SIGN_SPACE); 76 | } 77 | 78 | sendHandler(sendHandler) { 79 | if (!sendHandler || typeof sendHandler !== 'function') { 80 | throw new Error('Send handler as function is required'); 81 | } 82 | 83 | this._sendHandler = sendHandler; 84 | return this; 85 | } 86 | 87 | async send() { 88 | // validate query 89 | this.validate(); 90 | 91 | // serialize query 92 | const response = await this._sendHandler(this.serialize()); 93 | 94 | // parse response 95 | return response; 96 | } 97 | 98 | getSchema() { 99 | return this._schema; 100 | } 101 | } 102 | 103 | module.exports = Query; 104 | -------------------------------------------------------------------------------- /src/QuerySchema.js: -------------------------------------------------------------------------------- 1 | const SIGN_SPACE = ' '; 2 | const SIGN_UNDERSCORE = '_'; 3 | 4 | class QuerySchema { 5 | constructor(command) { 6 | if (!command || typeof command !== 'string') { 7 | throw new Error('Command is required to create a query'); 8 | } 9 | 10 | this._command = command; 11 | this._methodName = command.toLowerCase(); 12 | this._params = []; 13 | this._queries = []; 14 | this._data = {}; 15 | this._exports = {}; 16 | this._executable = false; 17 | this._serializedMethod = this._defaultSerializeMethod; 18 | } 19 | 20 | static create(command) { 21 | return new QuerySchema(command); 22 | } 23 | 24 | methodName(methodName) { 25 | if (!methodName) { 26 | throw new Error('Method name is required'); 27 | } 28 | this._methodName = methodName.split(SIGN_SPACE).join(SIGN_UNDERSCORE); 29 | return this; 30 | } 31 | 32 | data(data) { 33 | if (!data || typeof data !== 'object') { 34 | throw new Error('Data should be an object'); 35 | } 36 | Object.assign(this._data, data); 37 | return this; 38 | } 39 | 40 | exports(exportedData) { 41 | if (!exportedData || typeof exportedData !== 'object') { 42 | throw new Error('Exported data should be an object'); 43 | } 44 | 45 | Object.assign(this._exports, exportedData); 46 | return this; 47 | } 48 | 49 | param(name = null, validation = () => true) { 50 | if (!name) { 51 | throw new Error('Param name is required'); 52 | } 53 | 54 | this._params.push({ name, validation: validation.bind(this) }); 55 | return this; 56 | } 57 | 58 | subquery(query = null, required = false) { 59 | if (!query) { 60 | throw new Error('Subquery is required'); 61 | } 62 | 63 | this._queries.push({ query, required }); 64 | Object.assign(this._exports, query.getExports()); 65 | return this; 66 | } 67 | 68 | serialize(serializeMethod) { 69 | this._serializedMethod = serializeMethod.bind(this); 70 | return this; 71 | } 72 | 73 | getTemplateString() { 74 | let template = `${this._command}`; 75 | 76 | const params = this._params.map(({ name }) => name); 77 | if (params.length > 0) { 78 | template = `${template} ${params.join(' ')}`; 79 | } 80 | 81 | const queries = this._queries.map(({ query, required }) => ( 82 | required ? query.getTemplateString() : `[${query.getTemplateString()}]` 83 | )); 84 | if (queries.length > 0) { 85 | template = `${template} ${queries.join(' ')}`; 86 | } 87 | 88 | return template; 89 | } 90 | 91 | getCommand() { 92 | return this._command; 93 | } 94 | 95 | getMethodName() { 96 | return this._methodName; 97 | } 98 | 99 | getParams() { 100 | return this._params; 101 | } 102 | 103 | getSubqueries() { 104 | return this._queries; 105 | } 106 | 107 | getExports() { 108 | return this._exports; 109 | } 110 | 111 | getData() { 112 | return this._data; 113 | } 114 | 115 | getSerializator() { 116 | return this._serializedMethod; 117 | } 118 | 119 | _defaultSerializeMethod(command, ...params) { 120 | return [command, ...params].join(SIGN_SPACE); 121 | } 122 | } 123 | 124 | module.exports = QuerySchema; 125 | -------------------------------------------------------------------------------- /src/RedisTimeSeries.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis'); 2 | const Query = require('./Query'); 3 | const commands = require('./commands'); 4 | 5 | const SIGN_SPACE = ' '; 6 | 7 | class RedisTimeSeries { 8 | /** 9 | * Create a new RedisTimeSeries client, the {options} object is the same as for the redis library 10 | */ 11 | constructor(options = {}) { 12 | this.options = options; 13 | this.client = null; 14 | this._loadedSchemas = []; 15 | 16 | this.load(Object.values(commands)); 17 | } 18 | 19 | /** 20 | * Connect client to redis server 21 | */ 22 | async connect() { 23 | this.client = await Redis.createClient(this.options); 24 | } 25 | 26 | /** 27 | * Fetch redis client 28 | */ 29 | getClient() { 30 | return this.client; 31 | } 32 | 33 | /** 34 | * Set redis client 35 | */ 36 | setClient(client = null) { 37 | if (!client) { 38 | throw new Error('Client must be a valid object'); 39 | } 40 | 41 | this.client = client; 42 | } 43 | 44 | /** 45 | * Send plain command to redis 46 | */ 47 | async send(query) { 48 | const [command, ...args] = query.split(SIGN_SPACE); 49 | return new Promise((resolve, reject) => { 50 | this.client.send_command(command, args, (error, result) => { 51 | if (error) { 52 | return reject(error); 53 | } 54 | 55 | return resolve(result); 56 | }); 57 | }); 58 | } 59 | 60 | load(querySchemas) { 61 | this._loadedSchemas = this._loadedSchemas.concat(querySchemas); 62 | querySchemas.forEach((querySchema) => { 63 | this[querySchema.getMethodName()] = (...params) => Query 64 | .create(querySchema) 65 | .params(params) 66 | .sendHandler(this.send.bind(this)); 67 | }); 68 | } 69 | } 70 | 71 | module.exports = RedisTimeSeries; 72 | -------------------------------------------------------------------------------- /src/commands/add.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const { retention, labels, uncompressed, onDuplicate } = require('./fragments'); 5 | 6 | const TS_ADD = 'TS.ADD'; 7 | 8 | /** 9 | * TS.ADD key timestamp value [RETENTION retentionTime] [LABELS field value..] [UNCOMPRESSED] 10 | */ 11 | module.exports = QuerySchema 12 | .create(TS_ADD) 13 | .data({ executable: true }) 14 | .methodName('add') 15 | .param( 16 | 'key', 17 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 18 | ) 19 | .param( 20 | 'timestamp', 21 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value > 0 22 | ) 23 | .param( 24 | 'value', 25 | (value) => !Validator.isUndefined(value) && Validator.isFloat(value) 26 | ) 27 | .subquery(retention) 28 | .subquery(uncompressed) 29 | .subquery(onDuplicate) 30 | .subquery(labels); 31 | -------------------------------------------------------------------------------- /src/commands/alter.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const { retention, labels, duplicatePolicy } = require('./fragments'); 5 | 6 | const TS_ALTER = 'TS.ALTER'; 7 | 8 | /** 9 | * TS.ALTER key [RETENTION retentionTime] [LABELS field value..] 10 | */ 11 | module.exports = QuerySchema 12 | .create(TS_ALTER) 13 | .data({ executable: true }) 14 | .methodName('alter') 15 | .param( 16 | 'key', 17 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 18 | ) 19 | .subquery(retention) 20 | .subquery(duplicatePolicy) 21 | .subquery(labels); 22 | -------------------------------------------------------------------------------- /src/commands/create.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const { retention, labels, uncompressed, duplicatePolicy } = require('./fragments'); 5 | 6 | const TS_CREATE = 'TS.CREATE'; 7 | 8 | /** 9 | * TS.CREATE key [RETENTION retentionTime] [UNCOMPRESSED] [CHUNK_SIZE size] [DUPLICATE_POLICY policy] [LABELS label value..] 10 | */ 11 | module.exports = QuerySchema 12 | .create(TS_CREATE) 13 | .data({ executable: true }) 14 | .methodName('create') 15 | .param( 16 | 'key', 17 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 18 | ) 19 | .subquery(retention) 20 | .subquery(uncompressed) 21 | .subquery(duplicatePolicy) 22 | .subquery(labels); 23 | -------------------------------------------------------------------------------- /src/commands/createRule.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const { aggregation } = require('./fragments'); 5 | 6 | const TS_CREATERULE = 'TS.CREATERULE'; 7 | 8 | /** 9 | * TS.CREATERULE sourceKey destKey AGGREGATION aggregationType timeBucket 10 | */ 11 | module.exports = QuerySchema 12 | .create(TS_CREATERULE) 13 | .data({ executable: true }) 14 | .methodName('createRule') 15 | .param( 16 | 'sourceKey', 17 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 18 | ) 19 | .param( 20 | 'destKey', 21 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 22 | ) 23 | .subquery(aggregation, true); 24 | -------------------------------------------------------------------------------- /src/commands/decrBy.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const { retention, labels, timestamp, uncompressed } = require('./fragments'); 5 | 6 | const TS_DECRBY = 'TS.DECRBY'; 7 | 8 | /** 9 | * TS.DECRBY key value [TIMESTAMP timestamp] [RETENTION retentionTime] [UNCOMPRESSED] [LABELS field value..] 10 | */ 11 | module.exports = QuerySchema 12 | .create(TS_DECRBY) 13 | .data({ executable: true }) 14 | .methodName('decrBy') 15 | .param( 16 | 'key', 17 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 18 | ) 19 | .param( 20 | 'value', 21 | (value) => !Validator.isUndefined(value) && Validator.isFloat(value) 22 | ) 23 | .subquery(retention) 24 | .subquery(timestamp) 25 | .subquery(uncompressed) 26 | .subquery(labels); 27 | -------------------------------------------------------------------------------- /src/commands/deleteRule.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const TS_DELETERULE = 'TS.DELETERULE'; 5 | 6 | /** 7 | * TS.DELETERULE sourceKey destKey 8 | */ 9 | module.exports = QuerySchema 10 | .create(TS_DELETERULE) 11 | .data({ executable: true }) 12 | .methodName('deleteRule') 13 | .param( 14 | 'sourceKey', 15 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 16 | ) 17 | .param( 18 | 'destKey', 19 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 20 | ); 21 | -------------------------------------------------------------------------------- /src/commands/fragments/aggregation.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | const { Validator } = require('../utils'); 3 | 4 | const Aggregation = { 5 | AVG: 'avg', 6 | SUM: 'sum', 7 | MIN: 'min', 8 | MAX: 'max', 9 | RANGE: 'range', 10 | COUNT: 'count', 11 | FIRST: 'first', 12 | LAST: 'last', 13 | STDP: 'std.p', 14 | STDS: 'std.s', 15 | VARP: 'var.p', 16 | VARS: 'var.s' 17 | }; 18 | 19 | const AggregationTypes = Object.values(Aggregation); 20 | 21 | const AGGREGATION = 'AGGREGATION'; 22 | 23 | /** 24 | * AGGREGATION aggregationType timeBucket 25 | */ 26 | module.exports = QuerySchema 27 | .create(AGGREGATION) 28 | .exports({ Aggregation }) 29 | .param( 30 | 'aggregationType', 31 | (value) => AggregationTypes.includes(value) 32 | ) 33 | .param( 34 | 'timeBucket', 35 | (value) => ( 36 | !Validator.isUndefined(value) 37 | && Validator.isInteger(value) 38 | ) 39 | ); 40 | -------------------------------------------------------------------------------- /src/commands/fragments/count.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | const { Validator } = require('../utils'); 3 | 4 | const COUNT = 'COUNT'; 5 | 6 | /** 7 | *COUNT count 8 | */ 9 | module.exports = QuerySchema 10 | .create(COUNT) 11 | .param( 12 | 'count', 13 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 14 | ); 15 | -------------------------------------------------------------------------------- /src/commands/fragments/duplicatePolicy.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | 3 | const DuplicatePolicy = { 4 | BLOCK: 'BLOCK', // - an error will occur for any out of order sample 5 | FIRST: 'FIRST', // - ignore the new value 6 | LAST: 'LAST', // - override with latest value 7 | MIN: 'MIN', // - only override if the value is lower than the existing value 8 | MAX: 'MAX', // - only override if the value is higher than the existing value 9 | SUM: 'SUM' // - add the new sample to it so that the updated value is equal to (previous + new) 10 | }; 11 | 12 | const DuplicatePolicyTypes = Object.values(DuplicatePolicy); 13 | 14 | const DUPLICATE_POLICY = 'DUPLICATE_POLICY'; 15 | const METHOD_NAME = 'duplicatePolicy'; 16 | 17 | /** 18 | * DUPLICATE_POLICY duplicatePolicy 19 | */ 20 | module.exports = QuerySchema 21 | .create(DUPLICATE_POLICY) 22 | .methodName(METHOD_NAME) 23 | .exports({ DuplicatePolicy }) 24 | .param( 25 | 'duplicatePolicy', 26 | (value) => DuplicatePolicyTypes.includes(value) 27 | ); 28 | -------------------------------------------------------------------------------- /src/commands/fragments/filter.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | const { Validator, Filter } = require('../utils'); 3 | 4 | const FILTER = 'FILTER'; 5 | const SIGN_SPACE = ' '; 6 | 7 | /** 8 | * FILTER filter... 9 | */ 10 | module.exports = QuerySchema 11 | .create(FILTER) 12 | .exports({ Filter }) 13 | .param( 14 | 'filter', 15 | (value) => !Validator.isUndefined(value) && Array.isArray(value) && value.length > 0 16 | ) 17 | .serialize((command, filter) => [command, ...filter].join(SIGN_SPACE)); 18 | -------------------------------------------------------------------------------- /src/commands/fragments/index.js: -------------------------------------------------------------------------------- 1 | const aggregation = require('./aggregation'); 2 | const count = require('./count'); 3 | const duplicatePolicy = require('./duplicatePolicy'); 4 | const filter = require('./filter'); 5 | const labels = require('./labels'); 6 | const onDuplicate = require('./onDuplicate'); 7 | const retention = require('./retention'); 8 | const timestamp = require('./timestamp'); 9 | const uncompressed = require('./uncompressed'); 10 | const withLabels = require('./withLabels'); 11 | 12 | module.exports = { 13 | aggregation, 14 | count, 15 | duplicatePolicy, 16 | filter, 17 | labels, 18 | onDuplicate, 19 | retention, 20 | timestamp, 21 | uncompressed, 22 | withLabels 23 | }; 24 | -------------------------------------------------------------------------------- /src/commands/fragments/labels.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | const { Validator } = require('../utils'); 3 | 4 | const LABELS = 'LABELS'; 5 | const SIGN_SPACE = ' '; 6 | 7 | /** 8 | * LABELS field value.. 9 | */ 10 | module.exports = QuerySchema 11 | .create(LABELS) 12 | .param( 13 | 'labels', 14 | (value) => !Validator.isUndefined(value) && Validator.isObject(value) 15 | ) 16 | .serialize((command, labels) => Object.keys(labels).reduce( 17 | (acc, labelName) => acc.concat([labelName, labels[labelName]]), 18 | [LABELS] 19 | ) 20 | .join(SIGN_SPACE)); 21 | -------------------------------------------------------------------------------- /src/commands/fragments/onDuplicate.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | 3 | const OnDuplicate = { 4 | BLOCK: 'BLOCK', // - an error will occur for any out of order sample 5 | FIRST: 'FIRST', // - ignore the new value 6 | LAST: 'LAST', // - override with latest value 7 | MIN: 'MIN', // - only override if the value is lower than the existing value 8 | MAX: 'MAX', // - only override if the value is higher than the existing value 9 | SUM: 'SUM' // - add the new sample to it so that the updated value is equal to (previous + new) 10 | }; 11 | 12 | const DuplicatePolicyTypes = Object.values(OnDuplicate); 13 | 14 | const ON_DUPLICATE = 'ON_DUPLICATE'; 15 | const METHOD_NAME = 'onDuplicate'; 16 | 17 | /** 18 | * ON_DUPLICATE duplicatePolicy 19 | */ 20 | module.exports = QuerySchema 21 | .create(ON_DUPLICATE) 22 | .methodName(METHOD_NAME) 23 | .exports({ OnDuplicate }) 24 | .param( 25 | 'duplicatePolicy', 26 | (value) => DuplicatePolicyTypes.includes(value) 27 | ); 28 | -------------------------------------------------------------------------------- /src/commands/fragments/retention.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | const { Validator } = require('../utils'); 3 | 4 | const RETENTION = 'RETENTION'; 5 | 6 | /** 7 | * RETENTION retentionTime 8 | */ 9 | module.exports = QuerySchema 10 | .create(RETENTION) 11 | .param( 12 | 'retention', 13 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 14 | ); 15 | -------------------------------------------------------------------------------- /src/commands/fragments/timestamp.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | const { Validator } = require('../utils'); 3 | 4 | const TIMESTAMP = 'TIMESTAMP'; 5 | 6 | /** 7 | * TIMESTAMP timestamp 8 | */ 9 | module.exports = QuerySchema 10 | .create(TIMESTAMP) 11 | .param( 12 | 'timestamp', 13 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 14 | ); 15 | -------------------------------------------------------------------------------- /src/commands/fragments/uncompressed.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | 3 | const UNCOMPRESSED = 'UNCOMPRESSED'; 4 | 5 | /** 6 | * UNCOMPRESSED 7 | */ 8 | module.exports = QuerySchema.create(UNCOMPRESSED); 9 | -------------------------------------------------------------------------------- /src/commands/fragments/withLabels.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../QuerySchema'); 2 | 3 | const WITHLABELS = 'WITHLABELS'; 4 | 5 | /** 6 | * WITHLABELS 7 | */ 8 | module.exports = QuerySchema 9 | .create(WITHLABELS) 10 | .methodName('withLabels'); 11 | -------------------------------------------------------------------------------- /src/commands/get.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const TS_GET = 'TS.GET'; 5 | 6 | /** 7 | * TS.GET key 8 | */ 9 | module.exports = QuerySchema 10 | .create(TS_GET) 11 | .data({ executable: true }) 12 | .methodName('get') 13 | .param( 14 | 'key', 15 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 16 | ); 17 | -------------------------------------------------------------------------------- /src/commands/incrBy.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const { retention, labels, timestamp, uncompressed } = require('./fragments'); 5 | 6 | const TS_INCRBY = 'TS.INCRBY'; 7 | 8 | /** 9 | * TS.INCRBY key value [TIMESTAMP timestamp] [RETENTION retentionTime] [UNCOMPRESSED] [LABELS field value..] 10 | */ 11 | module.exports = QuerySchema 12 | .create(TS_INCRBY) 13 | .data({ executable: true }) 14 | .methodName('incrBy') 15 | .param( 16 | 'key', 17 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 18 | ) 19 | .param( 20 | 'value', 21 | (value) => !Validator.isUndefined(value) && Validator.isFloat(value) 22 | ) 23 | .subquery(retention) 24 | .subquery(timestamp) 25 | .subquery(uncompressed) 26 | .subquery(labels); 27 | -------------------------------------------------------------------------------- /src/commands/index.js: -------------------------------------------------------------------------------- 1 | const add = require('./add'); 2 | const alter = require('./alter'); 3 | const create = require('./create'); 4 | const createRule = require('./createRule'); 5 | const decrBy = require('./decrBy'); 6 | const deleteRule = require('./deleteRule'); 7 | const get = require('./get'); 8 | const incrBy = require('./incrBy'); 9 | const info = require('./info'); 10 | const madd = require('./madd'); 11 | const mget = require('./mget'); 12 | const mrange = require('./mrange'); 13 | const mrevrange = require('./mrevrange'); 14 | const queryIndex = require('./queryIndex'); 15 | const range = require('./range'); 16 | const revrange = require('./revrange'); 17 | 18 | module.exports = { 19 | add, 20 | alter, 21 | create, 22 | createRule, 23 | decrBy, 24 | deleteRule, 25 | get, 26 | incrBy, 27 | info, 28 | madd, 29 | mget, 30 | mrange, 31 | mrevrange, 32 | queryIndex, 33 | range, 34 | revrange 35 | }; 36 | -------------------------------------------------------------------------------- /src/commands/info.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const TS_INFO = 'TS.INFO'; 5 | 6 | /** 7 | * TS.INFO key 8 | */ 9 | module.exports = QuerySchema 10 | .create(TS_INFO) 11 | .data({ executable: true }) 12 | .methodName('info') 13 | .param( 14 | 'key', 15 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 16 | ); 17 | -------------------------------------------------------------------------------- /src/commands/madd.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | 4 | const TS_MADD = 'TS.MADD'; 5 | const SIGN_SPACE = ' '; 6 | 7 | /** 8 | * TS.MADD key timestamp value [key timestamp value ...] 9 | */ 10 | module.exports = QuerySchema 11 | .create(TS_MADD) 12 | .methodName('madd') 13 | .param( 14 | 'values', 15 | (values) => !Validator.isUndefined(values) && Array.isArray(values) && values.length > 0 16 | ) 17 | .serialize((command, values) => values 18 | .reduce( 19 | (acc, { key, timestamp, value }) => acc.concat([key, timestamp, value]), 20 | [command] 21 | ) 22 | .join(SIGN_SPACE)); 23 | -------------------------------------------------------------------------------- /src/commands/mget.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { filter } = require('./fragments'); 3 | 4 | const TS_MGET = 'TS.MGET'; 5 | 6 | /** 7 | * TS.MGET FILTER filter... 8 | */ 9 | module.exports = QuerySchema 10 | .create(TS_MGET) 11 | .data({ executable: true }) 12 | .methodName('mget') 13 | .subquery(filter, true); 14 | -------------------------------------------------------------------------------- /src/commands/mrange.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | const { count, aggregation, withLabels, filter } = require('./fragments'); 4 | 5 | const TS_MRANGE = 'TS.MRANGE'; 6 | 7 | /** 8 | * TS.MRANGE fromTimestamp toTimestamp [COUNT count] [AGGREGATION aggregationType timeBucket] [WITHLABELS] FILTER filter.. 9 | */ 10 | module.exports = QuerySchema 11 | .create(TS_MRANGE) 12 | .data({ executable: true }) 13 | .methodName('mrange') 14 | .param( 15 | 'fromTimestamp', 16 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 17 | ) 18 | .param( 19 | 'toTimestamp', 20 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 21 | ) 22 | .subquery(count) 23 | .subquery(aggregation) 24 | .subquery(withLabels) 25 | .subquery(filter, true); 26 | -------------------------------------------------------------------------------- /src/commands/mrevrange.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | const { count, aggregation, withLabels, filter } = require('./fragments'); 4 | 5 | const TS_MREVRANGE = 'TS.MREVRANGE'; 6 | 7 | /** 8 | * TS.MREVRANGE fromTimestamp toTimestamp [COUNT count] [AGGREGATION aggregationType timeBucket] [WITHLABELS] FILTER filter.. 9 | */ 10 | module.exports = QuerySchema 11 | .create(TS_MREVRANGE) 12 | .data({ executable: true }) 13 | .methodName('mrevrange') 14 | .param( 15 | 'fromTimestamp', 16 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 17 | ) 18 | .param( 19 | 'toTimestamp', 20 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 21 | ) 22 | .subquery(count) 23 | .subquery(aggregation) 24 | .subquery(withLabels) 25 | .subquery(filter, true); 26 | -------------------------------------------------------------------------------- /src/commands/queryIndex.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator, Filter } = require('./utils'); 3 | 4 | const TS_QUERYINDEX = 'TS.QUERYINDEX'; 5 | const SIGN_SPACE = ' '; 6 | 7 | /** 8 | * TS_QUERYINDEX filter... 9 | */ 10 | module.exports = QuerySchema 11 | .create(TS_QUERYINDEX) 12 | .methodName('queryIndex') 13 | .data({ executable: true }) 14 | .exports({ Filter }) 15 | .param( 16 | 'filter', 17 | (value) => !Validator.isUndefined(value) && Array.isArray(value) && value.length > 0 18 | ) 19 | .serialize((command, conditions) => [command, ...conditions].join(SIGN_SPACE)); 20 | -------------------------------------------------------------------------------- /src/commands/range.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | const { count, aggregation } = require('./fragments'); 4 | 5 | const TS_RANGE = 'TS.RANGE'; 6 | 7 | /** 8 | * TS.RANGE key fromTimestamp toTimestamp [COUNT count] [AGGREGATION aggregationType timeBucket] 9 | */ 10 | module.exports = QuerySchema 11 | .create(TS_RANGE) 12 | .data({ executable: true }) 13 | .methodName('range') 14 | .param( 15 | 'key', 16 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 17 | ) 18 | .param( 19 | 'fromTimestamp', 20 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 21 | ) 22 | .param( 23 | 'toTimestamp', 24 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 25 | ) 26 | .subquery(count) 27 | .subquery(aggregation); 28 | -------------------------------------------------------------------------------- /src/commands/revrange.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../QuerySchema'); 2 | const { Validator } = require('./utils'); 3 | const { count, aggregation } = require('./fragments'); 4 | 5 | const TS_REVRANGE = 'TS.REVRANGE'; 6 | 7 | /** 8 | * TS.REVRANGE key fromTimestamp toTimestamp [COUNT count] [AGGREGATION aggregationType timeBucket] 9 | */ 10 | module.exports = QuerySchema 11 | .create(TS_REVRANGE) 12 | .data({ executable: true }) 13 | .methodName('revrange') 14 | .param( 15 | 'key', 16 | (value) => !Validator.isUndefined(value) && Validator.isString(value) 17 | ) 18 | .param( 19 | 'fromTimestamp', 20 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 21 | ) 22 | .param( 23 | 'toTimestamp', 24 | (value) => !Validator.isUndefined(value) && Validator.isInteger(value) && value >= 0 25 | ) 26 | .subquery(count) 27 | .subquery(aggregation); 28 | -------------------------------------------------------------------------------- /src/commands/utils/Filter.js: -------------------------------------------------------------------------------- 1 | class Filter { 2 | constructor(filterString) { 3 | this.filterString = filterString; 4 | } 5 | 6 | toString() { 7 | return this.filterString; 8 | } 9 | 10 | static equal(label, value) { 11 | if (!label || !value) { 12 | throw new Error('equals filter requires label and value'); 13 | } 14 | 15 | return new Filter(`${label}=${value}`); 16 | } 17 | 18 | static notEqual(label, value) { 19 | if (!label || !value) { 20 | throw new Error('notEquals filter requires label and value'); 21 | } 22 | 23 | return new Filter(`${label}!=${value}`); 24 | } 25 | 26 | static exists(label) { 27 | if (!label) { 28 | throw new Error('exists filter requires label'); 29 | } 30 | 31 | return new Filter(`${label}!=`); 32 | } 33 | 34 | static notExists(label) { 35 | if (!label) { 36 | throw new Error('notExists filter requires label'); 37 | } 38 | 39 | return new Filter(`${label}=`); 40 | } 41 | 42 | static in(label, arrayOfValues = []) { 43 | if (!label || !arrayOfValues || !Array.isArray(arrayOfValues) || arrayOfValues.length === 0) { 44 | throw new Error('in filter requires label and arrayOfValues(must be an array)'); 45 | } 46 | 47 | return new Filter(`${label}=(${arrayOfValues.join(',')})`); 48 | } 49 | 50 | static notIn(label, arrayOfValues = []) { 51 | if (!label || !arrayOfValues || !Array.isArray(arrayOfValues) || arrayOfValues.length === 0) { 52 | throw new Error('notIn filter requires label and arrayOfValues(must be an array)'); 53 | } 54 | 55 | return new Filter(`${label}!=(${arrayOfValues.join(',')})`); 56 | } 57 | } 58 | 59 | module.exports = Filter; 60 | -------------------------------------------------------------------------------- /src/commands/utils/Validator.js: -------------------------------------------------------------------------------- 1 | class Validator { 2 | static isString(value) { 3 | return typeof value === 'string'; 4 | } 5 | 6 | static isFloat(value) { 7 | return !Number.isNaN(parseFloat(value)); 8 | } 9 | 10 | static isInteger(value) { 11 | return Number.isInteger(value); 12 | } 13 | 14 | static isUndefined(value) { 15 | return typeof value === 'undefined'; 16 | } 17 | 18 | static isObject(value) { 19 | return typeof value === 'object'; 20 | } 21 | } 22 | 23 | module.exports = Validator; 24 | -------------------------------------------------------------------------------- /src/commands/utils/index.js: -------------------------------------------------------------------------------- 1 | const Validator = require('./Validator'); 2 | const Filter = require('./Filter'); 3 | 4 | module.exports = { 5 | Validator, 6 | Filter 7 | }; 8 | -------------------------------------------------------------------------------- /tests/__mocks__/redis.js: -------------------------------------------------------------------------------- 1 | const Client = { 2 | send_command: jest.fn((command, args, callback) => callback()) 3 | }; 4 | 5 | const RedisMock = { 6 | createClient: jest.fn(() => Client) 7 | }; 8 | 9 | module.exports = RedisMock; 10 | -------------------------------------------------------------------------------- /tests/components/Query.test.js: -------------------------------------------------------------------------------- 1 | const Query = require('../../src/Query'); 2 | const QuerySchema = require('../../src/QuerySchema'); 3 | 4 | const TEST_PARAMS = { 5 | table: 'sometable', 6 | send: (queryString) => queryString 7 | }; 8 | 9 | const TEST_QS_1 = { 10 | command: 'SELECT', 11 | paramName: 'table', 12 | paramValidator: (value) => typeof value === 'string' 13 | }; 14 | 15 | const TEST_QS_2 = { 16 | command: 'WHERE', 17 | paramName: 'condition' 18 | }; 19 | 20 | let select = null; 21 | 22 | describe('Query component tests', () => { 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | 26 | const where = QuerySchema 27 | .create(TEST_QS_2.command) 28 | .param(TEST_QS_2.paramName); 29 | 30 | select = QuerySchema 31 | .create(TEST_QS_1.command) 32 | .param(TEST_QS_1.paramName, TEST_QS_1.paramValidator) 33 | .subquery(where); 34 | }); 35 | 36 | it('create query with given query schema', async () => { 37 | const query = Query.create(select).sendHandler(TEST_PARAMS.send); 38 | 39 | expect(query).toBeTruthy(); 40 | expect(query.getSchema()).toBe(select); 41 | }); 42 | 43 | it('create query with given query schema and params', async () => { 44 | expect(() => Query.create(select).params([TEST_PARAMS.table])).not.toThrow(); 45 | }); 46 | 47 | it('should fail to create query, no query schema', async () => { 48 | expect(() => Query.create()).toThrow(); 49 | }); 50 | 51 | it('should fail to create query, not a query schema', async () => { 52 | expect(() => Query.create(true)).toThrow(); 53 | }); 54 | 55 | it('should fail to set send handler, no function', async () => { 56 | expect(() => Query.create(select).sendHandler()).toThrow(); 57 | }); 58 | 59 | it('should fail to set send handler, not a function', async () => { 60 | expect(() => Query.create(select).sendHandler(true)).toThrow(); 61 | }); 62 | 63 | it('should fail to assign param values to query, no values', async () => { 64 | expect(() => Query.create(select).params()).toThrow(); 65 | }); 66 | 67 | it('should fail to assign param values to query, not an array', async () => { 68 | expect(() => Query.create(select).params(true)).toThrow(); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/components/QuerySchema.test.js: -------------------------------------------------------------------------------- 1 | const QuerySchema = require('../../src/QuerySchema'); 2 | 3 | const TEST_PARAMS = { 4 | command: 'SOME_COMMAND', 5 | methodName: 'send', 6 | paramName: 'someParam', 7 | paramValidator: (value) => Number.isInteger(value), 8 | data: { 9 | key: 'value' 10 | } 11 | }; 12 | 13 | const TEST_QS_1 = { 14 | command: 'SELECT', 15 | paramName: 'table', 16 | paramValidator: (value) => typeof value === 'string' 17 | }; 18 | 19 | const TEST_QS_2 = { 20 | command: 'WHERE', 21 | paramName: 'condition' 22 | }; 23 | 24 | const TEST_QS_3 = { 25 | command: 'ORDER BY', 26 | paramName: 'column' 27 | }; 28 | 29 | const TEST_QS_4 = { 30 | command: 'ASC' 31 | }; 32 | 33 | describe('QuerySchema component tests', () => { 34 | beforeEach(() => { 35 | jest.clearAllMocks(); 36 | }); 37 | 38 | it('create query schema with given command', async () => { 39 | const qs = QuerySchema.create(TEST_PARAMS.command); 40 | 41 | expect(qs).toBeTruthy(); 42 | expect(qs.getCommand()).toBe(TEST_PARAMS.command); 43 | expect(qs.getMethodName()).toBe(TEST_PARAMS.command.toLowerCase()); 44 | }); 45 | 46 | it('create query schema with custom method name', async () => { 47 | const { command, methodName } = TEST_PARAMS; 48 | 49 | const qs = QuerySchema 50 | .create(command) 51 | .methodName(methodName); 52 | 53 | expect(qs).toBeTruthy(); 54 | expect(qs.getCommand()).toBe(command); 55 | expect(qs.getMethodName()).toBe(methodName); 56 | }); 57 | 58 | it('create query schema with param and data', async () => { 59 | const { command, paramName, paramValidator, data } = TEST_PARAMS; 60 | 61 | const qs = QuerySchema 62 | .create(command) 63 | .param(paramName, paramValidator) 64 | .data(data); 65 | 66 | expect(qs).toBeTruthy(); 67 | expect(qs.getCommand()).toBe(command); 68 | expect(qs.getParams()[0].name).toBe(paramName); 69 | expect(qs.getData().key).toBe(data.key); 70 | expect(qs.getData().value).toBe(data.value); 71 | }); 72 | 73 | it('create query schema with param, no validation', async () => { 74 | const { command, paramName } = TEST_PARAMS; 75 | 76 | const qs = QuerySchema 77 | .create(command) 78 | .param(paramName); 79 | 80 | expect(qs).toBeTruthy(); 81 | expect(qs.getCommand()).toBe(command); 82 | expect(qs.getParams()[0].name).toBe(paramName); 83 | expect(qs.getParams()[0].validation()).toBe(true); 84 | }); 85 | 86 | it('create query schema with optional subqueries, template string validation', async () => { 87 | const where = QuerySchema 88 | .create(TEST_QS_2.command) 89 | .param(TEST_QS_2.paramName); 90 | 91 | const select = QuerySchema 92 | .create(TEST_QS_1.command) 93 | .param(TEST_QS_1.paramName, TEST_QS_1.paramValidator) 94 | .subquery(where); 95 | 96 | const templateString = `${TEST_QS_1.command} ${TEST_QS_1.paramName} [${TEST_QS_2.command} ${TEST_QS_2.paramName}]`; 97 | 98 | expect(select.getTemplateString()).toBe(templateString); 99 | }); 100 | 101 | it('create query schema with required subquery, template string validation', async () => { 102 | const where = QuerySchema 103 | .create(TEST_QS_2.command) 104 | .param(TEST_QS_2.paramName); 105 | 106 | const order = QuerySchema 107 | .create(TEST_QS_3.command) 108 | .param(TEST_QS_3.paramName); 109 | 110 | const asc = QuerySchema 111 | .create(TEST_QS_4.command); 112 | 113 | const select = QuerySchema 114 | .create(TEST_QS_1.command) 115 | .param(TEST_QS_1.paramName, TEST_QS_1.paramValidator) 116 | .subquery(where, true) 117 | .subquery(order) 118 | .subquery(asc); 119 | 120 | const ts2 = `${TEST_QS_2.command} ${TEST_QS_2.paramName}`; 121 | const ts3 = `${TEST_QS_3.command} ${TEST_QS_3.paramName}`; 122 | const ts4 = `${TEST_QS_4.command}`; 123 | 124 | const templateString = `${TEST_QS_1.command} ${TEST_QS_1.paramName} ${ts2} [${ts3}] [${ts4}]`; 125 | 126 | expect(select.getTemplateString()).toBe(templateString); 127 | }); 128 | 129 | it('it should fail, no command', async () => { 130 | expect(() => QuerySchema.create()).toThrow(); 131 | }); 132 | 133 | it('it should fail, no method name', async () => { 134 | expect(() => QuerySchema.create(TEST_PARAMS.command).methodName()).toThrow(); 135 | }); 136 | 137 | it('it should fail, no param name', async () => { 138 | expect(() => QuerySchema.create(TEST_PARAMS.command).param()).toThrow(); 139 | }); 140 | 141 | it('it should fail, data is missing', async () => { 142 | expect(() => QuerySchema.create(TEST_PARAMS.command).data()).toThrow(); 143 | }); 144 | 145 | it('it should fail, data is not object', async () => { 146 | expect(() => QuerySchema.create(TEST_PARAMS.command).data(true)).toThrow(); 147 | }); 148 | 149 | it('it should fail, exports is missing', async () => { 150 | expect(() => QuerySchema.create(TEST_PARAMS.command).exports()).toThrow(); 151 | }); 152 | 153 | it('it should fail, exports is not object', async () => { 154 | expect(() => QuerySchema.create(TEST_PARAMS.command).exports(true)).toThrow(); 155 | }); 156 | 157 | it('it should fail, subquery is null', async () => { 158 | expect(() => QuerySchema.create(TEST_PARAMS.command).subquery(null)).toThrow(); 159 | }); 160 | 161 | it('it should fail, subquery is missing', async () => { 162 | expect(() => QuerySchema.create(TEST_PARAMS.command).subquery()).toThrow(); 163 | }); 164 | }); 165 | -------------------------------------------------------------------------------- /tests/components/RedisTimeSeries.test.js: -------------------------------------------------------------------------------- 1 | const RedisMock = require('../__mocks__/redis'); 2 | 3 | const RedisTimeSeries = require('../../index'); 4 | 5 | const TEST_OPTIONS = { 6 | host: 'localhost', 7 | port: 6379 8 | }; 9 | 10 | const TEST_PARAMS = { 11 | command: 'somecommand', 12 | args: ['some', 'args'] 13 | }; 14 | 15 | let rts = null; 16 | 17 | describe('RedisTimeSeries component tests', () => { 18 | beforeEach(() => { 19 | jest.clearAllMocks(); 20 | rts = new RedisTimeSeries(TEST_OPTIONS); 21 | }); 22 | 23 | it('should create client with out options', async () => { 24 | expect(() => { 25 | rts = new RedisTimeSeries(); 26 | }).not.toThrow(); 27 | }); 28 | 29 | it('should fetch redis client', async () => { 30 | const redisClient = { dummy: 'client' }; 31 | RedisMock.createClient.mockReturnValueOnce(redisClient); 32 | 33 | await rts.connect(TEST_OPTIONS); 34 | expect(rts.getClient()).toBe(redisClient); 35 | }); 36 | 37 | it('should fail to set redis client, no arguments', async () => { 38 | expect(() => rts.setClient()).toThrow(); 39 | }); 40 | 41 | it('should set redis client', async () => { 42 | const redisClient = { dummy: 'client' }; 43 | expect(() => rts.setClient(redisClient)).not.toThrow(); 44 | }); 45 | 46 | it('should throw an error, to simulte redis exception', async () => { 47 | const error = { some: 'error' }; 48 | rts.client = { send_command: jest.fn((command, args, callback) => callback(error)) }; 49 | await expect(rts.send(TEST_PARAMS.command, ...TEST_PARAMS.args)).rejects.toEqual(error); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/components/filter.test.js: -------------------------------------------------------------------------------- 1 | const { Filter } = require('../../index'); 2 | 3 | const TEST_PARAMS = { 4 | label1: 'somelabel1', 5 | label2: 'somelabel1', 6 | value1: 22, 7 | value2: 23 8 | }; 9 | 10 | describe('Filter component tests', () => { 11 | beforeEach(() => { 12 | jest.clearAllMocks(); 13 | }); 14 | 15 | const { label1, label2, value1, value2 } = TEST_PARAMS; 16 | 17 | it('exists filter', async () => { 18 | expect(Filter.exists(label1).toString()).toBe(`${label1}!=`); 19 | }); 20 | 21 | it('notExists filter', async () => { 22 | expect(Filter.notExists(label1).toString()).toBe(`${label1}=`); 23 | }); 24 | 25 | it('equal filter', async () => { 26 | expect(Filter.equal(label1, value1).toString()).toBe(`${label1}=${value1}`); 27 | }); 28 | 29 | it('notEqual filter', async () => { 30 | expect(Filter.notEqual(label1, value1).toString()).toBe(`${`${label1}!=${value1}`}`); 31 | }); 32 | 33 | it('in filter', async () => { 34 | expect(Filter.in(label1, [value1, value2]).toString()).toBe(`${label1}=(${value1},${value2})`); 35 | }); 36 | 37 | it('notIn filter', async () => { 38 | expect(Filter.notIn(label1, [value1, value2]).toString()).toBe(`${label1}!=(${value1},${value2})`); 39 | }); 40 | 41 | it('exists filter should fail, invalid filter', async () => { 42 | expect(() => Filter.exists()).toThrow(); 43 | }); 44 | 45 | it('notExists filter should fail, invalid filter', async () => { 46 | expect(() => Filter.notExists()).toThrow(); 47 | }); 48 | 49 | it('equal filter should fail, invalid filter', async () => { 50 | expect(() => Filter.equal()).toThrow(); 51 | }); 52 | 53 | it('equal filter should fail, no value', async () => { 54 | expect(() => Filter.equal(label1)).toThrow(); 55 | }); 56 | 57 | it('notEqual filter should fail, invalid filter', async () => { 58 | expect(() => Filter.notEqual()).toThrow(); 59 | }); 60 | 61 | it('notEqual filter should fail, no value', async () => { 62 | expect(() => Filter.notEqual(label2)).toThrow(); 63 | }); 64 | 65 | it('in filter should fail, invalid filter', async () => { 66 | expect(() => Filter.in()).toThrow(); 67 | }); 68 | 69 | it('in filter should fail, no values', async () => { 70 | expect(() => Filter.in(label1)).toThrow(); 71 | }); 72 | 73 | it('in filter should fail, empty values array', async () => { 74 | expect(() => Filter.in(label1, [])).toThrow(); 75 | }); 76 | 77 | it('notIn filter should fail, invalid filter', async () => { 78 | expect(() => Filter.notIn()).toThrow(); 79 | }); 80 | 81 | it('notIn filter should fail, no values', async () => { 82 | expect(() => Filter.notIn(label1)).toThrow(); 83 | }); 84 | 85 | it('notIn filter should fail, empty values array', async () => { 86 | expect(() => Filter.notIn(label1, [])).toThrow(); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /tests/constants.js: -------------------------------------------------------------------------------- 1 | module.exports.commands = { 2 | TS_CREATE: 'TS.CREATE', 3 | TS_ALTER: 'TS.ALTER', 4 | TS_ADD: 'TS.ADD', 5 | TS_INCRBY: 'TS.INCRBY', 6 | TS_DECRBY: 'TS.DECRBY', 7 | TS_MADD: 'TS.MADD', 8 | TS_CREATERULE: 'TS.CREATERULE', 9 | TS_DELETERULE: 'TS.DELETERULE', 10 | TS_GET: 'TS.GET', 11 | TS_MGET: 'TS.MGET', 12 | TS_RANGE: 'TS.RANGE', 13 | TS_REVRANGE: 'TS.REVRANGE', 14 | TS_MRANGE: 'TS.MRANGE', 15 | TS_MREVRANGE: 'TS.MREVRANGE', 16 | TS_INFO: 'TS.INFO', 17 | TS_QUERYINDEX: 'TS.QUERYINDEX' 18 | }; 19 | 20 | module.exports.keywords = { 21 | AGGREGATION: 'AGGREGATION', 22 | COUNT: 'COUNT', 23 | DUPLICATE_POLICY: 'DUPLICATE_POLICY', 24 | FILTER: 'FILTER', 25 | LABELS: 'LABELS', 26 | ON_DUPLICATE: 'ON_DUPLICATE', 27 | RETENTION: 'RETENTION', 28 | TIMESTAMP: 'TIMESTAMP', 29 | UNCOMPRESSED: 'UNCOMPRESSED', 30 | WITHLABELS: 'WITHLABELS' 31 | }; 32 | -------------------------------------------------------------------------------- /tests/methods/add.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { OnDuplicate } = RedisTimeSeries; 5 | 6 | const { RETENTION, LABELS, UNCOMPRESSED, ON_DUPLICATE } = keywords; 7 | const { TS_ADD } = commands; 8 | 9 | const SIGN_SPACE = ' '; 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | key: 'sometestkey', 16 | retention: 60, 17 | onDuplicate: OnDuplicate.MAX, 18 | labels: { 19 | room: 'livingroom', 20 | section: 2 21 | }, 22 | value: 17.4, 23 | timestamp: Date.now() 24 | }; 25 | 26 | let rts = null; 27 | let labelsQuery = null; 28 | 29 | const validateQuery = (query) => { 30 | const [command, params] = rts.client.send_command.mock.calls[0]; 31 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 32 | }; 33 | 34 | describe('add method tests', () => { 35 | beforeEach(() => { 36 | jest.clearAllMocks(); 37 | 38 | rts = new RedisTimeSeries(TEST_OPTIONS); 39 | rts.connect(TEST_OPTIONS); 40 | 41 | const { labels } = TEST_PARAMS; 42 | 43 | labelsQuery = [LABELS, 'room', labels.room, 'section', labels.section]; 44 | }); 45 | 46 | it('should add a value to time series', async () => { 47 | const { key, timestamp, value } = TEST_PARAMS; 48 | const query = [TS_ADD, key, timestamp, value]; 49 | 50 | await rts.add(key, timestamp, value).send(); 51 | validateQuery(query); 52 | }); 53 | 54 | it('should add a value time series with retention', async () => { 55 | const { key, timestamp, value, retention } = TEST_PARAMS; 56 | const query = [TS_ADD, key, timestamp, value, RETENTION, retention]; 57 | 58 | await rts.add(key, timestamp, value).retention(retention).send(); 59 | validateQuery(query); 60 | }); 61 | 62 | it('should add a value time series with labels', async () => { 63 | const { key, timestamp, value, labels } = TEST_PARAMS; 64 | const query = [TS_ADD, key, timestamp, value, ...labelsQuery]; 65 | 66 | await rts.add(key, timestamp, value).labels(labels).send(); 67 | validateQuery(query); 68 | }); 69 | 70 | it('should add a value with an onDuplicate policy', async () => { 71 | const { key, timestamp, value, onDuplicate } = TEST_PARAMS; 72 | const query = [TS_ADD, key, timestamp, value, ON_DUPLICATE, onDuplicate]; 73 | 74 | await rts 75 | .add(key, timestamp, value) 76 | .onDuplicate(onDuplicate) 77 | .send(); 78 | validateQuery(query); 79 | }); 80 | 81 | it('should add a value time series with retention, uncompressed flag and labels', async () => { 82 | const { key, timestamp, value, retention, labels } = TEST_PARAMS; 83 | const query = [TS_ADD, key, timestamp, value, RETENTION, retention, UNCOMPRESSED, ...labelsQuery]; 84 | 85 | await rts 86 | .add(key, timestamp, value) 87 | .retention(retention) 88 | .labels(labels) 89 | .uncompressed() 90 | .send(); 91 | 92 | validateQuery(query); 93 | }); 94 | 95 | it('should throw an error, no arguments', async () => { 96 | await expect(rts.add().send()).rejects.toThrow(); 97 | }); 98 | 99 | it('should throw an error, timestamp and value are missing', async () => { 100 | await expect(rts.add(TEST_PARAMS.key).send()).rejects.toThrow(); 101 | }); 102 | 103 | it('should throw an error, value is missing', async () => { 104 | await expect(rts.add(TEST_PARAMS.key, TEST_PARAMS.timestamp).send()).rejects.toThrow(); 105 | }); 106 | 107 | it('should throw an error, value is not valid', async () => { 108 | const { key, timestamp } = TEST_PARAMS; 109 | await expect(rts.add(key, timestamp, true).send()).rejects.toThrow(); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/methods/alter.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { DuplicatePolicy } = RedisTimeSeries; 5 | 6 | const { RETENTION, LABELS, DUPLICATE_POLICY } = keywords; 7 | const { TS_ALTER } = commands; 8 | 9 | const SIGN_SPACE = ' '; 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | key: 'sometestkey', 16 | retention: 60, 17 | labels: { 18 | room: 'livingroom', 19 | section: 2 20 | }, 21 | duplicatePolicy: DuplicatePolicy.LAST 22 | }; 23 | 24 | let rts = null; 25 | let labelsQuery = null; 26 | 27 | const validateQuery = (query) => { 28 | const [command, params] = rts.client.send_command.mock.calls[0]; 29 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 30 | }; 31 | 32 | describe('alter method tests', () => { 33 | beforeEach(() => { 34 | jest.clearAllMocks(); 35 | 36 | rts = new RedisTimeSeries(TEST_OPTIONS); 37 | rts.connect(TEST_OPTIONS); 38 | 39 | const { labels } = TEST_PARAMS; 40 | labelsQuery = [LABELS, 'room', labels.room, 'section', labels.section]; 41 | }); 42 | 43 | it('should alter time series', async () => { 44 | const { key } = TEST_PARAMS; 45 | const query = [TS_ALTER, TEST_PARAMS.key]; 46 | 47 | await rts.alter(key).send(); 48 | validateQuery(query); 49 | }); 50 | 51 | it('should alter time series with retention', async () => { 52 | const { key, retention } = TEST_PARAMS; 53 | const query = [TS_ALTER, key, RETENTION, retention]; 54 | 55 | await rts.alter(key).retention(retention).send(); 56 | validateQuery(query); 57 | }); 58 | 59 | it('should alter time series duplicate policy', async () => { 60 | const { key, duplicatePolicy } = TEST_PARAMS; 61 | const query = [TS_ALTER, key, DUPLICATE_POLICY, duplicatePolicy]; 62 | 63 | await rts.alter(key).duplicatePolicy(duplicatePolicy).send(); 64 | validateQuery(query); 65 | }); 66 | 67 | it('should alter time series with labels', async () => { 68 | const { key, labels } = TEST_PARAMS; 69 | const query = [TS_ALTER, key, ...labelsQuery]; 70 | 71 | await rts.alter(key).labels(labels).send(); 72 | validateQuery(query); 73 | }); 74 | 75 | it('should alter time series with retention and labels', async () => { 76 | const { key, retention, labels } = TEST_PARAMS; 77 | const query = [TS_ALTER, key, RETENTION, retention, ...labelsQuery]; 78 | 79 | await rts.alter(key).retention(retention).labels(labels).send(); 80 | validateQuery(query); 81 | }); 82 | 83 | it('should throw an error, key is missing', async () => { 84 | await expect(rts.alter().send()).rejects.toThrow(); 85 | }); 86 | 87 | it('should throw an error, key is not valid', async () => { 88 | await expect(rts.alter(TEST_PARAMS.labels).send()).rejects.toThrow(); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /tests/methods/create.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { DuplicatePolicy } = RedisTimeSeries; 5 | 6 | const { RETENTION, LABELS, UNCOMPRESSED, DUPLICATE_POLICY } = keywords; 7 | const { TS_CREATE } = commands; 8 | 9 | const SIGN_SPACE = ' '; 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | key: 'sometestkey', 16 | retention: 60, 17 | duplicatePolicyType: DuplicatePolicy.LAST, 18 | labels: { 19 | room: 'livingroom', 20 | section: 2 21 | } 22 | }; 23 | 24 | let rts = null; 25 | let labelsQuery = null; 26 | 27 | const validateQuery = (query) => { 28 | const [command, params] = rts.client.send_command.mock.calls[0]; 29 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 30 | }; 31 | 32 | describe('create method tests', () => { 33 | beforeEach(() => { 34 | jest.clearAllMocks(); 35 | 36 | rts = new RedisTimeSeries(TEST_OPTIONS); 37 | rts.connect(TEST_OPTIONS); 38 | 39 | const { labels } = TEST_PARAMS; 40 | labelsQuery = [LABELS, 'room', labels.room, 'section', labels.section]; 41 | }); 42 | 43 | it('should create time series', async () => { 44 | const { key } = TEST_PARAMS; 45 | const query = [TS_CREATE, TEST_PARAMS.key]; 46 | 47 | await rts.create(key).send(); 48 | validateQuery(query); 49 | }); 50 | 51 | it('should create time series with retention', async () => { 52 | const { key, retention } = TEST_PARAMS; 53 | const query = [TS_CREATE, key, RETENTION, retention]; 54 | 55 | await rts.create(key).retention(retention).send(); 56 | validateQuery(query); 57 | }); 58 | 59 | it('should create time series with labels', async () => { 60 | const { key, labels } = TEST_PARAMS; 61 | const query = [TS_CREATE, key, ...labelsQuery]; 62 | 63 | await rts.create(key).labels(labels).send(); 64 | validateQuery(query); 65 | }); 66 | 67 | it('should create time series with uncompressed flag', async () => { 68 | const { key } = TEST_PARAMS; 69 | const query = [TS_CREATE, key, UNCOMPRESSED]; 70 | 71 | await rts.create(key).uncompressed().send(); 72 | validateQuery(query); 73 | }); 74 | 75 | it('should create time series with duplication policy', async () => { 76 | const { key, duplicatePolicyType } = TEST_PARAMS; 77 | const query = [TS_CREATE, key, DUPLICATE_POLICY, duplicatePolicyType]; 78 | 79 | await rts 80 | .create(key) 81 | .duplicatePolicy(duplicatePolicyType) 82 | .send(); 83 | validateQuery(query); 84 | }); 85 | 86 | it('should create time series with retention and labels', async () => { 87 | const { key, retention, labels } = TEST_PARAMS; 88 | const query = [TS_CREATE, key, RETENTION, retention, ...labelsQuery]; 89 | 90 | await rts 91 | .create(key) 92 | .retention(retention) 93 | .labels(labels) 94 | .send(); 95 | 96 | validateQuery(query); 97 | }); 98 | 99 | it('should create time series with retention, labels and uncompressed flag', async () => { 100 | const { key, retention, labels } = TEST_PARAMS; 101 | const query = [TS_CREATE, key, RETENTION, retention, UNCOMPRESSED, ...labelsQuery]; 102 | 103 | await rts 104 | .create(key) 105 | .retention(retention) 106 | .labels(labels) 107 | .uncompressed() 108 | .send(); 109 | 110 | validateQuery(query); 111 | }); 112 | 113 | it('should throw an error, key is missing', async () => { 114 | await expect(rts.create().send()).rejects.toThrow(); 115 | }); 116 | 117 | it('should throw an error, key is not string', async () => { 118 | await expect(rts.create(TEST_PARAMS.uncompressed).send()).rejects.toThrow(); 119 | }); 120 | 121 | it('should throw an error, retention is not valid', async () => { 122 | await expect(rts.create(TEST_PARAMS.key).retention(TEST_PARAMS.key).send()).rejects.toThrow(); 123 | }); 124 | 125 | it('should throw an error, labels are not valid', async () => { 126 | await expect(rts.create(TEST_PARAMS.key).labels(TEST_PARAMS.key).send()).rejects.toThrow(); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /tests/methods/createRule.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { Aggregation } = RedisTimeSeries; 5 | 6 | const { AGGREGATION } = keywords; 7 | const { TS_CREATERULE } = commands; 8 | 9 | const SIGN_SPACE = ' '; 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | srcKey: 'sourceKey', 16 | dstKey: 'destinationKey', 17 | aggregation: { 18 | type: Aggregation.AVG, 19 | timeBucket: 60000 20 | } 21 | }; 22 | 23 | let rts = null; 24 | let aggregationQuery = null; 25 | 26 | const validateQuery = (query) => { 27 | const [command, params] = rts.client.send_command.mock.calls[0]; 28 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 29 | }; 30 | 31 | describe('createRule method tests', () => { 32 | beforeEach(() => { 33 | jest.clearAllMocks(); 34 | 35 | rts = new RedisTimeSeries(TEST_OPTIONS); 36 | rts.connect(TEST_OPTIONS); 37 | 38 | const { aggregation } = TEST_PARAMS; 39 | aggregationQuery = [AGGREGATION, aggregation.type, aggregation.timeBucket]; 40 | }); 41 | 42 | it('should create aggregation rule', async () => { 43 | const { srcKey, dstKey, aggregation } = TEST_PARAMS; 44 | const query = [TS_CREATERULE, srcKey, dstKey, ...aggregationQuery]; 45 | 46 | await rts 47 | .createRule(srcKey, dstKey) 48 | .aggregation(aggregation.type, aggregation.timeBucket) 49 | .send(); 50 | 51 | validateQuery(query); 52 | }); 53 | 54 | it('should throw an error, no arguments', async () => { 55 | await expect(rts.createRule().send()).rejects.toThrow(); 56 | }); 57 | 58 | it('should throw an error, source key is invalid', async () => { 59 | await expect(rts.createRule({}).send()).rejects.toThrow(); 60 | }); 61 | 62 | it('should throw an error, destination key is invalid', async () => { 63 | await expect(rts.createRule(TEST_PARAMS.srcKey, {}).send()).rejects.toThrow(); 64 | }); 65 | 66 | it('should throw an error, aggregation is missing', async () => { 67 | const { srcKey, dstKey } = TEST_PARAMS; 68 | await expect(rts.createRule(srcKey, dstKey).send()).rejects.toThrow(); 69 | }); 70 | 71 | it('should throw an error, timeBucket is invalid', async () => { 72 | const { srcKey, dstKey, aggregation } = TEST_PARAMS; 73 | const { type } = aggregation; 74 | await expect(rts.createRule(srcKey, dstKey).aggregation(type, true).send()).rejects.toThrow(); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/methods/decrby.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { TIMESTAMP, RETENTION, LABELS } = keywords; 5 | const { TS_DECRBY } = commands; 6 | const SIGN_SPACE = ' '; 7 | 8 | const TEST_OPTIONS = { 9 | host: 'localhost', 10 | port: 6379 11 | }; 12 | const TEST_PARAMS = { 13 | key: 'sometestkey', 14 | retention: 60, 15 | labels: { 16 | room: 'livingroom', 17 | section: 2 18 | }, 19 | value: 17.4, 20 | timestamp: Date.now() 21 | }; 22 | 23 | let rts = null; 24 | let labelsQuery = null; 25 | 26 | const validateQuery = (query) => { 27 | const [command, params] = rts.client.send_command.mock.calls[0]; 28 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 29 | 30 | const { labels } = TEST_PARAMS; 31 | labelsQuery = [LABELS, 'room', labels.room, 'section', labels.section]; 32 | }; 33 | 34 | describe('decrBy method tests', () => { 35 | beforeEach(() => { 36 | jest.clearAllMocks(); 37 | 38 | rts = new RedisTimeSeries(TEST_OPTIONS); 39 | rts.connect(TEST_OPTIONS); 40 | }); 41 | 42 | it('should decrement the latest value of time series', async () => { 43 | const { key, value } = TEST_PARAMS; 44 | const query = [TS_DECRBY, key, value]; 45 | 46 | await rts.decrBy(key, value).send(); 47 | validateQuery(query); 48 | }); 49 | 50 | it('should decrement the latest value of time series with timestamp', async () => { 51 | const { key, value, timestamp } = TEST_PARAMS; 52 | const query = [TS_DECRBY, key, value, TIMESTAMP, timestamp]; 53 | 54 | await rts.decrBy(key, value).timestamp(timestamp).send(); 55 | validateQuery(query); 56 | }); 57 | 58 | it('should decrement the latest value of time series with retention', async () => { 59 | const { key, value, retention } = TEST_PARAMS; 60 | const query = [TS_DECRBY, key, value, RETENTION, retention]; 61 | 62 | await rts.decrBy(key, value).retention(retention).send(); 63 | validateQuery(query); 64 | }); 65 | 66 | it('should decrement the latest value of time series with labels', async () => { 67 | const { key, value, labels } = TEST_PARAMS; 68 | const query = [TS_DECRBY, key, value, ...labelsQuery]; 69 | 70 | await rts.decrBy(key, value).labels(labels).send(); 71 | validateQuery(query); 72 | }); 73 | 74 | it('should throw an error, no arguments', async () => { 75 | await expect(rts.decrBy().send()).rejects.toThrow(); 76 | }); 77 | 78 | it('should throw an error, value is missing', async () => { 79 | await expect(rts.decrBy(TEST_PARAMS.key).send()).rejects.toThrow(); 80 | }); 81 | 82 | it('should throw an error, value is not valid', async () => { 83 | await expect(rts.decrBy(TEST_PARAMS.key, TEST_PARAMS.key).send()).rejects.toThrow(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /tests/methods/deleteRule.test.js: -------------------------------------------------------------------------------- 1 | const { commands } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { TS_DELETERULE } = commands; 5 | 6 | const SIGN_SPACE = ' '; 7 | const TEST_OPTIONS = { 8 | host: 'localhost', 9 | port: 6379 10 | }; 11 | const TEST_PARAMS = { 12 | srcKey: 'sourceKey', 13 | dstKey: 'destinationKey' 14 | }; 15 | 16 | let rts = null; 17 | 18 | const validateQuery = (query) => { 19 | const [command, params] = rts.client.send_command.mock.calls[0]; 20 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 21 | }; 22 | 23 | describe('deleteRule method tests', () => { 24 | beforeEach(() => { 25 | jest.clearAllMocks(); 26 | 27 | rts = new RedisTimeSeries(TEST_OPTIONS); 28 | rts.connect(TEST_OPTIONS); 29 | }); 30 | 31 | it('should delete aggregation rule', async () => { 32 | const { srcKey, dstKey } = TEST_PARAMS; 33 | const query = [TS_DELETERULE, srcKey, dstKey]; 34 | 35 | await rts.deleteRule(srcKey, dstKey).send(); 36 | validateQuery(query); 37 | }); 38 | 39 | it('should throw an error, no arguments', async () => { 40 | await expect(rts.deleteRule().send()).rejects.toThrow(); 41 | }); 42 | 43 | it('should throw an error, source key is invalid', async () => { 44 | await expect(rts.deleteRule({}).send()).rejects.toThrow(); 45 | }); 46 | 47 | it('should throw an error, destination key is invalid', async () => { 48 | await expect(rts.deleteRule(TEST_PARAMS.srcKey, {}).send()).rejects.toThrow(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/methods/get.test.js: -------------------------------------------------------------------------------- 1 | const { commands } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { TS_GET } = commands; 5 | const SIGN_SPACE = ' '; 6 | 7 | const TEST_OPTIONS = { 8 | host: 'localhost', 9 | port: 6379 10 | }; 11 | const TEST_PARAMS = { 12 | key: 'sometestkey' 13 | }; 14 | 15 | let rts = null; 16 | 17 | const validateQuery = (query) => { 18 | const [command, params] = rts.client.send_command.mock.calls[0]; 19 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 20 | }; 21 | 22 | describe('get method tests', () => { 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | 26 | rts = new RedisTimeSeries(TEST_OPTIONS); 27 | rts.connect(TEST_OPTIONS); 28 | }); 29 | 30 | it('should fetch time series', async () => { 31 | const { key } = TEST_PARAMS; 32 | const query = [TS_GET, key]; 33 | 34 | await rts.get(key).send(); 35 | validateQuery(query); 36 | }); 37 | 38 | it('should throw an error, no arguments', async () => { 39 | await expect(rts.get().send()).rejects.toThrow(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/methods/incrby.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { TIMESTAMP, RETENTION, LABELS } = keywords; 5 | const { TS_INCRBY } = commands; 6 | const SIGN_SPACE = ' '; 7 | 8 | const TEST_OPTIONS = { 9 | host: 'localhost', 10 | port: 6379 11 | }; 12 | const TEST_PARAMS = { 13 | key: 'sometestkey', 14 | retention: 60, 15 | labels: { 16 | room: 'livingroom', 17 | section: 2 18 | }, 19 | value: 17.4, 20 | timestamp: Date.now() 21 | }; 22 | 23 | let rts = null; 24 | let labelsQuery = null; 25 | 26 | const validateQuery = (query) => { 27 | const [command, params] = rts.client.send_command.mock.calls[0]; 28 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 29 | 30 | const { labels } = TEST_PARAMS; 31 | labelsQuery = [LABELS, 'room', labels.room, 'section', labels.section]; 32 | }; 33 | 34 | describe('incrBy method tests', () => { 35 | beforeEach(() => { 36 | jest.clearAllMocks(); 37 | 38 | rts = new RedisTimeSeries(TEST_OPTIONS); 39 | rts.connect(TEST_OPTIONS); 40 | }); 41 | 42 | it('should increment the latest value of time series', async () => { 43 | const { key, value } = TEST_PARAMS; 44 | const query = [TS_INCRBY, key, value]; 45 | 46 | await rts.incrBy(key, value).send(); 47 | validateQuery(query); 48 | }); 49 | 50 | it('should increment the latest value of time series with timestamp', async () => { 51 | const { key, value, timestamp } = TEST_PARAMS; 52 | const query = [TS_INCRBY, key, value, TIMESTAMP, timestamp]; 53 | 54 | await rts.incrBy(key, value).timestamp(timestamp).send(); 55 | validateQuery(query); 56 | }); 57 | 58 | it('should increment the latest value of time series with retention', async () => { 59 | const { key, value, retention } = TEST_PARAMS; 60 | const query = [TS_INCRBY, key, value, RETENTION, retention]; 61 | 62 | await rts.incrBy(key, value).retention(retention).send(); 63 | validateQuery(query); 64 | }); 65 | 66 | it('should increment the latest value of time series with labels', async () => { 67 | const { key, value, labels } = TEST_PARAMS; 68 | const query = [TS_INCRBY, key, value, ...labelsQuery]; 69 | 70 | await rts.incrBy(key, value).labels(labels).send(); 71 | validateQuery(query); 72 | }); 73 | 74 | it('should throw an error, no arguments', async () => { 75 | await expect(rts.incrBy().send()).rejects.toThrow(); 76 | }); 77 | 78 | it('should throw an error, value is missing', async () => { 79 | await expect(rts.incrBy(TEST_PARAMS.key).send()).rejects.toThrow(); 80 | }); 81 | 82 | it('should throw an error, value is not valid', async () => { 83 | await expect(rts.incrBy(TEST_PARAMS.key, TEST_PARAMS.key).send()).rejects.toThrow(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /tests/methods/info.test.js: -------------------------------------------------------------------------------- 1 | const { commands } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { TS_INFO } = commands; 5 | const SIGN_SPACE = ' '; 6 | 7 | const TEST_OPTIONS = { 8 | host: 'localhost', 9 | port: 6379 10 | }; 11 | const TEST_PARAMS = { 12 | key: 'sometestkey' 13 | }; 14 | 15 | let rts = null; 16 | 17 | const validateQuery = (query) => { 18 | const [command, params] = rts.client.send_command.mock.calls[0]; 19 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 20 | }; 21 | 22 | describe('info method tests', () => { 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | 26 | rts = new RedisTimeSeries(TEST_OPTIONS); 27 | rts.connect(TEST_OPTIONS); 28 | }); 29 | 30 | it('should fetch time series info and statistics', async () => { 31 | const { key } = TEST_PARAMS; 32 | const query = [TS_INFO, key]; 33 | 34 | await rts.info(key).send(); 35 | validateQuery(query); 36 | }); 37 | 38 | it('should throw an error, no arguments', async () => { 39 | await expect(rts.info().send()).rejects.toThrow(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/methods/madd.test.js: -------------------------------------------------------------------------------- 1 | const { commands } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { TS_MADD } = commands; 5 | 6 | const SIGN_SPACE = ' '; 7 | const TEST_OPTIONS = { 8 | host: 'localhost', 9 | port: 6379 10 | }; 11 | const TEST_PARAMS = { 12 | multi: [ 13 | { key: 'sometestkey1', value: 15.4, timestamp: Date.now() }, 14 | { key: 'sometestkey2', value: 16.4, timestamp: Date.now() }, 15 | { key: 'sometestkey3', value: 17.4, timestamp: Date.now() } 16 | ] 17 | }; 18 | 19 | let rts = null; 20 | 21 | const validateQuery = (query) => { 22 | const [command, params] = rts.client.send_command.mock.calls[0]; 23 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 24 | }; 25 | 26 | describe('madd method tests', () => { 27 | beforeEach(() => { 28 | jest.clearAllMocks(); 29 | 30 | rts = new RedisTimeSeries(TEST_OPTIONS); 31 | rts.connect(TEST_OPTIONS); 32 | }); 33 | 34 | it('should add multiple values to different time series', async () => { 35 | const { multi } = TEST_PARAMS; 36 | 37 | const multiToArray = multi.reduce((acc, { key, timestamp, value }) => { 38 | acc.push(`${key} ${timestamp} ${value}`); 39 | return acc; 40 | }, []); 41 | const query = [TS_MADD, ...multiToArray]; 42 | 43 | await rts.madd(multi).send(); 44 | validateQuery(query); 45 | }); 46 | 47 | it('should fail, no arguments', async () => { 48 | await expect(rts.madd().send()).rejects.toThrow(); 49 | }); 50 | 51 | it('should fail, empty array', async () => { 52 | await expect(rts.madd([]).send()).rejects.toThrow(); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/methods/mget.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { Filter } = RedisTimeSeries; 5 | 6 | const { FILTER } = keywords; 7 | const { TS_MGET } = commands; 8 | const SIGN_SPACE = ' '; 9 | 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | label1: 'somelabel1', 16 | label2: 'somelabel1', 17 | value1: 22, 18 | value2: 23 19 | }; 20 | 21 | let rts = null; 22 | 23 | const validateQuery = (query) => { 24 | const [command, params] = rts.client.send_command.mock.calls[0]; 25 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 26 | }; 27 | 28 | describe('mget method tests', () => { 29 | beforeEach(() => { 30 | jest.clearAllMocks(); 31 | 32 | rts = new RedisTimeSeries(TEST_OPTIONS); 33 | rts.connect(TEST_OPTIONS); 34 | }); 35 | 36 | it('should fetch time series with given label', async () => { 37 | const { label1 } = TEST_PARAMS; 38 | const query = [TS_MGET, FILTER, `${label1}!=`]; 39 | 40 | await rts 41 | .mget() 42 | .filter([Filter.exists(label1)]) 43 | .send(); 44 | validateQuery(query); 45 | }); 46 | 47 | it('should fetch time series with multiple given labels', async () => { 48 | const { label1, label2 } = TEST_PARAMS; 49 | const query = [TS_MGET, FILTER, `${label1}!=`, `${label2}=`]; 50 | 51 | await rts 52 | .mget() 53 | .filter([ 54 | Filter.exists(label1), 55 | Filter.notExists(label2) 56 | ]) 57 | .send(); 58 | validateQuery(query); 59 | }); 60 | 61 | it('should fetch time series with multiple given labels', async () => { 62 | const { label1, label2, value1, value2 } = TEST_PARAMS; 63 | const query = [TS_MGET, FILTER, `${label1}=${value1}`, `${label2}=(${value1},${value2})`]; 64 | 65 | await rts 66 | .mget() 67 | .filter([ 68 | Filter.equal(label1, value1), 69 | Filter.in(label2, [value1, value2]) 70 | ]) 71 | .send(); 72 | validateQuery(query); 73 | }); 74 | 75 | it('should fail, no arguments', async () => { 76 | await expect(rts.mget().send()).rejects.toThrow(); 77 | }); 78 | 79 | it('should fail, empty filter', async () => { 80 | await expect(rts.mget().filter().send()).rejects.toThrow(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /tests/methods/mrange.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { Aggregation, Filter } = RedisTimeSeries; 5 | 6 | const { AGGREGATION, FILTER, WITHLABELS, COUNT } = keywords; 7 | const { TS_MRANGE } = commands; 8 | const SIGN_SPACE = ' '; 9 | 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | key: 'sometestkey', 16 | fromTimestamp: Date.now() - 10000, 17 | toTimestamp: Date.now(), 18 | aggregation: { 19 | type: Aggregation.AVG, 20 | timeBucket: 60000 21 | }, 22 | filter: { 23 | label1: 'somelabel1', 24 | label2: 'somelabel1', 25 | value1: 22, 26 | value2: 23 27 | }, 28 | count: 20 29 | }; 30 | 31 | let rts = null; 32 | 33 | const validateQuery = (query) => { 34 | const [command, params] = rts.client.send_command.mock.calls[0]; 35 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 36 | }; 37 | 38 | describe('mrange method tests', () => { 39 | beforeEach(() => { 40 | jest.clearAllMocks(); 41 | 42 | rts = new RedisTimeSeries(TEST_OPTIONS); 43 | rts.connect(TEST_OPTIONS); 44 | }); 45 | 46 | it('should fetch range of multiple time series', async () => { 47 | const { fromTimestamp, toTimestamp, filter } = TEST_PARAMS; 48 | const { label1, label2 } = filter; 49 | const query = [TS_MRANGE, fromTimestamp, toTimestamp, FILTER, `${label1}!=`, `${label2}=`]; 50 | 51 | await rts 52 | .mrange(fromTimestamp, toTimestamp) 53 | .filter([ 54 | Filter.exists(label1), 55 | Filter.notExists(label2) 56 | ]) 57 | .send(); 58 | 59 | validateQuery(query); 60 | }); 61 | 62 | it('should fetch N items in range of multiple time series', async () => { 63 | const { fromTimestamp, toTimestamp, count, filter } = TEST_PARAMS; 64 | const { label1, value1 } = filter; 65 | const filterQuery = `${FILTER} ${label1}=${value1}`; 66 | const query = [TS_MRANGE, fromTimestamp, toTimestamp, COUNT, count, filterQuery]; 67 | 68 | await rts 69 | .mrange(fromTimestamp, toTimestamp) 70 | .count(count) 71 | .filter([Filter.equal(label1, value1)]) 72 | .send(); 73 | 74 | validateQuery(query); 75 | }); 76 | 77 | it('should fetch N items in range of time series for given key and aggregation', async () => { 78 | const { fromTimestamp, toTimestamp, count, aggregation, filter } = TEST_PARAMS; 79 | const { type, timeBucket } = aggregation; 80 | const { label1, value1 } = filter; 81 | 82 | const filterQuery = `${FILTER} ${label1}!=${value1}`; 83 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 84 | const query = [TS_MRANGE, fromTimestamp, toTimestamp, COUNT, count, ...aggregationQuery, filterQuery]; 85 | 86 | await rts 87 | .mrange(fromTimestamp, toTimestamp) 88 | .filter([Filter.notEqual(label1, value1)]) 89 | .count(count) 90 | .aggregation(type, timeBucket) 91 | .send(); 92 | 93 | validateQuery(query); 94 | }); 95 | 96 | it('should fetch range of time series with labels for given key and aggregation', async () => { 97 | const { fromTimestamp, toTimestamp, aggregation, filter } = TEST_PARAMS; 98 | const { type, timeBucket } = aggregation; 99 | const { label1, value1 } = filter; 100 | 101 | const filterQuery = `${FILTER} ${label1}!=${value1}`; 102 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 103 | const query = [TS_MRANGE, fromTimestamp, toTimestamp, ...aggregationQuery, WITHLABELS, filterQuery]; 104 | 105 | await rts 106 | .mrange(fromTimestamp, toTimestamp) 107 | .filter([Filter.notEqual(label1, value1)]) 108 | .withLabels() 109 | .aggregation(type, timeBucket) 110 | .send(); 111 | 112 | validateQuery(query); 113 | }); 114 | 115 | it('should fail, no arguments', async () => { 116 | await expect(rts.mrange().send()).rejects.toThrow(); 117 | }); 118 | 119 | it('should fail, no toTimestamp', async () => { 120 | const { fromTimestamp } = TEST_PARAMS; 121 | await expect(rts.mrange(fromTimestamp).send()).rejects.toThrow(); 122 | }); 123 | 124 | it('should fail, no filter', async () => { 125 | const { fromTimestamp, toTimestamp } = TEST_PARAMS; 126 | await expect(rts.mrange(fromTimestamp, toTimestamp).send()).rejects.toThrow(); 127 | }); 128 | 129 | it('should fail, filter is empty', async () => { 130 | const { fromTimestamp, toTimestamp } = TEST_PARAMS; 131 | await expect(rts.mrange(fromTimestamp, toTimestamp).filter().send()).rejects.toThrow(); 132 | }); 133 | 134 | it('should fail, invalid filter ', async () => { 135 | const { fromTimestamp, toTimestamp } = TEST_PARAMS; 136 | await expect(rts.mrange(fromTimestamp, toTimestamp).filter().send()).rejects.toThrow(); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /tests/methods/mrevrange.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { Aggregation, Filter } = RedisTimeSeries; 5 | 6 | const { AGGREGATION, FILTER, WITHLABELS, COUNT } = keywords; 7 | const { TS_MREVRANGE } = commands; 8 | const SIGN_SPACE = ' '; 9 | 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | key: 'sometestkey', 16 | fromTimestamp: Date.now() - 10000, 17 | toTimestamp: Date.now(), 18 | aggregation: { 19 | type: Aggregation.AVG, 20 | timeBucket: 60000 21 | }, 22 | filter: { 23 | label1: 'somelabel1', 24 | label2: 'somelabel1', 25 | value1: 22, 26 | value2: 23 27 | }, 28 | count: 20 29 | }; 30 | 31 | let rts = null; 32 | 33 | const validateQuery = (query) => { 34 | const [command, params] = rts.client.send_command.mock.calls[0]; 35 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 36 | }; 37 | 38 | describe('mrange method tests', () => { 39 | beforeEach(() => { 40 | jest.clearAllMocks(); 41 | 42 | rts = new RedisTimeSeries(TEST_OPTIONS); 43 | rts.connect(TEST_OPTIONS); 44 | }); 45 | 46 | it('should fetch range of multiple time series', async () => { 47 | const { fromTimestamp, toTimestamp, filter } = TEST_PARAMS; 48 | const { label1, label2 } = filter; 49 | const query = [TS_MREVRANGE, fromTimestamp, toTimestamp, FILTER, `${label1}!=`, `${label2}=`]; 50 | 51 | await rts 52 | .mrevrange(fromTimestamp, toTimestamp) 53 | .filter([ 54 | Filter.exists(label1), 55 | Filter.notExists(label2) 56 | ]) 57 | .send(); 58 | 59 | validateQuery(query); 60 | }); 61 | 62 | it('should fetch N items in range of multiple time series', async () => { 63 | const { fromTimestamp, toTimestamp, count, filter } = TEST_PARAMS; 64 | const { label1, value1 } = filter; 65 | const filterQuery = `${FILTER} ${label1}=${value1}`; 66 | const query = [TS_MREVRANGE, fromTimestamp, toTimestamp, COUNT, count, filterQuery]; 67 | 68 | await rts 69 | .mrevrange(fromTimestamp, toTimestamp) 70 | .count(count) 71 | .filter([Filter.equal(label1, value1)]) 72 | .send(); 73 | 74 | validateQuery(query); 75 | }); 76 | 77 | it('should fetch N items in range of time series for given key and aggregation', async () => { 78 | const { fromTimestamp, toTimestamp, count, aggregation, filter } = TEST_PARAMS; 79 | const { type, timeBucket } = aggregation; 80 | const { label1, value1 } = filter; 81 | 82 | const filterQuery = `${FILTER} ${label1}!=${value1}`; 83 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 84 | const query = [TS_MREVRANGE, fromTimestamp, toTimestamp, COUNT, count, ...aggregationQuery, filterQuery]; 85 | 86 | await rts 87 | .mrevrange(fromTimestamp, toTimestamp) 88 | .filter([Filter.notEqual(label1, value1)]) 89 | .count(count) 90 | .aggregation(type, timeBucket) 91 | .send(); 92 | 93 | validateQuery(query); 94 | }); 95 | 96 | it('should fetch range of time series with labels for given key and aggregation', async () => { 97 | const { fromTimestamp, toTimestamp, aggregation, filter } = TEST_PARAMS; 98 | const { type, timeBucket } = aggregation; 99 | const { label1, value1 } = filter; 100 | 101 | const filterQuery = `${FILTER} ${label1}!=${value1}`; 102 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 103 | const query = [TS_MREVRANGE, fromTimestamp, toTimestamp, ...aggregationQuery, WITHLABELS, filterQuery]; 104 | 105 | await rts 106 | .mrevrange(fromTimestamp, toTimestamp) 107 | .filter([Filter.notEqual(label1, value1)]) 108 | .withLabels() 109 | .aggregation(type, timeBucket) 110 | .send(); 111 | 112 | validateQuery(query); 113 | }); 114 | 115 | it('should fail, no arguments', async () => { 116 | await expect(rts.mrevrange().send()).rejects.toThrow(); 117 | }); 118 | 119 | it('should fail, no toTimestamp', async () => { 120 | const { fromTimestamp } = TEST_PARAMS; 121 | await expect(rts.mrevrange(fromTimestamp).send()).rejects.toThrow(); 122 | }); 123 | 124 | it('should fail, no filter', async () => { 125 | const { fromTimestamp, toTimestamp } = TEST_PARAMS; 126 | await expect(rts.mrevrange(fromTimestamp, toTimestamp).send()).rejects.toThrow(); 127 | }); 128 | 129 | it('should fail, filter is empty', async () => { 130 | const { fromTimestamp, toTimestamp } = TEST_PARAMS; 131 | await expect(rts.mrevrange(fromTimestamp, toTimestamp).filter().send()).rejects.toThrow(); 132 | }); 133 | 134 | it('should fail, invalid filter ', async () => { 135 | const { fromTimestamp, toTimestamp } = TEST_PARAMS; 136 | await expect(rts.mrevrange(fromTimestamp, toTimestamp).filter().send()).rejects.toThrow(); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /tests/methods/queryIndex.test.js: -------------------------------------------------------------------------------- 1 | const { commands } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { Filter } = RedisTimeSeries; 5 | 6 | const { TS_QUERYINDEX } = commands; 7 | const SIGN_SPACE = ' '; 8 | 9 | const TEST_OPTIONS = { 10 | host: 'localhost', 11 | port: 6379 12 | }; 13 | const TEST_PARAMS = { 14 | label1: 'somelabel1', 15 | label2: 'somelabel1', 16 | value1: 22, 17 | value2: 23 18 | }; 19 | 20 | let rts = null; 21 | 22 | const validateQuery = (query) => { 23 | const [command, params] = rts.client.send_command.mock.calls[0]; 24 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 25 | }; 26 | 27 | describe('queryIndex method tests', () => { 28 | beforeEach(() => { 29 | jest.clearAllMocks(); 30 | 31 | rts = new RedisTimeSeries(TEST_OPTIONS); 32 | rts.connect(TEST_OPTIONS); 33 | }); 34 | 35 | it('should fetch time series keys with given label', async () => { 36 | const { label1 } = TEST_PARAMS; 37 | const query = [TS_QUERYINDEX, `${label1}!=`]; 38 | 39 | await rts.queryIndex([Filter.exists(label1)]).send(); 40 | validateQuery(query); 41 | }); 42 | 43 | it('should fetch time series keys with multiple given labels', async () => { 44 | const { label1, label2 } = TEST_PARAMS; 45 | const query = [TS_QUERYINDEX, `${label1}!=`, `${label2}=`]; 46 | 47 | await rts.queryIndex([ 48 | Filter.exists(label1), 49 | Filter.notExists(label2) 50 | ]) 51 | .send(); 52 | validateQuery(query); 53 | }); 54 | 55 | it('should fetch time series keys with multiple given labels', async () => { 56 | const { label1, label2, value1, value2 } = TEST_PARAMS; 57 | const query = [TS_QUERYINDEX, `${label1}=${value1}`, `${label2}=(${value1},${value2})`]; 58 | 59 | await rts 60 | .queryIndex([ 61 | Filter.equal(label1, value1), 62 | Filter.in(label2, [value1, value2]) 63 | ]) 64 | .send(); 65 | validateQuery(query); 66 | }); 67 | 68 | it('should fail, no arguments', async () => { 69 | await expect(rts.queryIndex().send()).rejects.toThrow(); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/methods/range.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { Aggregation } = RedisTimeSeries; 5 | 6 | const { AGGREGATION, COUNT } = keywords; 7 | const { TS_RANGE } = commands; 8 | const SIGN_SPACE = ' '; 9 | 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | key: 'sometestkey', 16 | fromTimestamp: Date.now() - 10000, 17 | toTimestamp: Date.now(), 18 | aggregation: { 19 | type: Aggregation.AVG, 20 | timeBucket: 60000 21 | }, 22 | count: 20 23 | }; 24 | 25 | let rts = null; 26 | 27 | const validateQuery = (query) => { 28 | const [command, params] = rts.client.send_command.mock.calls[0]; 29 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 30 | }; 31 | 32 | describe('range method tests', () => { 33 | beforeEach(() => { 34 | jest.clearAllMocks(); 35 | 36 | rts = new RedisTimeSeries(TEST_OPTIONS); 37 | rts.connect(TEST_OPTIONS); 38 | }); 39 | 40 | it('should fetch range of time series for given key', async () => { 41 | const { key, fromTimestamp, toTimestamp } = TEST_PARAMS; 42 | const query = [TS_RANGE, key, fromTimestamp, toTimestamp]; 43 | 44 | await rts.range(key, fromTimestamp, toTimestamp).send(); 45 | validateQuery(query); 46 | }); 47 | 48 | it('should fetch N items in range of time series for given key', async () => { 49 | const { key, fromTimestamp, toTimestamp, count } = TEST_PARAMS; 50 | const query = [TS_RANGE, key, fromTimestamp, toTimestamp, COUNT, count]; 51 | 52 | await rts 53 | .range(key, fromTimestamp, toTimestamp) 54 | .count(count) 55 | .send(); 56 | 57 | validateQuery(query); 58 | }); 59 | 60 | it('should fetch N items in range of time series for given key and aggregation', async () => { 61 | const { key, fromTimestamp, toTimestamp, count, aggregation } = TEST_PARAMS; 62 | const { type, timeBucket } = aggregation; 63 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 64 | const query = [TS_RANGE, key, fromTimestamp, toTimestamp, COUNT, count, ...aggregationQuery]; 65 | 66 | await rts 67 | .range(key, fromTimestamp, toTimestamp) 68 | .count(count) 69 | .aggregation(type, timeBucket) 70 | .send(); 71 | 72 | validateQuery(query); 73 | }); 74 | 75 | it('should fetch range of time series for given key and aggregation', async () => { 76 | const { key, fromTimestamp, toTimestamp, aggregation } = TEST_PARAMS; 77 | const { type, timeBucket } = aggregation; 78 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 79 | const query = [TS_RANGE, key, fromTimestamp, toTimestamp, ...aggregationQuery]; 80 | 81 | await rts 82 | .range(key, fromTimestamp, toTimestamp) 83 | .aggregation(type, timeBucket) 84 | .send(); 85 | 86 | validateQuery(query); 87 | }); 88 | 89 | it('should fail, no arguments', async () => { 90 | await expect(rts.range().send()).rejects.toThrow(); 91 | }); 92 | 93 | it('should fail, no range timestamps', async () => { 94 | await expect(rts.range(TEST_PARAMS.key).send()).rejects.toThrow(); 95 | }); 96 | 97 | it('should fail, no toTimestamp', async () => { 98 | const { key, fromTimestamp } = TEST_PARAMS; 99 | await expect(rts.range(key, fromTimestamp).send()).rejects.toThrow(); 100 | }); 101 | 102 | it('should fail, count not valid', async () => { 103 | const { key, fromTimestamp, toTimestamp } = TEST_PARAMS; 104 | await expect(rts.range(key, fromTimestamp, toTimestamp).count(true).send()).rejects.toThrow(); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /tests/methods/revrange.test.js: -------------------------------------------------------------------------------- 1 | const { commands, keywords } = require('../constants'); 2 | const RedisTimeSeries = require('../../index'); 3 | 4 | const { Aggregation } = RedisTimeSeries; 5 | 6 | const { AGGREGATION, COUNT } = keywords; 7 | const { TS_REVRANGE } = commands; 8 | const SIGN_SPACE = ' '; 9 | 10 | const TEST_OPTIONS = { 11 | host: 'localhost', 12 | port: 6379 13 | }; 14 | const TEST_PARAMS = { 15 | key: 'sometestkey', 16 | fromTimestamp: Date.now() - 10000, 17 | toTimestamp: Date.now(), 18 | aggregation: { 19 | type: Aggregation.AVG, 20 | timeBucket: 60000 21 | }, 22 | count: 20 23 | }; 24 | 25 | let rts = null; 26 | 27 | const validateQuery = (query) => { 28 | const [command, params] = rts.client.send_command.mock.calls[0]; 29 | expect([command, ...params].join(SIGN_SPACE)).toBe(query.join(SIGN_SPACE)); 30 | }; 31 | 32 | describe('range method tests', () => { 33 | beforeEach(() => { 34 | jest.clearAllMocks(); 35 | 36 | rts = new RedisTimeSeries(TEST_OPTIONS); 37 | rts.connect(TEST_OPTIONS); 38 | }); 39 | 40 | it('should fetch range of time series for given key', async () => { 41 | const { key, fromTimestamp, toTimestamp } = TEST_PARAMS; 42 | const query = [TS_REVRANGE, key, fromTimestamp, toTimestamp]; 43 | 44 | await rts.revrange(key, fromTimestamp, toTimestamp).send(); 45 | validateQuery(query); 46 | }); 47 | 48 | it('should fetch N items in range of time series for given key', async () => { 49 | const { key, fromTimestamp, toTimestamp, count } = TEST_PARAMS; 50 | const query = [TS_REVRANGE, key, fromTimestamp, toTimestamp, COUNT, count]; 51 | 52 | await rts 53 | .revrange(key, fromTimestamp, toTimestamp) 54 | .count(count) 55 | .send(); 56 | 57 | validateQuery(query); 58 | }); 59 | 60 | it('should fetch N items in range of time series for given key and aggregation', async () => { 61 | const { key, fromTimestamp, toTimestamp, count, aggregation } = TEST_PARAMS; 62 | const { type, timeBucket } = aggregation; 63 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 64 | const query = [TS_REVRANGE, key, fromTimestamp, toTimestamp, COUNT, count, ...aggregationQuery]; 65 | 66 | await rts 67 | .revrange(key, fromTimestamp, toTimestamp) 68 | .count(count) 69 | .aggregation(type, timeBucket) 70 | .send(); 71 | 72 | validateQuery(query); 73 | }); 74 | 75 | it('should fetch range of time series for given key and aggregation', async () => { 76 | const { key, fromTimestamp, toTimestamp, aggregation } = TEST_PARAMS; 77 | const { type, timeBucket } = aggregation; 78 | const aggregationQuery = [AGGREGATION, type, timeBucket]; 79 | const query = [TS_REVRANGE, key, fromTimestamp, toTimestamp, ...aggregationQuery]; 80 | 81 | await rts 82 | .revrange(key, fromTimestamp, toTimestamp) 83 | .aggregation(type, timeBucket) 84 | .send(); 85 | 86 | validateQuery(query); 87 | }); 88 | 89 | it('should fail, no arguments', async () => { 90 | await expect(rts.revrange().send()).rejects.toThrow(); 91 | }); 92 | 93 | it('should fail, no range timestamps', async () => { 94 | await expect(rts.revrange(TEST_PARAMS.key).send()).rejects.toThrow(); 95 | }); 96 | 97 | it('should fail, no toTimestamp', async () => { 98 | const { key, fromTimestamp } = TEST_PARAMS; 99 | await expect(rts.revrange(key, fromTimestamp).send()).rejects.toThrow(); 100 | }); 101 | 102 | it('should fail, count not valid', async () => { 103 | const { key, fromTimestamp, toTimestamp } = TEST_PARAMS; 104 | await expect(rts.revrange(key, fromTimestamp, toTimestamp).count(true).send()).rejects.toThrow(); 105 | }); 106 | }); 107 | --------------------------------------------------------------------------------