├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── integration │ │ ├── addRts.test.ts │ │ ├── alterRts.test.ts │ │ ├── changeByRts.test.ts │ │ ├── connection.test.ts │ │ ├── createRts.test.ts │ │ ├── disconnectRts.test.ts │ │ ├── expireRts.test.ts │ │ ├── getRts.test.ts │ │ ├── multiGetRts.test.ts │ │ ├── multiRange.test.ts │ │ ├── multiRevRange.test.ts │ │ ├── queryIndexRts.test.ts │ │ ├── range.test.ts │ │ ├── resetRts.test.ts │ │ ├── revRange.test.ts │ │ └── ruleRts.test.ts │ └── unit │ │ ├── builder │ │ ├── filterBuilder.test.ts │ │ ├── requestParamsBuilder.test.ts │ │ └── requestParamsDirector.test.ts │ │ ├── entity │ │ ├── aggregation.test.ts │ │ ├── count.test.ts │ │ ├── filter.test.ts │ │ ├── sample.test.ts │ │ └── timestampRange.test.ts │ │ ├── factory │ │ └── redisTimeSeries.test.ts │ │ ├── iterator │ │ └── list.test.ts │ │ └── response │ │ └── response.test.ts ├── __tests_config__ │ └── data.ts ├── builder │ ├── filterBuilder.ts │ ├── requestParamsBuilder.ts │ └── requestParamsDirector.ts ├── command │ ├── commandInvoker.ts │ ├── commandProvider.ts │ ├── commandReceiver.ts │ ├── deleteAllCommand.ts │ ├── deleteCommand.ts │ ├── disconnectCommand.ts │ ├── expireCommand.ts │ ├── interface │ │ ├── command.ts │ │ └── commandData.ts │ └── timeSeriesCommand.ts ├── entity │ ├── aggregation.ts │ ├── count.ts │ ├── filter.ts │ ├── label.ts │ ├── sample.ts │ └── timestampRange.ts ├── enum │ ├── aggregationType.ts │ ├── commandKeyword.ts │ ├── commandName.ts │ └── filterOperator.ts ├── factory │ ├── redisTimeSeries.ts │ └── render.ts ├── index.ts ├── iterator │ └── list.ts ├── redisTimeSeries.ts └── response │ ├── infoResponseRender.ts │ ├── interface │ ├── aggregationByKey.ts │ ├── baseMultiResponse.ts │ ├── infoResponse.ts │ ├── multiGetResponse.ts │ └── multiRangeResponse.ts │ ├── multiGetResponseRender.ts │ ├── multiRangeResponseRender.ts │ └── type │ └── multiAddResponseError.ts └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | env: { 4 | "es6": true, 5 | "node": true 6 | }, 7 | extends: [ 8 | "plugin:@typescript-eslint/recommended", 9 | "prettier/@typescript-eslint", 10 | "plugin:prettier/recommended", 11 | "eslint:recommended" 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 6, 15 | sourceType: "module" 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": "off", 19 | "@typescript-eslint/ban-ts-ignore": "off" 20 | }, 21 | overrides: [ 22 | { 23 | files: ['**/*.ts'], 24 | parser: '@typescript-eslint/parser', 25 | rules: { 26 | 'no-undef': 'off' 27 | } 28 | } 29 | ] 30 | }; 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /lib/ 3 | /coverage/ 4 | /.idea/ 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | trailingComma: "none", 4 | printWidth: 120, 5 | tabWidth: 4 6 | }; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | cache: npm 4 | 5 | node_js: 6 | - "8" 7 | - "10" 8 | - "12" 9 | 10 | services: 11 | - docker 12 | 13 | install: 14 | - npm ci 15 | 16 | before_install: 17 | - docker pull redislabs/redistimeseries:latest 18 | - docker run --name redislabs-redistimeseries -it -d -p 127.0.0.1:6379:6379 redislabs/redistimeseries:latest 19 | - docker ps -a 20 | 21 | before_script: 22 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 23 | - chmod +x ./cc-test-reporter 24 | - ./cc-test-reporter before-build 25 | - mkdir -p coverage 26 | 27 | script: 28 | - npm run build 29 | - npm run test 30 | 31 | after_script: 32 | - ./cc-test-reporter after-build -t lcov ./coverage/lcov.info --exit-code $TRAVIS_TEST_RESULT 33 | 34 | addons: 35 | hosts: 36 | - redislabs-redistimeseries 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.0 - 2021-01-09 2 | 3 | - fix missing exported classes: `Count`, `TimestampRange` 4 | - add missing parameters on `TS.CREATE` -- `chunkSize`, `duplicatepolicy` 5 | - add missing parameters on `TS.ADD` -- `chunksize`, `onDuplicate` 6 | - add `TS.REVRANGE` and `TS.MREVRANGE` 7 | - bump dependencies to latest versions 8 | - fix vulnerabilities 9 | - add tests for new changes 10 | - adds `UNCOMPRESSED` option on `.create()`, .`add()`, .`alter()` methods 11 | - adds `tsdoc` with usage examples on public classes, functions, enums, interfaces for better intellisense 12 | - updates `README.md` to reflect the latest changes 13 | 14 | ## 1.2.3 - 2020-03-15 15 | 16 | - update dependencies 17 | 18 | ## 1.2.2 - 2020-02-04 19 | 20 | - used `redislabs/redistimeseries:latest` image as default 21 | - when data are not found in a `multiRange` query, the response still will return a sample with value = 0, but now the timestamp = first timestamp found which will be 0 if the time-series has no data stored 22 | 23 | ## 1.2.1 - 2020-01-28 24 | 25 | - added Travis CI 26 | - changed RedisLabs docker image from `latest` to `edge` tag for running test and CI 27 | - added badgets 28 | 29 | ## 1.2.0 - 2020-01-27 30 | 31 | - updates for compatibility with `redislabs/redistimeseries` version 1.2.2 32 | 33 | ## 1.1.1 - 2020-01-25 34 | 35 | - update dependencies 36 | 37 | ## 1.1.0 - 2020-01-09 38 | 39 | - added RedisTimeFactory and ConnectionOptions to index.d.ts 40 | 41 | ## 1.0.2 - 2020-01-08 42 | 43 | - first implementation 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12 2 | WORKDIR '/app' 3 | COPY ./package.json ./ 4 | RUN npm install 5 | COPY ./ ./ 6 | CMD ["/bin/bash", "-c", "sleep infinity"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Rafael Campoy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Test Coverage](https://api.codeclimate.com/v1/badges/f178828dd4e2cbaa32c5/test_coverage)](https://codeclimate.com/github/averias/redis-time-series/test_coverage) 2 | [![Maintainability](https://api.codeclimate.com/v1/badges/f178828dd4e2cbaa32c5/maintainability)](https://codeclimate.com/github/averias/redis-time-series/maintainability) 3 | [![Build Status](https://travis-ci.org/averias/redis-time-series.svg?branch=master)](https://travis-ci.org/averias/redis-time-series) 4 | [![npm version](https://badge.fury.io/js/redis-time-series-ts.svg)](https://badge.fury.io/js/redis-time-series-ts) 5 | [![GitHub](https://img.shields.io/github/license/averias/redis-time-series.svg)](https://github.com/averias/redis-time-series) 6 | 7 | # Redis-Time-Series 8 | A Javascript client for [RedisLab/RedisTimeSeries Module](https://oss.redislabs.com/redistimeseries/) implemented in 9 | TypeScript and based on [ioredis](https://github.com/luin/ioredis) 10 | 11 | - [Redis-Time-Series](#redis-time-series) 12 | - [Requirements](#requirements) 13 | - [Install](#install) 14 | - [Usage](#usage) 15 | - [Commands](#commands) 16 | - [`.create`](#create) 17 | - [`.alter`](#alter) 18 | - [`.add`](#add) 19 | - [`.multiAdd`](#multiadd) 20 | - [`.incrementBy/.decrementBy`](#incrementbydecrementby) 21 | - [`.createRule/.deleteRule`](#createruledeleterule) 22 | - [`.range/.revRange`](#rangerevrange) 23 | - [`.multiRange/multiRevRange`](#multirangemultirevrange) 24 | - [`.get`](#get) 25 | - [`.multiGet`](#multiget) 26 | - [`.queryIndex`](#queryindex) 27 | - [`.info`](#info) 28 | - [`.expire`](#expire) 29 | - [`.delete`](#delete) 30 | - [`.deleteAll`](#deleteall) 31 | - [`.reset`](#reset) 32 | - [`.disconnect`](#disconnect) 33 | - [Testing](#testing) 34 | - [License](#license) 35 | 36 | ## Requirements 37 | - Redis server 4.0+ version (recommended version 5.0+) 38 | - RedisTimeSeries Module installed on Redis server as specified in [Build and Run it yourself](https://oss.redislabs.com/redistimeseries/#build-and-run-it-yourself) 39 | 40 | 41 | ## Install 42 | `npm i redis-time-series-ts` 43 | 44 | 45 | ## Usage 46 | 47 | ```ts 48 | import { 49 | Label, 50 | RedisTimeSeriesFactory 51 | } from "redis-time-series-ts"; 52 | 53 | const example = async () => { 54 | const factory = new RedisTimeSeriesFactory(); 55 | const redisTimeSeries = factory.create(); 56 | 57 | await redisTimeSeries.create("temperature", [new Label("sensor", 1)], 50000); 58 | 59 | const info = await redisTimeSeries.info("temperature"); 60 | const label = info.labels.shift(); 61 | 62 | if (label != null) { 63 | console.log(`label: ${label.getName()}=${label.getValue()}`); // label: sensor=1 64 | } 65 | 66 | console.log(`retention (ms): ${info.retentionTime}`); // retention (ms): 50000 67 | console.log(`last timestamp: ${info.lastTimestamp}`); // last timestamp: 0 68 | 69 | await redisTimeSeries.disconnect(); 70 | }; 71 | 72 | example(); 73 | ``` 74 | 75 | If no param is provided to `RedisTimeSeries` constructor, it creates `RedsiTimeSeries` object with a default 76 | connection (port: 6379, host: "127.0.0.1" and database: 15). You can specify your connection params by providing an 77 | object of `ConnectionOptions` type (an IORedis `RedisOptions` type) which will overwrite those default connection params: 78 | 79 | ```ts 80 | import { 81 | ConnectionOptions, 82 | RedisTimeSeriesFactory 83 | } from "redis-time-series-ts"; 84 | 85 | const options: ConnectionOptions = { 86 | port: 6381, 87 | host: "127.0.0.1", 88 | db: 15 89 | }; 90 | 91 | const factory = new RedisTimeSeriesFactory(options); 92 | const redisTimeSeries = factory.create(); 93 | ``` 94 | 95 | Take a look at the full [list of connections params](https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options) 96 | for IORedis. 97 | 98 | ## Commands 99 | After creating a `RedisTimeSeries` from `RedisTimeSeries::create` you can issue the following async commands. All of them return a `Promise` if the command was executed successfully, otherwise, an `Error` will be thrown. 100 | 101 | 102 | ### `.create` 103 | Creates a new time-series with an optional array of labels and optional retention. If the time-series `key` already 104 | exists, an `Error` will be thrown. 105 | 106 | ```ts 107 | redisTimeSeries.create( 108 | key: string, 109 | labels?: Label[], 110 | retention?: number, 111 | chunkSize?: number, 112 | duplicatePolicy?: string, 113 | uncompressed?: boolean 114 | ): Promise 115 | ``` 116 | - `retention` - Maximum age for samples compared to last event time (in milliseconds). Default: The global retention secs configuration of the database (by default, 0 ) 117 | When set to 0, the series is not trimmed at all 118 | - `chunkSize` - amount of memory, in bytes, allocated for data. Default: 4000. 119 | - `duplicatePolicy` configures what to do on encounterimg duplicate samples. When this is not set, the server-wide default will be used. See [DUPLICATE_POLICY](https://oss.redislabs.com/redistimeseries/configuration/#DUPLICATE_POLICY) for allowed values. 120 | - `uncompressed` By default, data are compressed in a time-series, you can revert this behavior by setting `uncompressed` to true 121 | 122 | **Label** 123 | 124 | It represents metadata labels of the time-series 125 | 126 | `Label(name: string, value: string | number)` 127 | 128 | **Response** 129 | 130 | True if the time-series was created, otherwise false 131 | 132 | **Example** 133 | 134 | ```ts 135 | import { 136 | Label, 137 | RedisTimeSeriesFactory 138 | } from "redis-time-series-ts"; 139 | 140 | const example = async () => { 141 | const factory = new RedisTimeSeriesFactory(); 142 | const redisTimeSeries = factory.create(); 143 | 144 | const created = await redisTimeSeries.create("temperature", [new Label("sensor", 1)], 50000); 145 | console.log(created); // true 146 | 147 | const info = await redisTimeSeries.info("temperature"); 148 | const label = info.labels.shift(); 149 | 150 | if (label != null) { 151 | console.log(`label: ${label.getName()}=${label.getValue()}`); // label: sensor=1 152 | } 153 | 154 | await redisTimeSeries.disconnect(); 155 | }; 156 | 157 | example(); 158 | ``` 159 | 160 | More info: [TS.CREATE](https://oss.redislabs.com/redistimeseries/commands/#tscreate) 161 | 162 | ### `.alter` 163 | Updates the retention and labels of an existing time-series. Same params as `create`. 164 | 165 | ```ts 166 | redisTimeSeries.alter( 167 | key: string, 168 | labels?: Label[], 169 | retention?: number, 170 | chunkSize?: number, 171 | duplicatePolicy?: string, 172 | uncompressed?: boolean 173 | ): Promise 174 | ``` 175 | 176 | - if time-series key doesn't exist an `Error` is thrown 177 | - only provided params will be updated 178 | - to remove all labels from an existing time-series, you must provide and empty Labels array 179 | 180 | **Response** 181 | 182 | True if the time-series was altered, otherwise false 183 | 184 | **Example** 185 | 186 | ```ts 187 | import { 188 | Label, 189 | RedisTimeSeriesFactory 190 | } from "redis-time-series-ts"; 191 | 192 | const example = async () => { 193 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 194 | const redisTimeSeries = factory.create(); 195 | 196 | await redisTimeSeries.create("temperature", [new Label("sensor", 1)], 50000); 197 | 198 | const infoCreate = await redisTimeSeries.info("temperature"); 199 | const labelCreate = infoCreate.labels.shift(); 200 | 201 | if (labelCreate != null) { 202 | console.log(`label: ${labelCreate.getName()}=${labelCreate.getValue()}`); // label: sensor=1 203 | } 204 | 205 | console.log(`retention (ms): ${infoCreate.retentionTime}`); // retention (ms): 50000 206 | 207 | // labels are removed and retention is updated to 70000 208 | const altered = await redisTimeSeries.alter("temperature", [], 70000); 209 | console.log(altered); // true 210 | 211 | const infoAltered = await redisTimeSeries.info("temperature"); 212 | const labelAltered = infoAltered.labels.shift(); 213 | 214 | if (labelAltered != null) { 215 | // never executed since we removed labels 216 | console.log(`label: ${labelAltered.getName()}=${labelAltered.getValue()}`); 217 | } 218 | 219 | console.log(`retention (ms): ${infoAltered.retentionTime}`); // retention (ms): 70000 220 | 221 | await redisTimeSeries.delete("temperature"); 222 | await redisTimeSeries.disconnect(); 223 | }; 224 | 225 | example(); 226 | ``` 227 | 228 | More info: [TS.ALTER](https://oss.redislabs.com/redistimeseries/commands/#tsalter) 229 | 230 | ### `.add` 231 | Appends, or first creates a time-series and then appends, a new value to the time-series. 232 | 233 | ```ts 234 | redisTimeSeries.add( 235 | sample: Sample, 236 | labels?: Label[], 237 | retention?: number, 238 | chunkSize?: number, 239 | onDuplicate?: string, 240 | uncompressed?: boolean 241 | ): Promise 242 | ``` 243 | 244 | If this command is used to add data to an existing time-series, retentionTime and labels are ignored. 245 | 246 | **Sample** 247 | 248 | A sample represents the new value to be added where: 249 | - `key`: is the time-series and 250 | - `value`: the value to add 251 | - `timestamp`: (optional) if it's provided, must be a valid timestamp and no older than the last one added. If it's omitted, it will store a string value `*`, which represents a current timestamp in Redis server 252 | 253 | `Sample(key: string, value: number, timestamp?: number)` 254 | 255 | **Response** 256 | 257 | The timestamp value of the sample added 258 | 259 | **Example** 260 | 261 | ```ts 262 | import { 263 | Label, 264 | Sample, 265 | RedisTimeSeriesFactory 266 | } from "redis-time-series-ts"; 267 | 268 | const example = async () => { 269 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 270 | const redisTimeSeries = factory.create(); 271 | const date = Date.now(); 272 | 273 | let added = await redisTimeSeries.add( 274 | new Sample("temperature", 100, date - 10000), 275 | [new Label("sensor", 1)], 276 | 50000 277 | ); 278 | console.log(added); // date - 10000 279 | 280 | let info = await redisTimeSeries.info("temperature"); 281 | let label = info.labels.shift(); 282 | 283 | if (label != null) { 284 | console.log(`label: ${label.getName()}=${label.getValue()}`); // label: sensor=1 285 | } 286 | 287 | console.log(`retention (ms): ${info.retentionTime}`); // retention (ms): 50000 288 | 289 | let sample = await redisTimeSeries.get("temperature"); 290 | console.log(`${sample.getKey()}`); // temperature 291 | console.log(`${sample.getValue()}`); // 100 292 | console.log(`${sample.getTimestamp()}`); // date - 10000 293 | 294 | // a new value is added, labels and retention are ignored since we added them previously 295 | added = await redisTimeSeries.add( 296 | new Sample("temperature", 500, date - 5000), 297 | [new Label("sensor", 2)], 298 | 70000 299 | ); 300 | console.log(added); // date - 5000 301 | 302 | info = await redisTimeSeries.info("temperature"); 303 | label = info.labels.shift(); 304 | 305 | if (label != null) { 306 | console.log(`label: ${label.getName()}=${label.getValue()}`); // still sensor=1 307 | } 308 | 309 | console.log(`retention (ms): ${info.retentionTime}`); // still retention (ms): 50000 310 | 311 | sample = await redisTimeSeries.get("temperature"); 312 | console.log(`${sample.getKey()}`); // temperature 313 | console.log(`${sample.getValue()}`); // 500 314 | console.log(`${sample.getTimestamp()}`); // date - 5000 315 | 316 | await redisTimeSeries.delete("temperature"); 317 | await redisTimeSeries.disconnect(); 318 | }; 319 | 320 | example(); 321 | ``` 322 | 323 | More info: [TS.ADD](https://oss.redislabs.com/redistimeseries/commands/#tsadd) 324 | 325 | ### `.multiAdd` 326 | Similar to `Add` but it appends a list of new values to a time-series, the `key` specified in each sample must exist. 327 | 328 | `redisTimeSeries.multiAdd(samples: Sample[]): Promise<(number | MultiAddResponseError)[]>` 329 | 330 | **Response** 331 | 332 | It returns an array of integers for each value added which is the timestamp specified in the sample, following the order 333 | the samples were added. If an error happens when the sample is added, instead of an integer and object of type `MultiAddResponseError` 334 | will be returned: 335 | 336 | ``` 337 | type MultiAddResponseError = { 338 | stack: string; 339 | message: string; 340 | }; 341 | ``` 342 | 343 | **Example** 344 | 345 | ```ts 346 | import { 347 | Label, 348 | Sample, 349 | FilterBuilder, 350 | RedisTimeSeriesFactory 351 | } from "redis-time-series-ts"; 352 | 353 | const example = async () => { 354 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 355 | const redisTimeSeries = factory.create(); 356 | const date = Date.now(); 357 | const label = new Label("sensor", 1); 358 | const sample1 = new Sample("temperature1", 100, date - 10000); 359 | const sample2 = new Sample("temperature2", 200, date - 5000); 360 | 361 | await redisTimeSeries.create("temperature1", [label]); 362 | await redisTimeSeries.create("temperature2", [label]); 363 | 364 | const multiAdded = await redisTimeSeries.multiAdd([sample1, sample2]); 365 | console.log(multiAdded[0]); // date - 10000 366 | console.log(multiAdded[1]); // date - 5000 367 | 368 | const multiGet = await redisTimeSeries.multiGet(new FilterBuilder("sensor", 1)); 369 | const temperature1 = multiGet[0]; 370 | const temperature2 = multiGet[1]; 371 | 372 | console.log(`${temperature1.data.getKey()}`); // temperature1 373 | console.log(`${temperature1.data.getValue()}`); // 100 374 | console.log(`${temperature1.data.getTimestamp()}`); // date - 10000 375 | 376 | console.log(`${temperature2.data.getKey()}`); // temperature2 377 | console.log(`${temperature2.data.getValue()}`); // 200 378 | console.log(`${temperature2.data.getTimestamp()}`); // date - 5000 379 | 380 | await redisTimeSeries.delete("temperature1", "temperature2"); 381 | await redisTimeSeries.disconnect(); 382 | }; 383 | 384 | example(); 385 | ``` 386 | 387 | More info: [TS.MADD](https://oss.redislabs.com/redistimeseries/commands/#tsmadd) 388 | 389 | 390 | ### `.incrementBy/.decrementBy` 391 | Increment or decrement the latest value in a time-series 392 | 393 | `redisTimeSeries.incrementBy(sample: Sample, labels?: Label[], retention?: number, uncompressed?: boolean): Promise` 394 | 395 | `redisTimeSeries.decrementBy(sample: Sample, labels?: Label[], retention?: number, uncompressed?: boolean): Promise` 396 | 397 | You can use these command to add data to an non existing time-series, then `labels` and `retention` are ignored. By default, 398 | data are compressed in a time-series, you can revert this behavior by setting `uncompressed` to true 399 | 400 | **Response** 401 | 402 | The timestamp value of the sample incremented/decremented 403 | 404 | **Example** 405 | 406 | ```ts 407 | import { 408 | Label, 409 | Sample, 410 | RedisTimeSeriesFactory 411 | } from "redis-time-series-ts"; 412 | 413 | const example = async () => { 414 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 415 | const redisTimeSeries = factory.create(); 416 | const date = Date.now(); 417 | const label = new Label("sensor", 1); 418 | const sample1 = new Sample("temperature", 100, date - 10000); 419 | const sample2 = new Sample("temperature", 200, date - 5000); 420 | 421 | const increment = await redisTimeSeries.incrementBy(sample1, [label]); 422 | console.log(increment); // date - 10000 423 | 424 | let temperature = await redisTimeSeries.get("temperature"); 425 | 426 | console.log(`${temperature.getKey()}`); // temperature 427 | console.log(`${temperature.getValue()}`); // 100 428 | console.log(`${temperature.getTimestamp()}`); // date - 10000 429 | 430 | const decrement = await redisTimeSeries.decrementBy(sample2, [label]); 431 | console.log(decrement); // date - 5000 432 | 433 | temperature = await redisTimeSeries.get("temperature"); 434 | 435 | console.log(`${temperature.getKey()}`); // temperature 436 | console.log(`${temperature.getValue()}`); // -100 437 | console.log(`${temperature.getTimestamp()}`); // date - 5000 438 | 439 | await redisTimeSeries.delete("temperature"); 440 | await redisTimeSeries.disconnect(); 441 | }; 442 | 443 | example(); 444 | ``` 445 | 446 | More info: [TS.INCRBY / TS.DECRBY](https://oss.redislabs.com/redistimeseries/commands/#tsincrbytsdecrby) 447 | 448 | ### `.createRule/.deleteRule` 449 | it creates a compaction rule. 450 | 451 | `redisTimeSeries.createRule(sourceKey: string, destKey: string, aggregation: Aggregation): Promise` 452 | 453 | Deletes a previous compaction rule. 454 | 455 | `redisTimeSeries.deleteRule(sourceKey: string, destKey: string): Promise` 456 | 457 | Source and destination key must exist and be different 458 | 459 | **Aggregation** 460 | 461 | A aggregation represents a rule: 462 | - `aggregationType`: avg, sum, min, max, range, count, first, last, std.p, std.s, var.p and var.s. See `AggregationType` enum 463 | - `timeBucket`: a positive integer time bucket in milliseconds 464 | 465 | `Aggregation(type: string, timeBucketInMs: number)` 466 | 467 | **Response** 468 | 469 | True if the aggregation rule was created/deleted, otherwise false 470 | 471 | **Example** 472 | 473 | ```ts 474 | import { 475 | Aggregation, 476 | AggregationType, 477 | RedisTimeSeriesFactory 478 | } from "redis-time-series-ts"; 479 | 480 | const example = async () => { 481 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 482 | const redisTimeSeries = factory.create(); 483 | 484 | await redisTimeSeries.create("rule1"); 485 | await redisTimeSeries.create("rule2"); 486 | 487 | const aggregation = new Aggregation(AggregationType.AVG, 50000); 488 | const ruled = await redisTimeSeries.createRule("rule1", "rule2", aggregation); 489 | console.log(ruled); // true 490 | 491 | let info1 = await redisTimeSeries.info("rule1"); 492 | console.log(info1.rules.rule2.getTimeBucketInMs()); // 50000 493 | console.log(info1.rules.rule2.getType()); // avg 494 | 495 | let info2 = await redisTimeSeries.info("rule2"); 496 | console.log(info2.sourceKey); // rule1 497 | 498 | const deleted = await redisTimeSeries.deleteRule("rule1", "rule2"); 499 | console.log(deleted); // true 500 | 501 | info1 = await redisTimeSeries.info("rule1"); 502 | console.log(info1.rules); // {}} 503 | 504 | info2 = await redisTimeSeries.info("rule2"); 505 | console.log(info2.sourceKey); // undefined 506 | 507 | await redisTimeSeries.delete("rule1", "rule2"); 508 | await redisTimeSeries.disconnect(); 509 | }; 510 | 511 | example(); 512 | ``` 513 | 514 | More info: 515 | - [TS.CREATERULE](https://oss.redislabs.com/redistimeseries/commands/#tscreaterule) 516 | - [TS.DELETERULE](https://oss.redislabs.com/redistimeseries/commands/#tsdeleterule) 517 | 518 | ### `.range/.revRange` 519 | It queries a timestamp range. 520 | 521 | `redisTimeSeries.range(key: string, range: TimestampRange, count?: number, aggregation?: Aggregation): Promise>` 522 | 523 | - `range`: a `TimestampRange` object 524 | - `count`: (optional) maximum number of returned samples per time-series 525 | - `aggregation`: (optional) aggregation rule 526 | 527 | 528 | **TimestampRange** 529 | 530 | It represents a timestamp filter for the query: 531 | - `from`: (optional) start timestamp value, if it's not specified or `undefined` represents the minimum possible timestamp (0) 532 | - `to`: (optional) end timestamp value, if it's not specified or `undefined` represents the maximum possible timestamp (current timestamp in the Redis server) 533 | 534 | `TimestampRange(from?: number, to?: number)` 535 | 536 | 537 | **Response** 538 | 539 | An array of samples which represent all the samples included in the query 540 | 541 | **Example** 542 | 543 | ```ts 544 | import { 545 | TimestampRange, 546 | AggregationType, 547 | Aggregation, 548 | Sample, 549 | RedisTimeSeriesFactory 550 | } from "redis-time-series-ts"; 551 | 552 | const example = async () => { 553 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 554 | const redisTimeSeries = factory.create(); 555 | const date = new Date(2020, 1, 6, 11).getTime(); 556 | 557 | await redisTimeSeries.create("range1"); 558 | for (let i = 0; i < 10; i++) { 559 | await redisTimeSeries.add(new Sample("range1", 20 + i, date + i * 1000)); 560 | } 561 | 562 | const aggregation = new Aggregation(AggregationType.AVG, 1000); 563 | const timestampRange = new TimestampRange(date, date + 10000); 564 | const samples = await redisTimeSeries.range("range1", timestampRange, undefined, aggregation); 565 | 566 | for (const sample of samples) { 567 | console.log(sample.getKey()); // range1 568 | console.log(sample.getValue()); // >=20 and < 30 569 | console.log(sample.getTimestamp()); // between 1580983200000 and 1580983209000 timestamp values 570 | } 571 | 572 | await redisTimeSeries.delete("range1"); 573 | await redisTimeSeries.disconnect(); 574 | }; 575 | 576 | example(); 577 | ``` 578 | 579 | More info: [TS.RANGE/TS.REVRANGE](https://oss.redislabs.com/redistimeseries/commands/#tsrangetsrevrange) 580 | 581 | ### `.multiRange/multiRevRange` 582 | It queries a timestamp range across multiple time-series by using filters. 583 | 584 | `redisTimeSeries.multiRange(range: TimestampRange, filters: FilterBuilder, count?: number, aggregation?: Aggregation, withLabels?: boolean): Promise>` 585 | 586 | - `range`: a `TimestampRange` object 587 | - `filters`: a `FilterBuilder` which will generate an array of filter to be applied across multiple time-series 588 | - `count`: (optional) maximum number of returned samples per time-series 589 | - `aggregation`: (optional) aggregation rule 590 | - `withLabels`: (optional) by default labels will be not included in the response, if true, they will 591 | 592 | 593 | **FilterBuilder** 594 | 595 | `FilterBuilder(label: string, value: string | number)` 596 | 597 | The `label` and `value` in the constructor create a first filter where `label=value`. More filters can be created by 598 | calling the different methods in `FilterBuilder`: 599 | - `equal(label: string, value: string | number)`: label=value 600 | - `notEqual(label: string, value: string | number)`: label!=value 601 | - `exists(label: string`: label exists in time-series 602 | - `notExists(label: string`: label doesn't exist in time-series 603 | - `in(label: string, value: StringNumberArray)`: where value is an array of strings and numbers, it specifies that label is equal to one of the values in the array 604 | - `notIn(label: string, value: StringNumberArray)`: where value is an array of strings and numbers, it specifies that label is NOT equal to one of the values in the array 605 | 606 | **Response** 607 | 608 | An array of `MultiRangeResponse` objects 609 | 610 | ``` 611 | interface MultiRangeResponse { 612 | key: string; 613 | labels: Label[]; 614 | data: Sample[]; 615 | } 616 | ``` 617 | 618 | if `withLabels` is true, `labels` in `MultiRangeResponse` will be empty. 619 | 620 | If some of the keys returned by the filter doesn't include any sample because, for instance, the chosen timestamp range 621 | doesn't match `MultiRangeResponse.data` will still include one sample in the array with value = 0 and timestamp = first 622 | timestamp found in the time-series which will be equel to 0 if the time-series has no data stored. 623 | 624 | **Example** 625 | 626 | ```ts 627 | import { 628 | Label, 629 | Sample, 630 | Aggregation, 631 | AggregationType, 632 | TimestampRange, 633 | FilterBuilder, 634 | RedisTimeSeriesFactory 635 | } from "redis-time-series-ts"; 636 | 637 | const example = async () => { 638 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 639 | const redisTimeSeries = factory.create(); 640 | const date = new Date(2020, 1, 6, 11).getTime(); 641 | const label1 = new Label("label", "1"); 642 | const sensor1 = new Label("sensor", "1"); 643 | const sensor2 = new Label("sensor", "2"); 644 | 645 | await redisTimeSeries.create("multirange1", [label1, sensor1]); 646 | await redisTimeSeries.create("multirange2", [label1, sensor2]); 647 | 648 | await redisTimeSeries.create("range1"); 649 | for (let i = 0; i < 10; i++) { 650 | await redisTimeSeries.add(new Sample("multirange1", 20 + i, date + i * 1000)); 651 | await redisTimeSeries.add(new Sample("multirange2", 30 + i, date + i * 1000)); 652 | } 653 | 654 | const aggregation = new Aggregation(AggregationType.MAX, 5000); 655 | const timestampRange = new TimestampRange(date, date + 10000); 656 | const filter = new FilterBuilder("label", 1).equal("sensor", 1); 657 | const multiRanges = await redisTimeSeries.multiRange(timestampRange, filter, undefined, aggregation, true); 658 | const multiRange = multiRanges.shift(); 659 | 660 | console.log(multiRange.key); //multirange1 661 | 662 | const labels = multiRange.labels; 663 | console.log(labels.shift()); // Label { name: 'label', value: '1' } 664 | console.log(labels.shift()); // Label { name: 'sensor', value: '1' } 665 | 666 | 667 | const samples = multiRange.data; 668 | 669 | console.log(samples.shift().getValue()); // 24 670 | console.log(samples.shift().getValue()); // 29; 671 | console.log(multiRanges.length); // 0 672 | 673 | await redisTimeSeries.delete("range1"); 674 | await redisTimeSeries.disconnect(); 675 | }; 676 | 677 | example(); 678 | ``` 679 | 680 | More info: [TS.MRANGE/TS.MREVRANGE](https://oss.redislabs.com/redistimeseries/commands/#tsmrangetsmrevrange) 681 | 682 | ### `.get` 683 | Get the last sample from an existing time-series. 684 | 685 | `redisTimeSeries.get(key: string): Promise` 686 | 687 | **Response** 688 | 689 | The last sample in the time-series specified by `key` 690 | 691 | **Example** 692 | 693 | ```ts 694 | import { 695 | Sample, 696 | RedisTimeSeriesFactory 697 | } from "redis-time-series-ts"; 698 | 699 | const example = async () => { 700 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 701 | const redisTimeSeries = factory.create(); 702 | const date = new Date(2020, 1, 6, 11).getTime(); 703 | 704 | await redisTimeSeries.add(new Sample("get", 20, date)); 705 | 706 | const sample = await redisTimeSeries.get("get"); 707 | 708 | console.log(sample.getKey()); // get 709 | console.log(sample.getValue()); // 20 710 | console.log(sample.getTimestamp()); // 1580983200000; 711 | 712 | await redisTimeSeries.delete("get"); 713 | await redisTimeSeries.disconnect(); 714 | }; 715 | 716 | example(); 717 | ``` 718 | 719 | More info: [TS.GET](https://oss.redislabs.com/redistimeseries/commands/#tsget) 720 | 721 | ### `.multiGet` 722 | Gets the last samples matching the specific filter. 723 | 724 | `redisTimeSeries.multiGet(filters: FilterBuilder): Promise>` 725 | 726 | The `filters` param is a `FilterBuilder` which will generate an array of filters to be applied across multiple time-series 727 | 728 | **Response** 729 | 730 | An array of `MultiGetResponse` objects 731 | 732 | ``` 733 | interface MultiRangeResponse { 734 | key: string; 735 | labels: Label[]; 736 | data: Sample; 737 | } 738 | ``` 739 | 740 | if `withLabels` is true, `labels` in `MultiRangeResponse` will be empty 741 | 742 | If for a key returned because matches the filter but doesn't contain any data, `MultiGetResponse.data` will contain a 743 | sample with value = 0 and timestamp = 0; 744 | 745 | **Example** 746 | 747 | ```ts 748 | import { 749 | FilterBuilder, 750 | Sample, 751 | Label, 752 | RedisTimeSeriesFactory 753 | } from "redis-time-series-ts"; 754 | 755 | const example = async () => { 756 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 757 | const redisTimeSeries = factory.create(); 758 | const date = new Date(2020, 1, 6, 11).getTime(); 759 | const label1 = new Label("label", "1"); 760 | const sensor1 = new Label("sensor", "1"); 761 | const sensor2 = new Label("sensor", "2"); 762 | 763 | await redisTimeSeries.create("multiget1", [label1, sensor1]); 764 | await redisTimeSeries.create("multiget2", [label1, sensor2]); 765 | 766 | for (let i = 0; i < 10; i++) { 767 | await redisTimeSeries.add(new Sample("multiget1", 20 + i, date + i * 1000)); 768 | await redisTimeSeries.add(new Sample("multiget2", 30 + i, date + i * 1000)); 769 | } 770 | 771 | const filter = new FilterBuilder("label", 1); 772 | const multiGets = await redisTimeSeries.multiGet(filter); 773 | 774 | const multiGet1 = multiGets.shift(); 775 | console.log(multiGet1.key); // multiget1 776 | 777 | const labels1 = multiGet1.labels; 778 | console.log(labels1.shift()); // Label { name: 'label', value: '1' } 779 | console.log(labels1.shift()); // Label { name: 'sensor', value: '1' } 780 | 781 | const sample1 = multiGet1.data; 782 | 783 | console.log(sample1.getValue()); // 29 784 | console.log(sample1.getTimestamp()); // 1580983209000 785 | 786 | const multiGet2 = multiGets.shift(); 787 | 788 | console.log(multiGet2.key); // multiget2 789 | 790 | const labels2 = multiGet2.labels; 791 | console.log(labels2.shift()); // Label { name: 'label', value: '1' } 792 | console.log(labels2.shift()); // Label { name: 'sensor', value: '2' } 793 | 794 | const sample2 = multiGet2.data; 795 | 796 | console.log(sample2.getValue()); // 39 797 | console.log(sample2.getTimestamp()); // 1580983209000 798 | 799 | await redisTimeSeries.delete("multiget1", "multiget2"); 800 | await redisTimeSeries.disconnect(); 801 | }; 802 | 803 | example(); 804 | ``` 805 | 806 | More info: [TS.MULTIGET](https://oss.redislabs.com/redistimeseries/commands/#tsmget) 807 | 808 | ### `.queryIndex` 809 | Get all time-series keys matching the filter list. 810 | 811 | `redisTimeSeries.mueryIndex(filters: FilterBuilder): Promise` 812 | 813 | The `filters` param is a `FilterBuilder` which will generate an array of filter to be applied across multiple time-series 814 | 815 | **Response** 816 | 817 | An array of time-series keys 818 | 819 | **Example** 820 | 821 | ```ts 822 | import { 823 | Label, 824 | Sample, 825 | FilterBuilder, 826 | RedisTimeSeriesFactory 827 | } from "redis-time-series-ts"; 828 | 829 | const example = async () => { 830 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 831 | const redisTimeSeries = factory.create(); 832 | const date = new Date(2020, 1, 6, 11).getTime(); 833 | const label1 = new Label("label", "1"); 834 | const sensor1 = new Label("sensor", "1"); 835 | const sensor2 = new Label("sensor", "2"); 836 | 837 | await redisTimeSeries.create("query1", [label1, sensor1]); 838 | await redisTimeSeries.create("query2", [label1, sensor2]); 839 | 840 | for (let i = 0; i < 10; i++) { 841 | await redisTimeSeries.add(new Sample("query1", 20 + i, date + i * 1000)); 842 | await redisTimeSeries.add(new Sample("query2", 30 + i, date + i * 1000)); 843 | } 844 | 845 | const filter = new FilterBuilder("label", 1); 846 | const keys = await redisTimeSeries.queryIndex(filter); 847 | 848 | // @ts-ignore 849 | console.log(keys.shift()); // query1 850 | // @ts-ignore 851 | console.log(keys.shift()); // query2 852 | 853 | await redisTimeSeries.delete("query1", "query2"); 854 | await redisTimeSeries.disconnect(); 855 | }; 856 | 857 | example(); 858 | ``` 859 | 860 | More info: [TS.QUERYINDEX](https://oss.redislabs.com/redistimeseries/commands/#tsqueryindex) 861 | 862 | ### `.info` 863 | Returns information and statistics a time-series specified by `key`. 864 | 865 | `redisTimeSeries.info(key: string): Promise` 866 | 867 | **Response** 868 | 869 | An `InfoResponse` object 870 | 871 | ```ts 872 | interface InfoResponse { 873 | totalSamples: number; 874 | memoryUsage: number; 875 | firstTimestamp: number; 876 | lastTimestamp: number; 877 | retentionTime: number; 878 | chunkCount: number; 879 | chunkSize: number; 880 | chunkType: string; 881 | labels: Label[]; 882 | duplicatePolicy: string; 883 | sourceKey?: string; 884 | rules: AggregationByKey; 885 | } 886 | 887 | interface AggregationByKey { 888 | [key: string]: Aggregation; 889 | } 890 | ``` 891 | 892 | **Example** 893 | 894 | ```ts 895 | import { 896 | Label, 897 | Sample, 898 | RedisTimeSeriesFactory 899 | } from "redis-time-series-ts"; 900 | 901 | const example = async () => { 902 | const factory = new RedisTimeSeriesFactory({ port: 6381, db: 15 }); 903 | const redisTimeSeries = factory.create(); 904 | const date = new Date(2020, 1, 6, 11).getTime(); 905 | const label = new Label("label", "1"); 906 | 907 | await redisTimeSeries.add(new Sample("info", 20, date), [label], 50000); 908 | 909 | const info = await redisTimeSeries.info("info"); 910 | 911 | console.log(info.totalSamples); // 1 912 | console.log(info.memoryUsage); // 1 913 | console.log(info.firstTimestamp); // 1580983200000 914 | console.log(info.lastTimestamp); // 1580983200000 915 | console.log(info.retentionTime); // 50000 916 | console.log(info.sourceKey); // undefined 917 | console.log(info.labels.shift()); // Label { name: 'label', value: '1' } 918 | console.log(info.chunkSize); // 256 919 | console.log(info.chunkCount); // 1 920 | console.log(info.chunkType); // 'uncompressed' 921 | console.log(info.duplicatePolicy); // 'LAST' 922 | console.log(info.rules); // {} 923 | 924 | await redisTimeSeries.delete("info"); 925 | await redisTimeSeries.disconnect(); 926 | }; 927 | 928 | example(); 929 | ``` 930 | 931 | More info: [TS.INFO](https://oss.redislabs.com/redistimeseries/commands/#tsinfo) 932 | 933 | ### `.expire` 934 | Expires a time-series `key` by using Redis `expire` command. 935 | 936 | `redisTimeSeries.expire(key: string, seconds: number): Promise` 937 | - `seconds` must be an integer or an exception will be thrown 938 | 939 | ### `.delete` 940 | Deletes a time-series `key` by using Redis `del` command. 941 | 942 | `redisTimeSeries.delete(key: string): Promise` 943 | 944 | **Response** 945 | 946 | It returns `true` if the key was deleted successfully, otherwise `false`. 947 | 948 | ### `.deleteAll` 949 | Deletes all keys in the current Redis database by using Redis `flushdb` command. 950 | 951 | `redisTimeSeries.deleteAll(): Promise` 952 | 953 | **Response** 954 | 955 | It returns `true` if all keys were deleted successfully, otherwise `false`. 956 | 957 | ### `.reset` 958 | Resets a time-series `key` by deleting and then recreating the time-series with the labels and retention specified, if any. 959 | 960 | `redisTimeSeries.reset(key: string, labels?: Label[], retention?: number): Promise` 961 | 962 | **Response** 963 | 964 | It returns `true` if `key` was created successfully, otherwise `false`. If `key` doesn't exist or could not be deleted and 965 | error is thrown. 966 | 967 | ### `.disconnect` 968 | Disconnects the `RedisTimeSeries` client from Redis server. 969 | 970 | `redisTimeSeries.disconnect(): Promise` 971 | 972 | **Response** 973 | 974 | It returns `true` if the client was disconnected successfully, otherwise `false`. 975 | 976 | 977 | ## Testing 978 | Tests can be run locally with docker. `docker.compose.yml` file will build two services: 979 | - redis-time-series: node container with the source code and all dependent packages installed, where you can run the tests from 980 | - redislabs-redistimeseries: redis container built from `redislabs/redistimeseries:latest` image 981 | 982 | You can follow these steps to build the Docker services and run the tests: 983 | - from the command line run: `docker-compose up --build -d` to build the Docker services 984 | - get access to `redis-times-series` service by running `docker exec -it redis-time-series bash` 985 | - after getting access to `redis-times-series` service, run `npm run test` from inside to run the tests 986 | 987 | 988 | ## License 989 | Redis-time-series code is distributed under MIT license, see [LICENSE](https://github.com/emmanuelnk/redis-time-series/blob/master/LICENSE) 990 | file 991 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | redistimeseries: 5 | container_name: redislabs-redistimeseries 6 | image: redislabs/redistimeseries:latest 7 | node: 8 | container_name: redis-time-series 9 | build: 10 | dockerfile: Dockerfile 11 | context: . 12 | volumes: 13 | - .:/app 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | "^.+\\.(t|j)sx?$": "ts-jest" 4 | }, 5 | testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", 6 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 7 | coverageThreshold: { 8 | global: { 9 | branches: 80, 10 | functions: 80, 11 | lines: 80, 12 | statements: 80 13 | } 14 | }, 15 | coverageReporters: ["json", "lcov", "text", "clover"] 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redis-time-series-ts", 3 | "version": "1.3.0", 4 | "description": "Javascript RedisTimeSeries client", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "test": "jest -i --config jest.config.js --coverage", 9 | "test:watch": "jest -i --config jest.config.js --coverage --watchAll", 10 | "build": "tsc", 11 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"", 12 | "lint": "eslint \"src/**\"", 13 | "prepare": "npm run build", 14 | "prepublishOnly": "npm test && npm run lint", 15 | "preversion": "npm run lint", 16 | "version": "npm run format && git add -A src", 17 | "postversion": "git push && git push --tags" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/averias/redis-time-series.git" 22 | }, 23 | "keywords": [ 24 | "redis", 25 | "redistimeseries", 26 | "redislabs", 27 | "timeseries", 28 | "javascript", 29 | "typescript" 30 | ], 31 | "author": "Rafa Campoy", 32 | "license": "MIT", 33 | "homepage": "https://github.com/averias/redis-time-series", 34 | "dependencies": { 35 | "ioredis": "^4.19.4" 36 | }, 37 | "devDependencies": { 38 | "@types/ioredis": "^4.17.10", 39 | "@types/jest": "^24.9.1", 40 | "@types/node": "^12.19.11", 41 | "@typescript-eslint/eslint-plugin": "^2.34.0", 42 | "@typescript-eslint/parser": "^2.34.0", 43 | "eslint": "^6.8.0", 44 | "eslint-config-prettier": "^6.15.0", 45 | "eslint-plugin-prettier": "^3.3.0", 46 | "jest": "^25.5.4", 47 | "prettier": "^1.19.1", 48 | "ts-jest": "^25.5.1", 49 | "typescript": "^3.9.7" 50 | }, 51 | "files": [ 52 | "lib/**/*" 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /src/__tests__/integration/addRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Label } from "../../entity/label"; 5 | 6 | const factory = new RedisTimeSeriesFactory(testOptions); 7 | const rtsClient = factory.create(); 8 | 9 | beforeAll(async () => { 10 | await rtsClient.create("add1"); 11 | }); 12 | 13 | afterAll(async () => { 14 | await rtsClient.delete("add1", "add2", "add3", "add4", "add5", "add6", "add7", "add8"); 15 | await rtsClient.disconnect(); 16 | }); 17 | 18 | test("append values successfully", async () => { 19 | const now = Date.now(); 20 | const sample = new Sample("add1", 10, now); 21 | const added = await rtsClient.add(sample); 22 | expect(added).toEqual(now); 23 | 24 | const testSample = await rtsClient.get("add1"); 25 | expect(testSample).toEqual(sample); 26 | }); 27 | 28 | test("create and append values successfully", async () => { 29 | const now = Date.now(); 30 | const sample = new Sample("add2", 100, now); 31 | const label = new Label("color2", "15"); 32 | const added = await rtsClient.add(sample, [label], 50000); 33 | expect(added).toEqual(now); 34 | 35 | const testSample = await rtsClient.get("add2"); 36 | expect(testSample).toEqual(sample); 37 | 38 | const info = await rtsClient.info("add2"); 39 | expect(info.retentionTime).toEqual(50000); 40 | 41 | const sensor1Label = info.labels.pop(); 42 | expect(sensor1Label).toStrictEqual(label); 43 | expect(info.labels.length).toEqual(0); 44 | }); 45 | 46 | test("append to existing key with retention and labels is ignored", async () => { 47 | const previousInfo = await rtsClient.info("add2"); 48 | const now = Date.now(); 49 | const sample = new Sample("add2", 1000, now); 50 | const label = new Label("color3", "75"); 51 | const added = await rtsClient.add(sample, [label], 45000); 52 | expect(added).toEqual(now); 53 | 54 | const info = await rtsClient.info("add2"); 55 | expect(info.retentionTime).toEqual(previousInfo.retentionTime); 56 | expect(info.labels.pop()).toEqual(previousInfo.labels.pop()); 57 | }); 58 | 59 | test("append with default timestamp", async () => { 60 | const startTime = Date.now(); 61 | const sample = new Sample("add3", 1000); 62 | const added = await rtsClient.add(sample); 63 | const endTime = Date.now(); 64 | 65 | expect(added).toBeGreaterThanOrEqual(startTime); 66 | expect(added).toBeLessThanOrEqual(endTime); 67 | expect(sample.getTimestamp()).toEqual("*"); 68 | }); 69 | 70 | test("append with float timestamp, truncate it", async () => { 71 | const almostNow = Date.now() - 1000.69; 72 | const sample = new Sample("add6", 700, almostNow); 73 | const added = await rtsClient.add(sample); 74 | expect(added).toEqual(Math.trunc(almostNow)); 75 | }); 76 | 77 | test("multi add successfully", async () => { 78 | const now = Date.now(); 79 | const sample10 = new Sample("add4", 10, now - 2000); 80 | const addedSample10 = await rtsClient.add(sample10, undefined, 10000); 81 | expect(addedSample10).toEqual(now - 2000); 82 | 83 | const sample20 = new Sample("add4", 20, now - 1000); 84 | const sample30 = new Sample("add4", 30, now); 85 | const added = await rtsClient.multiAdd([sample20, sample30]); 86 | 87 | expect(added[0]).toEqual(now - 1000); 88 | expect(added[1]).toEqual(now); 89 | }); 90 | 91 | test("multi add a too old sample fails", async () => { 92 | const now = Date.now(); 93 | const sample40 = new Sample("add4", 40, now - 150000); 94 | const sample50 = new Sample("add4", 50, now); 95 | const added = await rtsClient.multiAdd([sample40, sample50]); 96 | 97 | // @ts-ignore 98 | expect(added[0].message).toMatch(/ERR TSDB: Timestamp is older than retention/); 99 | expect(added[1]).toEqual(now); 100 | }); 101 | 102 | test("multi add a non existent key fails", async () => { 103 | const now = Date.now(); 104 | const sample50 = new Sample("add5", 40, now - 500000); 105 | const added = await rtsClient.multiAdd([sample50]); 106 | 107 | // @ts-ignore 108 | expect(added[0].message).toMatch(/the key is not a TSDB key/); 109 | }); 110 | 111 | test("append with valid chunk size and duplicate policy to existing key", async () => { 112 | const sample = new Sample("add6", 1000); 113 | await rtsClient.add(sample, [], 3000, 8000, "LAST"); 114 | 115 | const info = await rtsClient.info("add6"); 116 | 117 | expect(info.chunkSize).toBe(4096); // because series already exists 118 | }); 119 | 120 | test("append with valid chunk size and duplicate policy to new key", async () => { 121 | const sample = new Sample("add7", 1000); 122 | await rtsClient.add(sample, [], 3000, 8000, "LAST"); 123 | 124 | const info = await rtsClient.info("add7"); 125 | 126 | expect(info.chunkSize).toBe(8000); 127 | }); 128 | 129 | test("create time series with invalid duplication policy", async () => { 130 | const sample = new Sample("add8", 1000); 131 | await expect(rtsClient.add(sample, [], 3000, 8000, "HELLO")).rejects.toThrow( 132 | "duplicate policy must be either BLOCK, FIRST, LAST, MIN, MAX or SUM, found: HELLO" 133 | ); 134 | }); 135 | -------------------------------------------------------------------------------- /src/__tests__/integration/alterRts.test.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../../entity/label"; 2 | import { FilterBuilder } from "../../builder/filterBuilder"; 3 | import { testOptions } from "../../__tests_config__/data"; 4 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 5 | 6 | const factory = new RedisTimeSeriesFactory(testOptions); 7 | const rtsClient = factory.create(); 8 | 9 | beforeAll(async () => { 10 | await rtsClient.create("alter1"); 11 | }); 12 | 13 | afterAll(async () => { 14 | await rtsClient.delete("alter1"); 15 | await rtsClient.disconnect(); 16 | }); 17 | 18 | test("alter labels successfully", async () => { 19 | const label = new Label("sensor1", "15"); 20 | const altered = await rtsClient.alter("alter1", [label]); 21 | expect(altered).toEqual(true); 22 | 23 | const filter = new FilterBuilder("sensor1", "15"); 24 | const indexes = await rtsClient.queryIndex(filter); 25 | expect(indexes.length).toBe(1); 26 | expect(indexes).toContain("alter1"); 27 | 28 | const info = await rtsClient.info("alter1"); 29 | expect(info.retentionTime).toEqual(0); 30 | 31 | const sensor1Label = info.labels.pop(); 32 | expect(sensor1Label).toStrictEqual(label); 33 | expect(info.labels.length).toEqual(0); 34 | }); 35 | 36 | test("alter retention and add label successfully", async () => { 37 | const label1 = new Label("sensor1", "15"); 38 | const label2 = new Label("sensor2", "75"); 39 | const altered = await rtsClient.alter("alter1", [label1, label2], 30000); 40 | expect(altered).toEqual(true); 41 | 42 | const filter1 = new FilterBuilder("sensor1", "15"); 43 | const indexes1 = await rtsClient.queryIndex(filter1); 44 | expect(indexes1.length).toBe(1); 45 | expect(indexes1).toContain("alter1"); 46 | 47 | const filter2 = new FilterBuilder("sensor2", "75"); 48 | const indexes2 = await rtsClient.queryIndex(filter2); 49 | expect(indexes2.length).toBe(1); 50 | expect(indexes2).toContain("alter1"); 51 | 52 | const allFilters = new FilterBuilder("sensor1", "15").equal("sensor2", "75"); 53 | const indexes = await rtsClient.queryIndex(allFilters); 54 | expect(indexes.length).toBe(1); 55 | expect(indexes2).toContain("alter1"); 56 | 57 | const info = await rtsClient.info("alter1"); 58 | expect(info.retentionTime).toEqual(30000); 59 | 60 | expect(info.labels.length).toEqual(2); 61 | const sensor1Label2 = info.labels.pop(); 62 | expect(sensor1Label2).toStrictEqual(label2); 63 | const sensor1Label1 = info.labels.pop(); 64 | expect(sensor1Label1).toStrictEqual(label1); 65 | }); 66 | 67 | test("alter retention and labels with no param doesn't change them", async () => { 68 | const label1 = new Label("sensor1", "15"); 69 | const label2 = new Label("sensor2", "75"); 70 | const altered = await rtsClient.alter("alter1"); 71 | expect(altered).toEqual(true); 72 | 73 | const filter1 = new FilterBuilder("sensor1", "15"); 74 | const indexes1 = await rtsClient.queryIndex(filter1); 75 | expect(indexes1.length).toBe(1); 76 | expect(indexes1).toContain("alter1"); 77 | 78 | const filter2 = new FilterBuilder("sensor2", "75"); 79 | const indexes2 = await rtsClient.queryIndex(filter2); 80 | expect(indexes2.length).toBe(1); 81 | expect(indexes2).toContain("alter1"); 82 | 83 | const allFilters = new FilterBuilder("sensor1", "15").equal("sensor2", "75"); 84 | const indexes = await rtsClient.queryIndex(allFilters); 85 | expect(indexes.length).toBe(1); 86 | expect(indexes2).toContain("alter1"); 87 | 88 | const info = await rtsClient.info("alter1"); 89 | expect(info.retentionTime).toEqual(30000); 90 | 91 | expect(info.labels.length).toEqual(2); 92 | const sensor1Label2 = info.labels.pop(); 93 | expect(sensor1Label2).toStrictEqual(label2); 94 | const sensor1Label1 = info.labels.pop(); 95 | expect(sensor1Label1).toStrictEqual(label1); 96 | }); 97 | 98 | test("update labels successfully", async () => { 99 | const label = new Label("sensor3", "25"); 100 | const altered = await rtsClient.alter("alter1", [label]); 101 | expect(altered).toEqual(true); 102 | 103 | const filter = new FilterBuilder("sensor3", "25"); 104 | let indexes = await rtsClient.queryIndex(filter); 105 | expect(indexes.length).toBe(1); 106 | expect(indexes).toContain("alter1"); 107 | 108 | const filter2 = new FilterBuilder("sensor2", "75"); 109 | indexes = await rtsClient.queryIndex(filter2); 110 | expect(indexes.length).toBe(0); 111 | 112 | const filter3 = new FilterBuilder("sensor1", "15"); 113 | indexes = await rtsClient.queryIndex(filter3); 114 | expect(indexes.length).toBe(0); 115 | 116 | const info = await rtsClient.info("alter1"); 117 | 118 | expect(info.labels.length).toEqual(1); 119 | const sensor1Label = info.labels.pop(); 120 | expect(sensor1Label).toStrictEqual(label); 121 | }); 122 | 123 | test("remove labels successfully", async () => { 124 | const altered = await rtsClient.alter("alter1", []); 125 | expect(altered).toEqual(true); 126 | 127 | const info = await rtsClient.info("alter1"); 128 | expect(info.retentionTime).toEqual(30000); 129 | 130 | expect(info.labels.length).toEqual(0); 131 | }); 132 | -------------------------------------------------------------------------------- /src/__tests__/integration/changeByRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Label } from "../../entity/label"; 5 | 6 | const factory = new RedisTimeSeriesFactory(testOptions); 7 | const rtsClient = factory.create(); 8 | const date = Date.now(); 9 | let memoryUsageChange2: number; 10 | let memoryUsageChange3: number; 11 | 12 | beforeAll(async () => { 13 | const sample = new Sample("change1", 20, date - 10000); 14 | await rtsClient.add(sample); 15 | }); 16 | 17 | afterAll(async () => { 18 | await rtsClient.delete("change1", "change2", "change3", "change4", "change5"); 19 | await rtsClient.disconnect(); 20 | }); 21 | 22 | test("increment successfully", async () => { 23 | const sample = new Sample("change1", 50, date - 5000); 24 | const incr = await rtsClient.incrementBy(sample); 25 | expect(incr).toEqual(date - 5000); 26 | 27 | const changed = await rtsClient.get("change1"); 28 | expect(changed.getValue()).toEqual(70); 29 | }); 30 | 31 | test("decrement successfully", async () => { 32 | const sample = new Sample("change1", 30, date); 33 | const decr = await rtsClient.decrementBy(sample); 34 | expect(decr).toEqual(date); 35 | 36 | const changed = await rtsClient.get("change1"); 37 | expect(changed.getValue()).toEqual(40); 38 | }); 39 | 40 | test("use increment for adding successfully", async () => { 41 | const sample = new Sample("change2", 50, date); 42 | const label = new Label("city1", "15"); 43 | const incr = await rtsClient.incrementBy(sample, [label], 50000); 44 | expect(incr).toEqual(date); 45 | 46 | const changed = await rtsClient.get("change2"); 47 | expect(changed.getValue()).toEqual(50); 48 | 49 | const info = await rtsClient.info("change2"); 50 | expect(info.retentionTime).toEqual(50000); 51 | expect(info.labels.pop()).toEqual(label); 52 | memoryUsageChange2 = info.memoryUsage; 53 | }); 54 | 55 | test("use decrement for adding successfully", async () => { 56 | const sample = new Sample("change3", 50, date); 57 | const label = new Label("city2", "75"); 58 | const decr = await rtsClient.decrementBy(sample, [label], 70000); 59 | expect(decr).toEqual(date); 60 | 61 | const changed = await rtsClient.get("change3"); 62 | expect(changed.getValue()).toEqual(-50); 63 | 64 | const info = await rtsClient.info("change3"); 65 | expect(info.retentionTime).toEqual(70000); 66 | expect(info.labels.pop()).toEqual(label); 67 | memoryUsageChange3 = info.memoryUsage; 68 | }); 69 | 70 | test("use increment for adding successfully with uncompressed", async () => { 71 | const sample = new Sample("change4", 50, date); 72 | const label = new Label("city3", "15"); 73 | const incr = await rtsClient.incrementBy(sample, [label], 50000, true); 74 | expect(incr).toEqual(date); 75 | 76 | const changed = await rtsClient.get("change4"); 77 | expect(changed.getValue()).toEqual(50); 78 | 79 | const info = await rtsClient.info("change4"); 80 | expect(info.retentionTime).toEqual(50000); 81 | expect(info.labels.pop()).toEqual(label); 82 | expect(memoryUsageChange2).not.toEqual(info.memoryUsage); 83 | }); 84 | 85 | test("use decrement for adding successfully", async () => { 86 | const sample = new Sample("change5", 50, date); 87 | const label = new Label("city3", "75"); 88 | const decr = await rtsClient.decrementBy(sample, [label], 70000, true); 89 | expect(decr).toEqual(date); 90 | 91 | const changed = await rtsClient.get("change5"); 92 | expect(changed.getValue()).toEqual(-50); 93 | 94 | const info = await rtsClient.info("change5"); 95 | expect(info.retentionTime).toEqual(70000); 96 | expect(info.labels.pop()).toEqual(label); 97 | expect(memoryUsageChange3).not.toEqual(info.memoryUsage); 98 | }); 99 | -------------------------------------------------------------------------------- /src/__tests__/integration/connection.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { ConnectionOptions } from "../../index"; 3 | 4 | test("lazy connection", async () => { 5 | const redisOptions: ConnectionOptions = { 6 | host: "redislabs-redistimeseries", 7 | db: 15, 8 | lazyConnect: true 9 | }; 10 | const factory = new RedisTimeSeriesFactory(redisOptions); 11 | const rtsClient = factory.create(); 12 | const created = await rtsClient.create("connection"); 13 | expect(created).toEqual(true); 14 | 15 | const deleted = await rtsClient.delete("connection"); 16 | expect(deleted).toEqual(true); 17 | 18 | const disconnected = await rtsClient.disconnect(); 19 | expect(disconnected).toEqual(true); 20 | }); 21 | -------------------------------------------------------------------------------- /src/__tests__/integration/createRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { Label } from "../../entity/label"; 3 | import { FilterBuilder } from "../../builder/filterBuilder"; 4 | import { testOptions } from "../../__tests_config__/data"; 5 | 6 | const factory = new RedisTimeSeriesFactory(testOptions); 7 | const rtsClient = factory.create(); 8 | 9 | afterAll(async () => { 10 | await rtsClient.delete( 11 | "create1", 12 | "create2", 13 | "create3", 14 | "create4", 15 | "create5", 16 | "create6", 17 | "create7", 18 | "create8", 19 | "create9" 20 | ); 21 | await rtsClient.disconnect(); 22 | }); 23 | 24 | test("create time series successfully", async () => { 25 | const label = new Label("cpu1", "15"); 26 | const created = await rtsClient.create("create1", [label], 50000); 27 | expect(created).toEqual(true); 28 | 29 | const filter = new FilterBuilder("cpu1", "15"); 30 | const indexes = await rtsClient.queryIndex(filter); 31 | expect(indexes.length).toBe(1); 32 | expect(indexes).toContain("create1"); 33 | 34 | const info = await rtsClient.info("create1"); 35 | expect(info.retentionTime).toEqual(50000); 36 | 37 | const sensor1Label = info.labels.pop(); 38 | expect(sensor1Label).toStrictEqual(label); 39 | expect(info.labels.length).toEqual(0); 40 | }); 41 | 42 | test("create time series successfully without retention", async () => { 43 | const label = new Label("cpu2", "150"); 44 | const created = await rtsClient.create("create2", [label]); 45 | expect(created).toEqual(true); 46 | 47 | const filter = new FilterBuilder("cpu2", "150"); 48 | const indexes = await rtsClient.queryIndex(filter); 49 | expect(indexes.length).toBe(1); 50 | expect(indexes).toContain("create2"); 51 | 52 | const info = await rtsClient.info("create2"); 53 | expect(info.retentionTime).toEqual(0); 54 | 55 | const sensor1Label = info.labels.pop(); 56 | expect(sensor1Label).toStrictEqual(label); 57 | expect(info.labels.length).toEqual(0); 58 | }); 59 | 60 | test("create time series successfully without labels", async () => { 61 | const created = await rtsClient.create("create3", undefined, 3000); 62 | expect(created).toEqual(true); 63 | 64 | const info = await rtsClient.info("create3"); 65 | expect(info.retentionTime).toEqual(3000); 66 | expect(info.labels.length).toEqual(0); 67 | }); 68 | 69 | test("create time series successfully without optional params", async () => { 70 | const created = await rtsClient.create("create4"); 71 | expect(created).toEqual(true); 72 | 73 | const info = await rtsClient.info("create4"); 74 | expect(info.retentionTime).toEqual(0); 75 | expect(info.labels.length).toEqual(0); 76 | }); 77 | 78 | test("create time series successfully with empty labels array", async () => { 79 | const created = await rtsClient.create("create5", []); 80 | expect(created).toEqual(true); 81 | 82 | const info = await rtsClient.info("create5"); 83 | expect(info.retentionTime).toEqual(0); 84 | expect(info.labels.length).toEqual(0); 85 | }); 86 | 87 | test("create time series successfully with valid chunk size and duplicate policy", async () => { 88 | const created = await rtsClient.create("create6", [], 3000, 8000, "FIRST"); 89 | expect(created).toEqual(true); 90 | 91 | const info = await rtsClient.info("create6"); 92 | expect(info.chunkSize).toEqual(8000); 93 | expect(info.duplicatePolicy).toEqual("first"); 94 | }); 95 | 96 | test("create duplicate time series fails", async () => { 97 | const label = new Label("cpu2", "17"); 98 | await expect(rtsClient.create("create1", [label])).rejects.toThrow(); 99 | }); 100 | 101 | test("create time series with negative retention fails", async () => { 102 | const label = new Label("cpu1", "15"); 103 | await expect(rtsClient.create("create7", [label], -10000)).rejects.toThrow( 104 | "retention must be positive integer, found: -10000" 105 | ); 106 | }); 107 | 108 | test("create time series with invalid chunk size", async () => { 109 | const label = new Label("cpu1", "15"); 110 | await expect(rtsClient.create("create8", [label], undefined, -10000)).rejects.toThrow( 111 | "chunkSize must be positive integer, found: -10000" 112 | ); 113 | }); 114 | 115 | test("create time series with invalid duplication policy", async () => { 116 | const label = new Label("cpu1", "15"); 117 | await expect(rtsClient.create("create9", [label], undefined, undefined, "HELLO")).rejects.toThrow( 118 | "duplicate policy must be either BLOCK, FIRST, LAST, MIN, MAX or SUM, found: HELLO" 119 | ); 120 | }); 121 | -------------------------------------------------------------------------------- /src/__tests__/integration/disconnectRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | 4 | const factory = new RedisTimeSeriesFactory(testOptions); 5 | const rtsClient = factory.create(); 6 | 7 | afterAll(async () => { 8 | const newRtsClient = factory.create(); 9 | await newRtsClient.delete("disconnect"); 10 | await newRtsClient.disconnect(); 11 | }); 12 | 13 | test("disconnect", async () => { 14 | const created = await rtsClient.create("disconnect"); 15 | expect(created).toEqual(true); 16 | 17 | const disconnected = await rtsClient.disconnect(); 18 | expect(disconnected).toEqual(true); 19 | 20 | await expect(rtsClient.create("after-disconnect")).rejects.toThrow(); 21 | }); 22 | -------------------------------------------------------------------------------- /src/__tests__/integration/expireRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | 5 | const factory = new RedisTimeSeriesFactory(testOptions); 6 | const rtsClient = factory.create(); 7 | 8 | beforeAll(async () => { 9 | await rtsClient.delete("exp1", "exp2"); 10 | await rtsClient.create("exp1"); 11 | await rtsClient.create("exp2"); 12 | }); 13 | 14 | afterAll(async () => { 15 | await rtsClient.delete("exp1", "exp2"); 16 | await rtsClient.disconnect(); 17 | }); 18 | 19 | const wait = async (ms): Promise => new Promise(res => setTimeout(() => res(), ms)); 20 | 21 | test("expire series successfully", async () => { 22 | const now = Date.now(); 23 | const sample = new Sample("exp1", 10, now); 24 | const added = await rtsClient.add(sample); 25 | expect(added).toEqual(now); 26 | expect(await rtsClient.get("exp1")).toEqual(sample); 27 | 28 | const expired = await rtsClient.expire("exp1", 1); 29 | expect(expired).toEqual(true); 30 | await wait(1100); 31 | 32 | try { 33 | await rtsClient.get("exp1"); 34 | } catch (error) { 35 | expect(error.message).toBe('error when executing command "TS.GET exp1": ERR TSDB: the key does not exist'); 36 | } 37 | }); 38 | 39 | test("fail to expire series with non-integer parameter", async () => { 40 | const now = Date.now(); 41 | const sample = new Sample("exp2", 10, now); 42 | const added = await rtsClient.add(sample); 43 | expect(added).toEqual(now); 44 | expect(await rtsClient.get("exp2")).toEqual(sample); 45 | 46 | await expect(rtsClient.expire("exp2", 1.5)).rejects.toThrow( 47 | "Only integers allowed for 'seconds' parameter. 1.5 is not an integer." 48 | ); 49 | }); 50 | 51 | test("fail to expire series with non-existent key", async () => { 52 | const expired = await rtsClient.expire("exp3", 1); 53 | expect(expired).toEqual(false); 54 | }); 55 | -------------------------------------------------------------------------------- /src/__tests__/integration/getRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | 5 | const factory = new RedisTimeSeriesFactory(testOptions); 6 | const rtsClient = factory.create(); 7 | const date = new Date(2019, 11, 24, 20).getTime(); 8 | 9 | beforeAll(async () => { 10 | for (let i = 0; i < 10; i++) { 11 | await rtsClient.add(new Sample("get1", 20 + i, date + i * 1000)); 12 | } 13 | }); 14 | 15 | afterAll(async () => { 16 | await rtsClient.delete("get1"); 17 | await rtsClient.disconnect(); 18 | }); 19 | 20 | test("get last value successfully", async () => { 21 | const sample = new Sample("get1", 29, date + 9000); 22 | const last = await rtsClient.get("get1"); 23 | expect(last).toEqual(sample); 24 | }); 25 | 26 | test("get recently added value successfully", async () => { 27 | const now = Date.now(); 28 | const sample = new Sample("get1", 30, now); 29 | await rtsClient.add(sample); 30 | 31 | const last = await rtsClient.get("get1"); 32 | expect(last).toEqual(sample); 33 | }); 34 | 35 | test("get last value from a non existent key fails", async () => { 36 | await expect(rtsClient.get("get2")).rejects.toThrow(); 37 | }); 38 | -------------------------------------------------------------------------------- /src/__tests__/integration/multiGetRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Label } from "../../entity/label"; 5 | import { FilterBuilder } from "../../builder/filterBuilder"; 6 | 7 | const factory = new RedisTimeSeriesFactory(testOptions); 8 | const rtsClient = factory.create(); 9 | const date = new Date(2019, 11, 24, 19).getTime(); 10 | 11 | const label1 = new Label("label", "1"); 12 | const sensor1 = new Label("sensor", "1"); 13 | const sensor2 = new Label("sensor", "2"); 14 | const sensor3 = new Label("sensor", "3"); 15 | const sensor4 = new Label("sensor", "4"); 16 | 17 | beforeAll(async () => { 18 | await rtsClient.create("multiget1", [label1, sensor1]); 19 | await rtsClient.create("multiget2", [label1, sensor2]); 20 | await rtsClient.create("multiget3", [sensor2, sensor3]); 21 | await rtsClient.create("multiget4", [sensor4]); 22 | 23 | for (let i = 0; i < 10; i++) { 24 | await rtsClient.add(new Sample("multiget1", 20 + i, date + i * 1000)); 25 | await rtsClient.add(new Sample("multiget2", 30 + i, date + i * 1000)); 26 | await rtsClient.add(new Sample("multiget3", 40 + i, date + i * 1000)); 27 | } 28 | }); 29 | 30 | afterAll(async () => { 31 | await rtsClient.delete("multiget1", "multiget2", "multiget3", "multiget4"); 32 | await rtsClient.disconnect(); 33 | }); 34 | 35 | test("query multi get with label1 filter successfully", async () => { 36 | const filter = new FilterBuilder("label", 1); 37 | const multiGets = await rtsClient.multiGet(filter, true); 38 | expect(Array.isArray(multiGets)).toBe(true); 39 | 40 | const multiGet1 = multiGets.shift(); 41 | expect(multiGet1).not.toEqual(undefined); 42 | // @ts-ignore 43 | expect(multiGet1.key).toEqual("multiget1"); 44 | // @ts-ignore 45 | const labels1 = multiGet1.labels; 46 | expect(labels1.shift()).toEqual(label1); 47 | expect(labels1.shift()).toEqual(sensor1); 48 | 49 | // @ts-ignore 50 | const sample1 = multiGet1.data; 51 | // @ts-ignore 52 | expect(sample1.getValue()).toEqual(29); 53 | expect(sample1.getTimestamp()).toEqual(date + 9000); 54 | 55 | const multiGet2 = multiGets.shift(); 56 | expect(multiGet2).not.toEqual(undefined); 57 | // @ts-ignore 58 | expect(multiGet2.key).toEqual("multiget2"); 59 | // @ts-ignore 60 | const labels2 = multiGet2.labels; 61 | expect(labels2.shift()).toEqual(label1); 62 | expect(labels2.shift()).toEqual(sensor2); 63 | 64 | // @ts-ignore 65 | const sample2 = multiGet2.data; 66 | // @ts-ignore 67 | expect(sample2.getValue()).toEqual(39); 68 | expect(sample2.getTimestamp()).toEqual(date + 9000); 69 | 70 | expect(multiGets.length).toBe(0); 71 | }); 72 | 73 | test("query multi get with sensor2 filter successfully", async () => { 74 | const filter = new FilterBuilder("sensor", 2); 75 | const multiGets = await rtsClient.multiGet(filter); 76 | expect(Array.isArray(multiGets)).toBe(true); 77 | 78 | const multiGet1 = multiGets.shift(); 79 | expect(multiGet1).not.toEqual(undefined); 80 | // @ts-ignore 81 | expect(multiGet1.key).toEqual("multiget2"); 82 | // @ts-ignore 83 | const labels = multiGet1.labels; 84 | expect(labels.length).toBe(0); 85 | 86 | // @ts-ignore 87 | const sample1 = multiGet1.data; 88 | // @ts-ignore 89 | expect(sample1.getValue()).toEqual(39); 90 | expect(sample1.getTimestamp()).toEqual(date + 9000); 91 | 92 | const multiGet2 = multiGets.shift(); 93 | expect(multiGet2).not.toEqual(undefined); 94 | // @ts-ignore 95 | expect(multiGet2.key).toEqual("multiget3"); 96 | // @ts-ignore 97 | const labels2 = multiGet2.labels; 98 | expect(labels2.length).toBe(0); 99 | 100 | // @ts-ignore 101 | const sample2 = multiGet2.data; 102 | // @ts-ignore 103 | expect(sample2.getValue()).toEqual(49); 104 | expect(sample2.getTimestamp()).toEqual(date + 9000); 105 | 106 | expect(multiGets.length).toBe(0); 107 | }); 108 | 109 | test("query multi get with filter not matching", async () => { 110 | const filter = new FilterBuilder("sensor", 3).notIn("sensor", [1, 2]); 111 | const multiGets = await rtsClient.multiGet(filter); 112 | expect(Array.isArray(multiGets)).toBe(true); 113 | expect(multiGets.length).toBe(0); 114 | }); 115 | 116 | test("query multi get with filter matching but no data", async () => { 117 | const filter = new FilterBuilder("sensor", 4); 118 | const multiGets = await rtsClient.multiGet(filter, true); 119 | expect(Array.isArray(multiGets)).toBe(true); 120 | 121 | const multiGet1 = multiGets.shift(); 122 | expect(multiGet1).not.toEqual(undefined); 123 | // @ts-ignore 124 | expect(multiGet1.key).toEqual("multiget4"); 125 | // @ts-ignore 126 | const labels1 = multiGet1.labels; 127 | expect(labels1.shift()).toEqual(sensor4); 128 | expect(labels1.length).toBe(0); 129 | 130 | // @ts-ignore 131 | const sample1 = multiGet1.data; 132 | // @ts-ignore 133 | expect(sample1.getValue()).toEqual(0); 134 | expect(sample1.getTimestamp()).toEqual(0); 135 | }); 136 | -------------------------------------------------------------------------------- /src/__tests__/integration/multiRange.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Aggregation } from "../../entity/aggregation"; 5 | import { AggregationType } from "../../enum/aggregationType"; 6 | import { TimestampRange } from "../../entity/timestampRange"; 7 | import { Label } from "../../entity/label"; 8 | import { FilterBuilder } from "../../builder/filterBuilder"; 9 | 10 | const factory = new RedisTimeSeriesFactory(testOptions); 11 | const rtsClient = factory.create(); 12 | const date = new Date(2019, 11, 24, 19).getTime(); 13 | 14 | const label1 = new Label("label", "1"); 15 | const sensor1 = new Label("sensor", "1"); 16 | const sensor2 = new Label("sensor", "2"); 17 | const sensor3 = new Label("sensor", "3"); 18 | const sensorString = new Label("sensor", "sensorvalue"); 19 | 20 | beforeAll(async () => { 21 | await rtsClient.create("multirange1", [label1, sensor1]); 22 | await rtsClient.create("multirange2", [label1, sensor2]); 23 | await rtsClient.create("multirange3", [sensor2, sensor3]); 24 | await rtsClient.create("multirange4", [sensorString]); 25 | 26 | for (let i = 0; i < 10; i++) { 27 | await rtsClient.add(new Sample("multirange1", 20 + i, date + i * 1000)); 28 | await rtsClient.add(new Sample("multirange2", 30 + i, date + i * 1000)); 29 | await rtsClient.add(new Sample("multirange3", 40 + i, date + i * 1000)); 30 | await rtsClient.add(new Sample("multirange4", 50 + i, date + i * 1000)); 31 | } 32 | }); 33 | 34 | afterAll(async () => { 35 | await rtsClient.delete("multirange1", "multirange2", "multirange3", "multirange4"); 36 | await rtsClient.disconnect(); 37 | }); 38 | 39 | test("sum aggregated query multi range with label1 filter successfully", async () => { 40 | const aggregation = new Aggregation(AggregationType.SUM, 5000); 41 | const timestampRange = new TimestampRange(date, date + 10000); 42 | const filter = new FilterBuilder("label", 1); 43 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, undefined, aggregation, true); 44 | expect(Array.isArray(multiRanges)).toBe(true); 45 | 46 | const multiRange1 = multiRanges.shift(); 47 | expect(multiRange1).not.toEqual(undefined); 48 | // @ts-ignore 49 | expect(multiRange1.key).toEqual("multirange1"); 50 | // @ts-ignore 51 | const labels1 = multiRange1.labels; 52 | expect(labels1.shift()).toEqual(label1); 53 | expect(labels1.shift()).toEqual(sensor1); 54 | 55 | // @ts-ignore 56 | const samples1 = multiRange1.data; 57 | // @ts-ignore 58 | expect(samples1.shift().getValue()).toEqual(110); 59 | // @ts-ignore 60 | expect(samples1.shift().getValue()).toEqual(135); 61 | 62 | const multiRange2 = multiRanges.shift(); 63 | expect(multiRange2).not.toEqual(undefined); 64 | // @ts-ignore 65 | expect(multiRange2.key).toEqual("multirange2"); 66 | // @ts-ignore 67 | const labels2 = multiRange2.labels; 68 | expect(labels2.shift()).toEqual(label1); 69 | expect(labels2.shift()).toEqual(sensor2); 70 | 71 | // @ts-ignore 72 | const samples2 = multiRange2.data; 73 | // @ts-ignore 74 | expect(samples2.shift().getValue()).toEqual(160); 75 | // @ts-ignore 76 | expect(samples2.shift().getValue()).toEqual(185); 77 | 78 | expect(multiRanges.length).toBe(0); 79 | }); 80 | 81 | test("max aggregated query multi range with label1 and sensor1 filters successfully", async () => { 82 | const aggregation = new Aggregation(AggregationType.MAX, 5000); 83 | const timestampRange = new TimestampRange(date, date + 10000); 84 | const filter = new FilterBuilder("label", 1).equal("sensor", 1); 85 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, undefined, aggregation, true); 86 | expect(Array.isArray(multiRanges)).toBe(true); 87 | 88 | const multiRange = multiRanges.shift(); 89 | expect(multiRange).not.toEqual(undefined); 90 | // @ts-ignore 91 | expect(multiRange.key).toEqual("multirange1"); 92 | // @ts-ignore 93 | const labels = multiRange.labels; 94 | expect(labels.shift()).toEqual(label1); 95 | expect(labels.shift()).toEqual(sensor1); 96 | 97 | // @ts-ignore 98 | const samples = multiRange.data; 99 | // @ts-ignore 100 | expect(samples.shift().getValue()).toEqual(24); 101 | // @ts-ignore 102 | expect(samples.shift().getValue()).toEqual(29); 103 | 104 | expect(multiRanges.length).toBe(0); 105 | }); 106 | 107 | test("max aggregated query multi range with sensor string filter successfully", async () => { 108 | const aggregation = new Aggregation(AggregationType.MAX, 5000); 109 | const timestampRange = new TimestampRange(date, date + 10000); 110 | const filter = new FilterBuilder("sensor", "sensorvalue"); 111 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, undefined, aggregation, true); 112 | expect(Array.isArray(multiRanges)).toBe(true); 113 | 114 | const multiRange = multiRanges.shift(); 115 | expect(multiRange).not.toEqual(undefined); 116 | // @ts-ignore 117 | expect(multiRange.key).toEqual("multirange4"); 118 | // @ts-ignore 119 | const labels = multiRange.labels; 120 | expect(labels.shift()).toEqual(sensorString); 121 | 122 | // @ts-ignore 123 | const samples = multiRange.data; 124 | // @ts-ignore 125 | expect(samples.shift().getValue()).toEqual(54); 126 | // @ts-ignore 127 | expect(samples.shift().getValue()).toEqual(59); 128 | 129 | expect(multiRanges.length).toBe(0); 130 | }); 131 | 132 | test("min aggregated query multi range with not label1 ans sensor2 filters successfully", async () => { 133 | const aggregation = new Aggregation(AggregationType.MIN, 5000); 134 | const timestampRange = new TimestampRange(date, date + 10000); 135 | const filter = new FilterBuilder("sensor", 2).notEqual("label", 1); 136 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, undefined, aggregation, true); 137 | expect(Array.isArray(multiRanges)).toBe(true); 138 | 139 | const multiRange = multiRanges.shift(); 140 | expect(multiRange).not.toEqual(undefined); 141 | // @ts-ignore 142 | expect(multiRange.key).toEqual("multirange3"); 143 | // @ts-ignore 144 | const labels = multiRange.labels; 145 | expect(labels.shift()).toEqual(sensor2); 146 | expect(labels.shift()).toEqual(sensor3); 147 | 148 | // @ts-ignore 149 | const samples = multiRange.data; 150 | // @ts-ignore 151 | expect(samples.shift().getValue()).toEqual(40); 152 | // @ts-ignore 153 | expect(samples.shift().getValue()).toEqual(45); 154 | 155 | expect(multiRanges.length).toBe(0); 156 | }); 157 | 158 | test("count aggregated query multi range with filters not matching", async () => { 159 | const aggregation = new Aggregation(AggregationType.COUNT, 5000); 160 | const timestampRange = new TimestampRange(date, date + 10000); 161 | const filter = new FilterBuilder("sensor", 3).notIn("sensor", [1, 2]); 162 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, undefined, aggregation); 163 | expect(Array.isArray(multiRanges)).toBe(true); 164 | expect(multiRanges.length).toBe(0); 165 | }); 166 | 167 | test("max aggregated query multi range with sensor3 filter and count optional param", async () => { 168 | const aggregation = new Aggregation(AggregationType.MAX, 5000); 169 | const timestampRange = new TimestampRange(date, date + 10000); 170 | const filter = new FilterBuilder("sensor", 3).notEqual("label", 1); 171 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, 1, aggregation, true); 172 | expect(Array.isArray(multiRanges)).toBe(true); 173 | 174 | const multiRange = multiRanges.shift(); 175 | expect(multiRange).not.toEqual(undefined); 176 | // @ts-ignore 177 | expect(multiRange.key).toEqual("multirange3"); 178 | // @ts-ignore 179 | const labels = multiRange.labels; 180 | expect(labels.shift()).toEqual(sensor2); 181 | expect(labels.shift()).toEqual(sensor3); 182 | 183 | // @ts-ignore 184 | const samples = multiRange.data; 185 | // @ts-ignore 186 | expect(samples.shift().getValue()).toEqual(44); 187 | 188 | expect(samples.length).toBe(0); 189 | expect(multiRanges.length).toBe(0); 190 | }); 191 | 192 | test("aggregated query multi range with filter not matching", async () => { 193 | const aggregation = new Aggregation(AggregationType.COUNT, 5000); 194 | const timestampRange = new TimestampRange(date, date + 10000); 195 | const filter = new FilterBuilder("sensor", 3).notIn("sensor", [1, 2]); 196 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, undefined, aggregation, true); 197 | expect(Array.isArray(multiRanges)).toBe(true); 198 | expect(multiRanges.length).toBe(0); 199 | }); 200 | 201 | test("aggregated query multi range with default timestamp and without labels", async () => { 202 | const timestampRange = new TimestampRange(); 203 | const filter = new FilterBuilder("sensor", 3).notEqual("label", 1); 204 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, 3); 205 | expect(Array.isArray(multiRanges)).toBe(true); 206 | 207 | const multiRange = multiRanges.shift(); 208 | expect(multiRange).not.toEqual(undefined); 209 | // @ts-ignore 210 | expect(multiRange.key).toEqual("multirange3"); 211 | // @ts-ignore 212 | expect(multiRange.labels.length).toBe(0); 213 | 214 | // @ts-ignore 215 | const samples = multiRange.data; 216 | // @ts-ignore 217 | expect(samples.shift().getValue()).toEqual(40); 218 | // @ts-ignore 219 | expect(samples.shift().getValue()).toEqual(41); 220 | // @ts-ignore 221 | expect(samples.shift().getValue()).toEqual(42); 222 | 223 | expect(samples.length).toBe(0); 224 | expect(multiRanges.length).toBe(0); 225 | }); 226 | 227 | test("aggregated query multi range with timestamp range not matching", async () => { 228 | const now = Date.now(); 229 | const start = now - 10000; 230 | const timestampRange = new TimestampRange(start, now); 231 | const filter = new FilterBuilder("sensor", 3); 232 | for (const key in AggregationType) { 233 | const aggregationType = AggregationType[key]; 234 | const aggregation = new Aggregation(aggregationType, 5000); 235 | const multiRanges = await rtsClient.multiRange(timestampRange, filter, undefined, aggregation, true); 236 | expect(Array.isArray(multiRanges)).toBe(true); 237 | 238 | const multiRange1 = multiRanges.shift(); 239 | expect(multiRange1).not.toEqual(undefined); 240 | // @ts-ignore 241 | expect(multiRange1.key).toEqual("multirange3"); 242 | // @ts-ignore 243 | const labels1 = multiRange1.labels; 244 | expect(labels1.shift()).toEqual(sensor2); 245 | expect(labels1.shift()).toEqual(sensor3); 246 | 247 | // @ts-ignore 248 | const samples = multiRange1.data; 249 | const sample1 = samples.shift(); 250 | 251 | if (aggregationType === "count") { 252 | // @ts-ignore 253 | expect(sample1.getValue()).toEqual(0); 254 | // @ts-ignore 255 | expect(sample1.getTimestamp()).toEqual(date); 256 | } else { 257 | // @ts-ignore 258 | expect(sample1).toEqual(undefined); 259 | } 260 | 261 | expect(samples.length).toBe(0); 262 | expect(multiRanges.length).toBe(0); 263 | } 264 | }); 265 | -------------------------------------------------------------------------------- /src/__tests__/integration/multiRevRange.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Aggregation } from "../../entity/aggregation"; 5 | import { AggregationType } from "../../enum/aggregationType"; 6 | import { TimestampRange } from "../../entity/timestampRange"; 7 | import { Label } from "../../entity/label"; 8 | import { FilterBuilder } from "../../builder/filterBuilder"; 9 | 10 | const factory = new RedisTimeSeriesFactory(testOptions); 11 | const rtsClient = factory.create(); 12 | const date = new Date(2019, 11, 24, 19).getTime(); 13 | 14 | const label1 = new Label("label", "1"); 15 | const sensor1 = new Label("sensor", "1"); 16 | const sensor2 = new Label("sensor", "2"); 17 | const sensor3 = new Label("sensor", "3"); 18 | const sensorString = new Label("sensor", "sensorvalue"); 19 | 20 | beforeAll(async () => { 21 | await rtsClient.create("multirange1", [label1, sensor1]); 22 | await rtsClient.create("multirange2", [label1, sensor2]); 23 | await rtsClient.create("multirange3", [sensor2, sensor3]); 24 | await rtsClient.create("multirange4", [sensorString]); 25 | 26 | for (let i = 0; i < 10; i++) { 27 | await rtsClient.add(new Sample("multirange1", 20 + i, date + i * 1000)); 28 | await rtsClient.add(new Sample("multirange2", 30 + i, date + i * 1000)); 29 | await rtsClient.add(new Sample("multirange3", 40 + i, date + i * 1000)); 30 | await rtsClient.add(new Sample("multirange4", 50 + i, date + i * 1000)); 31 | } 32 | }); 33 | 34 | afterAll(async () => { 35 | await rtsClient.delete("multirange1", "multirange2", "multirange3", "multirange4"); 36 | await rtsClient.disconnect(); 37 | }); 38 | 39 | test("sum aggregated query multi range with label1 filter successfully", async () => { 40 | const aggregation = new Aggregation(AggregationType.SUM, 5000); 41 | const timestampRange = new TimestampRange(date, date + 10000); 42 | const filter = new FilterBuilder("label", 1); 43 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, undefined, aggregation, true); 44 | expect(Array.isArray(multiRanges)).toBe(true); 45 | 46 | const multiRange1 = multiRanges.shift(); 47 | expect(multiRange1).not.toEqual(undefined); 48 | // @ts-ignore 49 | expect(multiRange1.key).toEqual("multirange1"); 50 | // @ts-ignore 51 | const labels1 = multiRange1.labels; 52 | expect(labels1.shift()).toEqual(label1); 53 | expect(labels1.shift()).toEqual(sensor1); 54 | 55 | // @ts-ignore 56 | const samples1 = multiRange1.data; 57 | // @ts-ignore 58 | expect(samples1.shift().getValue()).toEqual(135); 59 | // @ts-ignore 60 | expect(samples1.shift().getValue()).toEqual(110); 61 | 62 | const multiRange2 = multiRanges.shift(); 63 | expect(multiRange2).not.toEqual(undefined); 64 | // @ts-ignore 65 | expect(multiRange2.key).toEqual("multirange2"); 66 | // @ts-ignore 67 | const labels2 = multiRange2.labels; 68 | expect(labels2.shift()).toEqual(label1); 69 | expect(labels2.shift()).toEqual(sensor2); 70 | 71 | // @ts-ignore 72 | const samples2 = multiRange2.data; 73 | // @ts-ignore 74 | expect(samples2.shift().getValue()).toEqual(185); 75 | // @ts-ignore 76 | expect(samples2.shift().getValue()).toEqual(160); 77 | 78 | expect(multiRanges.length).toBe(0); 79 | }); 80 | 81 | test("max aggregated query multi range with label1 and sensor1 filters successfully", async () => { 82 | const aggregation = new Aggregation(AggregationType.MAX, 5000); 83 | const timestampRange = new TimestampRange(date, date + 10000); 84 | const filter = new FilterBuilder("label", 1).equal("sensor", 1); 85 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, undefined, aggregation, true); 86 | expect(Array.isArray(multiRanges)).toBe(true); 87 | 88 | const multiRange = multiRanges.shift(); 89 | expect(multiRange).not.toEqual(undefined); 90 | // @ts-ignore 91 | expect(multiRange.key).toEqual("multirange1"); 92 | // @ts-ignore 93 | const labels = multiRange.labels; 94 | expect(labels.shift()).toEqual(label1); 95 | expect(labels.shift()).toEqual(sensor1); 96 | 97 | // @ts-ignore 98 | const samples = multiRange.data; 99 | // @ts-ignore 100 | expect(samples.shift().getValue()).toEqual(29); 101 | // @ts-ignore 102 | expect(samples.shift().getValue()).toEqual(24); 103 | 104 | expect(multiRanges.length).toBe(0); 105 | }); 106 | 107 | test("max aggregated query multi range with sensor string filter successfully", async () => { 108 | const aggregation = new Aggregation(AggregationType.MAX, 5000); 109 | const timestampRange = new TimestampRange(date, date + 10000); 110 | const filter = new FilterBuilder("sensor", "sensorvalue"); 111 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, undefined, aggregation, true); 112 | expect(Array.isArray(multiRanges)).toBe(true); 113 | 114 | const multiRange = multiRanges.shift(); 115 | expect(multiRange).not.toEqual(undefined); 116 | // @ts-ignore 117 | expect(multiRange.key).toEqual("multirange4"); 118 | // @ts-ignore 119 | const labels = multiRange.labels; 120 | expect(labels.shift()).toEqual(sensorString); 121 | 122 | // @ts-ignore 123 | const samples = multiRange.data; 124 | // @ts-ignore 125 | expect(samples.shift().getValue()).toEqual(59); 126 | // @ts-ignore 127 | expect(samples.shift().getValue()).toEqual(54); 128 | 129 | expect(multiRanges.length).toBe(0); 130 | }); 131 | 132 | test("min aggregated query multi range with not label1 ans sensor2 filters successfully", async () => { 133 | const aggregation = new Aggregation(AggregationType.MIN, 5000); 134 | const timestampRange = new TimestampRange(date, date + 10000); 135 | const filter = new FilterBuilder("sensor", 2).notEqual("label", 1); 136 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, undefined, aggregation, true); 137 | expect(Array.isArray(multiRanges)).toBe(true); 138 | 139 | const multiRange = multiRanges.shift(); 140 | expect(multiRange).not.toEqual(undefined); 141 | // @ts-ignore 142 | expect(multiRange.key).toEqual("multirange3"); 143 | // @ts-ignore 144 | const labels = multiRange.labels; 145 | expect(labels.shift()).toEqual(sensor2); 146 | expect(labels.shift()).toEqual(sensor3); 147 | 148 | // @ts-ignore 149 | const samples = multiRange.data; 150 | // @ts-ignore 151 | expect(samples.shift().getValue()).toEqual(45); 152 | // @ts-ignore 153 | expect(samples.shift().getValue()).toEqual(40); 154 | 155 | expect(multiRanges.length).toBe(0); 156 | }); 157 | 158 | test("count aggregated query multi range with filters not matching", async () => { 159 | const aggregation = new Aggregation(AggregationType.COUNT, 5000); 160 | const timestampRange = new TimestampRange(date, date + 10000); 161 | const filter = new FilterBuilder("sensor", 3).notIn("sensor", [1, 2]); 162 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, undefined, aggregation); 163 | expect(Array.isArray(multiRanges)).toBe(true); 164 | 165 | expect(multiRanges.length).toBe(0); 166 | }); 167 | 168 | test("max aggregated query multi range with sensor3 filter and count optional param", async () => { 169 | const aggregation = new Aggregation(AggregationType.MAX, 5000); 170 | const timestampRange = new TimestampRange(date, date + 10000); 171 | const filter = new FilterBuilder("sensor", 3).notEqual("label", 1); 172 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, 1, aggregation, true); 173 | expect(Array.isArray(multiRanges)).toBe(true); 174 | 175 | const multiRange = multiRanges.shift(); 176 | expect(multiRange).not.toEqual(undefined); 177 | // @ts-ignore 178 | expect(multiRange.key).toEqual("multirange3"); 179 | // @ts-ignore 180 | const labels = multiRange.labels; 181 | expect(labels.shift()).toEqual(sensor2); 182 | expect(labels.shift()).toEqual(sensor3); 183 | 184 | // @ts-ignore 185 | const samples = multiRange.data; 186 | // @ts-ignore 187 | expect(samples.shift().getValue()).toEqual(49); 188 | 189 | expect(samples.length).toBe(0); 190 | expect(multiRanges.length).toBe(0); 191 | }); 192 | 193 | test("aggregated query multi range with filter not matching", async () => { 194 | const aggregation = new Aggregation(AggregationType.COUNT, 5000); 195 | const timestampRange = new TimestampRange(date, date + 10000); 196 | const filter = new FilterBuilder("sensor", 3).notIn("sensor", [1, 2]); 197 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, undefined, aggregation, true); 198 | expect(Array.isArray(multiRanges)).toBe(true); 199 | 200 | expect(multiRanges.length).toBe(0); 201 | }); 202 | 203 | test("aggregated query multi range with default timestamp and without labels", async () => { 204 | const timestampRange = new TimestampRange(); 205 | const filter = new FilterBuilder("sensor", 3).notEqual("label", 1); 206 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, 3); 207 | expect(Array.isArray(multiRanges)).toBe(true); 208 | 209 | const multiRange = multiRanges.shift(); 210 | expect(multiRange).not.toEqual(undefined); 211 | // @ts-ignore 212 | expect(multiRange.key).toEqual("multirange3"); 213 | // @ts-ignore 214 | expect(multiRange.labels.length).toBe(0); 215 | 216 | // @ts-ignore 217 | const samples = multiRange.data; 218 | // @ts-ignore 219 | expect(samples.shift().getValue()).toEqual(49); 220 | // @ts-ignore 221 | expect(samples.shift().getValue()).toEqual(48); 222 | // @ts-ignore 223 | expect(samples.shift().getValue()).toEqual(47); 224 | 225 | expect(samples.length).toBe(0); 226 | expect(multiRanges.length).toBe(0); 227 | }); 228 | 229 | test("aggregated query multi range with timestamp range not matching", async () => { 230 | const now = Date.now(); 231 | const start = now - 10000; 232 | const timestampRange = new TimestampRange(start, now); 233 | const filter = new FilterBuilder("sensor", 3); 234 | for (const key in AggregationType) { 235 | const aggregationType = AggregationType[key]; 236 | const aggregation = new Aggregation(aggregationType, 5000); 237 | const multiRanges = await rtsClient.multiRevRange(timestampRange, filter, undefined, aggregation, true); 238 | expect(Array.isArray(multiRanges)).toBe(true); 239 | 240 | const multiRange1 = multiRanges.shift(); 241 | expect(multiRange1).not.toEqual(undefined); 242 | // @ts-ignore 243 | expect(multiRange1.key).toEqual("multirange3"); 244 | // @ts-ignore 245 | const labels1 = multiRange1.labels; 246 | expect(labels1.shift()).toEqual(sensor2); 247 | expect(labels1.shift()).toEqual(sensor3); 248 | 249 | // @ts-ignore 250 | const samples = multiRange1.data; 251 | const sample1 = samples.shift(); 252 | 253 | if (aggregationType === "count") { 254 | // @ts-ignore 255 | expect(sample1.getValue()).toEqual(0); 256 | // @ts-ignore 257 | expect(sample1.getTimestamp()).toEqual(date + 5000); 258 | } else { 259 | // @ts-ignore 260 | expect(sample1).toEqual(undefined); 261 | } 262 | 263 | expect(samples.length).toBe(0); 264 | expect(multiRanges.length).toBe(0); 265 | } 266 | }); 267 | -------------------------------------------------------------------------------- /src/__tests__/integration/queryIndexRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Label } from "../../entity/label"; 5 | import { FilterBuilder } from "../../builder/filterBuilder"; 6 | 7 | const factory = new RedisTimeSeriesFactory(testOptions); 8 | const rtsClient = factory.create(); 9 | const date = new Date(2019, 11, 24, 19).getTime(); 10 | 11 | const label1 = new Label("label", "1"); 12 | const sensor1 = new Label("sensor", "1"); 13 | const sensor2 = new Label("sensor", "2"); 14 | const sensor3 = new Label("sensor", "3"); 15 | 16 | beforeAll(async () => { 17 | await rtsClient.create("queryindex1", [label1, sensor1]); 18 | await rtsClient.create("queryindex2", [label1, sensor2]); 19 | await rtsClient.create("queryindex3", [sensor2, sensor3]); 20 | 21 | await rtsClient.add(new Sample("queryindex1", 20, date)); 22 | await rtsClient.add(new Sample("queryindex2", 30, date)); 23 | await rtsClient.add(new Sample("queryindex3", 40, date)); 24 | }); 25 | 26 | afterAll(async () => { 27 | await rtsClient.delete("queryindex1", "queryindex2", "queryindex3"); 28 | await rtsClient.disconnect(); 29 | }); 30 | 31 | test("query index with label1 filter successfully", async () => { 32 | const filter = new FilterBuilder("label", 1); 33 | const timeSeries = await rtsClient.queryIndex(filter); 34 | expect(Array.isArray(timeSeries)).toBe(true); 35 | 36 | expect(timeSeries.shift()).toEqual("queryindex1"); 37 | expect(timeSeries.shift()).toEqual("queryindex2"); 38 | expect(timeSeries.length).toBe(0); 39 | }); 40 | 41 | test("query index with all sensors filter successfully", async () => { 42 | const filter = new FilterBuilder("sensor", 3).in("sensor", [1, 2]); 43 | const timeSeries = await rtsClient.queryIndex(filter); 44 | expect(Array.isArray(timeSeries)).toBe(true); 45 | 46 | expect(timeSeries.shift()).toEqual("queryindex3"); 47 | expect(timeSeries.length).toBe(0); 48 | }); 49 | 50 | test("query index with no sensors filter successfully", async () => { 51 | const filter = new FilterBuilder("sensor", 3).notIn("sensor", [1, 2]); 52 | const timeSeries = await rtsClient.queryIndex(filter); 53 | expect(Array.isArray(timeSeries)).toBe(true); 54 | 55 | expect(timeSeries.length).toBe(0); 56 | }); 57 | 58 | test("query index with no labels in sensors filter successfully", async () => { 59 | const filter = new FilterBuilder("sensor", 3).notExists("sensor"); 60 | const timeSeries = await rtsClient.queryIndex(filter); 61 | expect(Array.isArray(timeSeries)).toBe(true); 62 | 63 | expect(timeSeries.length).toBe(0); 64 | }); 65 | 66 | test("query index with labels in sensors filter successfully", async () => { 67 | const filter = new FilterBuilder("sensor", 2).exists("sensor"); 68 | const timeSeries = await rtsClient.queryIndex(filter); 69 | expect(Array.isArray(timeSeries)).toBe(true); 70 | 71 | expect(timeSeries.shift()).toEqual("queryindex2"); 72 | expect(timeSeries.shift()).toEqual("queryindex3"); 73 | expect(timeSeries.length).toBe(0); 74 | }); 75 | 76 | test("query index with filter not matching", async () => { 77 | const filter = new FilterBuilder("sensor", 4); 78 | const timeSeries = await rtsClient.queryIndex(filter); 79 | expect(Array.isArray(timeSeries)).toBe(true); 80 | 81 | expect(timeSeries.length).toBe(0); 82 | }); 83 | -------------------------------------------------------------------------------- /src/__tests__/integration/range.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Aggregation } from "../../entity/aggregation"; 5 | import { AggregationType } from "../../enum/aggregationType"; 6 | import { TimestampRange } from "../../entity/timestampRange"; 7 | 8 | const factory = new RedisTimeSeriesFactory(testOptions); 9 | const rtsClient = factory.create(); 10 | const date = new Date(2019, 11, 24, 18).getTime(); 11 | 12 | beforeAll(async () => { 13 | await rtsClient.create("range1"); 14 | for (let i = 0; i < 10; i++) { 15 | await rtsClient.add(new Sample("range1", 20 + i, date + i * 1000)); 16 | } 17 | }); 18 | 19 | afterAll(async () => { 20 | await rtsClient.delete("range1"); 21 | await rtsClient.disconnect(); 22 | }); 23 | 24 | test("average aggregated query full range successfully", async () => { 25 | const aggregation = new Aggregation(AggregationType.AVG, 1000); 26 | const timestampRange = new TimestampRange(date, date + 10000); 27 | const samples = await rtsClient.range("range1", timestampRange, undefined, aggregation); 28 | expect(Array.isArray(samples)).toBe(true); 29 | let j = 0; 30 | for (const sample of samples) { 31 | const expectedSample = new Sample("range1", 20 + j, date + j * 1000); 32 | expect(sample).toEqual(expectedSample); 33 | j++; 34 | } 35 | }); 36 | 37 | test("sum aggregated query full range successfully", async () => { 38 | const aggregation = new Aggregation(AggregationType.SUM, 5000); 39 | const timestampRange = new TimestampRange(date, date + 10000); 40 | const samples = await rtsClient.range("range1", timestampRange, undefined, aggregation); 41 | expect(Array.isArray(samples)).toBe(true); 42 | 43 | const expectedSample1 = new Sample("range1", 110, date); 44 | expect(samples[0]).toEqual(expectedSample1); 45 | 46 | const expectedSample2 = new Sample("range1", 135, date + 5000); 47 | expect(samples[1]).toEqual(expectedSample2); 48 | }); 49 | 50 | test("min aggregated query full range successfully", async () => { 51 | const aggregation = new Aggregation(AggregationType.MIN, 5000); 52 | const timestampRange = new TimestampRange(date, date + 10000); 53 | const samples = await rtsClient.range("range1", timestampRange, undefined, aggregation); 54 | expect(Array.isArray(samples)).toBe(true); 55 | 56 | const expectedSample1 = new Sample("range1", 20, date); 57 | expect(samples[0]).toEqual(expectedSample1); 58 | 59 | const expectedSample2 = new Sample("range1", 25, date + 5000); 60 | expect(samples[1]).toEqual(expectedSample2); 61 | }); 62 | 63 | test("std.p aggregated query full range successfully", async () => { 64 | const aggregation = new Aggregation(AggregationType.STD_P, 5000); 65 | const timestampRange = new TimestampRange(date, date + 10000); 66 | const samples = await rtsClient.range("range1", timestampRange, undefined, aggregation); 67 | expect(Array.isArray(samples)).toBe(true); 68 | 69 | expect(samples[0].getValue()).toBeCloseTo(1.4142); 70 | expect(samples[1].getValue()).toBeCloseTo(1.4142); 71 | }); 72 | 73 | test("last aggregated query full range successfully with count", async () => { 74 | const aggregation = new Aggregation(AggregationType.LAST, 5000); 75 | const timestampRange = new TimestampRange(date, date + 10000); 76 | const samples = await rtsClient.range("range1", timestampRange, 1, aggregation); 77 | expect(Array.isArray(samples)).toBe(true); 78 | 79 | const expectedSample1 = new Sample("range1", 24, date); 80 | expect(samples.pop()).toEqual(expectedSample1); 81 | 82 | expect(samples.length).toBe(0); 83 | }); 84 | 85 | test("query range on non existent key fails", async () => { 86 | const timestampRange = new TimestampRange(date, date + 10000); 87 | await expect(rtsClient.range("range2", timestampRange)).rejects.toThrow(); 88 | }); 89 | -------------------------------------------------------------------------------- /src/__tests__/integration/resetRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { Label } from "../../entity/label"; 3 | import { ConnectionOptions } from "../../index"; 4 | 5 | const options: ConnectionOptions = { 6 | host: "redislabs-redistimeseries", 7 | db: 14 8 | }; 9 | const factory = new RedisTimeSeriesFactory(options); 10 | const rtsClient = factory.create(); 11 | 12 | const sensor1 = new Label("sensor", "1"); 13 | const sensor2 = new Label("sensor", "2"); 14 | const sensor3 = new Label("sensor", "3"); 15 | 16 | beforeAll(async () => { 17 | await rtsClient.create("reset1", [sensor1], 50000); 18 | await rtsClient.create("reset2", [sensor2], 60000); 19 | await rtsClient.create("reset3", [sensor3], 70000); 20 | }); 21 | 22 | afterAll(async () => { 23 | await rtsClient.deleteAll(); 24 | await rtsClient.disconnect(); 25 | }); 26 | 27 | test("reset successfully", async () => { 28 | let info1 = await rtsClient.info("reset1"); 29 | expect(info1.retentionTime).toEqual(50000); 30 | expect(info1.labels.shift()).toEqual(sensor1); 31 | 32 | let info2 = await rtsClient.info("reset2"); 33 | expect(info2.retentionTime).toEqual(60000); 34 | expect(info2.labels.shift()).toEqual(sensor2); 35 | 36 | let info3 = await rtsClient.info("reset3"); 37 | expect(info3.retentionTime).toEqual(70000); 38 | expect(info3.labels.shift()).toEqual(sensor3); 39 | 40 | const label1 = new Label("label", "1"); 41 | const reset1 = await rtsClient.reset("reset1", [label1], 150000); 42 | expect(reset1).toEqual(true); 43 | 44 | const label2 = new Label("label", "2"); 45 | const reset2 = await rtsClient.reset("reset2", [label2], 160000); 46 | expect(reset2).toEqual(true); 47 | 48 | const label3 = new Label("label", "3"); 49 | const reset3 = await rtsClient.reset("reset3", [label3], 170000); 50 | expect(reset3).toEqual(true); 51 | 52 | info1 = await rtsClient.info("reset1"); 53 | expect(info1.retentionTime).toEqual(150000); 54 | expect(info1.labels.shift()).not.toEqual(sensor1); 55 | 56 | info2 = await rtsClient.info("reset2"); 57 | expect(info2.retentionTime).toEqual(160000); 58 | expect(info2.labels.shift()).not.toEqual(sensor2); 59 | 60 | info3 = await rtsClient.info("reset3"); 61 | expect(info3.retentionTime).toEqual(170000); 62 | expect(info3.labels.shift()).not.toEqual(sensor3); 63 | }); 64 | 65 | test("reset not existent key fails", async () => { 66 | await expect(rtsClient.reset("reset4")).rejects.toThrow(/redis time series with key reset4 could not be deleted/); 67 | }); 68 | -------------------------------------------------------------------------------- /src/__tests__/integration/revRange.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Aggregation } from "../../entity/aggregation"; 5 | import { AggregationType } from "../../enum/aggregationType"; 6 | import { TimestampRange } from "../../entity/timestampRange"; 7 | 8 | const factory = new RedisTimeSeriesFactory(testOptions); 9 | const rtsClient = factory.create(); 10 | const date = new Date(2019, 11, 24, 18).getTime(); 11 | 12 | beforeAll(async () => { 13 | await rtsClient.create("range1"); 14 | for (let i = 0; i < 10; i++) { 15 | await rtsClient.add(new Sample("range1", 20 + i, date + i * 1000)); 16 | } 17 | }); 18 | 19 | afterAll(async () => { 20 | await rtsClient.delete("range1"); 21 | await rtsClient.disconnect(); 22 | }); 23 | 24 | test("average aggregated query full range successfully", async () => { 25 | const aggregation = new Aggregation(AggregationType.AVG, 1000); 26 | const timestampRange = new TimestampRange(date, date + 10000); 27 | const samples = await rtsClient.revRange("range1", timestampRange, undefined, aggregation); 28 | expect(Array.isArray(samples)).toBe(true); 29 | let j = 0; 30 | for (const sample of samples.reverse()) { 31 | const expectedSample = new Sample("range1", 20 + j, date + j * 1000); 32 | expect(sample).toEqual(expectedSample); 33 | j++; 34 | } 35 | }); 36 | 37 | test("sum aggregated query full range successfully", async () => { 38 | const aggregation = new Aggregation(AggregationType.SUM, 5000); 39 | const timestampRange = new TimestampRange(date, date + 10000); 40 | const samples = await rtsClient.revRange("range1", timestampRange, undefined, aggregation); 41 | expect(Array.isArray(samples)).toBe(true); 42 | 43 | const expectedSample1 = new Sample("range1", 110, date); 44 | 45 | samples.reverse(); // reverse it as revrange does 46 | 47 | expect(samples[0]).toEqual(expectedSample1); 48 | 49 | const expectedSample2 = new Sample("range1", 135, date + 5000); 50 | expect(samples[1]).toEqual(expectedSample2); 51 | }); 52 | 53 | test("min aggregated query full range successfully", async () => { 54 | const aggregation = new Aggregation(AggregationType.MIN, 5000); 55 | const timestampRange = new TimestampRange(date, date + 10000); 56 | const samples = await rtsClient.revRange("range1", timestampRange, undefined, aggregation); 57 | expect(Array.isArray(samples)).toBe(true); 58 | 59 | samples.reverse(); // reverse it as revrange does 60 | 61 | const expectedSample1 = new Sample("range1", 20, date); 62 | expect(samples[0]).toEqual(expectedSample1); 63 | 64 | const expectedSample2 = new Sample("range1", 25, date + 5000); 65 | expect(samples[1]).toEqual(expectedSample2); 66 | }); 67 | 68 | test("std.p aggregated query full range successfully", async () => { 69 | const aggregation = new Aggregation(AggregationType.STD_P, 5000); 70 | const timestampRange = new TimestampRange(date, date + 10000); 71 | const samples = await rtsClient.revRange("range1", timestampRange, undefined, aggregation); 72 | expect(Array.isArray(samples)).toBe(true); 73 | 74 | samples.reverse(); // reverse it as revrange does 75 | 76 | expect(samples[0].getValue()).toBeCloseTo(1.4142); 77 | expect(samples[1].getValue()).toBeCloseTo(1.4142); 78 | }); 79 | 80 | test("query range on non existent key fails", async () => { 81 | const timestampRange = new TimestampRange(date, date + 10000); 82 | await expect(rtsClient.revRange("range2", timestampRange)).rejects.toThrow(); 83 | }); 84 | -------------------------------------------------------------------------------- /src/__tests__/integration/ruleRts.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../factory/redisTimeSeries"; 2 | import { testOptions } from "../../__tests_config__/data"; 3 | import { Sample } from "../../entity/sample"; 4 | import { Aggregation } from "../../entity/aggregation"; 5 | import { AggregationType } from "../../enum/aggregationType"; 6 | 7 | const factory = new RedisTimeSeriesFactory(testOptions); 8 | const rtsClient = factory.create(); 9 | 10 | beforeAll(async () => { 11 | const sample1 = new Sample("rule1", 20); 12 | const sample2 = new Sample("rule2", 30); 13 | await rtsClient.add(sample1); 14 | await rtsClient.add(sample2); 15 | }); 16 | 17 | afterAll(async () => { 18 | await rtsClient.delete("rule1", "rule2"); 19 | await rtsClient.disconnect(); 20 | }); 21 | 22 | test("create rule successfully", async () => { 23 | const aggregation = new Aggregation(AggregationType.AVG, 50000); 24 | const ruled = await rtsClient.createRule("rule1", "rule2", aggregation); 25 | expect(ruled).toEqual(true); 26 | 27 | const info1 = await rtsClient.info("rule1"); 28 | expect(info1.rules.rule2).toEqual(aggregation); 29 | const info2 = await rtsClient.info("rule2"); 30 | expect(info2.sourceKey).toEqual("rule1"); 31 | }); 32 | 33 | test("delete rule successfully", async () => { 34 | const ruled = await rtsClient.deleteRule("rule1", "rule2"); 35 | expect(ruled).toEqual(true); 36 | 37 | const info = await rtsClient.info("rule1"); 38 | expect(info.rules).toEqual({}); 39 | }); 40 | 41 | test("create rule with non existent destination key fails", async () => { 42 | const aggregation = new Aggregation(AggregationType.COUNT, 50000); 43 | await expect(rtsClient.createRule("rule1", "rule3", aggregation)).rejects.toThrow(); 44 | }); 45 | 46 | test("delete rule with non existent destination key fails", async () => { 47 | await expect(rtsClient.deleteRule("rule1", "rule3")).rejects.toThrow(); 48 | }); 49 | 50 | test("create rule with non existent source key fails", async () => { 51 | const aggregation = new Aggregation(AggregationType.COUNT, 50000); 52 | await expect(rtsClient.createRule("rule3", "rule2", aggregation)).rejects.toThrow(); 53 | }); 54 | 55 | test("delete rule with non existent source key fails", async () => { 56 | await expect(rtsClient.deleteRule("rule3", "rule2")).rejects.toThrow(); 57 | }); 58 | 59 | test("create rule with same source and destination key fails", async () => { 60 | const aggregation = new Aggregation(AggregationType.COUNT, 50000); 61 | await expect(rtsClient.createRule("rule1", "rule1", aggregation)).rejects.toThrow( 62 | /source and destination key cannot be equals/ 63 | ); 64 | }); 65 | -------------------------------------------------------------------------------- /src/__tests__/unit/builder/filterBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { FilterOperator } from "../../../enum/filterOperator"; 2 | import { Filter } from "../../../entity/filter"; 3 | import { FilterBuilder } from "../../../builder/filterBuilder"; 4 | 5 | test("all filter creation", () => { 6 | const filterBuilder = new FilterBuilder("label1", 21); 7 | const filters = filterBuilder 8 | .equal("label2", "22") 9 | .notEqual("label3", 23) 10 | .exists("label4") 11 | .notExists("label5") 12 | .in("label6", [24, "25"]) 13 | .notIn("label7", ["26", 27]) 14 | .get(); 15 | 16 | expect(filters.shift()).toEqual(new Filter("label1", FilterOperator.EQUAL, 21)); 17 | expect(filters.shift()).toEqual(new Filter("label2", FilterOperator.EQUAL, "22")); 18 | expect(filters.shift()).toEqual(new Filter("label3", FilterOperator.NOT_EQUAL, 23)); 19 | expect(filters.shift()).toEqual(new Filter("label4", FilterOperator.NOT_EQUAL)); 20 | expect(filters.shift()).toEqual(new Filter("label5", FilterOperator.EQUAL)); 21 | expect(filters.shift()).toEqual(new Filter("label6", FilterOperator.EQUAL, [24, "25"])); 22 | expect(filters.shift()).toEqual(new Filter("label7", FilterOperator.NOT_EQUAL, ["26", 27])); 23 | expect(filters.length).toEqual(0); 24 | }); 25 | -------------------------------------------------------------------------------- /src/__tests__/unit/builder/requestParamsBuilder.test.ts: -------------------------------------------------------------------------------- 1 | import { AggregationType } from "../../../enum/aggregationType"; 2 | import { CommandKeyword } from "../../../enum/commandKeyword"; 3 | import { Label } from "../../../entity/label"; 4 | import { Sample } from "../../../entity/sample"; 5 | import { Aggregation } from "../../../entity/aggregation"; 6 | import { TimestampRange } from "../../../entity/timestampRange"; 7 | import { FilterBuilder } from "../../../builder/filterBuilder"; 8 | import { RequestParamsBuilder } from "../../../builder/requestParamsBuilder"; 9 | 10 | let builder: RequestParamsBuilder; 11 | 12 | beforeEach(() => { 13 | builder = new RequestParamsBuilder(); 14 | }); 15 | 16 | afterEach(() => { 17 | builder.reset(); 18 | }); 19 | 20 | test("add key", () => { 21 | expect(builder.addKey("key").get()).toEqual(["key"]); 22 | }); 23 | 24 | test("add keys", () => { 25 | expect(builder.addKeys("source", "target").get()).toEqual(["source", "target"]); 26 | }); 27 | 28 | test("add same keys fails", () => { 29 | expect(() => { 30 | builder.addKeys("source", "source").get(); 31 | }).toThrow(/source and destination key cannot be equals/); 32 | }); 33 | 34 | test("add retention", () => { 35 | expect(builder.addRetention(1000).get()).toEqual([CommandKeyword.RETENTION, 1000]); 36 | }); 37 | 38 | test("add no retention", () => { 39 | expect(builder.addRetention().get().length).toEqual(0); 40 | }); 41 | 42 | test("add negative retention fails", () => { 43 | expect(() => { 44 | builder.addRetention(-100).get(); 45 | }).toThrow(/retention must be positive integer/); 46 | }); 47 | 48 | test("add labels", () => { 49 | const labels = [new Label("label1", 100), new Label("label2", 200)]; 50 | expect(builder.addLabels(labels).get()).toEqual([CommandKeyword.LABELS, "label1", 100, "label2", 200]); 51 | }); 52 | 53 | test("add empty labels", () => { 54 | expect(builder.addLabels([]).get().length).toEqual(0); 55 | }); 56 | 57 | test("add no labels", () => { 58 | expect(builder.addLabels().get().length).toEqual(0); 59 | }); 60 | 61 | test("remove labels", () => { 62 | expect(builder.removeLabels().get()).toEqual([CommandKeyword.LABELS]); 63 | }); 64 | 65 | test("add samples", () => { 66 | const date = Date.now(); 67 | const samples = [new Sample("sample1", 100, date), new Sample("sample2", 200)]; 68 | expect(builder.addSamples(samples).get()).toEqual(["sample1", date, 100, "sample2", "*", 200]); 69 | }); 70 | 71 | test("add empty samples", () => { 72 | expect(builder.addSamples([]).get().length).toEqual(0); 73 | }); 74 | 75 | test("add sample", () => { 76 | const date = Date.now(); 77 | expect(builder.addSample(new Sample("sample1", 100, date)).get()).toEqual(["sample1", date, 100]); 78 | }); 79 | 80 | test("add sample with optional timestamp", () => { 81 | const date = Date.now(); 82 | expect(builder.addSampleWithOptionalTimeStamp(new Sample("sample1", 100, date)).get()).toEqual([ 83 | "sample1", 84 | 100, 85 | CommandKeyword.TIMESTAMP, 86 | date 87 | ]); 88 | }); 89 | 90 | test("add count", () => { 91 | expect(builder.addCount(7).get()).toEqual([CommandKeyword.COUNT, 7]); 92 | }); 93 | 94 | test("add no count", () => { 95 | expect(builder.addCount().get().length).toEqual(0); 96 | }); 97 | 98 | test("add aggregation", () => { 99 | const date = Date.now(); 100 | expect(builder.addAggregation(new Aggregation(AggregationType.MAX, date)).get()).toEqual([ 101 | CommandKeyword.AGGREGATION, 102 | AggregationType.MAX, 103 | date 104 | ]); 105 | }); 106 | 107 | test("add no aggregation", () => { 108 | expect(builder.addAggregation().get().length).toEqual(0); 109 | }); 110 | 111 | test("add full range", () => { 112 | const date = Date.now(); 113 | expect(builder.addRange(new TimestampRange(date - 10000, date)).get()).toEqual([date - 10000, date]); 114 | }); 115 | 116 | test("add default left range", () => { 117 | const date = Date.now(); 118 | expect(builder.addRange(new TimestampRange(undefined, date)).get()).toEqual([CommandKeyword.MIN_TIMESTAMP, date]); 119 | }); 120 | 121 | test("add default right range", () => { 122 | const date = Date.now(); 123 | expect(builder.addRange(new TimestampRange(date - 10000)).get()).toEqual([ 124 | date - 10000, 125 | CommandKeyword.MAX_TIMESTAMP 126 | ]); 127 | }); 128 | 129 | test("add full default range", () => { 130 | expect(builder.addRange(new TimestampRange()).get()).toEqual([ 131 | CommandKeyword.MIN_TIMESTAMP, 132 | CommandKeyword.MAX_TIMESTAMP 133 | ]); 134 | }); 135 | 136 | test("add no range", () => { 137 | expect(builder.addRange().get().length).toEqual(0); 138 | }); 139 | 140 | test("add filters", () => { 141 | const filters = new FilterBuilder("filter1", 100); 142 | expect(builder.addFilters(filters).get()).toEqual(["filter1=100"]); 143 | }); 144 | 145 | test("add filters with keyword", () => { 146 | const filters = new FilterBuilder("filter2", 200); 147 | expect(builder.addFiltersWithKeyword(filters).get()).toEqual([CommandKeyword.FILTER, "filter2=200"]); 148 | }); 149 | 150 | test("reset", () => { 151 | expect(builder.reset().get().length).toEqual(0); 152 | }); 153 | -------------------------------------------------------------------------------- /src/__tests__/unit/builder/requestParamsDirector.test.ts: -------------------------------------------------------------------------------- 1 | import { RequestParamsDirector } from "../../../builder/requestParamsDirector"; 2 | import { Label } from "../../../entity/label"; 3 | import { AggregationType } from "../../../enum/aggregationType"; 4 | import { CommandKeyword } from "../../../enum/commandKeyword"; 5 | import { Sample } from "../../../entity/sample"; 6 | import { Aggregation } from "../../../entity/aggregation"; 7 | import { TimestampRange } from "../../../entity/timestampRange"; 8 | import { FilterBuilder } from "../../../builder/filterBuilder"; 9 | import { RequestParamsBuilder } from "../../../builder/requestParamsBuilder"; 10 | 11 | let director: RequestParamsDirector; 12 | let label1: Label; 13 | let label2: Label; 14 | let sample1: Sample; 15 | let sample2: Sample; 16 | let date: number; 17 | let aggregation: Aggregation; 18 | let timestampRange: TimestampRange; 19 | let filterBuilder: FilterBuilder; 20 | 21 | beforeAll(() => { 22 | date = Date.now(); 23 | label1 = new Label("label1", 100); 24 | label2 = new Label("label2", 200); 25 | sample1 = new Sample("sample1", 10, date - 10000); 26 | sample2 = new Sample("sample2", 20, date - 20000); 27 | aggregation = new Aggregation(AggregationType.MAX, date); 28 | timestampRange = new TimestampRange(date - 20000, date - 10000); 29 | filterBuilder = new FilterBuilder("filter", 5); 30 | }); 31 | 32 | beforeEach(() => { 33 | director = new RequestParamsDirector(new RequestParamsBuilder()); 34 | }); 35 | 36 | test("create with labels and retention", () => { 37 | expect(director.create("key", [label1, label2], 10000).get()).toEqual([ 38 | "key", 39 | CommandKeyword.RETENTION, 40 | 10000, 41 | CommandKeyword.LABELS, 42 | label1.getName(), 43 | label1.getValue(), 44 | label2.getName(), 45 | label2.getValue() 46 | ]); 47 | }); 48 | 49 | test("create without retention", () => { 50 | expect(director.create("key", [label1, label2]).get()).toEqual([ 51 | "key", 52 | CommandKeyword.LABELS, 53 | label1.getName(), 54 | label1.getValue(), 55 | label2.getName(), 56 | label2.getValue() 57 | ]); 58 | }); 59 | 60 | test("create without labels", () => { 61 | expect(director.create("key", undefined, 10000).get()).toEqual(["key", CommandKeyword.RETENTION, 10000]); 62 | }); 63 | 64 | test("create with empty labels", () => { 65 | expect(director.create("key", [], 10000).get()).toEqual(["key", CommandKeyword.RETENTION, 10000]); 66 | }); 67 | 68 | test("create default", () => { 69 | expect(director.create("key").get()).toEqual(["key"]); 70 | }); 71 | 72 | test("alter with labels and retention", () => { 73 | expect(director.alter("key", [label1, label2], 10000).get()).toEqual([ 74 | "key", 75 | CommandKeyword.RETENTION, 76 | 10000, 77 | CommandKeyword.LABELS, 78 | label1.getName(), 79 | label1.getValue(), 80 | label2.getName(), 81 | label2.getValue() 82 | ]); 83 | }); 84 | 85 | test("alter without retention", () => { 86 | expect(director.alter("key", [label1, label2]).get()).toEqual([ 87 | "key", 88 | CommandKeyword.LABELS, 89 | label1.getName(), 90 | label1.getValue(), 91 | label2.getName(), 92 | label2.getValue() 93 | ]); 94 | }); 95 | 96 | test("alter without labels", () => { 97 | expect(director.alter("key", undefined, 10000).get()).toEqual(["key", CommandKeyword.RETENTION, 10000]); 98 | }); 99 | 100 | test("alter with empty labels", () => { 101 | expect(director.alter("key", [], 10000).get()).toEqual([ 102 | "key", 103 | CommandKeyword.RETENTION, 104 | 10000, 105 | CommandKeyword.LABELS 106 | ]); 107 | }); 108 | 109 | test("alter default", () => { 110 | expect(director.alter("key").get()).toEqual(["key"]); 111 | }); 112 | 113 | test("add with labels and retention", () => { 114 | expect(director.add(sample1, [label1, label2], 10000).get()).toEqual([ 115 | "sample1", 116 | date - 10000, 117 | 10, 118 | CommandKeyword.RETENTION, 119 | 10000, 120 | CommandKeyword.LABELS, 121 | label1.getName(), 122 | label1.getValue(), 123 | label2.getName(), 124 | label2.getValue() 125 | ]); 126 | }); 127 | 128 | test("add without retention", () => { 129 | expect(director.add(sample1, [label1, label2]).get()).toEqual([ 130 | "sample1", 131 | date - 10000, 132 | 10, 133 | CommandKeyword.LABELS, 134 | label1.getName(), 135 | label1.getValue(), 136 | label2.getName(), 137 | label2.getValue() 138 | ]); 139 | }); 140 | 141 | test("add without labels", () => { 142 | expect(director.add(sample1, undefined, 10000).get()).toEqual([ 143 | "sample1", 144 | date - 10000, 145 | 10, 146 | CommandKeyword.RETENTION, 147 | 10000 148 | ]); 149 | }); 150 | 151 | test("add with empty labels", () => { 152 | expect(director.add(sample1, [], 10000).get()).toEqual([ 153 | "sample1", 154 | date - 10000, 155 | 10, 156 | CommandKeyword.RETENTION, 157 | 10000 158 | ]); 159 | }); 160 | 161 | test("create default", () => { 162 | expect(director.add(sample1).get()).toEqual(["sample1", date - 10000, 10]); 163 | }); 164 | 165 | test("multiadd", () => { 166 | expect(director.multiAdd([sample1, sample2]).get()).toEqual([ 167 | "sample1", 168 | date - 10000, 169 | 10, 170 | "sample2", 171 | date - 20000, 172 | 20 173 | ]); 174 | }); 175 | 176 | test("multiadd with empty labels", () => { 177 | expect(director.multiAdd([]).get().length).toEqual(0); 178 | }); 179 | 180 | test("change by with labels and retention", () => { 181 | expect(director.changeBy(sample1, [label1, label2], 10000).get()).toEqual([ 182 | "sample1", 183 | 10, 184 | CommandKeyword.TIMESTAMP, 185 | date - 10000, 186 | CommandKeyword.RETENTION, 187 | 10000, 188 | CommandKeyword.LABELS, 189 | label1.getName(), 190 | label1.getValue(), 191 | label2.getName(), 192 | label2.getValue() 193 | ]); 194 | }); 195 | 196 | test("change by without retention", () => { 197 | expect(director.changeBy(sample1, [label1, label2]).get()).toEqual([ 198 | "sample1", 199 | 10, 200 | CommandKeyword.TIMESTAMP, 201 | date - 10000, 202 | CommandKeyword.LABELS, 203 | label1.getName(), 204 | label1.getValue(), 205 | label2.getName(), 206 | label2.getValue() 207 | ]); 208 | }); 209 | 210 | test("change by without labels", () => { 211 | expect(director.changeBy(sample1, undefined, 10000).get()).toEqual([ 212 | "sample1", 213 | 10, 214 | CommandKeyword.TIMESTAMP, 215 | date - 10000, 216 | CommandKeyword.RETENTION, 217 | 10000 218 | ]); 219 | }); 220 | 221 | test("change by with empty labels", () => { 222 | expect(director.changeBy(sample1, [], 10000).get()).toEqual([ 223 | "sample1", 224 | 10, 225 | CommandKeyword.TIMESTAMP, 226 | date - 10000, 227 | CommandKeyword.RETENTION, 228 | 10000 229 | ]); 230 | }); 231 | 232 | test("change by default", () => { 233 | expect(director.changeBy(sample1).get()).toEqual(["sample1", 10, CommandKeyword.TIMESTAMP, date - 10000]); 234 | }); 235 | 236 | test("create rule", () => { 237 | expect(director.createRule("source", "destination", aggregation).get()).toEqual([ 238 | "source", 239 | "destination", 240 | CommandKeyword.AGGREGATION, 241 | AggregationType.MAX, 242 | date 243 | ]); 244 | }); 245 | 246 | test("create rule with same source and destination fails", () => { 247 | expect(() => { 248 | director.createRule("key", "key", aggregation); 249 | }).toThrow(/source and destination key cannot be equals/); 250 | }); 251 | 252 | test("delete rule", () => { 253 | expect(director.deleteRule("source", "destination").get()).toEqual(["source", "destination"]); 254 | }); 255 | 256 | test("delete rule with same source and destination fails", () => { 257 | expect(() => { 258 | director.deleteRule("key", "key"); 259 | }).toThrow(/source and destination key cannot be equals/); 260 | }); 261 | 262 | test("range with count and aggregation", () => { 263 | expect(director.range("key", timestampRange, 3, aggregation).get()).toEqual([ 264 | "key", 265 | date - 20000, 266 | date - 10000, 267 | CommandKeyword.COUNT, 268 | 3, 269 | CommandKeyword.AGGREGATION, 270 | AggregationType.MAX, 271 | date 272 | ]); 273 | }); 274 | 275 | test("range without aggregation", () => { 276 | expect(director.range("key", timestampRange, 3).get()).toEqual([ 277 | "key", 278 | date - 20000, 279 | date - 10000, 280 | CommandKeyword.COUNT, 281 | 3 282 | ]); 283 | }); 284 | 285 | test("range without count", () => { 286 | expect(director.range("key", timestampRange, undefined, aggregation).get()).toEqual([ 287 | "key", 288 | date - 20000, 289 | date - 10000, 290 | CommandKeyword.AGGREGATION, 291 | AggregationType.MAX, 292 | date 293 | ]); 294 | }); 295 | 296 | test("range default", () => { 297 | expect(director.range("key", timestampRange).get()).toEqual(["key", date - 20000, date - 10000]); 298 | }); 299 | 300 | test("multi range with count and aggregation", () => { 301 | expect(director.multiRange(timestampRange, filterBuilder, 3, aggregation).get()).toEqual([ 302 | date - 20000, 303 | date - 10000, 304 | CommandKeyword.COUNT, 305 | 3, 306 | CommandKeyword.AGGREGATION, 307 | AggregationType.MAX, 308 | date, 309 | CommandKeyword.FILTER, 310 | "filter=5" 311 | ]); 312 | }); 313 | 314 | test("multi range without aggregation", () => { 315 | expect(director.multiRange(timestampRange, filterBuilder, 3).get()).toEqual([ 316 | date - 20000, 317 | date - 10000, 318 | CommandKeyword.COUNT, 319 | 3, 320 | CommandKeyword.FILTER, 321 | "filter=5" 322 | ]); 323 | }); 324 | 325 | test("multi range without count", () => { 326 | expect(director.multiRange(timestampRange, filterBuilder, undefined, aggregation).get()).toEqual([ 327 | date - 20000, 328 | date - 10000, 329 | CommandKeyword.AGGREGATION, 330 | AggregationType.MAX, 331 | date, 332 | CommandKeyword.FILTER, 333 | "filter=5" 334 | ]); 335 | }); 336 | 337 | test("multi range default", () => { 338 | expect(director.multiRange(timestampRange, filterBuilder).get()).toEqual([ 339 | date - 20000, 340 | date - 10000, 341 | CommandKeyword.FILTER, 342 | "filter=5" 343 | ]); 344 | }); 345 | 346 | test("get key", () => { 347 | expect(director.getKey("key").get()).toEqual(["key"]); 348 | }); 349 | 350 | test("multi get", () => { 351 | expect(director.multiGet(filterBuilder).get()).toEqual([CommandKeyword.FILTER, "filter=5"]); 352 | }); 353 | 354 | test("query index", () => { 355 | expect(director.queryIndex(filterBuilder).get()).toEqual(["filter=5"]); 356 | }); 357 | -------------------------------------------------------------------------------- /src/__tests__/unit/entity/aggregation.test.ts: -------------------------------------------------------------------------------- 1 | import { AggregationType } from "../../../enum/aggregationType"; 2 | import { CommandKeyword } from "../../../enum/commandKeyword"; 3 | import { Aggregation } from "../../../entity/aggregation"; 4 | 5 | test("create valid aggregation with integer time bucket", () => { 6 | const aggregation = new Aggregation(AggregationType.AVG, 5000); 7 | expect(aggregation.getType()).toBe(AggregationType.AVG); 8 | expect(aggregation.getTimeBucketInMs()).toBe(5000); 9 | 10 | const flattenedAggregation = aggregation.flatten(); 11 | expect(flattenedAggregation).toContain(CommandKeyword.AGGREGATION); 12 | expect(flattenedAggregation).toContain(AggregationType.AVG); 13 | expect(flattenedAggregation).toContain(5000); 14 | }); 15 | 16 | test("create valid aggregation with float time bucket", () => { 17 | const aggregation = new Aggregation(AggregationType.AVG, 5000.5556); 18 | expect(aggregation.getTimeBucketInMs()).toBe(5000); 19 | 20 | const flattenedAggregation = aggregation.flatten(); 21 | expect(flattenedAggregation).toContain(5000); 22 | }); 23 | 24 | test("create aggregation with invalid time bucket", () => { 25 | expect(() => { 26 | new Aggregation(AggregationType.AVG, -3500.5625); 27 | }).toThrow("invalid timestamp '-3500.5625'"); 28 | }); 29 | 30 | test("create aggregation with invalid type", () => { 31 | expect(() => { 32 | new Aggregation("invalid", 15000); 33 | }).toThrow("aggregation type 'invalid' not found"); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/unit/entity/count.test.ts: -------------------------------------------------------------------------------- 1 | import { Count } from "../../../entity/count"; 2 | import { CommandKeyword } from "../../../enum/commandKeyword"; 3 | 4 | test("count creation with valid integer number", () => { 5 | const count = new Count(5); 6 | const flatten = count.flatten(); 7 | expect(flatten).toContain(CommandKeyword.COUNT); 8 | expect(flatten).toContain(5); 9 | expect(flatten.length).toBe(2); 10 | }); 11 | 12 | test("count creation with valid float number", () => { 13 | const count = new Count(1.56); 14 | const flatten = count.flatten(); 15 | expect(flatten).toContain(CommandKeyword.COUNT); 16 | expect(flatten).toContain(1); 17 | expect(flatten.length).toBe(2); 18 | }); 19 | 20 | test("count creation throws error with integer number", () => { 21 | expect(() => { 22 | new Count(-1); 23 | }).toThrow("count must be positive, provided -1"); 24 | }); 25 | 26 | test("count creation throws error with float number", () => { 27 | expect(() => { 28 | new Count(-1.17); 29 | }).toThrow("count must be positive, provided -1"); 30 | }); 31 | -------------------------------------------------------------------------------- /src/__tests__/unit/entity/filter.test.ts: -------------------------------------------------------------------------------- 1 | import { FilterOperator } from "../../../enum/filterOperator"; 2 | import { Filter } from "../../../entity/filter"; 3 | 4 | test("simple numeric filter creation", () => { 5 | const filter = new Filter("label", FilterOperator.EQUAL, 28); 6 | expect(filter.flatten()).toEqual([`label${FilterOperator.EQUAL}${28}`]); 7 | }); 8 | 9 | test("simple string filter creation", () => { 10 | const filter = new Filter("label", FilterOperator.EQUAL, "28"); 11 | expect(filter.flatten()).toEqual([`label${FilterOperator.EQUAL}${28}`]); 12 | }); 13 | 14 | test("array filter creation", () => { 15 | const filter = new Filter("label", FilterOperator.NOT_EQUAL, ["28", 36, "limit"]); 16 | expect(filter.flatten()).toEqual([`label${FilterOperator.NOT_EQUAL}(28,36,limit)`]); 17 | }); 18 | 19 | test("exist filter creation", () => { 20 | const filter = new Filter("label", FilterOperator.NOT_EQUAL); 21 | expect(filter.flatten()).toEqual([`label${FilterOperator.NOT_EQUAL}`]); 22 | }); 23 | 24 | test("filter creation with wrong operator", () => { 25 | expect(() => { 26 | new Filter("label", ">"); 27 | }).toThrow(/not allowed operator/); 28 | }); 29 | -------------------------------------------------------------------------------- /src/__tests__/unit/entity/sample.test.ts: -------------------------------------------------------------------------------- 1 | import { Sample } from "../../../entity/sample"; 2 | import { CommandKeyword } from "../../../enum/commandKeyword"; 3 | 4 | test("sample creation with integer value and without timestamp", () => { 5 | const sample = new Sample("sample1", 34); 6 | expect(sample.getKey()).toEqual("sample1"); 7 | expect(sample.getValue()).toEqual(34); 8 | expect(sample.getTimestamp()).toEqual(CommandKeyword.CURRENT_TIMESTAMP); 9 | expect(sample.flatten()).toEqual(["sample1", CommandKeyword.CURRENT_TIMESTAMP, 34]); 10 | }); 11 | 12 | test("sample creation with float value and without timestamp", () => { 13 | const sample = new Sample("sample2", 34.76); 14 | expect(sample.getKey()).toEqual("sample2"); 15 | expect(sample.getValue()).toEqual(34.76); 16 | expect(sample.getTimestamp()).toEqual(CommandKeyword.CURRENT_TIMESTAMP); 17 | expect(sample.flatten()).toEqual(["sample2", CommandKeyword.CURRENT_TIMESTAMP, 34.76]); 18 | }); 19 | 20 | test("sample creation with valid timestamp", () => { 21 | const date = new Date(2019, 12, 28, 18).getTime(); 22 | const sample = new Sample("sample3", 34, date); 23 | expect(sample.getKey()).toEqual("sample3"); 24 | expect(sample.getValue()).toEqual(34); 25 | expect(sample.getTimestamp()).toEqual(date); 26 | expect(sample.flatten()).toEqual(["sample3", date, 34]); 27 | }); 28 | 29 | test("sample creation with float timestamp truncate it", () => { 30 | const date = new Date(2019, 12, 28, 18).getTime() + 0.45; 31 | const sample = new Sample("sample4", 34, date); 32 | expect(sample.getTimestamp()).toEqual(Math.trunc(date)); 33 | }); 34 | 35 | test("sample creation with invalid timestamp fails", () => { 36 | expect(() => { 37 | new Sample("sample4", 34, -1); 38 | }).toThrow(/wrong timestamp/); 39 | }); 40 | -------------------------------------------------------------------------------- /src/__tests__/unit/entity/timestampRange.test.ts: -------------------------------------------------------------------------------- 1 | import { CommandKeyword } from "../../../enum/commandKeyword"; 2 | import { TimestampRange } from "../../../entity/timestampRange"; 3 | 4 | test("timestamp range creation with default values", () => { 5 | const tsRange = new TimestampRange(); 6 | expect(tsRange.getFrom()).toEqual(CommandKeyword.MIN_TIMESTAMP); 7 | expect(tsRange.getTo()).toEqual(CommandKeyword.MAX_TIMESTAMP); 8 | expect(tsRange.flatten()).toEqual([CommandKeyword.MIN_TIMESTAMP, CommandKeyword.MAX_TIMESTAMP]); 9 | }); 10 | 11 | test("timestamp range creation with provided values", () => { 12 | const start = new Date(2019, 11, 29, 11).getTime(); 13 | const end = start + 360000; 14 | const tsRange = new TimestampRange(start, end); 15 | expect(tsRange.getFrom()).toEqual(start); 16 | expect(tsRange.getTo()).toEqual(end); 17 | expect(tsRange.flatten()).toEqual([start, end]); 18 | }); 19 | 20 | test("timestamp range creation with default from value", () => { 21 | const end = new Date(2019, 11, 29, 11).getTime() + 360000; 22 | const tsRange = new TimestampRange(undefined, end); 23 | expect(tsRange.getFrom()).toEqual(CommandKeyword.MIN_TIMESTAMP); 24 | expect(tsRange.getTo()).toEqual(end); 25 | expect(tsRange.flatten()).toEqual([CommandKeyword.MIN_TIMESTAMP, end]); 26 | }); 27 | 28 | test("timestamp range creation with default to value", () => { 29 | const start = new Date(2019, 11, 29, 11).getTime(); 30 | const tsRange = new TimestampRange(start); 31 | expect(tsRange.getFrom()).toEqual(start); 32 | expect(tsRange.getTo()).toEqual(CommandKeyword.MAX_TIMESTAMP); 33 | expect(tsRange.flatten()).toEqual([start, CommandKeyword.MAX_TIMESTAMP]); 34 | }); 35 | 36 | test("timestamp range creation with invalid from value", () => { 37 | expect(() => { 38 | new TimestampRange(-1, Date.now()); 39 | }).toThrow(/invalid timestamp/); 40 | }); 41 | 42 | test("timestamp range creation with invalid to value", () => { 43 | expect(() => { 44 | new TimestampRange(Date.now(), -1); 45 | }).toThrow(/invalid timestamp/); 46 | }); 47 | -------------------------------------------------------------------------------- /src/__tests__/unit/factory/redisTimeSeries.test.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "../../../factory/redisTimeSeries"; 2 | import { CommandProvider } from "../../../command/commandProvider"; 3 | import { CommandReceiver } from "../../../command/commandReceiver"; 4 | import { RequestParamsDirector } from "../../../builder/requestParamsDirector"; 5 | import { RedisTimeSeries } from "../../../redisTimeSeries"; 6 | import * as Redis from "ioredis"; 7 | import { RequestParamsBuilder } from "../../../builder/requestParamsBuilder"; 8 | import { ConnectionOptions } from "../../../index"; 9 | 10 | jest.mock("../../../command/commandProvider"); 11 | jest.mock("../../../command/commandReceiver"); 12 | jest.mock("../../../builder/requestParamsDirector"); 13 | jest.mock("ioredis"); 14 | 15 | const options: ConnectionOptions = { 16 | port: 6379, 17 | host: "127.0.0.1", 18 | db: 0 19 | }; 20 | 21 | it("Factory creates a RedisTimeSeries object", () => { 22 | const factory = new RedisTimeSeriesFactory(); 23 | const redisTimeSeries = factory.create(); 24 | 25 | expect(CommandProvider).toHaveBeenCalledTimes(1); 26 | expect(Redis).toHaveBeenCalledTimes(1); 27 | expect(Redis).toHaveBeenCalledWith(options); 28 | expect(CommandReceiver).toHaveBeenCalledTimes(1); 29 | expect(RequestParamsDirector).toHaveBeenCalledTimes(1); 30 | expect(RequestParamsDirector).toHaveBeenCalledWith(new RequestParamsBuilder()); 31 | expect(redisTimeSeries).toBeInstanceOf(RedisTimeSeries); 32 | }); 33 | -------------------------------------------------------------------------------- /src/__tests__/unit/iterator/list.test.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../../../entity/label"; 2 | import { List } from "../../../iterator/list"; 3 | import { Sample } from "../../../entity/sample"; 4 | import { Filter } from "../../../entity/filter"; 5 | import { FilterOperator } from "../../../enum/filterOperator"; 6 | 7 | test("label list", () => { 8 | const labelList: Label[] = []; 9 | for (let i = 1; i < 4; i++) { 10 | labelList.push(new Label("label" + i, i)); 11 | } 12 | const flattenedLabelList = new List(labelList).flatten(); 13 | for (let i = 1; i < 4; i++) { 14 | expect(flattenedLabelList.shift()).toEqual("label" + i); 15 | expect(flattenedLabelList.shift()).toEqual(i); 16 | } 17 | expect(flattenedLabelList.length).toBe(0); 18 | }); 19 | 20 | test("sample list", () => { 21 | const sampleList: Sample[] = []; 22 | const date = new Date(2019, 11, 30, 19).getTime(); 23 | for (let i = 1; i < 4; i++) { 24 | sampleList.push(new Sample("sample" + i, i, date + i)); 25 | } 26 | const flattenedSampleList = new List(sampleList).flatten(); 27 | for (let i = 1; i < 4; i++) { 28 | expect(flattenedSampleList.shift()).toEqual("sample" + i); 29 | expect(flattenedSampleList.shift()).toEqual(date + i); 30 | expect(flattenedSampleList.shift()).toEqual(i); 31 | } 32 | expect(flattenedSampleList.length).toBe(0); 33 | }); 34 | 35 | test("filter list", () => { 36 | const filterList: Filter[] = []; 37 | for (let i = 1; i < 4; i++) { 38 | filterList.push(new Filter("filter" + i, FilterOperator.EQUAL, i)); 39 | } 40 | const flattenedFilterList = new List(filterList).flatten(); 41 | for (let i = 1; i < 4; i++) { 42 | expect(flattenedFilterList.shift()).toEqual(`filter${i}${FilterOperator.EQUAL}${i}`); 43 | } 44 | expect(flattenedFilterList.length).toBe(0); 45 | }); 46 | 47 | test("combined list", () => { 48 | const label = new Label("label", 30); 49 | const sample = new Sample("sample", 50, 1000); 50 | const filter = new Filter("filter", FilterOperator.NOT_EQUAL, [1, 2]); 51 | const list: (Label | Sample | Filter)[] = [label, sample, filter]; 52 | const flattenedList = new List(list).flatten(); 53 | 54 | expect(flattenedList.shift()).toEqual("label"); 55 | expect(flattenedList.shift()).toEqual(30); 56 | expect(flattenedList.shift()).toEqual("sample"); 57 | expect(flattenedList.shift()).toEqual(1000); 58 | expect(flattenedList.shift()).toEqual(50); 59 | expect(flattenedList.shift()).toEqual(`filter${FilterOperator.NOT_EQUAL}(1,2)`); 60 | expect(flattenedList.length).toBe(0); 61 | }); 62 | -------------------------------------------------------------------------------- /src/__tests__/unit/response/response.test.ts: -------------------------------------------------------------------------------- 1 | import { Sample } from "../../../entity/sample"; 2 | import { RenderFactory } from "../../../factory/render"; 3 | import { Label } from "../../../entity/label"; 4 | import { AggregationType } from "../../../enum/aggregationType"; 5 | import { Aggregation } from "../../../entity/aggregation"; 6 | import { MultiRangeResponse } from "../../../response/interface/multiRangeResponse"; 7 | import { MultiGetResponse } from "../../../response/interface/multiGetResponse"; 8 | import { InfoResponse } from "../../../response/interface/infoResponse"; 9 | 10 | let renderFactory: RenderFactory; 11 | const date = Date.now(); 12 | 13 | beforeAll(() => { 14 | renderFactory = new RenderFactory(); 15 | }); 16 | 17 | test("multi range response render", () => { 18 | const responseRender = renderFactory.getMultiRangeRender(); 19 | const response = [ 20 | [ 21 | "key", 22 | [ 23 | ["label1", 10], 24 | ["label2", 20] 25 | ], 26 | [ 27 | [date - 10000, "10"], 28 | [date - 20000, 20], 29 | [date - 30000, "-nan"] 30 | ] 31 | ] 32 | ]; 33 | const rendered: MultiRangeResponse = responseRender.render(response).shift(); 34 | expect(rendered.key).toBe("key"); 35 | 36 | const labels = rendered.labels; 37 | expect(labels.shift()).toEqual(new Label("label1", 10)); 38 | expect(labels.shift()).toEqual(new Label("label2", 20)); 39 | 40 | const samples = rendered.data; 41 | expect(samples.shift()).toEqual(new Sample("key", 10, date - 10000)); 42 | expect(samples.shift()).toEqual(new Sample("key", 20, date - 20000)); 43 | expect(samples.shift()).toEqual(new Sample("key", 0, date - 30000)); 44 | }); 45 | 46 | test("multi get response render", () => { 47 | const responseRender = renderFactory.getMultiGetRender(); 48 | const response = [ 49 | [ 50 | "key", 51 | [ 52 | ["label1", 10], 53 | ["label2", 20] 54 | ], 55 | [date - 10000, "10"] 56 | ] 57 | ]; 58 | const rendered: MultiGetResponse = responseRender.render(response).shift(); 59 | expect(rendered.key).toBe("key"); 60 | 61 | const labels = rendered.labels; 62 | expect(labels.shift()).toEqual(new Label("label1", 10)); 63 | expect(labels.shift()).toEqual(new Label("label2", 20)); 64 | 65 | const sample = rendered.data; 66 | expect(sample).toEqual(new Sample("key", 10, date - 10000)); 67 | }); 68 | 69 | test("info response render without source key", () => { 70 | const responseRender = renderFactory.getInfoRender(); 71 | const response = [ 72 | "totalSamples", 73 | 10, 74 | "memoryUsage", 75 | 4209, 76 | "firstTimestamp", 77 | date - 10000, 78 | "lastTimestamp", 79 | date, 80 | "retentionTime", 81 | 80000, 82 | "chunkCount", 83 | 1, 84 | "chunkSize", 85 | 360, 86 | "chunkType", 87 | "compressed", 88 | "duplicatePolicy", 89 | "LAST", 90 | "labels", 91 | [ 92 | ["label1", 10], 93 | ["label2", 20] 94 | ], 95 | "sourceKey", 96 | undefined, 97 | "rules", 98 | [ 99 | ["rule1", 45000, AggregationType.MAX], 100 | ["rule2", 75000, AggregationType.COUNT] 101 | ] 102 | ]; 103 | const rendered: InfoResponse = responseRender.render(response); 104 | expect(rendered.totalSamples).toBe(10); 105 | expect(rendered.memoryUsage).toBe(4209); 106 | expect(rendered.firstTimestamp).toBe(date - 10000); 107 | expect(rendered.lastTimestamp).toBe(date); 108 | expect(rendered.retentionTime).toBe(80000); 109 | expect(rendered.chunkCount).toBe(1); 110 | expect(rendered.chunkSize).toBe(360); 111 | expect(rendered.chunkType).toBe("compressed"); 112 | expect(rendered.duplicatePolicy).toBe("LAST"); 113 | 114 | const labels = rendered.labels; 115 | expect(labels.shift()).toEqual(new Label("label1", 10)); 116 | expect(labels.shift()).toEqual(new Label("label2", 20)); 117 | 118 | const rules = rendered.rules; 119 | expect(rules.rule1).toEqual(new Aggregation(AggregationType.MAX, 45000)); 120 | expect(rules.rule2).toEqual(new Aggregation(AggregationType.COUNT, 75000)); 121 | }); 122 | 123 | test("info response render with source key", () => { 124 | const responseRender = renderFactory.getInfoRender(); 125 | const response = [ 126 | "totalSamples", 127 | 10, 128 | "memoryUsage", 129 | 4209, 130 | "firstTimestamp", 131 | date - 10000, 132 | "lastTimestamp", 133 | date, 134 | "retentionTime", 135 | 80000, 136 | "chunkCount", 137 | 1, 138 | "chunkSize", 139 | 360, 140 | "chunkType", 141 | "compressed", 142 | "duplicatePolicy", 143 | "LAST", 144 | "labels", 145 | [ 146 | ["label1", 10], 147 | ["label2", 20] 148 | ], 149 | "sourceKey", 150 | "key", 151 | "rules", 152 | [ 153 | ["rule1", 45000, AggregationType.MAX], 154 | ["rule2", 75000, AggregationType.COUNT] 155 | ] 156 | ]; 157 | const rendered: InfoResponse = responseRender.render(response); 158 | expect(rendered.sourceKey).toBe("key"); 159 | expect(rendered.totalSamples).toBe(10); 160 | expect(rendered.memoryUsage).toBe(4209); 161 | expect(rendered.firstTimestamp).toBe(date - 10000); 162 | expect(rendered.lastTimestamp).toBe(date); 163 | expect(rendered.retentionTime).toBe(80000); 164 | expect(rendered.chunkCount).toBe(1); 165 | expect(rendered.chunkSize).toBe(360); 166 | expect(rendered.chunkType).toBe("compressed"); 167 | expect(rendered.duplicatePolicy).toBe("LAST"); 168 | 169 | const labels = rendered.labels; 170 | expect(labels.shift()).toEqual(new Label("label1", 10)); 171 | expect(labels.shift()).toEqual(new Label("label2", 20)); 172 | 173 | const rules = rendered.rules; 174 | expect(rules.rule1).toEqual(new Aggregation(AggregationType.MAX, 45000)); 175 | expect(rules.rule2).toEqual(new Aggregation(AggregationType.COUNT, 75000)); 176 | }); 177 | -------------------------------------------------------------------------------- /src/__tests_config__/data.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from "../index"; 2 | 3 | export const testOptions: ConnectionOptions = { 4 | host: "redislabs-redistimeseries", 5 | db: 15 6 | }; 7 | -------------------------------------------------------------------------------- /src/builder/filterBuilder.ts: -------------------------------------------------------------------------------- 1 | import { FilterOperator } from "../enum/filterOperator"; 2 | import { Filter } from "../entity/filter"; 3 | import { StringNumberArray } from "../index"; 4 | 5 | /** 6 | * A Filter object for use in queries 7 | */ 8 | export class FilterBuilder { 9 | private readonly filters: Filter[]; 10 | 11 | /** 12 | * The label and value in the constructor create a first filter where label=value. 13 | * More filters can be created by calling the different methods in FilterBuilder 14 | * 15 | * @param label the label of the first filter 16 | * @param value the value represented by the label 17 | * 18 | * @remarks 19 | * ``` 20 | * // Example 21 | * const aggregation = new Aggregation(AggregationType.MAX, 5000); 22 | * const timestampRange = new TimestampRange(date, date + 10000); 23 | * const filter = new FilterBuilder("label", 1).equal("sensor", 1); 24 | * const multiRanges = await redisTimeSeries.multiRange(timestampRange, filter, undefined, aggregation, true); 25 | * ``` 26 | */ 27 | constructor(label: string, value: string | number) { 28 | this.filters = []; 29 | this.equal(label, value); 30 | } 31 | 32 | public equal(label: string, value: string | number): FilterBuilder { 33 | this.filters.push(new Filter(label, FilterOperator.EQUAL, value)); 34 | return this; 35 | } 36 | 37 | public notEqual(label: string, value: string | number): FilterBuilder { 38 | this.filters.push(new Filter(label, FilterOperator.NOT_EQUAL, value)); 39 | return this; 40 | } 41 | 42 | public notExists(label: string): FilterBuilder { 43 | this.filters.push(new Filter(label, FilterOperator.EQUAL)); 44 | return this; 45 | } 46 | 47 | public exists(label: string): FilterBuilder { 48 | this.filters.push(new Filter(label, FilterOperator.NOT_EQUAL)); 49 | return this; 50 | } 51 | 52 | public in(label: string, value: StringNumberArray): FilterBuilder { 53 | this.filters.push(new Filter(label, FilterOperator.EQUAL, value)); 54 | return this; 55 | } 56 | 57 | public notIn(label: string, value: StringNumberArray): FilterBuilder { 58 | this.filters.push(new Filter(label, FilterOperator.NOT_EQUAL, value)); 59 | return this; 60 | } 61 | 62 | public get(): Filter[] { 63 | return this.filters; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/builder/requestParamsBuilder.ts: -------------------------------------------------------------------------------- 1 | import { CommandKeyword } from "../enum/commandKeyword"; 2 | import { List } from "../iterator/list"; 3 | import { Count } from "../entity/count"; 4 | import { TimestampRange } from "../entity/timestampRange"; 5 | import { StringNumberArray } from "../index"; 6 | import { Label } from "../entity/label"; 7 | import { Sample } from "../entity/sample"; 8 | import { Aggregation } from "../entity/aggregation"; 9 | import { FilterBuilder } from "./filterBuilder"; 10 | 11 | export class RequestParamsBuilder { 12 | private params: StringNumberArray; 13 | 14 | constructor() { 15 | this.reset(); 16 | } 17 | 18 | public addKey(key: string): RequestParamsBuilder { 19 | this.params.push(key); 20 | return this; 21 | } 22 | 23 | public addKeys(sourceKey: string, destKey: string): RequestParamsBuilder { 24 | if (sourceKey === destKey) { 25 | throw new Error(`source and destination key cannot be equals: ${sourceKey} != ${destKey}`); 26 | } 27 | this.params.push(sourceKey, destKey); 28 | 29 | return this; 30 | } 31 | 32 | public addRetention(retention?: number): RequestParamsBuilder { 33 | if (retention != null) { 34 | if (retention < 0) { 35 | throw new Error(`retention must be positive integer, found: ${retention}`); 36 | } 37 | this.params.push(CommandKeyword.RETENTION, retention); 38 | } 39 | 40 | return this; 41 | } 42 | 43 | public addLabels(labels?: Label[]): RequestParamsBuilder { 44 | if (labels != null && labels.length !== 0) { 45 | const flatLabels = new List(labels).flatten(); 46 | this.params.push(CommandKeyword.LABELS, ...flatLabels); 47 | } 48 | 49 | return this; 50 | } 51 | 52 | public addChunkSize(chunkSize?: number): RequestParamsBuilder { 53 | if (chunkSize != null) { 54 | if (chunkSize < 0) { 55 | throw new Error(`chunkSize must be positive integer, found: ${chunkSize}`); 56 | } 57 | this.params.push(CommandKeyword.CHUNK_SIZE, chunkSize); 58 | } 59 | 60 | return this; 61 | } 62 | 63 | public addDuplicatePolicy(policy?: string): RequestParamsBuilder { 64 | if (policy != null) { 65 | if (![`BLOCK`, `FIRST`, `LAST`, `MIN`, `MAX`, `SUM`].includes(policy)) { 66 | throw new Error( 67 | `duplicate policy must be either BLOCK, FIRST, LAST, MIN, MAX or SUM, found: ${policy}` 68 | ); 69 | } 70 | this.params.push(CommandKeyword.DUPLICATE_POLICY, policy); 71 | } 72 | 73 | return this; 74 | } 75 | 76 | public addOnDuplicate(policy?: string): RequestParamsBuilder { 77 | if (policy != null) { 78 | if (![`BLOCK`, `FIRST`, `LAST`, `MIN`, `MAX`, `SUM`].includes(policy)) { 79 | throw new Error( 80 | `duplicate policy must be either BLOCK, FIRST, LAST, MIN, MAX or SUM, found: ${policy}` 81 | ); 82 | } 83 | this.params.push(CommandKeyword.ON_DUPLICATE, policy); 84 | } 85 | 86 | return this; 87 | } 88 | 89 | public removeLabels(): RequestParamsBuilder { 90 | this.params.push(CommandKeyword.LABELS); 91 | return this; 92 | } 93 | 94 | public addSamples(samples: Sample[]): RequestParamsBuilder { 95 | if (samples.length !== 0) { 96 | const flatSamples = new List(samples).flatten(); 97 | this.params.push(...flatSamples); 98 | } 99 | 100 | return this; 101 | } 102 | 103 | public addSample(sample: Sample): RequestParamsBuilder { 104 | this.params.push(...sample.flatten()); 105 | return this; 106 | } 107 | 108 | public addSampleWithOptionalTimeStamp(sample: Sample): RequestParamsBuilder { 109 | this.params.push(sample.getKey(), sample.getValue(), CommandKeyword.TIMESTAMP, sample.getTimestamp()); 110 | return this; 111 | } 112 | 113 | public addCount(count?: number): RequestParamsBuilder { 114 | if (count != null) { 115 | this.params.push(...new Count(count).flatten()); 116 | } 117 | 118 | return this; 119 | } 120 | 121 | public addAggregation(aggregation?: Aggregation): RequestParamsBuilder { 122 | if (aggregation != null) { 123 | this.params.push(...aggregation.flatten()); 124 | } 125 | 126 | return this; 127 | } 128 | 129 | public addRange(range?: TimestampRange): RequestParamsBuilder { 130 | if (range != null) { 131 | this.params.push(...range.flatten()); 132 | } 133 | 134 | return this; 135 | } 136 | 137 | public addFilters(filters: FilterBuilder): RequestParamsBuilder { 138 | const flatFilters = new List(filters.get()).flatten(); 139 | this.params.push(...flatFilters); 140 | 141 | return this; 142 | } 143 | 144 | public addFiltersWithKeyword(filters: FilterBuilder): RequestParamsBuilder { 145 | const flatFilters = new List(filters.get()).flatten(); 146 | this.params.push(CommandKeyword.FILTER, ...flatFilters); 147 | 148 | return this; 149 | } 150 | 151 | public addWithLabels(withLabels?: boolean): RequestParamsBuilder { 152 | if (withLabels != null && withLabels) { 153 | this.params.push(CommandKeyword.WITHLABELS); 154 | } 155 | 156 | return this; 157 | } 158 | 159 | public addUncompressed(uncompressed?: boolean): RequestParamsBuilder { 160 | if (uncompressed != null && uncompressed) { 161 | this.params.push(CommandKeyword.UNCOMPRESSED); 162 | } 163 | 164 | return this; 165 | } 166 | 167 | public reset(): RequestParamsBuilder { 168 | this.params = []; 169 | return this; 170 | } 171 | 172 | public get(): StringNumberArray { 173 | const params: StringNumberArray = this.params; 174 | this.reset(); 175 | 176 | return params; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/builder/requestParamsDirector.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../entity/label"; 2 | import { Aggregation } from "../entity/aggregation"; 3 | import { TimestampRange } from "../entity/timestampRange"; 4 | import { FilterBuilder } from "./filterBuilder"; 5 | import { Sample } from "../entity/sample"; 6 | import { RequestParamsBuilder } from "./requestParamsBuilder"; 7 | 8 | class RequestParamsDirector { 9 | private paramsBuilder: RequestParamsBuilder; 10 | 11 | constructor(paramsBuilder: RequestParamsBuilder) { 12 | this.paramsBuilder = paramsBuilder; 13 | } 14 | 15 | public create( 16 | key: string, 17 | labels?: Label[], 18 | retention?: number, 19 | chunkSize?: number, 20 | duplicatePolicy?: string, 21 | uncompressed?: boolean 22 | ): RequestParamsBuilder { 23 | return this.paramsBuilder 24 | .addKey(key) 25 | .addRetention(retention) 26 | .addUncompressed(uncompressed) 27 | .addLabels(labels) 28 | .addChunkSize(chunkSize) 29 | .addDuplicatePolicy(duplicatePolicy); 30 | } 31 | 32 | public alter( 33 | key: string, 34 | labels?: Label[], 35 | retention?: number, 36 | chunkSize?: number, 37 | duplicatePolicy?: string, 38 | uncompressed?: boolean 39 | ): RequestParamsBuilder { 40 | const builder = this.paramsBuilder 41 | .addKey(key) 42 | .addRetention(retention) 43 | .addUncompressed(uncompressed) 44 | .addChunkSize(chunkSize) 45 | .addDuplicatePolicy(duplicatePolicy); 46 | // if labels is an empty array it means deleting previous labels 47 | if (labels != null && labels.length === 0) { 48 | return builder.removeLabels(); 49 | } 50 | 51 | return builder.addLabels(labels); 52 | } 53 | 54 | public add( 55 | sample: Sample, 56 | labels?: Label[], 57 | retention?: number, 58 | chunkSize?: number, 59 | onDuplicate?: string, 60 | uncompressed?: boolean 61 | ): RequestParamsBuilder { 62 | return this.paramsBuilder 63 | .addSample(sample) 64 | .addRetention(retention) 65 | .addUncompressed(uncompressed) 66 | .addLabels(labels) 67 | .addChunkSize(chunkSize) 68 | .addOnDuplicate(onDuplicate); 69 | } 70 | 71 | public multiAdd(samples: Sample[]): RequestParamsBuilder { 72 | return this.paramsBuilder.addSamples(samples); 73 | } 74 | 75 | public changeBy( 76 | sample: Sample, 77 | labels?: Label[], 78 | retention?: number, 79 | uncompressed?: boolean, 80 | chunkSize?: number 81 | ): RequestParamsBuilder { 82 | return this.paramsBuilder 83 | .addSampleWithOptionalTimeStamp(sample) 84 | .addRetention(retention) 85 | .addLabels(labels) 86 | .addUncompressed(uncompressed) 87 | .addChunkSize(chunkSize); 88 | } 89 | 90 | public createRule(sourceKey: string, destKey: string, aggregation: Aggregation): RequestParamsBuilder { 91 | return this.paramsBuilder.addKeys(sourceKey, destKey).addAggregation(aggregation); 92 | } 93 | 94 | public deleteRule(sourceKey: string, destKey: string): RequestParamsBuilder { 95 | return this.paramsBuilder.addKeys(sourceKey, destKey); 96 | } 97 | 98 | public range(key: string, range: TimestampRange, count?: number, aggregation?: Aggregation): RequestParamsBuilder { 99 | return this.paramsBuilder 100 | .addKey(key) 101 | .addRange(range) 102 | .addCount(count) 103 | .addAggregation(aggregation); 104 | } 105 | 106 | public multiRange( 107 | range: TimestampRange, 108 | filters: FilterBuilder, 109 | count?: number, 110 | aggregation?: Aggregation, 111 | withLabels?: boolean 112 | ): RequestParamsBuilder { 113 | return this.paramsBuilder 114 | .addRange(range) 115 | .addCount(count) 116 | .addAggregation(aggregation) 117 | .addWithLabels(withLabels) 118 | .addFiltersWithKeyword(filters); 119 | } 120 | 121 | public getKey(key: string): RequestParamsBuilder { 122 | return this.paramsBuilder.addKey(key); 123 | } 124 | 125 | public multiGet(filters: FilterBuilder, withLabels?: boolean): RequestParamsBuilder { 126 | return this.paramsBuilder.addWithLabels(withLabels).addFiltersWithKeyword(filters); 127 | } 128 | 129 | public queryIndex(filters: FilterBuilder): RequestParamsBuilder { 130 | return this.paramsBuilder.addFilters(filters); 131 | } 132 | } 133 | 134 | export { RequestParamsDirector }; 135 | -------------------------------------------------------------------------------- /src/command/commandInvoker.ts: -------------------------------------------------------------------------------- 1 | import { CommandInterface } from "./interface/command"; 2 | 3 | export class CommandInvoker { 4 | protected command: CommandInterface; 5 | 6 | public setCommand(command: CommandInterface): CommandInvoker { 7 | this.command = command; 8 | return this; 9 | } 10 | 11 | public async run(): Promise { 12 | return this.command.execute(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/command/commandProvider.ts: -------------------------------------------------------------------------------- 1 | import * as Redis from "ioredis"; 2 | import { CommandData } from "./interface/commandData"; 3 | import { CommandName } from "../enum/commandName"; 4 | import { StringNumberArray } from "../index"; 5 | 6 | export class CommandProvider { 7 | protected readonly client: Redis.Redis; 8 | protected commands: object; 9 | 10 | constructor(redisClient: Redis.Redis) { 11 | this.client = redisClient; 12 | this.commands = {}; 13 | this.buildCommands(); 14 | } 15 | 16 | public getCommand(commandName: string): () => any { 17 | return this.commands[commandName]; 18 | } 19 | 20 | public getCommandData(commandName: string, params: StringNumberArray): CommandData { 21 | return { 22 | commandName: commandName, 23 | commandFunction: this.getCommand(commandName), 24 | commandParams: params 25 | }; 26 | } 27 | 28 | public getRTSClient(): Redis.Redis { 29 | return this.client; 30 | } 31 | 32 | protected buildCommands(): void { 33 | for (const key in CommandName) { 34 | const command: string = CommandName[key]; 35 | const redisCommand = this.client.createBuiltinCommand(command); 36 | // @ts-ignore 37 | this.commands[command] = redisCommand.string; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/command/commandReceiver.ts: -------------------------------------------------------------------------------- 1 | import * as Redis from "ioredis"; 2 | import { CommandData } from "./interface/commandData"; 3 | 4 | export class CommandReceiver { 5 | protected readonly client: Redis.Redis; 6 | 7 | constructor(client: Redis.Redis) { 8 | this.client = client; 9 | } 10 | 11 | public executeCommand(commandData: CommandData): Promise { 12 | return new Promise(resolve => { 13 | const command = commandData.commandFunction; 14 | resolve(command.call(this.client, ...commandData.commandParams)); 15 | }) 16 | .then(res => { 17 | return res; 18 | }) 19 | .catch(err => { 20 | throw new Error( 21 | `error when executing command "${commandData.commandName} ${commandData.commandParams.join( 22 | " " 23 | )}": ${err.message}` 24 | ); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/command/deleteAllCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandInterface } from "./interface/command"; 2 | import * as Redis from "ioredis"; 3 | 4 | export class DeleteAllCommand implements CommandInterface { 5 | protected readonly receiver: Redis.Redis; 6 | 7 | constructor(receiver: Redis.Redis) { 8 | this.receiver = receiver; 9 | } 10 | 11 | public execute(): Promise { 12 | return this.receiver.flushdb(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/command/deleteCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandInterface } from "./interface/command"; 2 | import * as Redis from "ioredis"; 3 | 4 | export class DeleteCommand implements CommandInterface { 5 | protected readonly receiver: Redis.Redis; 6 | protected readonly keys: string[]; 7 | 8 | constructor(receiver: Redis.Redis, keys: string[]) { 9 | this.keys = keys; 10 | this.receiver = receiver; 11 | } 12 | 13 | public execute(): Promise { 14 | return this.receiver.del(...this.keys); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/command/disconnectCommand.ts: -------------------------------------------------------------------------------- 1 | import * as Redis from "ioredis"; 2 | import { CommandInterface } from "./interface/command"; 3 | 4 | export class DisconnectCommand implements CommandInterface { 5 | protected readonly receiver: Redis.Redis; 6 | 7 | constructor(receiver: Redis.Redis) { 8 | this.receiver = receiver; 9 | } 10 | 11 | public execute(): Promise { 12 | return this.receiver.quit(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/command/expireCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandInterface } from "./interface/command"; 2 | import * as Redis from "ioredis"; 3 | 4 | export class ExpireCommand implements CommandInterface { 5 | protected readonly receiver: Redis.Redis; 6 | protected readonly key: string; 7 | protected readonly seconds: number; 8 | 9 | constructor(receiver: Redis.Redis, key: string, seconds: number) { 10 | this.key = key; 11 | this.seconds = seconds; 12 | this.receiver = receiver; 13 | 14 | if (!Number.isInteger(this.seconds)) 15 | throw new Error(`Only integers allowed for 'seconds' parameter. ${seconds} is not an integer.`); 16 | } 17 | 18 | public execute(): Promise { 19 | return this.receiver.expire(this.key, this.seconds); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/command/interface/command.ts: -------------------------------------------------------------------------------- 1 | export interface CommandInterface { 2 | execute(): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/command/interface/commandData.ts: -------------------------------------------------------------------------------- 1 | import { StringNumberArray } from "../../index"; 2 | 3 | export interface CommandData { 4 | commandName: string; 5 | commandFunction: () => any; 6 | commandParams: StringNumberArray; 7 | } 8 | -------------------------------------------------------------------------------- /src/command/timeSeriesCommand.ts: -------------------------------------------------------------------------------- 1 | import { CommandInterface } from "./interface/command"; 2 | import { CommandReceiver } from "./commandReceiver"; 3 | import { CommandData } from "./interface/commandData"; 4 | 5 | export class TimeSeriesCommand implements CommandInterface { 6 | protected readonly receiver: CommandReceiver; 7 | protected readonly commandData: CommandData; 8 | 9 | constructor(commandData: CommandData, receiver: CommandReceiver) { 10 | this.commandData = commandData; 11 | this.receiver = receiver; 12 | } 13 | 14 | public execute(): Promise { 15 | return this.receiver.executeCommand(this.commandData); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entity/aggregation.ts: -------------------------------------------------------------------------------- 1 | import { AggregationType } from "../enum/aggregationType"; 2 | import { CommandKeyword } from "../enum/commandKeyword"; 3 | import { StringNumberArray } from "../index"; 4 | 5 | /** 6 | * A `aggregation-type`-`timeBucket` object 7 | */ 8 | export class Aggregation { 9 | private readonly type: string; 10 | private readonly timeBucketInMs: number; 11 | 12 | /** 13 | * Creates a new Aggregation object 14 | * 15 | * @param type The type of aggregation e.g. AggregationType.AVG | AggregationType.SUM etc 16 | * @param timeBucketInMs The time bucket for the aggregation 17 | * 18 | * @remarks 19 | * // Example 20 | * ``` 21 | * const aggregation = new Aggregation(AggregationType.AVG, 1000); 22 | * const timestampRange = new TimestampRange(date, date + 10000); 23 | * const samples = await redisTimeSeries.range("range1", timestampRange, undefined, aggregation); 24 | * 25 | * aggregation.getType() // avg 26 | * aggregation.getTimeBucketInMs() // 1000 27 | * aggregation.flatten() // ['AGGREGATION', 'avg', 1000] 28 | * ``` 29 | */ 30 | constructor(type: string, timeBucketInMs: number) { 31 | this.type = this.validateType(type); 32 | this.timeBucketInMs = this.validateTimeBucket(timeBucketInMs); 33 | } 34 | 35 | public getType(): string { 36 | return this.type; 37 | } 38 | 39 | public getTimeBucketInMs(): number { 40 | return this.timeBucketInMs; 41 | } 42 | 43 | public flatten(): StringNumberArray { 44 | return [CommandKeyword.AGGREGATION, this.getType(), this.getTimeBucketInMs()]; 45 | } 46 | 47 | protected validateType(type: string): string { 48 | const keyEnum = Object.keys(AggregationType).find(key => AggregationType[key] === type.toLowerCase()); 49 | if (keyEnum == null) { 50 | throw new Error(`aggregation type '${type}' not found`); 51 | } 52 | 53 | return AggregationType[keyEnum]; 54 | } 55 | 56 | protected validateTimeBucket(timeBucketInMs: number): number { 57 | const truncatedTimeBucket = Math.trunc(timeBucketInMs); 58 | if (truncatedTimeBucket <= 0) { 59 | throw new Error(`invalid timestamp '${timeBucketInMs}'`); 60 | } 61 | 62 | return truncatedTimeBucket; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/entity/count.ts: -------------------------------------------------------------------------------- 1 | import { CommandKeyword } from "../enum/commandKeyword"; 2 | import { StringNumberArray } from "../index"; 3 | 4 | /** 5 | * A `count` object representing the maximum number of results to return 6 | */ 7 | export class Count { 8 | private readonly count: number; 9 | 10 | /** 11 | * Creates a count object. Limits the number of items returned in a series 12 | * 13 | * @param count maximum number of results to return 14 | * ``` 15 | * // Example 16 | * const count = new Count(5000) 17 | * count.flatten() // ['COUNT', 5000] 18 | * ``` 19 | */ 20 | constructor(count: number) { 21 | this.count = this.validate(count); 22 | } 23 | 24 | public flatten(): StringNumberArray { 25 | return [CommandKeyword.COUNT, this.count]; 26 | } 27 | 28 | protected validate(count: number): number { 29 | const truncatedCount = Math.trunc(count); 30 | if (truncatedCount < 0) { 31 | throw new Error(`count must be positive, provided ${truncatedCount}`); 32 | } 33 | 34 | return truncatedCount; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/entity/filter.ts: -------------------------------------------------------------------------------- 1 | import { FilterOperator } from "../enum/filterOperator"; 2 | import { StringNumberArray } from "../index"; 3 | 4 | /** 5 | * A `label`-`operator`-`value` object used for filtering queries 6 | */ 7 | export class Filter { 8 | private readonly label: string; 9 | private readonly operator: string; 10 | private readonly value?: string | number | StringNumberArray; 11 | private readonly allowedOperator: string[] = [FilterOperator.EQUAL, FilterOperator.NOT_EQUAL]; 12 | 13 | /** 14 | * The label and value in the constructor create a filter. 15 | * 16 | * @param label the label of the filter 17 | * @param operator the operator used for the filter i.e. FilterOperator.EQUAL or FilterOperator.NOT_EQUAL 18 | * @param value the value of the filter 19 | */ 20 | constructor(label: string, operator: string, value?: string | number | StringNumberArray) { 21 | this.label = label; 22 | this.operator = this.validateOperator(operator); 23 | this.value = value; 24 | } 25 | 26 | public flatten(): string[] { 27 | let filterString = ""; 28 | if (this.value != null) { 29 | filterString = this.value instanceof Array ? `(${this.value.join(",")})` : this.value.toString(); 30 | } 31 | 32 | return [`${this.label}${this.operator}${filterString}`]; 33 | } 34 | 35 | protected validateOperator(operator: string): string { 36 | if (!this.allowedOperator.includes(operator)) { 37 | throw new Error(`not allowed operator ${operator}`); 38 | } 39 | 40 | return operator; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/entity/label.ts: -------------------------------------------------------------------------------- 1 | import { StringNumberArray } from "../index"; 2 | 3 | /** 4 | * A `label`-`value` object 5 | */ 6 | export class Label { 7 | private readonly name: string; 8 | private readonly value: string | number; 9 | 10 | /** 11 | * Creates a new label 12 | * 13 | * @param name the name of the label 14 | * @param value the value represented by the label 15 | * 16 | * @remarks 17 | * ``` 18 | * 19 | * // Example 20 | * 21 | * const sensorLabel = new Label('sensor', 1); 22 | * sensorLabel.getName() // 'sensor' 23 | * sensorLabel.getValue() // 1 24 | * sensorLabel.flatten() // [sensor, 1] 25 | * 26 | * const temp = 24, retention=24*3600, 27 | * chunkSize=8000, dupPolicy='LAST'; 28 | * const tempLabel = new Label('temp', temp < 15 ? 'cold': 'warm'); 29 | * const labels = [sensorLabel, tempLabel] 30 | * const rts.create( 31 | * 'sensor:temp', 32 | * labels, 33 | * retention, 34 | * chunkSize, 35 | * dupPolicy 36 | * ); 37 | * ``` 38 | */ 39 | constructor(name: string, value: string | number) { 40 | this.name = name; 41 | this.value = value; 42 | } 43 | 44 | public getName(): string { 45 | return this.name; 46 | } 47 | 48 | public getValue(): string | number { 49 | return this.value; 50 | } 51 | 52 | public flatten(): StringNumberArray { 53 | return [this.getName(), this.getValue()]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/entity/sample.ts: -------------------------------------------------------------------------------- 1 | import { CommandKeyword } from "../enum/commandKeyword"; 2 | import { StringNumberArray } from "../index"; 3 | 4 | /** 5 | * A `key`-`value`-`timestamp` object 6 | */ 7 | export class Sample { 8 | private key: string; 9 | private value: number; 10 | private timestamp: string | number; 11 | 12 | /** 13 | * Creates a Sample object 14 | * 15 | * @param key Key name for timeseries 16 | * @param value The value of the sample 17 | * @param timestamp The timestamp of the sample in ms 18 | * @returns the created Sample object 19 | * 20 | * @remarks 21 | * ``` 22 | * // Example 23 | * const tsSample = new Sample('temperature:2:32', 25, 1609682633000); 24 | * 25 | * tsSample.getKey(); // 'temperature:2:32' 26 | * tsSample.setKey('temperature:5:15'); // Sample('temperature:5:15', 25, 1609682633000) 27 | * tsSample.getValue(); // 25 28 | * tsSample.setValue(27); // Sample('temperature:5:15', 27, 1609682633000) 29 | * tsSample.getTimestamp(); // 1609682633000 30 | * tsSample.setTimestamp(1609682634000); // Sample('temperature:5:15', 27, 1609682634000) 31 | * tsSample.flatten(); // ['temperature:5:15', 1609682634000, 27] 32 | * ``` 33 | */ 34 | constructor(key: string, value: number, timestamp?: number) { 35 | this.setKey(key); 36 | this.setValue(value); 37 | this.setTimestamp(timestamp); 38 | } 39 | 40 | public getKey(): string { 41 | return this.key; 42 | } 43 | 44 | public setKey(key: string): Sample { 45 | this.key = key; 46 | return this; 47 | } 48 | 49 | public getValue(): number { 50 | return this.value; 51 | } 52 | 53 | public setValue(value: number): Sample { 54 | this.value = value; 55 | return this; 56 | } 57 | 58 | public getTimestamp(): string | number { 59 | return this.timestamp; 60 | } 61 | 62 | public setTimestamp(timestamp?: number): Sample { 63 | this.timestamp = this.validateTimestamp(timestamp); 64 | return this; 65 | } 66 | 67 | public flatten(): StringNumberArray { 68 | return [this.getKey(), this.getTimestamp(), this.getValue()]; 69 | } 70 | 71 | protected validateTimestamp(timestamp?: number): string | number { 72 | if (timestamp == null) { 73 | return CommandKeyword.CURRENT_TIMESTAMP; 74 | } 75 | 76 | timestamp = Math.trunc(timestamp); 77 | 78 | if (this.isValidTimestamp(timestamp)) { 79 | return timestamp; 80 | } 81 | 82 | throw new Error(`wrong timestamp: ${timestamp}`); 83 | } 84 | 85 | protected isValidTimestamp(timestamp: number): boolean { 86 | return new Date(timestamp).getTime() >= 0; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/entity/timestampRange.ts: -------------------------------------------------------------------------------- 1 | import { CommandKeyword } from "../enum/commandKeyword"; 2 | import { StringNumberArray } from "../index"; 3 | 4 | /** 5 | * A `from` - `to` timestamp object 6 | */ 7 | export class TimestampRange { 8 | private from: string | number; 9 | private to: string | number; 10 | 11 | /** 12 | * Creates a timestamp range object 13 | * 14 | * @param from timestamp to begin from (milliseconds) 15 | * @param to timestamp to end at (milliseconds) 16 | * 17 | * @remarks 18 | * ``` 19 | * // Example 20 | * const oneDayAgo = new Date().getTime() - 24 * 3600 * 1000 21 | * const now = new Date().getTime() 22 | * const tsRange = new TimestampRange(oneDayAgo, now) 23 | * tsRange.getFrom() // oneDayAgo 24 | * tsRange.getTo() // now 25 | * tsRange.setFrom(oneDayAgo - 24 * 3600 * 1000) // twoDaysAgo 26 | * tsRange.getTo(oneDayAgo) // oneDayAgo 27 | * tsRange.flatten() // [twoDaysAgo, oneDayAgo] 28 | * ``` 29 | */ 30 | constructor(from?: number, to?: number) { 31 | this.setFrom(from); 32 | this.setTo(to); 33 | } 34 | 35 | public getFrom(): string | number { 36 | return this.from; 37 | } 38 | 39 | public setFrom(from?: number): TimestampRange { 40 | this.from = from == null ? CommandKeyword.MIN_TIMESTAMP : this.validateTimestamp(from); 41 | return this; 42 | } 43 | 44 | public getTo(): string | number { 45 | return this.to; 46 | } 47 | 48 | public setTo(to?: number): TimestampRange { 49 | this.to = to == null ? CommandKeyword.MAX_TIMESTAMP : this.validateTimestamp(to); 50 | return this; 51 | } 52 | 53 | public flatten(): StringNumberArray { 54 | return [this.getFrom(), this.getTo()]; 55 | } 56 | 57 | protected validateTimestamp(timestamp: number): number { 58 | timestamp = Math.trunc(timestamp); 59 | if (new Date(timestamp).getTime() >= 0) { 60 | return timestamp; 61 | } 62 | 63 | throw new Error(`invalid timestamp ${timestamp}`); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/enum/aggregationType.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The type of aggregation to be executed 3 | * 4 | * Types: 5 | * 6 | * AVG, SUM, MIN, MAX, RANGE, COUNT, FIRST, LAST, STD_P, STD_S, VAR_P, VAR_S 7 | */ 8 | export enum AggregationType { 9 | AVG = "avg", 10 | SUM = "sum", 11 | MIN = "min", 12 | MAX = "max", 13 | RANGE = "range", 14 | COUNT = "count", 15 | FIRST = "first", 16 | LAST = "last", 17 | STD_P = "std.p", 18 | STD_S = "std.s", 19 | VAR_P = "var.p", 20 | VAR_S = "var.s" 21 | } 22 | -------------------------------------------------------------------------------- /src/enum/commandKeyword.ts: -------------------------------------------------------------------------------- 1 | export enum CommandKeyword { 2 | LABELS = "LABELS", 3 | RETENTION = "RETENTION", 4 | AGGREGATION = "AGGREGATION", 5 | TIMESTAMP = "TIMESTAMP", 6 | MIN_TIMESTAMP = "-", 7 | MAX_TIMESTAMP = "+", 8 | CURRENT_TIMESTAMP = "*", 9 | COUNT = "COUNT", 10 | FILTER = "FILTER", 11 | WITHLABELS = "WITHLABELS", 12 | UNCOMPRESSED = "UNCOMPRESSED", 13 | CHUNK_SIZE = "CHUNK_SIZE", 14 | DUPLICATE_POLICY = "DUPLICATE_POLICY", 15 | ON_DUPLICATE = "ON_DUPLICATE" 16 | } 17 | -------------------------------------------------------------------------------- /src/enum/commandName.ts: -------------------------------------------------------------------------------- 1 | export enum CommandName { 2 | CREATE = "TS.CREATE", 3 | ALTER = "TS.ALTER", 4 | ADD = "TS.ADD", 5 | MADD = "TS.MADD", 6 | INCRBY = "TS.INCRBY", 7 | DECRBY = "TS.DECRBY", 8 | CREATE_RULE = "TS.CREATERULE", 9 | DELETE_RULE = "TS.DELETERULE", 10 | RANGE = "TS.RANGE", 11 | REV_RANGE = "TS.REVRANGE", 12 | MULTI_RANGE = "TS.MRANGE", 13 | MULTI_REV_RANGE = "TS.MREVRANGE", 14 | GET = "TS.GET", 15 | MULTI_GET = "TS.MGET", 16 | INFO = "TS.INFO", 17 | QUERY_INDEX = "TS.QUERYINDEX" 18 | } 19 | -------------------------------------------------------------------------------- /src/enum/filterOperator.ts: -------------------------------------------------------------------------------- 1 | export enum FilterOperator { 2 | EQUAL = "=", 3 | NOT_EQUAL = "!=" 4 | } 5 | -------------------------------------------------------------------------------- /src/factory/redisTimeSeries.ts: -------------------------------------------------------------------------------- 1 | import * as Redis from "ioredis"; 2 | import { RedisTimeSeries } from "../redisTimeSeries"; 3 | import { RequestParamsDirector } from "../builder/requestParamsDirector"; 4 | import { RenderFactory } from "./render"; 5 | import { CommandProvider } from "../command/commandProvider"; 6 | import { CommandInvoker } from "../command/commandInvoker"; 7 | import { CommandReceiver } from "../command/commandReceiver"; 8 | import { RequestParamsBuilder } from "../builder/requestParamsBuilder"; 9 | import { ConnectionOptions } from "../index"; 10 | 11 | /** 12 | * Set redis connection options and return a factory object that create a clients connection 13 | */ 14 | export class RedisTimeSeriesFactory { 15 | protected options: ConnectionOptions = { 16 | port: 6379, 17 | host: "127.0.0.1", 18 | db: 0 19 | }; 20 | 21 | /** 22 | * Set connection options 23 | * 24 | * @param options Connection options for the redis database 25 | * @returns factory object that can be used to create a client connection 26 | */ 27 | constructor(options: ConnectionOptions = {}) { 28 | this.options = { ...this.options, ...options }; 29 | } 30 | 31 | /** 32 | * Create a client connection to the host 33 | * 34 | * @returns The client connection object 35 | */ 36 | public create(): RedisTimeSeries { 37 | const commandProvider: CommandProvider = new CommandProvider(this.getRedisClient()); 38 | const commandReceiver: CommandReceiver = new CommandReceiver(commandProvider.getRTSClient()); 39 | const director: RequestParamsDirector = new RequestParamsDirector(new RequestParamsBuilder()); 40 | 41 | return new RedisTimeSeries( 42 | commandProvider, 43 | commandReceiver, 44 | new CommandInvoker(), 45 | director, 46 | new RenderFactory() 47 | ); 48 | } 49 | 50 | protected getRedisClient(): Redis.Redis { 51 | return new Redis(this.options); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/factory/render.ts: -------------------------------------------------------------------------------- 1 | import { InfoResponseRender } from "../response/infoResponseRender"; 2 | import { MultiGetResponseRender } from "../response/multiGetResponseRender"; 3 | import { MultiRangeResponseRender } from "../response/multiRangeResponseRender"; 4 | 5 | export class RenderFactory { 6 | public getMultiRangeRender(): MultiRangeResponseRender { 7 | return new MultiRangeResponseRender(); 8 | } 9 | 10 | public getMultiGetRender(): MultiGetResponseRender { 11 | return new MultiGetResponseRender(); 12 | } 13 | 14 | public getInfoRender(): InfoResponseRender { 15 | return new InfoResponseRender(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { RedisTimeSeriesFactory } from "./factory/redisTimeSeries"; 2 | import { Label } from "./entity/label"; 3 | import { Sample } from "./entity/sample"; 4 | import { Count } from "./entity/count"; 5 | import { TimestampRange } from "./entity/timestampRange"; 6 | import { Aggregation } from "./entity/aggregation"; 7 | import { FilterOperator } from "./enum/filterOperator"; 8 | import { Filter } from "./entity/filter"; 9 | import { List } from "./iterator/list"; 10 | import { AggregationType } from "./enum/aggregationType"; 11 | import { FilterBuilder } from "./builder/filterBuilder"; 12 | import * as Redis from "ioredis"; 13 | 14 | type StringNumberArray = (string | number)[]; 15 | type ConnectionOptions = Redis.RedisOptions; 16 | 17 | export { 18 | RedisTimeSeriesFactory, 19 | Label, 20 | Sample, 21 | Count, 22 | TimestampRange, 23 | List, 24 | Aggregation, 25 | AggregationType, 26 | Filter, 27 | FilterBuilder, 28 | FilterOperator, 29 | StringNumberArray, 30 | ConnectionOptions 31 | }; 32 | -------------------------------------------------------------------------------- /src/iterator/list.ts: -------------------------------------------------------------------------------- 1 | import { Sample } from "../entity/sample"; 2 | import { Label } from "../entity/label"; 3 | import { Filter } from "../entity/filter"; 4 | import { StringNumberArray } from "../index"; 5 | 6 | /** 7 | * An array that contains either Label, Sample or Filter objects 8 | */ 9 | export class List { 10 | private readonly list: (Label | Sample | Filter)[]; 11 | 12 | /** 13 | * Creates a new List object containing either Label, Sample or Filter objects 14 | * @param list 15 | */ 16 | constructor(list: (Label | Sample | Filter)[]) { 17 | this.list = list; 18 | } 19 | 20 | public flatten(): StringNumberArray { 21 | const result: StringNumberArray = []; 22 | for (const element of this.list) { 23 | result.push(...element.flatten()); 24 | } 25 | 26 | return result; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/response/infoResponseRender.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../entity/label"; 2 | import { Aggregation } from "../entity/aggregation"; 3 | import { InfoResponse } from "./interface/infoResponse"; 4 | import { AggregationByKey } from "./interface/aggregationByKey"; 5 | 6 | export class InfoResponseRender { 7 | public render(response: any[]): InfoResponse { 8 | const labels: Label[] = []; 9 | for (const label of response[19]) { 10 | labels.push(new Label(label[0], label[1])); 11 | } 12 | 13 | const rules: AggregationByKey = {}; 14 | for (const rule of response[23]) { 15 | rules[rule[0]] = new Aggregation(rule[2], rule[1]); 16 | } 17 | 18 | const info: InfoResponse = { 19 | totalSamples: response[1], 20 | memoryUsage: response[3], 21 | firstTimestamp: response[5], 22 | lastTimestamp: response[7], 23 | retentionTime: response[9], 24 | chunkCount: response[11], 25 | chunkSize: response[13], 26 | chunkType: response[15], 27 | duplicatePolicy: response[17], 28 | labels: labels, 29 | sourceKey: response[21], 30 | rules: rules 31 | }; 32 | 33 | if (response[21]) { 34 | info["sourceKey"] = response[21]; 35 | } 36 | 37 | return info; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/response/interface/aggregationByKey.ts: -------------------------------------------------------------------------------- 1 | import { Aggregation } from "../../entity/aggregation"; 2 | 3 | export interface AggregationByKey { 4 | [key: string]: Aggregation; 5 | } 6 | -------------------------------------------------------------------------------- /src/response/interface/baseMultiResponse.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../../entity/label"; 2 | 3 | export interface BaseMultiResponse { 4 | key: string; 5 | labels: Label[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/response/interface/infoResponse.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../../entity/label"; 2 | import { AggregationByKey } from "./aggregationByKey"; 3 | 4 | export interface InfoResponse { 5 | totalSamples: number; 6 | memoryUsage: number; 7 | firstTimestamp: number; 8 | lastTimestamp: number; 9 | retentionTime: number; 10 | chunkCount: number; 11 | chunkSize: number; 12 | chunkType: string; 13 | labels: Label[]; 14 | duplicatePolicy: string; 15 | sourceKey?: string; 16 | rules: AggregationByKey; 17 | } 18 | -------------------------------------------------------------------------------- /src/response/interface/multiGetResponse.ts: -------------------------------------------------------------------------------- 1 | import { Sample } from "../../entity/sample"; 2 | import { BaseMultiResponse } from "./baseMultiResponse"; 3 | 4 | export interface MultiGetResponse extends BaseMultiResponse { 5 | data: Sample; 6 | } 7 | -------------------------------------------------------------------------------- /src/response/interface/multiRangeResponse.ts: -------------------------------------------------------------------------------- 1 | import { Sample } from "../../entity/sample"; 2 | import { BaseMultiResponse } from "./baseMultiResponse"; 3 | 4 | export interface MultiRangeResponse extends BaseMultiResponse { 5 | data: Sample[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/response/multiGetResponseRender.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../entity/label"; 2 | import { Sample } from "../entity/sample"; 3 | import { MultiGetResponse } from "./interface/multiGetResponse"; 4 | 5 | export class MultiGetResponseRender { 6 | public render(response: any[]): any[] { 7 | const ranges: MultiGetResponse[] = []; 8 | for (const bucket of response) { 9 | const key = bucket[0]; 10 | const labels: Label[] = []; 11 | for (const label of bucket[1]) { 12 | labels.push(new Label(label[0], label[1])); 13 | } 14 | let sampleValue = "0"; 15 | let sampleTimestamp = 0; 16 | if (bucket[2].length !== 0) { 17 | sampleValue = bucket[2][1]; 18 | sampleTimestamp = bucket[2][0]; 19 | } 20 | ranges.push({ 21 | key: key, 22 | labels: labels, 23 | data: new Sample(key, Number(sampleValue), sampleTimestamp) 24 | }); 25 | } 26 | 27 | return ranges; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/response/multiRangeResponseRender.ts: -------------------------------------------------------------------------------- 1 | import { Label } from "../entity/label"; 2 | import { Sample } from "../entity/sample"; 3 | import { MultiRangeResponse } from "./interface/multiRangeResponse"; 4 | 5 | export class MultiRangeResponseRender { 6 | public render(response: any[]): any[] { 7 | const ranges: MultiRangeResponse[] = []; 8 | for (const bucket of response) { 9 | const key = bucket[0]; 10 | const labels: Label[] = []; 11 | for (const label of bucket[1]) { 12 | labels.push(new Label(label[0], label[1])); 13 | } 14 | const samples: Sample[] = []; 15 | for (const sample of bucket[2]) { 16 | const sampleValue = Number(sample[1]); 17 | samples.push(new Sample(key, isNaN(sampleValue) ? 0 : sampleValue, sample[0])); 18 | } 19 | ranges.push({ 20 | key: key, 21 | labels: labels, 22 | data: samples 23 | }); 24 | } 25 | 26 | return ranges; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/response/type/multiAddResponseError.ts: -------------------------------------------------------------------------------- 1 | export type MultiAddResponseError = { 2 | stack: string; 3 | message: string; 4 | }; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "strictNullChecks": true, 5 | "declaration": true, 6 | "declarationMap": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "outDir": "lib", 10 | "target": "es6" 11 | }, 12 | "include": ["src"], 13 | "exclude": ["node_modules", "**/__tests__/*", "**/__tests_config__/*"] 14 | } 15 | --------------------------------------------------------------------------------