diff --git a/.markdownlint.yml b/.markdownlint.yml index f043d4f6..1e1d9b7e 100644 --- a/.markdownlint.yml +++ b/.markdownlint.yml @@ -1,5 +1,6 @@ { "MD013": false, + "MD024": false, "MD033": { "allowed_elements": [ "a", "img", "p", "details", "summary" ] }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 264dc120..d6a56cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## 0.3.0 [unreleased] +### Features + +1. [#89](https://github.com/InfluxCommunity/influxdb3-js/pull/89): Add structured query support + ## 0.2.0 [2023-08-11] ### Features diff --git a/README.md b/README.md index e2948274..a0209aae 100644 --- a/README.md +++ b/README.md @@ -65,18 +65,18 @@ export INFLUXDB_TOKEN="" ### powershell -```console -set INFLUXDB_URL= -set INFLUXDB_DATABASE= -set INFLUXDB_TOKEN= +```powershell +$env:INFLUXDB_URL = "" +$env:INFLUXDB_DATABASE = "" +$env:INFLUXDB_TOKEN = "" ``` ### cmd -```powershell -$env:INFLUXDB_URL "" -$env:INFLUXDB_DATABASE "" -$env:INFLUXDB_TOKEN "" +```console +set INFLUXDB_URL= +set INFLUXDB_DATABASE= +set INFLUXDB_TOKEN= ``` @@ -138,6 +138,23 @@ for await (const row of queryResult) { } ``` +or use typesafe `PointValues` structure with `client.queryPoints` + +```ts +const queryPointsResult = client.queryPoints( + query, + database, + queryType, + 'stat' +) + +for await (const row of queryPointsResult) { + console.log(`avg is ${row.getField('avg', 'float')}`) + console.log(`max is ${row.getField('max', 'float')}`) + console.log(`lp: ${row.toLineProtocol()}`) +} +``` + ## Examples For more advanced usage, see [examples](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/examples/README.md). diff --git a/examples/README.md b/examples/README.md index 46564928..a3794522 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,8 +1,7 @@ ## Examples -### Basic - - [IOxExample](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/examples/basic/README.md) - How to use write and query data from InfluxDB IOx +- [Downsampling](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/downsampling/basic/README.md) - How to use queries to structure data for downsampling ### Browser diff --git a/examples/basic/README.md b/examples/basic/README.md index d485ac85..2385bd84 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -18,7 +18,7 @@ set environment variables. - `INFLUXDB_TOKEN` read/write token generated in cloud - `INFLUXDB_DATABASE` name of database e.g .*`my-database`* -For simplicity you can use dotenv library to load environment variables in this example. Create `.env` file and paste your variables as follows: +For simplicity, you can use dotenv library to load environment variables in this example. Create `.env` file and paste your variables as follows: ```conf INFLUXDB_URL="" diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 2dfaf929..2369e5a2 100644 --- a/examples/basic/src/index.ts +++ b/examples/basic/src/index.ts @@ -1,4 +1,4 @@ -import {InfluxDBClient, Point, PointRecord} from '@influxdata/influxdb3-client' +import {InfluxDBClient, Point} from '@influxdata/influxdb3-client' type Defined = Exclude @@ -24,30 +24,32 @@ async function main() { try { // Write point - const p = new Point('stat') - .tag('unit', 'temperature') - .floatField('avg', 24.5) - .floatField('max', 45.0) - .timestamp(new Date()) + const p = Point.measurement('stat') + .setTag('unit', 'temperature') + .setFloatField('avg', 24.5) + .setFloatField('max', 45.0) + .setTimestamp(new Date()) await client.write(p, database) - // Write record - const sensorData: PointRecord = { - measurement: 'stat', - tags: { - unit: 'temperature', - }, - fields: { - avg: 28, - max: 40.3, - }, - timestamp: new Date(), + // Write point as template with anonymous fields object + const pointTemplate = Object.freeze( + Point.measurement('stat').setTag('unit', 'temperature') + ) + + const sensorData = { + avg: 28, + max: 40.3, } - await client.write([sensorData], database) + const p2 = pointTemplate + .copy() + .setFields(sensorData) + .setTimestamp(new Date()) + + await client.write(p2, database) // Or write directly line protocol - const line = `stat,unit=temperature avg=20.5,max=43.0` - await client.write(line, database) + const lp = `stat,unit=temperature avg=20.5,max=43.0` + await client.write(lp, database) // Prepare flightsql query const query = ` @@ -67,6 +69,14 @@ async function main() { console.log(`avg is ${row.avg}`) console.log(`max is ${row.max}`) } + + // Execute query again as points + const queryPointsResult = client.queryPoints(query, database, queryType) + + for await (const row of queryPointsResult) { + console.log(`avg is ${row.getField('avg', 'float')}`) + console.log(`max is ${row.getField('max', 'float')}`) + } } catch (err) { console.error(err) } finally { diff --git a/examples/browser/src/main.ts b/examples/browser/src/main.ts index c0057e20..4d95b407 100644 --- a/examples/browser/src/main.ts +++ b/examples/browser/src/main.ts @@ -56,14 +56,14 @@ view.setOnRandomize(() => { view.setOnWrite(async () => { const data = view.getWriteInput() - const p = new Point('stat') - .tag('Device', data['Device']) - .floatField('Temperature', data['Temperature']) - .floatField('Humidity', data['Humidity']) - .floatField('Pressure', data['Pressure']) - .intField('CO2', data['CO2']) - .intField('TVOC', data['TVOC']) - .timestamp(new Date()) + const p = Point.measurement('stat') + .setTag('Device', data['Device']) + .setFloatField('Temperature', data['Temperature']) + .setFloatField('Humidity', data['Humidity']) + .setFloatField('Pressure', data['Pressure']) + .setIntField('CO2', data['CO2']) + .setIntField('TVOC', data['TVOC']) + .setTimestamp(new Date()) try { view.setWriteInfo('writing') diff --git a/examples/downsampling/README.md b/examples/downsampling/README.md new file mode 100644 index 00000000..1f609bae --- /dev/null +++ b/examples/downsampling/README.md @@ -0,0 +1,32 @@ +## Downsampling Example + +- [index.ts](./src/index.ts) - How to use queries to structure data for downsampling + +## prerequisites + +- `node` and `yarn` installed + +- build influxdb-client: *(in project root directory)* + - run `yarn install` + - run `yarn build` + +## Usage + +set environment variables. + +- `INFLUXDB_URL` region of your influxdb cloud e.g. *`https://us-east-1-1.aws.cloud2.influxdata.com/`* +- `INFLUXDB_TOKEN` read/write token generated in cloud +- `INFLUXDB_DATABASE` name of database e.g .*`my-database`* + +For simplicity, you can use dotenv library to load environment variables in this example. Create `.env` file and paste your variables as follows: + +```conf +INFLUXDB_URL="" +INFLUXDB_DATABASE="" +INFLUXDB_TOKEN="" +``` + +### Run example + +- run `yarn install` +- run `yarn dev` diff --git a/examples/downsampling/package.json b/examples/downsampling/package.json new file mode 100644 index 00000000..603774b2 --- /dev/null +++ b/examples/downsampling/package.json @@ -0,0 +1,17 @@ +{ + "name": "influxdb3-client-example-downsampling", + "main": "index.js", + "license": "MIT", + "private": true, + "scripts": { + "dev": "ts-node -r dotenv/config ./src/index.ts" + }, + "dependencies": { + "@influxdata/influxdb3-client": "link:../../packages/client" + }, + "devDependencies": { + "dotenv": "^16.3.1", + "ts-node": "^10.9.1", + "typescript": "^5.1.3" + } +} diff --git a/examples/downsampling/src/index.ts b/examples/downsampling/src/index.ts new file mode 100644 index 00000000..53f1337f --- /dev/null +++ b/examples/downsampling/src/index.ts @@ -0,0 +1,84 @@ +import {InfluxDBClient} from '@influxdata/influxdb3-client' + +/* get environment value or throw error if missing */ +const getEnv = (variableName: string): string => { + if (process.env[variableName] == null) + throw new Error(`missing ${variableName} environment variable`) + return process.env[variableName] as string +} + +/* eslint-disable no-console */ +async function main() { + // + // Use environment variables to initialize client + // + const host = getEnv('INFLUXDB_URL') + const token = getEnv('INFLUXDB_TOKEN') + const database = getEnv('INFLUXDB_DATABASE') + + // + // Create a new client using an InfluxDB server base URL and an authentication token + // + const client = new InfluxDBClient({host, token, database}) + + try { + // + // Write data + // + await client.write(`stat,unit=temperature avg=24.5,max=45.0`) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + await client.write(`stat,unit=temperature avg=28,max=40.3`) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + await client.write(`stat,unit=temperature avg=20.5,max=49.0`) + + // + // Query downsampled data + // + const downSamplingQuery = `\ + SELECT + date_bin('5 minutes', "time") as window_start, + AVG("avg") as avg, + MAX("max") as max + FROM "stat" + WHERE + "time" >= now() - interval '1 hour' + GROUP BY window_start + ORDER BY window_start ASC;` + + // + // Execute downsampling query into pointValues + // + const queryPointsResult = client.queryPoints( + downSamplingQuery, + database, + 'sql' + ) + + for await (const row of queryPointsResult) { + const timestamp = new Date(row.getFloatField('window_start') as number) + console.log( + `${timestamp.toISOString()}: avg is ${row.getField( + 'avg', + 'float' + )}, max is ${row.getField('max', 'float')}` + ) + + // + // write back downsampled date to 'stat_downsampled' measurement + // + const downSampledPoint = row + .asPoint('stat_downsampled') + .setTimestamp(timestamp) + + await client.write(downSampledPoint, database) + } + } catch (err) { + console.error(err) + } finally { + await client.close() + } +} + +main() diff --git a/examples/downsampling/tsconfig.json b/examples/downsampling/tsconfig.json new file mode 100644 index 00000000..f9fb3109 --- /dev/null +++ b/examples/downsampling/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["es2018"], + "strict": true, + "moduleResolution": "node", + "esModuleInterop": true + }, + "include": ["src/**/*.ts"], + "exclude": ["*.js"] +} diff --git a/examples/downsampling/yarn.lock b/examples/downsampling/yarn.lock new file mode 100644 index 00000000..a3409941 --- /dev/null +++ b/examples/downsampling/yarn.lock @@ -0,0 +1,547 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + +"@grpc/grpc-js@^1.8.16": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.9.3.tgz#811cc49966ab7ed96efa31d213e80d671fd13839" + integrity sha512-b8iWtdrYIeT5fdZdS4Br/6h/kuk0PW5EVBUGk1amSbrpL8DlktJD43CdcCWwRdd6+jgwHhADSbL9CsNnm6EUPA== + dependencies: + "@grpc/proto-loader" "^0.7.8" + "@types/node" ">=12.12.47" + +"@grpc/proto-loader@^0.7.8": + version "0.7.10" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.10.tgz#6bf26742b1b54d0a473067743da5d3189d06d720" + integrity sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.4" + yargs "^17.7.2" + +"@influxdata/influxdb3-client@link:../../packages/client": + version "0.0.0" + uid "" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@protobuf-ts/grpc-transport@^2.9.0": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@protobuf-ts/grpc-transport/-/grpc-transport-2.9.1.tgz#dd6429fa35dc22c0dcc76c5e3c3c0c10ea1c5c09" + integrity sha512-p3o69oQUqMX1dG0QcBsnK7/2h0ReEIfJRbZykMCumTn2uAc9znTfh74xB8aH8I5Q+sWphucG8mPytJ/QIW9WSA== + dependencies: + "@protobuf-ts/runtime" "^2.9.1" + "@protobuf-ts/runtime-rpc" "^2.9.1" + +"@protobuf-ts/grpcweb-transport@^2.9.0": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@protobuf-ts/grpcweb-transport/-/grpcweb-transport-2.9.1.tgz#523647acbf98de54b291a57e770e3127820ba879" + integrity sha512-42bvBX312qhPlosMNTZE9XI+lt58ISM5vEJKv/wOx2Fu70J0TdlLa4Bjz8xcuRlv4Pq1CA+94DC1IgNxNRsQdg== + dependencies: + "@protobuf-ts/runtime" "^2.9.1" + "@protobuf-ts/runtime-rpc" "^2.9.1" + +"@protobuf-ts/runtime-rpc@^2.9.0", "@protobuf-ts/runtime-rpc@^2.9.1": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.9.1.tgz#6a1c8f189005de5dc6bce7a18751ef3fe304c8eb" + integrity sha512-pzO20J6s07LTWcj8hKAXh/dAacU5HIVir6SANKXXH8G0pn0VIIB4FFECq5Hbv25/8PQoOGZ7iApq/DMHaSjGhg== + dependencies: + "@protobuf-ts/runtime" "^2.9.1" + +"@protobuf-ts/runtime@^2.9.1": + version "2.9.1" + resolved "https://registry.yarnpkg.com/@protobuf-ts/runtime/-/runtime-2.9.1.tgz#faec7653ca9c01ced49b0ee01818d46b4b3cf2ac" + integrity sha512-ZTc8b+pQ6bwxZa3qg9/IO/M/brRkvr0tic9cSGgAsDByfPrtatT2300wTIRLDk8X9WTW1tT+FhyqmcrbMHTeww== + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + +"@tsconfig/node10@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" + integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + +"@types/command-line-args@5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@types/command-line-args/-/command-line-args-5.2.0.tgz#adbb77980a1cc376bb208e3f4142e907410430f6" + integrity sha512-UuKzKpJJ/Ief6ufIaIzr3A/0XnluX7RvFgwkV89Yzvm77wCh1kFaFmqN8XEnGcN62EuHdedQjEMb8mYxFLGPyA== + +"@types/command-line-usage@5.0.2": + version "5.0.2" + resolved "https://registry.yarnpkg.com/@types/command-line-usage/-/command-line-usage-5.0.2.tgz#ba5e3f6ae5a2009d466679cc431b50635bf1a064" + integrity sha512-n7RlEEJ+4x4TS7ZQddTmNSxP+zziEG0TNsMfiRIxcIVXt71ENJ9ojeXmGO3wPoTdn7pJcU2xc3CJYMktNT6DPg== + +"@types/node@18.14.5": + version "18.14.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.14.5.tgz#4a13a6445862159303fc38586598a9396fc408b3" + integrity sha512-CRT4tMK/DHYhw1fcCEBwME9CSaZNclxfzVMe7GsO6ULSwsttbj70wSiX6rZdIjGblu93sTJxLdhNIT85KKI7Qw== + +"@types/node@>=12.12.47", "@types/node@>=13.7.0": + version "20.6.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.2.tgz#a065925409f59657022e9063275cd0b9bd7e1b12" + integrity sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw== + +"@types/pad-left@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@types/pad-left/-/pad-left-2.1.1.tgz#17d906fc75804e1cc722da73623f1d978f16a137" + integrity sha512-Xd22WCRBydkGSApl5Bw0PhAOHKSVjNL3E3AwzKaps96IMraPqy5BvZIsBVK6JLwdybUzjHnuWVwpDd0JjTfHXA== + +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.4.1: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +apache-arrow@^12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/apache-arrow/-/apache-arrow-12.0.1.tgz#dffd865850d1d94896f1e1aa8332d586fb9e7de1" + integrity sha512-g17ARsc/KEAzViy8PEFsDBlL4ZLx3BesgQCplDLgUWtY0aFWNdEmfaZsbbXVRDfQ21D7vbUKtu0ZWNgcbxDrig== + dependencies: + "@types/command-line-args" "5.2.0" + "@types/command-line-usage" "5.0.2" + "@types/node" "18.14.5" + "@types/pad-left" "2.1.1" + command-line-args "5.2.1" + command-line-usage "6.1.3" + flatbuffers "23.3.3" + json-bignum "^0.0.3" + pad-left "^2.1.0" + tslib "^2.5.0" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1, array-back@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +command-line-args@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.1.tgz#c44c32e437a57d7c51157696893c5909e9cec42e" + integrity sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.3.tgz#428fa5acde6a838779dfa30e44686f4b6761d957" + integrity sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw== + dependencies: + array-back "^4.0.2" + chalk "^2.4.2" + table-layout "^1.0.2" + typical "^5.2.0" + +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + +deep-extend@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dotenv@^16.3.1: + version "16.3.1" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + +flatbuffers@23.3.3: + version "23.3.3" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-23.3.3.tgz#23654ba7a98d4b866a977ae668fe4f8969f34a66" + integrity sha512-jmreOaAT1t55keaf+Z259Tvh8tR/Srry9K8dgCgvizhKSEr6gLGgaOJI2WFL5fkOpGOGRZwxUrlFn0GCmXUy6g== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +grpc-web@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/grpc-web/-/grpc-web-1.4.2.tgz#86995f76471ce6b2119106ec26f909b7b69e7d43" + integrity sha512-gUxWq42l5ldaRplcKb4Pw5O4XBONWZgz3vxIIXnfIeJj8Jc3wYiq2O4c9xzx/NGbbPEej4rhI62C9eTENwLGNw== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +json-bignum@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/json-bignum/-/json-bignum-0.0.3.tgz#41163b50436c773d82424dbc20ed70db7604b8d7" + integrity sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg== + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +long@^5.0.0: + version "5.2.3" + resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" + integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +pad-left@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pad-left/-/pad-left-2.1.0.tgz#16e6a3b2d44a8e138cb0838cc7cb403a4fc9e994" + integrity sha512-HJxs9K9AztdIQIAIa/OIazRAUW/L6B9hbQDxO4X07roW3eo9XqZc2ur9bn1StH9CnbbI9EgvejHQX7CBpCF1QA== + dependencies: + repeat-string "^1.5.4" + +protobufjs@^7.2.4: + version "7.2.5" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.5.tgz#45d5c57387a6d29a17aab6846dcc283f9b8e7f2d" + integrity sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/node" ">=13.7.0" + long "^5.0.0" + +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + +repeat-string@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table-layout@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + +ts-node@^10.9.1: + version "10.9.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" + integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + +tslib@^2.5.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +typescript@^5.1.3: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index 699482d2..9fa2a0bb 100644 --- a/packages/client/src/InfluxDBClient.ts +++ b/packages/client/src/InfluxDBClient.ts @@ -6,6 +6,7 @@ import {ClientOptions, QueryType, WriteOptions} from './options' import {IllegalArgumentError} from './errors' import {WritableData, writableDataToLineProtocol} from './util/generics' import {throwReturn} from './util/common' +import {PointValues} from './PointValues' const argumentErrorMessage = `\ Please specify the 'database' as a method parameter or use default configuration \ @@ -44,6 +45,13 @@ export default class InfluxDBClient { } } + /** + * Write data into specified database. + * @param data - data to write + * @param database - database to write into + * @param org - organization to write into + * @param writeOptions - write options + */ async write( data: WritableData, database?: string, @@ -60,6 +68,14 @@ export default class InfluxDBClient { ) } + /** + * Execute a query and return the results as an async generator. + * + * @param query - The query string. + * @param database - The name of the database to query. + * @param queryType - The type of query (default: 'sql'). + * @returns An async generator that yields maps of string keys to any values. + */ query( query: string, database?: string, @@ -74,6 +90,31 @@ export default class InfluxDBClient { ) } + /** + * Execute a query and return the results as an async generator. + * + * @param query - The query string. + * @param database - The name of the database to query. + * @param queryType - The type of query (default: 'sql'). + * @returns An async generator that yields PointValues object. + */ + queryPoints( + query: string, + database?: string, + queryType: QueryType = 'sql' + ): AsyncGenerator { + return this._queryApi.queryPoints( + query, + database ?? + this._options.database ?? + throwReturn(new Error(argumentErrorMessage)), + queryType + ) + } + + /** + * Closes the client and all its resources (connections, ...) + */ async close(): Promise { await this._writeApi.close() await this._queryApi.close() diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 4f46a144..e2b84978 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -2,31 +2,91 @@ import {TimeConverter} from './WriteApi' import {convertTimeToNanos, convertTime} from './util/time' import {escape} from './util/escape' import {WritePrecision} from './options' +import {PointFieldType, PointValues} from './PointValues' -export type PointRecord = { - measurement: string - fields: Record - tags?: Record - timestamp?: string | number | Date +const fieldToLPString: { + (type: 'float', value: number): string + (type: 'integer', value: number): string + (type: 'uinteger', value: number): string + (type: 'string', value: string): string + (type: 'boolean', value: boolean): string + (type: PointFieldType, value: number | string | boolean): string +} = (type: PointFieldType, value: number | string | boolean): string => { + switch (type) { + case 'string': + return escape.quoted(value as string) + case 'boolean': + return value ? 'T' : 'F' + case 'float': + return `${value}` + case 'integer': + return `${value}i` + case 'uinteger': + return `${value}u` + } } /** * Point defines values of a single measurement. */ export class Point { - private _name: string - private _tags: {[key: string]: string} = {} - private _time: string | number | Date | undefined - /** escaped field values */ - public fields: {[key: string]: string} = {} + private readonly _values: PointValues /** * Create a new Point with specified a measurement name. * * @param measurementName - the measurement name */ - constructor(measurementName?: string) { - if (measurementName) this._name = measurementName + private constructor(measurementName: string) + /** + * Create a new Point with given values. + * After creating Point, it's values shouldn't be modified directly by PointValues object. + * + * @param values - point values + */ + private constructor(values: PointValues) + private constructor(arg0?: PointValues | string) { + if (arg0 instanceof PointValues) { + this._values = arg0 + } else { + this._values = new PointValues() + } + + if (typeof arg0 === 'string') this._values.setMeasurement(arg0) + } + + /** + * Creates new Point with given measurement. + * + * @param name - measurement name + * @returns new Point + */ + public static measurement(name: string): Point { + return new Point(name) + } + + /** + * Creates new point from PointValues object. + * Can throw error if measurement missing. + * + * @param values - point values object with measurement + * @throws missing measurement + * @returns new point from values + */ + public static fromValues(values: PointValues): Point { + if (!values.getMeasurement() || values.getMeasurement() === '') { + throw new Error('Cannot convert values to point without measurement set!') + } + return new Point(values) + } + + /** + * Get measurement name. + * + * @returns measurement name + */ + public getMeasurement(): string { + return this._values.getMeasurement() as string } /** @@ -35,192 +95,370 @@ export class Point { * @param name - measurement name * @returns this */ - public measurement(name: string): Point { - this._name = name + public setMeasurement(name: string): Point { + if (name !== '') { + this._values.setMeasurement(name) + } return this } /** - * Adds a tag. The caller has to ensure that both name and value are not empty + * Get timestamp. Can be undefined if not set. + * + * @returns timestamp or undefined + */ + public getTimestamp(): Date | number | string | undefined { + return this._values.getTimestamp() + } + + /** + * Sets point timestamp. Timestamp can be specified as a Date (preferred), number, string + * or an undefined value. An undefined value instructs to assign a local timestamp using + * the client's clock. An empty string can be used to let the server assign + * the timestamp. A number value represents time as a count of time units since epoch, the + * exact time unit then depends on the {@link InfluxDBClient.write | precision} of the API + * that writes the point. + * + * Beware that the current time in nanoseconds can't precisely fit into a JS number, + * which can hold at most 2^53 integer number. Nanosecond precision numbers are thus supplied as + * a (base-10) string. An application can also use ES2020 BigInt to represent nanoseconds, + * BigInt's `toString()` returns the required high-precision string. + * + * Note that InfluxDB requires the timestamp to fit into int64 data type. + * + * @param value - point time + * @returns this + */ + public setTimestamp(value: Date | number | string | undefined): Point { + this._values.setTimestamp(value) + return this + } + + /** + * Gets value of tag with given name. Returns undefined if tag not found. + * + * @param name - tag name + * @returns tag value or undefined + */ + public getTag(name: string): string | undefined { + return this._values.getTag(name) + } + + /** + * Sets a tag. The caller has to ensure that both name and value are not empty * and do not end with backslash. * * @param name - tag name * @param value - tag value * @returns this */ - public tag(name: string, value: string): Point { - this._tags[name] = value + public setTag(name: string, value: string): Point { + this._values.setTag(name, value) return this } /** - * Adds a boolean field. + * Removes a tag with the specified name if it exists; otherwise, it does nothing. + * + * @param name - The name of the tag to be removed. + * @returns this + */ + public removeTag(name: string): Point { + this._values.removeTag(name) + return this + } + + /** + * Gets an array of tag names. + * + * @returns An array of tag names. + */ + public getTagNames(): string[] { + return this._values.getTagNames() + } + + /** + * Gets the float field value associated with the specified name. + * Throws if actual type of field with given name is not float. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match float type. + * @returns The float field value or undefined. + */ + public getFloatField(name: string): number | undefined { + return this._values.getFloatField(name) + } + + /** + * Sets a number field. * - * @param field - field name + * @param name - field name * @param value - field value * @returns this + * @throws NaN/Infinity/-Infinity is supplied */ - public booleanField(name: string, value: boolean | any): Point { - this.fields[name] = value ? 'T' : 'F' + public setFloatField(name: string, value: number | any): Point { + this._values.setFloatField(name, value) return this } /** - * Adds an integer field. + * Gets the integer field value associated with the specified name. + * Throws if actual type of field with given name is not integer. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match integer type. + * @returns The integer field value or undefined. + */ + public getIntegerField(name: string): number | undefined { + return this._values.getIntegerField(name) + } + + /** + * Sets an integer field. * * @param name - field name * @param value - field value * @returns this * @throws NaN or out of int64 range value is supplied */ - public intField(name: string, value: number | any): Point { - let val: number - if (typeof value === 'number') { - val = value - } else { - val = parseInt(String(value)) - } - if (isNaN(val) || val <= -9223372036854776e3 || val >= 9223372036854776e3) { - throw new Error(`invalid integer value for field '${name}': '${value}'!`) - } - this.fields[name] = `${Math.floor(val)}i` + public setIntegerField(name: string, value: number | any): Point { + this._values.setIntegerField(name, value) return this } /** - * Adds an unsigned integer field. + * Gets the uint field value associated with the specified name. + * Throws if actual type of field with given name is not uint. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match uint type. + * @returns The uint field value or undefined. + */ + public getUintegerField(name: string): number | undefined { + return this._values.getUintegerField(name) + } + + /** + * Sets an unsigned integer field. * * @param name - field name * @param value - field value * @returns this * @throws NaN out of range value is supplied */ - public uintField(name: string, value: number | any): Point { - if (typeof value === 'number') { - if (isNaN(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) { - throw new Error(`uint value for field '${name}' out of range: ${value}`) - } - this.fields[name] = `${Math.floor(value as number)}u` - } else { - const strVal = String(value) - for (let i = 0; i < strVal.length; i++) { - const code = strVal.charCodeAt(i) - if (code < 48 || code > 57) { - throw new Error( - `uint value has an unsupported character at pos ${i}: ${value}` - ) - } - } - if ( - strVal.length > 20 || - (strVal.length === 20 && - strVal.localeCompare('18446744073709551615') > 0) - ) { - throw new Error( - `uint value for field '${name}' out of range: ${strVal}` - ) - } - this.fields[name] = `${strVal}u` - } + public setUintegerField(name: string, value: number | any): Point { + this._values.setUintegerField(name, value) return this } /** - * Adds a number field. + * Gets the string field value associated with the specified name. + * Throws if actual type of field with given name is not string. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match string type. + * @returns The string field value or undefined. + */ + public getStringField(name: string): string | undefined { + return this._values.getStringField(name) + } + + /** + * Sets a string field. * * @param name - field name * @param value - field value * @returns this - * @throws NaN/Infinity/-Infinity is supplied */ - public floatField(name: string, value: number | any): Point { - let val: number - if (typeof value === 'number') { - val = value - } else { - val = parseFloat(value) - } - if (!isFinite(val)) { - throw new Error(`invalid float value for field '${name}': '${value}'!`) - } - - this.fields[name] = String(val) + public setStringField(name: string, value: string | any): Point { + this._values.setStringField(name, value) return this } /** - * Adds a string field. + * Gets the boolean field value associated with the specified name. + * Throws if actual type of field with given name is not boolean. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match boolean type. + * @returns The boolean field value or undefined. + */ + public getBooleanField(name: string): boolean | undefined { + return this._values.getBooleanField(name) + } + + /** + * Sets a boolean field. * * @param name - field name * @param value - field value * @returns this */ - public stringField(name: string, value: string | any): Point { - if (value !== null && value !== undefined) { - if (typeof value !== 'string') value = String(value) - this.fields[name] = escape.quoted(value) - } + public setBooleanField(name: string, value: boolean | any): Point { + this._values.setBooleanField(name, value) return this } /** - * Sets point timestamp. Timestamp can be specified as a Date (preferred), number, string - * or an undefined value. An undefined value instructs to assign a local timestamp using - * the client's clock. An empty string can be used to let the server assign - * the timestamp. A number value represents time as a count of time units since epoch, the - * exact time unit then depends on the {@link InfluxDBClient.write | precision} of the API - * that writes the point. + * Get field of numeric type. * - * Beware that the current time in nanoseconds can't precisely fit into a JS number, - * which can hold at most 2^53 integer number. Nanosecond precision numbers are thus supplied as - * a (base-10) string. An application can also use ES2020 BigInt to represent nanoseconds, - * BigInt's `toString()` returns the required high-precision string. + * @param name - field name + * @param type - field numeric type + * @throws Field type doesn't match actual type + * @returns this + */ + public getField( + name: string, + type: 'float' | 'integer' | 'uinteger' + ): number | undefined + /** + * Get field of string type. * - * Note that InfluxDB requires the timestamp to fit into int64 data type. + * @param name - field name + * @param type - field string type + * @throws Field type doesn't match actual type + * @returns this + */ + public getField(name: string, type: 'string'): string | undefined + /** + * Get field of boolean type. * - * @param value - point time + * @param name - field name + * @param type - field boolean type + * @throws Field type doesn't match actual type * @returns this */ - public timestamp(value: Date | number | string | undefined): Point { - this._time = value + public getField(name: string, type: 'boolean'): boolean | undefined + /** + * Get field without type check. + * + * @param name - field name + * @returns this + */ + public getField(name: string): number | string | boolean | undefined + public getField( + name: string, + type?: PointFieldType + ): number | string | boolean | undefined { + return this._values.getField(name, type as any) + } + + /** + * Gets the type of field with given name, if it exists. + * If the field is not present, returns undefined. + * + * @param name - field name + * @returns The field type or undefined. + */ + public getFieldType(name: string): PointFieldType | undefined { + return this._values.getFieldType(name) + } + + /** + * Sets field based on provided type. + * + * @param name - field name + * @param value - field value + * @param type - field type + * @returns this + */ + public setField(name: string, value: any, type?: PointFieldType): Point { + this._values.setField(name, value, type) return this } + /** + * Add fields according to their type. All numeric type is considered float + * + * @param fields - name-value map + * @returns this + */ + public setFields(fields: {[key: string]: number | boolean | string}): Point { + this._values.setFields(fields) + return this + } + + /** + * Removes a field with the specified name if it exists; otherwise, it does nothing. + * + * @param name - The name of the field to be removed. + * @returns this + */ + public removeField(name: string): Point { + this._values.removeField(name) + return this + } + + /** + * Gets an array of field names associated with this object. + * + * @returns An array of field names. + */ + public getFieldNames(): string[] { + return this._values.getFieldNames() + } + + /** + * Checks if this object has any fields. + * + * @returns true if fields are present, false otherwise. + */ + public hasFields(): boolean { + return this._values.hasFields() + } + + /** + * Creates a copy of this object. + * + * @returns A new instance with same values. + */ + copy(): Point { + return new Point(this._values.copy()) + } + /** * Creates an InfluxDB protocol line out of this instance. - * @param settings - settings control serialization of a point timestamp and can also add default tags, + * @param convertTimePrecision - settings control serialization of a point timestamp and can also add default tags, * nanosecond timestamp precision is used when no `settings` or no `settings.convertTime` is supplied. * @returns an InfluxDB protocol line out of this instance */ public toLineProtocol( convertTimePrecision?: TimeConverter | WritePrecision ): string | undefined { - if (!this._name) return undefined + if (!this._values.getMeasurement()) return undefined let fieldsLine = '' - Object.keys(this.fields) + this._values + .getFieldNames() .sort() - .forEach((x) => { - if (x) { - const val = this.fields[x] + .forEach((name) => { + if (name) { + const type = this._values.getFieldType(name) + const value = this._values.getField(name) + if (type === undefined || value === undefined) return + const lpStringValue = fieldToLPString(type, value) if (fieldsLine.length > 0) fieldsLine += ',' - fieldsLine += `${escape.tag(x)}=${val}` + fieldsLine += `${escape.tag(name)}=${lpStringValue}` } }) if (fieldsLine.length === 0) return undefined // no fields present let tagsLine = '' - const tags = this._tags - Object.keys(tags) - .sort() - .forEach((x) => { - if (x) { - const val = tags[x] - if (val) { - tagsLine += ',' - tagsLine += `${escape.tag(x)}=${escape.tag(val)}` - } + const tagNames = this._values.getTagNames() + tagNames.sort().forEach((x) => { + if (x) { + const val = this._values.getTag(x) + if (val) { + tagsLine += ',' + tagsLine += `${escape.tag(x)}=${escape.tag(val)}` } - }) - let time = this._time + } + }) + let time = this._values.getTimestamp() if (!convertTimePrecision) { time = convertTimeToNanos(time) @@ -230,39 +468,13 @@ export class Point { time = convertTimePrecision(time) } - return `${escape.measurement(this._name)}${tagsLine} ${fieldsLine}${ - time !== undefined ? ` ${time}` : '' - }` + return `${escape.measurement( + this.getMeasurement() + )}${tagsLine} ${fieldsLine}${time !== undefined ? ` ${time}` : ''}` } toString(): string { const line = this.toLineProtocol(undefined) return line ? line : `invalid point: ${JSON.stringify(this, undefined)}` } - - static fromRecord(record: PointRecord): Point { - const {measurement, fields, tags, timestamp} = record - - if (!measurement) - throw new Error('measurement must be defined on the Point record!') - - if (!fields) throw new Error('fields must be defined on the Point record!') - - const point = new Point(measurement) - if (timestamp !== undefined) point.timestamp(timestamp) - - for (const [name, value] of Object.entries(fields)) { - if (typeof value === 'number') point.floatField(name, value) - else if (typeof value === 'string') point.stringField(name, value) - else throw new Error(`unsuported type of field ${name}: ${typeof value}`) - } - - if (tags) - for (const [name, value] of Object.entries(tags)) { - if (typeof value === 'string') point.tag(name, value) - else throw new Error(`tag has to be string ${name}: ${typeof value}`) - } - - return point - } } diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts new file mode 100644 index 00000000..4adc7934 --- /dev/null +++ b/packages/client/src/PointValues.ts @@ -0,0 +1,504 @@ +import {Point} from './Point' + +export type PointFieldType = + | 'float' + | 'integer' + | 'uinteger' + | 'string' + | 'boolean' + +type FieldEntryFloat = ['float', number] +type FieldEntryInteger = ['integer', number] +type FieldEntryUinteger = ['uinteger', number] +type FieldEntryString = ['string', string] +type FieldEntryBoolean = ['boolean', boolean] + +type FieldEntry = + | FieldEntryFloat + | FieldEntryInteger + | FieldEntryUinteger + | FieldEntryString + | FieldEntryBoolean + +const inferType = ( + value: number | string | boolean | undefined +): PointFieldType | undefined => { + if (typeof value === 'number') return 'float' + else if (typeof value === 'string') return 'string' + else if (typeof value === 'boolean') return 'boolean' + else return undefined +} + +export class GetFieldTypeMissmatchError extends Error { + /* istanbul ignore next */ + constructor( + fieldName: string, + expectedType: PointFieldType, + actualType: PointFieldType + ) { + super( + `field ${fieldName} of type ${actualType} doesn't match expected type ${expectedType}!` + ) + this.name = 'GetFieldTypeMissmatchError' + Object.setPrototypeOf(this, GetFieldTypeMissmatchError.prototype) + } +} + +/** + * Point defines values of a single measurement. + */ +export class PointValues { + private _name: string | undefined + private _time: string | number | Date | undefined + private _tags: {[key: string]: string} = {} + private _fields: {[key: string]: FieldEntry} = {} + + /** + * Create an empty PointValues. + */ + constructor() {} + + /** + * Get measurement name. Can be undefined if not set. + * + * @returns measurement name or undefined + */ + getMeasurement(): string | undefined { + return this._name + } + + /** + * Sets point's measurement. + * + * @param name - measurement name + * @returns this + */ + public setMeasurement(name: string): PointValues { + this._name = name + return this + } + + /** + * Get timestamp. Can be undefined if not set. + * + * @returns timestamp or undefined + */ + public getTimestamp(): Date | number | string | undefined { + return this._time + } + + /** + * Sets point timestamp. Timestamp can be specified as a Date (preferred), number, string + * or an undefined value. An undefined value instructs to assign a local timestamp using + * the client's clock. An empty string can be used to let the server assign + * the timestamp. A number value represents time as a count of time units since epoch, the + * exact time unit then depends on the {@link InfluxDBClient.write | precision} of the API + * that writes the point. + * + * Beware that the current time in nanoseconds can't precisely fit into a JS number, + * which can hold at most 2^53 integer number. Nanosecond precision numbers are thus supplied as + * a (base-10) string. An application can also use ES2020 BigInt to represent nanoseconds, + * BigInt's `toString()` returns the required high-precision string. + * + * Note that InfluxDB requires the timestamp to fit into int64 data type. + * + * @param value - point time + * @returns this + */ + public setTimestamp(value: Date | number | string | undefined): PointValues { + this._time = value + return this + } + + /** + * Gets value of tag with given name. Returns undefined if tag not found. + * + * @param name - tag name + * @returns tag value or undefined + */ + public getTag(name: string): string | undefined { + return this._tags[name] + } + + /** + * Sets a tag. The caller has to ensure that both name and value are not empty + * and do not end with backslash. + * + * @param name - tag name + * @param value - tag value + * @returns this + */ + public setTag(name: string, value: string): PointValues { + this._tags[name] = value + return this + } + + /** + * Removes a tag with the specified name if it exists; otherwise, it does nothing. + * + * @param name - The name of the tag to be removed. + * @returns this + */ + public removeTag(name: string): PointValues { + delete this._tags[name] + return this + } + + /** + * Gets an array of tag names. + * + * @returns An array of tag names. + */ + public getTagNames(): string[] { + return Object.keys(this._tags) + } + + /** + * Gets the float field value associated with the specified name. + * Throws if actual type of field with given name is not float. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match float type. + * @returns The float field value or undefined. + */ + public getFloatField(name: string): number | undefined { + return this.getField(name, 'float') + } + + /** + * Sets a number field. + * + * @param name - field name + * @param value - field value + * @returns this + * @throws NaN/Infinity/-Infinity is supplied + */ + public setFloatField(name: string, value: number | any): PointValues { + let val: number + if (typeof value === 'number') { + val = value + } else { + val = parseFloat(value) + } + if (!isFinite(val)) { + throw new Error(`invalid float value for field '${name}': '${value}'!`) + } + + this._fields[name] = ['float', val] + return this + } + + /** + * Gets the integer field value associated with the specified name. + * Throws if actual type of field with given name is not integer. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match integer type. + * @returns The integer field value or undefined. + */ + public getIntegerField(name: string): number | undefined { + return this.getField(name, 'integer') + } + + /** + * Sets an integer field. + * + * @param name - field name + * @param value - field value + * @returns this + * @throws NaN or out of int64 range value is supplied + */ + public setIntegerField(name: string, value: number | any): PointValues { + let val: number + if (typeof value === 'number') { + val = value + } else { + val = parseInt(String(value)) + } + if (isNaN(val) || val <= -9223372036854776e3 || val >= 9223372036854776e3) { + throw new Error(`invalid integer value for field '${name}': '${value}'!`) + } + this._fields[name] = ['integer', Math.floor(val)] + return this + } + + /** + * Gets the uint field value associated with the specified name. + * Throws if actual type of field with given name is not uint. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match uint type. + * @returns The uint field value or undefined. + */ + public getUintegerField(name: string): number | undefined { + return this.getField(name, 'uinteger') + } + + /** + * Sets an unsigned integer field. + * + * @param name - field name + * @param value - field value + * @returns this + * @throws NaN out of range value is supplied + */ + public setUintegerField(name: string, value: number | any): PointValues { + if (typeof value === 'number') { + if (isNaN(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) { + throw new Error(`uint value for field '${name}' out of range: ${value}`) + } + this._fields[name] = ['uinteger', Math.floor(value as number)] + } else { + const strVal = String(value) + for (let i = 0; i < strVal.length; i++) { + const code = strVal.charCodeAt(i) + if (code < 48 || code > 57) { + throw new Error( + `uint value has an unsupported character at pos ${i}: ${value}` + ) + } + } + if ( + strVal.length > 20 || + (strVal.length === 20 && + strVal.localeCompare('18446744073709551615') > 0) + ) { + throw new Error( + `uint value for field '${name}' out of range: ${strVal}` + ) + } + this._fields[name] = ['uinteger', +strVal] + } + return this + } + + /** + * Gets the string field value associated with the specified name. + * Throws if actual type of field with given name is not string. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match string type. + * @returns The string field value or undefined. + */ + public getStringField(name: string): string | undefined { + return this.getField(name, 'string') + } + + /** + * Sets a string field. + * + * @param name - field name + * @param value - field value + * @returns this + */ + public setStringField(name: string, value: string | any): PointValues { + if (value !== null && value !== undefined) { + if (typeof value !== 'string') value = String(value) + this._fields[name] = ['string', value] + } + return this + } + + /** + * Gets the boolean field value associated with the specified name. + * Throws if actual type of field with given name is not boolean. + * If the field is not present, returns undefined. + * + * @param name - field name + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match boolean type. + * @returns The boolean field value or undefined. + */ + public getBooleanField(name: string): boolean | undefined { + return this.getField(name, 'boolean') + } + + /** + * Sets a boolean field. + * + * @param name - field name + * @param value - field value + * @returns this + */ + public setBooleanField(name: string, value: boolean | any): PointValues { + this._fields[name] = ['boolean', !!value] + return this + } + + /** + * Get field of numeric type. + * Throws if actual type of field with given name is not given numeric type. + * If the field is not present, returns undefined. + * + * @param name - field name + * @param type - field numeric type + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match provided numeric type. + * @returns this + */ + public getField( + name: string, + type: 'float' | 'integer' | 'uinteger' + ): number | undefined + /** + * Get field of string type. + * Throws if actual type of field with given name is not string. + * If the field is not present, returns undefined. + * + * @param name - field name + * @param type - field string type + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match provided 'string' type. + * @returns this + */ + public getField(name: string, type: 'string'): string | undefined + /** + * Get field of boolean type. + * Throws if actual type of field with given name is not boolean. + * If the field is not present, returns undefined. + * + * @param name - field name + * @param type - field boolean type + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match provided 'boolean' type. + * @returns this + */ + public getField(name: string, type: 'boolean'): boolean | undefined + /** + * Get field without type check. + * If the field is not present, returns undefined. + * + * @param name - field name + * @returns this + */ + public getField(name: string): number | string | boolean | undefined + public getField( + name: string, + type?: PointFieldType + ): number | string | boolean | undefined { + const fieldEntry = this._fields[name] + if (!fieldEntry) return undefined + const [actualType, value] = fieldEntry + if (type !== undefined && type !== actualType) + throw new GetFieldTypeMissmatchError(name, type, actualType) + return value + } + + /** + * Gets the type of field with given name, if it exists. + * If the field is not present, returns undefined. + * + * @param name - field name + * @returns The field type or undefined. + */ + public getFieldType(name: string): PointFieldType | undefined { + const fieldEntry = this._fields[name] + if (!fieldEntry) return undefined + return fieldEntry[0] + } + + /** + * Sets field based on provided type. + * + * @param name - field name + * @param value - field value + * @param type - field type + * @returns this + */ + public setField( + name: string, + value: any, + type?: PointFieldType + ): PointValues { + const inferedType = type ?? inferType(value) + switch (inferedType) { + case 'string': + return this.setStringField(name, value) + case 'boolean': + return this.setBooleanField(name, value) + case 'float': + return this.setFloatField(name, value) + case 'integer': + return this.setIntegerField(name, value) + case 'uinteger': + return this.setUintegerField(name, value) + case undefined: + return this + default: + throw new Error( + `invalid field type for field '${name}': type -> ${type}, value -> ${value}!` + ) + } + } + + /** + * Add fields according to their type. All numeric type is considered float + * + * @param fields - name-value map + * @returns this + */ + public setFields(fields: { + [key: string]: number | boolean | string + }): PointValues { + for (const [name, value] of Object.entries(fields)) { + this.setField(name, value) + } + return this + } + + /** + * Removes a field with the specified name if it exists; otherwise, it does nothing. + * + * @param name - The name of the field to be removed. + * @returns this + */ + public removeField(name: string): PointValues { + delete this._fields[name] + return this + } + + /** + * Gets an array of field names associated with this object. + * + * @returns An array of field names. + */ + public getFieldNames(): string[] { + return Object.keys(this._fields) + } + + /** + * Checks if this object has any fields. + * + * @returns true if fields are present, false otherwise. + */ + public hasFields(): boolean { + return this.getFieldNames().length > 0 + } + + /** + * Creates a copy of this object. + * + * @returns A new instance with same values. + */ + copy(): PointValues { + const copy = new PointValues() + copy._name = this._name + copy._time = this._time + copy._tags = Object.fromEntries(Object.entries(this._tags)) + copy._fields = Object.fromEntries( + Object.entries(this._fields).map((entry) => [...entry]) + ) + return copy + } + + /** + * Creates new Point with this as values. + * + * @returns Point from this values. + */ + public asPoint(measurement?: string): Point { + return Point.fromValues( + measurement ? this.setMeasurement(measurement) : this + ) + } +} diff --git a/packages/client/src/QueryApi.ts b/packages/client/src/QueryApi.ts index f3b78da2..394937e9 100644 --- a/packages/client/src/QueryApi.ts +++ b/packages/client/src/QueryApi.ts @@ -1,3 +1,4 @@ +import {PointValues} from './PointValues' import {QueryType} from './options' /** @@ -18,5 +19,19 @@ export default interface QueryApi { queryType: QueryType ): AsyncGenerator, void, void> + /** + * Execute a query and return the results as an async generator. + * + * @param query - The query string. + * @param database - The name of the database to query. + * @param queryType - The type of query (default: 'sql'). + * @returns An async generator that yields PointValues object. + */ + queryPoints( + query: string, + database: string, + queryType: QueryType + ): AsyncGenerator + close(): Promise } diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index 3de2692c..cd2927c9 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -1,4 +1,4 @@ -import {RecordBatchReader} from 'apache-arrow' +import {RecordBatchReader, Type as ArrowType} from 'apache-arrow' import QueryApi from '../QueryApi' import {Ticket} from '../generated/flight/Flight' import {FlightServiceClient} from '../generated/flight/Flight.client' @@ -6,6 +6,7 @@ import {ConnectionOptions, QueryType} from '../options' import {createInt32Uint8Array} from '../util/common' import {RpcMetadata, RpcOptions} from '@protobuf-ts/runtime-rpc' import {impl} from './implSelector' +import {PointFieldType, PointValues} from '../PointValues' export default class QueryApiImpl implements QueryApi { private _closed = false @@ -17,11 +18,11 @@ export default class QueryApiImpl implements QueryApi { this._flightClient = new FlightServiceClient(this._transport) } - async *query( + private async *_queryRawBatches( query: string, database: string, queryType: QueryType - ): AsyncGenerator, void, void> { + ) { if (this._closed) { throw new Error('queryApi: already closed!') } @@ -57,7 +58,17 @@ export default class QueryApiImpl implements QueryApi { const reader = await RecordBatchReader.from(binaryStream) - for await (const batch of reader) { + yield* reader + } + + async *query( + query: string, + database: string, + queryType: QueryType + ): AsyncGenerator, void, void> { + const batches = this._queryRawBatches(query, database, queryType) + + for await (const batch of batches) { for (let rowIndex = 0; rowIndex < batch.numRows; rowIndex++) { const row: Record = {} for (let columnIndex = 0; columnIndex < batch.numCols; columnIndex++) { @@ -71,6 +82,60 @@ export default class QueryApiImpl implements QueryApi { } } + async *queryPoints( + query: string, + database: string, + queryType: QueryType + ): AsyncGenerator { + const batches = this._queryRawBatches(query, database, queryType) + + for await (const batch of batches) { + for (let rowIndex = 0; rowIndex < batch.numRows; rowIndex++) { + const values = new PointValues() + for (let columnIndex = 0; columnIndex < batch.numCols; columnIndex++) { + const columnSchema = batch.schema.fields[columnIndex] + const name = columnSchema.name + const value = batch.getChildAt(columnIndex)?.get(rowIndex) + const arrowTypeId = columnSchema.typeId + const metaType = columnSchema.metadata.get('iox::column::type') + + if (value === undefined || value === null) continue + + if ( + (name === 'measurement' || name == 'iox::measurement') && + typeof value === 'string' + ) { + values.setMeasurement(value) + continue + } + + if (!metaType) { + if (name === 'time' && arrowTypeId === ArrowType.Timestamp) { + values.setTimestamp(value) + } else { + values.setField(name, value) + } + + continue + } + + const [, , valueType, _fieldType] = metaType.split('::') + + if (valueType === 'field') { + if (_fieldType && value !== undefined && value !== null) + values.setField(name, value, _fieldType as PointFieldType) + } else if (valueType === 'tag') { + values.setTag(name, value) + } else if (valueType === 'timestamp') { + values.setTimestamp(value) + } + } + + yield values + } + } + } + async close(): Promise { this._closed = true this._transport.close?.() diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index 64b9a204..4087d025 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -8,5 +8,6 @@ export * from './util/time' export * from './util/generics' export {collectAll} from './util/common' export * from './Point' +export * from './PointValues' export {default as InfluxDBClient} from './InfluxDBClient' export {TimeConverter} from './WriteApi' diff --git a/packages/client/src/util/generics.ts b/packages/client/src/util/generics.ts index ff544a83..f2d4c080 100644 --- a/packages/client/src/util/generics.ts +++ b/packages/client/src/util/generics.ts @@ -1,12 +1,6 @@ -import {Point, PointRecord} from '../Point' +import {Point} from '../Point' import {isArrayLike, isDefined} from './common' -/** Prevents confusion with the ArrayLike type. Use with PointRecord */ -export type NotArrayLike = T & {length?: string} - -/** Prevents confusion with the PointRecord type. */ -export type NotPointRecord = T & {measurement?: void} - /** * The `WritableData` type represents different types of data that can be written. * The data can either be a uniform ArrayLike collection or a single value of the following types: @@ -14,36 +8,20 @@ export type NotPointRecord = T & {measurement?: void} * - `Point`: Represents a {@link Point} object. * * - `string`: Represents lines of the [Line Protocol](https://bit.ly/2QL99fu). - * - * - `PointRecord`: Represents an anonymous object. Note that a single `PointRecord` - * should not have a property of name length, as it could be misinterpreted as ArrayLike. - * If unsure, encapsulate your record in an array, i.e. [record]. */ -export type WritableData = - | NotPointRecord< - ArrayLike | ArrayLike | ArrayLike - > - | NotArrayLike - | string - | Point +export type WritableData = ArrayLike | ArrayLike | string | Point export const writableDataToLineProtocol = (data: WritableData): string[] => { const arrayData = ( isArrayLike(data) && typeof data !== 'string' ? Array.from(data as any) : [data] - ) as string[] | Point[] | PointRecord[] + ) as string[] | Point[] if (arrayData.length === 0) return [] const isLine = typeof arrayData[0] === 'string' - const isPoint = arrayData[0] instanceof Point return isLine ? (arrayData as string[]) - : (isPoint - ? (arrayData as Point[]) - : (arrayData as PointRecord[]).map(Point.fromRecord) - ) - .map((p) => p.toLineProtocol()) - .filter(isDefined) + : (arrayData as Point[]).map((p) => p.toLineProtocol()).filter(isDefined) } diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index 7c959cfe..11895c3c 100644 --- a/packages/client/test/integration/e2e.test.ts +++ b/packages/client/test/integration/e2e.test.ts @@ -1,6 +1,7 @@ import {expect} from 'chai' import {InfluxDBClient, Point} from '../../src' import {rejects} from 'assert' +import {PointValues} from '../../src' const getEnvVariables = () => { const { @@ -42,11 +43,11 @@ describe('e2e test', () => { const avg1 = getRandomInt(110, 500) const max1 = getRandomInt(900, 1000) - const point = new Point('stat') - .tag('unit', 'temperature') - .floatField('avg', avg1) - .floatField('max', max1) - .intField('testId', testId) + const point = Point.measurement('stat') + .setTag('unit', 'temperature') + .setFloatField('avg', avg1) + .setFloatField('max', max1) + .setIntegerField('testId', testId) await client.write(point, database) const query = ` @@ -74,6 +75,35 @@ describe('e2e test', () => { row = await data.next() expect(row.done).to.equal(true) + let dataPoints = client.queryPoints(query, database, queryType) + + let pointRow: IteratorResult + pointRow = await dataPoints.next() + + expect(pointRow.done).to.equal(false) + expect(pointRow.value?.getField('avg')).to.equal(avg1) + expect(pointRow.value?.getField('max')).to.equal(max1) + + pointRow = await dataPoints.next() + expect(pointRow.done).to.equal(true) + + // + // test aggregation query + // + const queryAggregation = ` + SELECT sum("avg") as "sum_avg", sum("max") as "sum_max" + FROM "stat" + WHERE "testId" = ${testId} + ` + + dataPoints = client.queryPoints(queryAggregation, database, queryType) + + pointRow = await dataPoints.next() + + expect(pointRow.done).to.equal(false) + expect(pointRow.value?.getField('sum_avg')).to.equal(avg1) + expect(pointRow.value?.getField('sum_max')).to.equal(max1) + await client.close() await rejects(client.query(query, database, queryType).next()) }) diff --git a/packages/client/test/unit/Influxdb.test.ts b/packages/client/test/unit/Influxdb.test.ts index 779d4bfa..86989b41 100644 --- a/packages/client/test/unit/Influxdb.test.ts +++ b/packages/client/test/unit/Influxdb.test.ts @@ -1,7 +1,80 @@ import {expect} from 'chai' +import sinon from 'sinon' import {InfluxDBClient, ClientOptions, Transport} from '../../src' +import type WriteApi from '../../src/WriteApi' +import type QueryApi from '../../src/QueryApi' +import {rejects} from 'assert' describe('InfluxDB', () => { + afterEach(() => { + sinon.restore() + }) + + it('uses options database', () => { + const database = 'my-db' + const client = new InfluxDBClient({ + host: 'http://localhost:8086', + database, + }) + const writeApi: WriteApi = (client as any)._writeApi + const queryApi: QueryApi = (client as any)._queryApi + const writeStub = sinon.stub(writeApi, 'doWrite') + const queryStub = sinon.stub(queryApi, 'query') + const queryPointsStub = sinon.stub(queryApi, 'queryPoints') + + const lines = ['lpdata'] + + client.write(lines) + + expect(writeStub.calledWith(lines, database)).to.be.true + writeStub.resetHistory() + + client.write(lines, 'another') + expect(writeStub.calledOnceWith(lines, 'another')).to.be.true + + const query = 'select *' + client.query(query) + + expect(queryStub.calledOnceWith(query, database, 'sql')).to.be.true + queryStub.resetHistory() + + client.query(query, 'another') + expect(queryStub.calledOnceWith(query, 'another', 'sql')).to.be.true + + // queryPoints + client.queryPoints(query) + + expect(queryPointsStub.calledOnceWith(query, database, 'sql')).to.be.true + queryPointsStub.resetHistory() + + client.queryPoints(query, 'another') + expect(queryPointsStub.calledOnceWith(query, 'another', 'sql')).to.be.true + }) + + it('throws when no database provided', async () => { + const client = new InfluxDBClient({ + host: 'http://localhost:8086', + }) + + expect(() => client.query('query')).to.throw(`\ +Please specify the 'database' as a method parameter or use default configuration \ +at 'ClientOptions.database' +`) + await rejects(client.write('data')) + }) + + it('throws when no database provided queryPoints', async () => { + const client = new InfluxDBClient({ + host: 'http://localhost:8086', + }) + + expect(() => client.queryPoints('query')).to.throw(`\ +Please specify the 'database' as a method parameter or use default configuration \ +at 'ClientOptions.database' +`) + await rejects(client.write('data')) + }) + describe('constructor', () => { it('is created from configuration with host', () => { expect( diff --git a/packages/client/test/unit/Write.test.ts b/packages/client/test/unit/Write.test.ts index 91978728..d120dc33 100644 --- a/packages/client/test/unit/Write.test.ts +++ b/packages/client/test/unit/Write.test.ts @@ -71,10 +71,16 @@ describe('Write', () => { await rejects(subject.write('text value=1', DATABASE)) await rejects(subject.write(['text value=1', 'text value=2'], DATABASE)) await rejects( - subject.write(new Point('test').floatField('value', 1), DATABASE) + subject.write( + Point.measurement('test').setFloatField('value', 1), + DATABASE + ) ) await rejects( - subject.write([new Point('test').floatField('value', 1)], DATABASE) + subject.write( + [Point.measurement('test').setFloatField('value', 1)], + DATABASE + ) ) }) }) @@ -129,10 +135,10 @@ describe('Write', () => { } }) .persist() - const point = new Point('test') - .tag('t', ' ') - .floatField('value', 1) - .timestamp('') + const point = Point.measurement('test') + .setTag('t', ' ') + .setFloatField('value', 1) + .setTimestamp('') failNextRequest = true await subject @@ -155,7 +161,7 @@ describe('Write', () => { requests = 0 // generates no lines, no requests done - await subject.write(new Point(), DATABASE) + await subject.write(Point.measurement('m'), DATABASE) await subject.write([], DATABASE) await subject.write('', DATABASE) expect(requests).to.equal(0) @@ -163,12 +169,18 @@ describe('Write', () => { expect(logs.warn).has.length(0) const points = [ - new Point('test').floatField('value', 1).timestamp('1'), - new Point('test').floatField('value', 2).timestamp(2.1), - new Point('test').floatField('value', 3).timestamp(new Date(3)), - new Point('test') - .floatField('value', 4) - .timestamp(false as any as string), // server decides what to do with such values + Point.measurement('test') + .setFloatField('value', 1) + .setTimestamp('1'), + Point.measurement('test') + .setFloatField('value', 2) + .setTimestamp(2.1), + Point.measurement('test') + .setFloatField('value', 3) + .setTimestamp(new Date(3)), + Point.measurement('test') + .setFloatField('value', 4) + .setTimestamp(false as any as string), // server decides what to do with such values ] await subject.write(points, DATABASE) expect(logs.error).to.length(0) @@ -224,7 +236,10 @@ describe('Write', () => { return [204, '', {}] }) .persist() - await subject.write(new Point('test').floatField('value', 1), DATABASE) + await subject.write( + Point.measurement('test').setFloatField('value', 1), + DATABASE + ) expect(logs.error).has.length(0) expect(logs.warn).has.length(0) expect(authorization).equals(`Token customToken`) @@ -241,7 +256,10 @@ describe('Write', () => { return [204, '', {}] }) .persist() - await subject.write(new Point('test').floatField('value', 1), DATABASE) + await subject.write( + Point.measurement('test').setFloatField('value', 1), + DATABASE + ) await subject.close() expect(logs.error).has.length(0) expect(logs.warn).deep.equals([]) diff --git a/packages/client/test/unit/util/generics.test.ts b/packages/client/test/unit/util/generics.test.ts index 9e668ce6..c5415320 100644 --- a/packages/client/test/unit/util/generics.test.ts +++ b/packages/client/test/unit/util/generics.test.ts @@ -1,5 +1,5 @@ import {expect} from 'chai' -import {Point, PointRecord, convertTimeToNanos} from '../../../src' +import {Point} from '../../../src' import { WritableData, writableDataToLineProtocol, @@ -22,7 +22,7 @@ describe('writableDataToLineProtocol', () => { }) it('should convert single Point to line protocol', () => { - const point = new Point('test').floatField('blah', 123.6) + const point = Point.measurement('test').setFloatField('blah', 123.6) const output = writableDataToLineProtocol(point) expect(output.length).to.equal(1) expect(output[0]).satisfies((x: string) => { @@ -31,10 +31,14 @@ describe('writableDataToLineProtocol', () => { }) it('should convert array-like Point to line protocol', () => { - const point1 = new Point('test').floatField('blah', 123.6) + const point1 = Point.measurement('test').setFloatField('blah', 123.6) const date = Date.now() - const point2 = new Point('test').floatField('blah', 456.7).timestamp(date) - const point3 = new Point('test').floatField('blah', 789.8).timestamp('') + const point2 = Point.measurement('test') + .setFloatField('blah', 456.7) + .setTimestamp(date) + const point3 = Point.measurement('test') + .setFloatField('blah', 789.8) + .setTimestamp('') const input: WritableData = [point1, point2, point3] const output = writableDataToLineProtocol(input) expect(output.length).to.equal(3) @@ -44,51 +48,4 @@ describe('writableDataToLineProtocol', () => { expect(output[1]).to.equal(`test blah=456.7 ${date}`) expect(output[2]).to.equal('test blah=789.8') }) - - it('should convert PointRecord to line protocol', () => { - const pointRecord: PointRecord = { - measurement: 'foo', - fields: { - bar: 3.14, - }, - } - const output = writableDataToLineProtocol(pointRecord) - expect(output.length).to.equal(1) - expect(output[0]).satisfies((x: string) => { - return x.startsWith('foo bar=3.14') - }, `does not start with 'foo bar=3.14'`) - }) - - it('should convert array-like PointRecord to line protocol', () => { - const date = Date.now() - const date2 = new Date() - const pointRecord1: PointRecord = { - measurement: 'foo', - fields: { - bar: 3.14, - }, - timestamp: '', - } - const pointRecord2: PointRecord = { - measurement: 'baz', - fields: { - bar: 6.28, - }, - timestamp: date, - } - const pointRecord3: PointRecord = { - measurement: 'qux', - fields: { - bar: 9.42, - }, - timestamp: date2, - } - const input: WritableData = [pointRecord1, pointRecord2, pointRecord3] - const output = writableDataToLineProtocol(input) - expect(output).to.deep.equal([ - 'foo bar=3.14', - `baz bar=6.28 ${date}`, - `qux bar=9.42 ${convertTimeToNanos(date2)}`, - ]) - }) }) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index 3eff7572..29e6fbf8 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -1,48 +1,52 @@ import {expect} from 'chai' -import {Point, PointRecord, convertTime} from '../../../src' +import {Point, PointValues, convertTime} from '../../../src' describe('point', () => { it('creates point with various fields', () => { - const point = new Point() - .measurement('blah') - .booleanField('truthy', true) - .booleanField('falsy', false) - .intField('intFromString', '20') - .floatField('floatFromString', '60.3') - .timestamp('') + const point = Point.measurement('blah') + .setMeasurement('') + .setBooleanField('truthy', true) + .setBooleanField('falsy', false) + .setIntegerField('intFromString', '20') + .setFloatField('floatFromString', '60.3') + .setStringField('str', 'abc') + .setTimestamp('') expect(point.toLineProtocol()).to.equal( - 'blah falsy=F,floatFromString=60.3,intFromString=20i,truthy=T' + 'blah falsy=F,floatFromString=60.3,intFromString=20i,str="abc",truthy=T' ) }) it('fails on invalid fields', () => { expect(() => { - new Point().intField('fails', NaN) + Point.measurement('a').setIntegerField('fails', NaN) }).to.throw(`invalid integer value for field 'fails': 'NaN'`) expect(() => { - new Point().intField('fails', Infinity) + Point.measurement('a').setIntegerField('fails', Infinity) }).to.throw(`invalid integer value for field 'fails': 'Infinity'!`) expect(() => { - new Point().intField('fails', 9223372036854776e3) + Point.measurement('a').setIntegerField('fails', 9223372036854776e3) }).to.throw( `invalid integer value for field 'fails': '9223372036854776000'!` ) expect(() => { - new Point().floatField('fails', Infinity) + Point.measurement('a').setFloatField('fails', Infinity) }).to.throw(`invalid float value for field 'fails': 'Infinity'!`) expect(() => { - new Point().uintField('fails', NaN) + Point.measurement('a').setUintegerField('fails', NaN) }).to.throw(`uint value for field 'fails' out of range: NaN`) expect(() => { - new Point().uintField('fails', -1) + Point.measurement('a').setUintegerField('fails', -1) }).to.throw(`uint value for field 'fails' out of range: -1`) expect(() => { - new Point().uintField('fails', Number.MAX_SAFE_INTEGER + 10) + Point.measurement('a').setUintegerField( + 'fails', + Number.MAX_SAFE_INTEGER + 10 + ) }).to.throw( `uint value for field 'fails' out of range: ${ Number.MAX_SAFE_INTEGER + 10 @@ -50,98 +54,178 @@ describe('point', () => { ) expect(() => { - new Point().uintField('fails', '10a8') + Point.measurement('a').setUintegerField('fails', '10a8') }).to.throw(`uint value has an unsupported character at pos 2: 10a8`) expect(() => { - new Point().uintField('fails', '18446744073709551616') + Point.measurement('a').setUintegerField('fails', '18446744073709551616') }).to.throw( `uint value for field 'fails' out of range: 18446744073709551616` ) }) + it('infers type when no type supported', () => { + const point = Point.measurement('a') + .setFields({ + float: 20.3, + float2: 20, + string: 'text', + bool: true, + nothing: undefined as any, + }) + .setTimestamp('') + expect(point.toLineProtocol()).to.equal( + 'a bool=T,float=20.3,float2=20,string="text"' + ) + }) + + it('throws when invalid type for method field is provided', () => { + expect(() => { + Point.measurement('a').setField('errorlike', undefined, 'bad-type' as any) + }).to.throw( + `invalid field type for field 'errorlike': type -> bad-type, value -> undefined!` + ) + }) + + it('adds field using field method', () => { + const point = Point.measurement('blah') + .setField('truthy', true, 'boolean') + .setField('falsy', false, 'boolean') + .setField('intFromString', '20', 'integer') + .setField('uintFromString', '30', 'uinteger') + .setField('floatFromString', '60.3', 'float') + .setField('str', 'abc', 'string') + .setTimestamp('') + expect(point.toLineProtocol()).to.equal( + 'blah falsy=F,floatFromString=60.3,intFromString=20i,str="abc",truthy=T,uintFromString=30u' + ) + }) + it('creates point with uint fields', () => { - const point = new Point('a') - .uintField('floored', 10.88) - .uintField('fromString', '789654123') - .timestamp('') + const point = Point.measurement('a') + .setUintegerField('floored', 10.88) + .setUintegerField('fromString', '789654123') + .setTimestamp('') expect(point.toLineProtocol()).to.equal( 'a floored=10u,fromString=789654123u' ) }) - describe('convert record to point', () => { - it('should correctly convert PointRecord to Point', () => { - const record: PointRecord = { - measurement: 'testMeasurement', - timestamp: 1624512793, - fields: { - text: 'testString', - value: 123.45, - }, - } - const point = Point.fromRecord(record) - expect(point.toLineProtocol()).equals( - 'testMeasurement text="testString",value=123.45 1624512793' - ) + it('returns field of with getField and throws if type not match', () => { + const point = Point.measurement('a').setFields({ + float: 20.3, + float2: 20, + string: 'text', + bool: true, + nothing: undefined as any, }) - it('should accept string as timestamp', () => { - const record: PointRecord = { - measurement: 'testMeasurement', - timestamp: '', - fields: { - text: 'testString', - value: 123.45, - }, - } - const point = Point.fromRecord(record) - expect(point.toLineProtocol()).equals( - 'testMeasurement text="testString",value=123.45' - ) - }) - it('should accept Date as timestamp', () => { - const date = new Date() - const record: PointRecord = { - measurement: 'testMeasurement', - timestamp: date, - fields: { - text: 'testString', - value: 123.45, - }, - } - const point = Point.fromRecord(record) - expect(point.toLineProtocol()).equals( - `testMeasurement text="testString",value=123.45 ${convertTime(date)}` - ) - }) - it('should fail on invalid record', () => { - expect(() => { - // no measurement - Point.fromRecord({} as PointRecord) - }).to.throw('measurement must be defined on the Point record!') + expect(point.getField('float', 'float')).to.equal(20.3) + expect(point.getField('float2', 'float')).to.equal(20) + expect(point.getField('string', 'string')).to.equal('text') + expect(point.getField('bool', 'boolean')).to.equal(true) + expect(() => { + point.getField('bool', 'float') + }).to.throw(`field bool of type boolean doesn't match expected type float!`) + expect(() => { + point.getField('string', 'boolean') + }).to.throw( + `field string of type string doesn't match expected type boolean!` + ) + }) - expect(() => { - // no fields prop - Point.fromRecord({measurement: 'a'} as PointRecord) - }).to.throw('fields must be defined on the Point record!') + it('creates deep copy of point', () => { + const point = Point.measurement('measure1') + .setBooleanField('truthy', true) + .setBooleanField('falsy', false) + .setIntegerField('intFromString', '20') + .setUintegerField('intFromString', '20') + .setFloatField('floatFromString', '60.3') + .setStringField('str', 'abc') + .setTimestamp('') - expect(() => { - // invalid field type - Point.fromRecord({ - measurement: 'a', - fields: {a: {}}, - } as any as PointRecord) - }).to.throw('unsuported type of field') + const copy = point.copy() - expect(() => { - // invalid tag type - Point.fromRecord({ - measurement: 'a', - fields: {}, - tags: {a: 8}, - } as any as PointRecord) - }).to.throw('tag has to be string') - }) + expect(copy.toLineProtocol()).to.equal(point.toLineProtocol()) + + copy.setIntegerField('truthy', 1) + + expect(copy.toLineProtocol()).to.not.equal(point.toLineProtocol()) + }) + + it('change measurement', () => { + const point = Point.measurement('measurement').setBooleanField( + 'truthy', + true + ) + + expect('measurement').to.equal(point.getMeasurement()) + + point.setMeasurement('measurement2') + expect('measurement2').to.equal(point.getMeasurement()) + }) + + it('get typed fields', () => { + const point = Point.measurement('measurement') + .setMeasurement('a') + .setIntegerField('b', 1) + .setField('c', 'xyz') + .setField('d', false) + .setField('e', 3.45) + .setUintegerField('f', 8) + .setStringField('g', 88) + .setStringField('h', undefined) + .setBooleanField('i', true) + .setTimestamp(150) + + expect(1).to.equal(point.getIntegerField('b')) + expect('88').to.equal(point.getStringField('g')) + expect(8).to.equal(point.getUintegerField('f')) + expect(true).to.equal(point.getBooleanField('i')) + expect(3.45).to.equal(point.getFloatField('e')) + }) + + it('get field type', () => { + const point = Point.measurement('measurement').setField('a', 3.45) + + expect('float').to.equal(point.getFieldType('a')) + }) + + it('get timestamp', () => { + const point = Point.measurement('measurement') + .setField('a', 3.45) + .setTimestamp(156) + + expect(156).to.equal(point.getTimestamp()) + }) + + it('tags', () => { + const point = Point.measurement('measurement') + .setTag('tag', 'b') + .setField('a', 3.45) + .setTimestamp(156) + + expect('b').to.equal(point.getTag('tag')) + expect(undefined).to.equal(point.getTag('xyz')) + expect(['tag']).to.deep.equal(point.getTagNames()) + + point.removeTag('tag') + expect([]).to.deep.equal(point.getTagNames()) + }) + + it('has fields', () => { + const point1 = Point.measurement('measurement') + .setTag('c', 'd') + .setTimestamp(150) + expect(false).equals(point1.hasFields()) + const point2 = Point.measurement('a') + .setField('b', 1) + .setTag('c', 'd') + .setTimestamp(150) + expect(true).equals(point2.hasFields()) + expect(['b']).to.deep.equals(point2.getFieldNames()) + + point2.removeField('b') + expect(false).equals(point2.hasFields()) }) describe('convert point time to line protocol', () => { @@ -150,20 +234,20 @@ describe('point', () => { convertTime(value, precision) it('converts empty string to no timestamp', () => { - const p = new Point('a').floatField('b', 1).timestamp('') + const p = Point.measurement('a').setFloatField('b', 1).setTimestamp('') expect(p.toLineProtocol(clinetConvertTime)).equals('a b=1') }) it('converts number to timestamp', () => { - const p = new Point('a').floatField('b', 1).timestamp(1.2) + const p = Point.measurement('a').setFloatField('b', 1).setTimestamp(1.2) expect(p.toLineProtocol(clinetConvertTime)).equals('a b=1 1') }) it('converts Date to timestamp', () => { const d = new Date() - const p = new Point('a').floatField('b', 1).timestamp(d) + const p = Point.measurement('a').setFloatField('b', 1).setTimestamp(d) expect(p.toLineProtocol(precision)).equals(`a b=1 ${d.getTime()}`) }) it('converts undefined to local timestamp', () => { - const p = new Point('a').floatField('b', 1) + const p = Point.measurement('a').setFloatField('b', 1) expect(p.toLineProtocol(precision)).satisfies((x: string) => { return x.startsWith('a b=1') }, `does not start with 'a b=1'`) @@ -172,8 +256,110 @@ describe('point', () => { }) }) it('toString() works same as toLineProtocol()', () => { - const p = new Point('a').floatField('b', 1).tag('c', 'd').timestamp('') + const p = Point.measurement('a') + .setFloatField('b', 1) + .setTag('c', 'd') + .setTimestamp('') expect(p.toLineProtocol()).equals(p.toString()) }) + it('without measurement', () => { + const p = Point.measurement('') + .setFloatField('b', 1) + .setTag('c', 'd') + .setTimestamp('') + expect(p.toLineProtocol()).equals(undefined) + }) + }) + + describe('point values', () => { + it('convert point values to point', () => { + const v = new PointValues() + .setMeasurement('a') + .setField('b', 1) + .setTag('c', 'd') + .setTimestamp(150) + const p = Point.fromValues(v) + expect('a,c=d b=1 150').equals(p.toString()) + }) + it('as point', () => { + const v = new PointValues() + .setMeasurement('a') + .setField('b', 1) + .setTag('c', 'd') + .setTimestamp(150) + let p = v.asPoint() + expect('a,c=d b=1 150').equals(p.toString()) + p = v.asPoint('x') + expect('x,c=d b=1 150').equals(p.toString()) + }) + it('convert point values to point with undefined measurement', () => { + const v = new PointValues() + .setMeasurement('') + .setField('b', 1) + .setTag('c', 'd') + .setTimestamp(150) + expect(() => { + Point.fromValues(v) + }).to.throw(`Cannot convert values to point without measurement set!`) + }) + it('has fields', () => { + const v1 = new PointValues() + .setMeasurement('a') + .setTag('c', 'd') + .setTimestamp(150) + expect(false).equals(v1.hasFields()) + const v2 = new PointValues() + .setMeasurement('a') + .setField('b', 1) + .setTag('c', 'd') + .setTimestamp(150) + expect(true).equals(v2.hasFields()) + }) + it('remove field', () => { + const v = new PointValues() + .setMeasurement('a') + .setField('b', 1) + .setTag('c', 'd') + .setTimestamp(150) + expect(true).eq(v.hasFields()) + v.removeField('b') + expect(false).equals(v.hasFields()) + }) + it('remove tag', () => { + const v = new PointValues() + .setMeasurement('a') + .setField('b', 1) + .setTag('c', 'd') + .setTimestamp(150) + expect(true).eq(v.getTagNames().includes('c')) + v.removeTag('c') + expect(false).eq(v.getTagNames().includes('c')) + }) + it('field values', () => { + const v = new PointValues() + .setMeasurement('a') + .setIntegerField('b', 1) + .setField('c', 'xyz') + .setField('d', false) + .setField('e', 3.45) + .setUintegerField('f', 8) + .setStringField('g', 88) + .setStringField('h', undefined) + .setTimestamp(150) + expect(1).deep.equals(v.getIntegerField('b')) + expect('xyz').deep.equals(v.getStringField('c')) + expect(false).deep.equals(v.getBooleanField('d')) + expect(3.45).deep.equals(v.getFloatField('e')) + expect(8).deep.equals(v.getUintegerField('f')) + expect('88').deep.equals(v.getStringField('g')) + }) + }) + it('undefined field', () => { + const v = new PointValues() + .setMeasurement('a') + .setField('c', 'xyz') + .setTimestamp(150) + expect(undefined).deep.equals(v.getField('x')) + expect(undefined).deep.equals(v.getFieldType('x')) }) })