From caeb221209fd23983bf8e0adf3dc039ac8d222b3 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 01/45] feat: queryPoints --- examples/basic/src/index.ts | 12 ++++++ packages/client/src/InfluxDBClient.ts | 20 ++++++++++ packages/client/src/QueryApi.ts | 15 ++++++++ packages/client/src/impl/QueryApiImpl.ts | 48 ++++++++++++++++++++++-- 4 files changed, 92 insertions(+), 3 deletions(-) diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 2dfaf929..155fc1fb 100644 --- a/examples/basic/src/index.ts +++ b/examples/basic/src/index.ts @@ -67,6 +67,18 @@ 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, + 'stat' + ) + + for await (const row of queryPointsResult) { + console.log(row.toLineProtocol()) + } } catch (err) { console.error(err) } finally { diff --git a/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index 699482d2..29e314b0 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 {Point} from './Point' const argumentErrorMessage = `\ Please specify the 'database' as a method parameter or use default configuration \ @@ -74,6 +75,25 @@ export default class InfluxDBClient { ) } + async *queryPoints( + query: string, + database: string, + queryType: QueryType, + measurement: string + ): AsyncGenerator { + const points = this._queryApi.queryPoints( + query, + database ?? + this._options.database ?? + throwReturn(new Error(argumentErrorMessage)), + queryType + ) + + for await (const point of points) { + yield point.measurement(measurement) + } + } + async close(): Promise { await this._writeApi.close() await this._queryApi.close() diff --git a/packages/client/src/QueryApi.ts b/packages/client/src/QueryApi.ts index f3b78da2..15f302ce 100644 --- a/packages/client/src/QueryApi.ts +++ b/packages/client/src/QueryApi.ts @@ -1,3 +1,4 @@ +import {Point} from './Point' 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 Point 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..cebec939 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -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 {Point} from '../Point' 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( + 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,37 @@ 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 point = new Point() + 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 type = columnSchema.metadata.get('iox::column::type')! + const [, , valueType, _fieldType] = type.split('::') + + if (valueType === 'field') { + point.fields[name] = value?.toString?.() + } else if (valueType === 'tag') { + point.tag(name, value) + } else if (valueType === 'timestamp') { + point.timestamp(value) + } + } + + yield point + } + } + } + async close(): Promise { this._closed = true this._transport.close?.() From 9efdefb4fbc2ee227d378b84aa448ca9c67c6883 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 02/45] fix: construct Point with relevant types --- packages/client/src/Point.ts | 34 ++++++++++++++++++++++++ packages/client/src/impl/QueryApiImpl.ts | 3 ++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 4f46a144..33726eae 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -10,6 +10,13 @@ export type PointRecord = { timestamp?: string | number | Date } +export type PointFieldType = + | 'float' + | 'integer' + | 'uinteger' + | 'string' + | 'boolean' + /** * Point defines values of a single measurement. */ @@ -163,6 +170,33 @@ export class Point { return this } + /** + * Adds field based on provided type. + * + * @param name - field name + * @param type - field type + * @param value - field value + * @returns this + */ + public field(name: string, type: PointFieldType, value: any): Point { + switch (type) { + case 'string': + return this.stringField(name, value) + case 'boolean': + return this.booleanField(name, value) + case 'float': + return this.floatField(name, value) + case 'integer': + return this.intField(name, value) + case 'uinteger': + return this.uintField(name, value) + default: + throw new Error( + `invalid field type for field '${name}': type -> ${type}, value -> ${value}!` + ) + } + } + /** * 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 diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index cebec939..9f7e8507 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -100,7 +100,8 @@ export default class QueryApiImpl implements QueryApi { const [, , valueType, _fieldType] = type.split('::') if (valueType === 'field') { - point.fields[name] = value?.toString?.() + if (_fieldType && value !== undefined && value !== null) + point.field(name, _fieldType as any, value) } else if (valueType === 'tag') { point.tag(name, value) } else if (valueType === 'timestamp') { From a67fe1549ebf072cf4834aea1c5b20a4410fe389 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 03/45] style: linted --- packages/client/src/impl/QueryApiImpl.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index 9f7e8507..92378318 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -18,7 +18,7 @@ export default class QueryApiImpl implements QueryApi { this._flightClient = new FlightServiceClient(this._transport) } - async *queryRawBatches( + private async *_queryRawBatches( query: string, database: string, queryType: QueryType @@ -66,7 +66,7 @@ export default class QueryApiImpl implements QueryApi { database: string, queryType: QueryType ): AsyncGenerator, void, void> { - const batches = this.queryRawBatches(query, database, queryType) + const batches = this._queryRawBatches(query, database, queryType) for await (const batch of batches) { for (let rowIndex = 0; rowIndex < batch.numRows; rowIndex++) { @@ -87,7 +87,7 @@ export default class QueryApiImpl implements QueryApi { database: string, queryType: QueryType ): AsyncGenerator { - const batches = this.queryRawBatches(query, database, queryType) + const batches = this._queryRawBatches(query, database, queryType) for await (const batch of batches) { for (let rowIndex = 0; rowIndex < batch.numRows; rowIndex++) { @@ -96,7 +96,7 @@ export default class QueryApiImpl implements QueryApi { const columnSchema = batch.schema.fields[columnIndex] const name = columnSchema.name const value = batch.getChildAt(columnIndex)?.get(rowIndex) - const type = columnSchema.metadata.get('iox::column::type')! + const type = columnSchema.metadata.get('iox::column::type') as string const [, , valueType, _fieldType] = type.split('::') if (valueType === 'field') { From 14bf406ea59a1d842fe3494628491a859d0ec768 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 04/45] feat: Point field types, getField --- examples/basic/src/index.ts | 4 +- packages/client/src/Point.ts | 100 +++++++++++++++++++++++++++++++---- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 155fc1fb..43270844 100644 --- a/examples/basic/src/index.ts +++ b/examples/basic/src/index.ts @@ -77,7 +77,9 @@ async function main() { ) for await (const row of queryPointsResult) { - console.log(row.toLineProtocol()) + console.log(`avg is ${row.getField('avg', 'float')}`) + console.log(`max is ${row.getField('max', 'float')}`) + console.log(`lp: ${row.toLineProtocol()}`) } } catch (err) { console.error(err) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 33726eae..ee0d3a9c 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -17,6 +17,34 @@ export type PointFieldType = | '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 fieldEntryToLPString = (fe: FieldEntry): string => { + switch (fe[0]) { + case 'string': + return escape.quoted(fe[1]) + case 'boolean': + return fe[1] ? 'T' : 'F' + case 'float': + return `${fe[1]}` + case 'integer': + return `${fe[1]}i` + case 'uinteger': + return `${fe[1]}u` + } +} + /** * Point defines values of a single measurement. */ @@ -25,7 +53,7 @@ export class Point { private _tags: {[key: string]: string} = {} private _time: string | number | Date | undefined /** escaped field values */ - public fields: {[key: string]: string} = {} + private _fields: {[key: string]: FieldEntry} = {} /** * Create a new Point with specified a measurement name. @@ -68,7 +96,7 @@ export class Point { * @returns this */ public booleanField(name: string, value: boolean | any): Point { - this.fields[name] = value ? 'T' : 'F' + this._fields[name] = ['boolean', !!value] return this } @@ -90,7 +118,7 @@ export class Point { if (isNaN(val) || val <= -9223372036854776e3 || val >= 9223372036854776e3) { throw new Error(`invalid integer value for field '${name}': '${value}'!`) } - this.fields[name] = `${Math.floor(val)}i` + this._fields[name] = ['integer', Math.floor(val)] return this } @@ -107,7 +135,7 @@ export class Point { 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` + this._fields[name] = ['uinteger', Math.floor(value as number)] } else { const strVal = String(value) for (let i = 0; i < strVal.length; i++) { @@ -127,7 +155,7 @@ export class Point { `uint value for field '${name}' out of range: ${strVal}` ) } - this.fields[name] = `${strVal}u` + this._fields[name] = ['uinteger', +strVal] } return this } @@ -151,7 +179,7 @@ export class Point { throw new Error(`invalid float value for field '${name}': '${value}'!`) } - this.fields[name] = String(val) + this._fields[name] = ['float', val] return this } @@ -165,7 +193,7 @@ export class Point { 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) + this._fields[name] = ['string', value] } return this } @@ -197,6 +225,57 @@ export class Point { } } + /** + * Get field of numeric type. + * + * @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. + * + * @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 name - field name + * @param type - field boolean type + * @throws Field type doesn't match actual type + * @returns this + */ + 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 { + const fieldEntry = this._fields[name] + if (!fieldEntry) return undefined + const [actualType, value] = fieldEntry + if (!type || type !== actualType) + throw new Error( + `field ${name} of type ${actualType} doesn't match expected type ${type}!` + ) + return value + } + /** * 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 @@ -231,13 +310,14 @@ export class Point { ): string | undefined { if (!this._name) return undefined let fieldsLine = '' - Object.keys(this.fields) + Object.keys(this._fields) .sort() .forEach((x) => { if (x) { - const val = this.fields[x] + const fieldEntry = this._fields[x] + const lpStringValue = fieldEntryToLPString(fieldEntry) if (fieldsLine.length > 0) fieldsLine += ',' - fieldsLine += `${escape.tag(x)}=${val}` + fieldsLine += `${escape.tag(x)}=${lpStringValue}` } }) if (fieldsLine.length === 0) return undefined // no fields present From 71a2a9b3fb694a9e436cdba0e42002a7859c4428 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 05/45] feat: Point.fields, PointRecord removed --- examples/basic/src/index.ts | 29 ++++--- packages/client/src/Point.ts | 75 +++++++++--------- packages/client/src/impl/QueryApiImpl.ts | 2 +- packages/client/src/util/generics.ts | 30 +------- .../client/test/unit/util/generics.test.ts | 49 +----------- packages/client/test/unit/util/point.test.ts | 76 +------------------ 6 files changed, 60 insertions(+), 201 deletions(-) diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 43270844..58171000 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 @@ -31,23 +31,22 @@ async function main() { .timestamp(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( + new Point('stat').tag('unit', 'temperature') + ) + + const sensorData = { + avg: 28, + max: 40.3, } - await client.write([sensorData], database) + const p2 = pointTemplate.copy().fields(sensorData).timestamp(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 = ` diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index ee0d3a9c..5693aab8 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -3,13 +3,6 @@ import {convertTimeToNanos, convertTime} from './util/time' import {escape} from './util/escape' import {WritePrecision} from './options' -export type PointRecord = { - measurement: string - fields: Record - tags?: Record - timestamp?: string | number | Date -} - export type PointFieldType = | 'float' | 'integer' @@ -45,14 +38,22 @@ const fieldEntryToLPString = (fe: FieldEntry): string => { } } +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 +} + /** * 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 */ + private _tags: {[key: string]: string} = {} private _fields: {[key: string]: FieldEntry} = {} /** @@ -202,12 +203,13 @@ export class Point { * Adds field based on provided type. * * @param name - field name - * @param type - field type * @param value - field value + * @param type - field type * @returns this */ - public field(name: string, type: PointFieldType, value: any): Point { - switch (type) { + public field(name: string, value: any, type?: PointFieldType): Point { + const inferedType = type ?? inferType(value) + switch (inferedType) { case 'string': return this.stringField(name, value) case 'boolean': @@ -218,6 +220,8 @@ export class Point { return this.intField(name, value) case 'uinteger': return this.uintField(name, value) + case undefined: + return this default: throw new Error( `invalid field type for field '${name}': type -> ${type}, value -> ${value}!` @@ -225,6 +229,20 @@ export class Point { } } + /** + * Add fields according to their type. All numeric type is considered float + * + * @param name - field name + * @param value - field value + * @returns this + */ + public fields(fields: {[key: string]: number | boolean | string}): Point { + for (const [name, value] of Object.entries(fields)) { + this.field(name, value) + } + return this + } + /** * Get field of numeric type. * @@ -354,29 +372,14 @@ export class Point { 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 + copy(): Point { + const copy = new Point() + 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 } } diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index 92378318..d7feace3 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -101,7 +101,7 @@ export default class QueryApiImpl implements QueryApi { if (valueType === 'field') { if (_fieldType && value !== undefined && value !== null) - point.field(name, _fieldType as any, value) + point.field(_fieldType as any, name, value) } else if (valueType === 'tag') { point.tag(name, value) } else if (valueType === 'timestamp') { 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/unit/util/generics.test.ts b/packages/client/test/unit/util/generics.test.ts index 9e668ce6..43ab0ce1 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, @@ -44,51 +44,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..bc568dd7 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -1,5 +1,5 @@ import {expect} from 'chai' -import {Point, PointRecord, convertTime} from '../../../src' +import {Point, convertTime} from '../../../src' describe('point', () => { it('creates point with various fields', () => { @@ -70,80 +70,6 @@ describe('point', () => { ) }) - 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('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(() => { - // no fields prop - Point.fromRecord({measurement: 'a'} as PointRecord) - }).to.throw('fields must be defined on the Point record!') - - expect(() => { - // invalid field type - Point.fromRecord({ - measurement: 'a', - fields: {a: {}}, - } as any as PointRecord) - }).to.throw('unsuported type of field') - - expect(() => { - // invalid tag type - Point.fromRecord({ - measurement: 'a', - fields: {}, - tags: {a: 8}, - } as any as PointRecord) - }).to.throw('tag has to be string') - }) - }) - describe('convert point time to line protocol', () => { const precision = 'ms' const clinetConvertTime = (value: Parameters[0]) => From 004f75d621ceed9ea936a20b545dfeff4d5f5e2b Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 06/45] docs: readme for queryPoints --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index e2948274..2797c9ad 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,24 @@ for await (const row of queryResult) { } ``` +or use typesafe Point 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). From 07d2ef0399b8229aca1da7beaa47d397b57d21a9 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 07/45] test: structured query tests --- packages/client/test/integration/e2e.test.ts | 12 +++ packages/client/test/unit/util/point.test.ts | 82 +++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index 7c959cfe..5d5e4f7e 100644 --- a/packages/client/test/integration/e2e.test.ts +++ b/packages/client/test/integration/e2e.test.ts @@ -74,6 +74,18 @@ describe('e2e test', () => { row = await data.next() expect(row.done).to.equal(true) + const dataPoints = client.queryPoints(query, database, queryType, 'stat') + + 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) + await client.close() await rejects(client.query(query, database, queryType).next()) }) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index bc568dd7..54ed4647 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -9,9 +9,10 @@ describe('point', () => { .booleanField('falsy', false) .intField('intFromString', '20') .floatField('floatFromString', '60.3') + .stringField('str', 'abc') .timestamp('') 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' ) }) @@ -60,6 +61,43 @@ describe('point', () => { ) }) + it('infers type when no type supported', () => { + const point = new Point('a') + .fields({ + float: 20.3, + float2: 20, + string: 'text', + bool: true, + nothing: undefined as any, + }) + .timestamp('') + 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(() => { + new Point().field('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 = new Point() + .measurement('blah') + .field('truthy', true, 'boolean') + .field('falsy', false, 'boolean') + .field('intFromString', '20', 'integer') + .field('floatFromString', '60.3', 'float') + .field('str', 'abc', 'string') + .timestamp('') + expect(point.toLineProtocol()).to.equal( + 'blah falsy=F,floatFromString=60.3,intFromString=20i,str="abc",truthy=T' + ) + }) + it('creates point with uint fields', () => { const point = new Point('a') .uintField('floored', 10.88) @@ -70,6 +108,48 @@ describe('point', () => { ) }) + it('returns field of with getField and throws if type not match', () => { + const point = new Point('a').fields({ + float: 20.3, + float2: 20, + string: 'text', + bool: true, + nothing: undefined as any, + }) + 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!` + ) + }) + + it('creates deep copy of point', () => { + const point = new Point() + .measurement('measure1') + .booleanField('truthy', true) + .booleanField('falsy', false) + .intField('intFromString', '20') + .uintField('intFromString', '20') + .floatField('floatFromString', '60.3') + .stringField('str', 'abc') + .timestamp('') + + const copy = point.copy() + + expect(copy.toLineProtocol()).to.equal(point.toLineProtocol()) + + copy.intField('truthy', 1) + + expect(copy.toLineProtocol()).to.not.equal(point.toLineProtocol()) + }) + describe('convert point time to line protocol', () => { const precision = 'ms' const clinetConvertTime = (value: Parameters[0]) => From be12c32bb21277d6d4aa1dc768b183b936f04d59 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 08/45] fix: getField, queryPoints --- packages/client/src/Point.ts | 2 +- packages/client/src/impl/QueryApiImpl.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 5693aab8..fc8acd84 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -287,7 +287,7 @@ export class Point { const fieldEntry = this._fields[name] if (!fieldEntry) return undefined const [actualType, value] = fieldEntry - if (!type || type !== actualType) + if (type !== undefined && type !== actualType) throw new Error( `field ${name} of type ${actualType} doesn't match expected type ${type}!` ) diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index d7feace3..3fb79090 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -6,7 +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 {Point} from '../Point' +import {Point, PointFieldType} from '../Point' export default class QueryApiImpl implements QueryApi { private _closed = false @@ -101,7 +101,7 @@ export default class QueryApiImpl implements QueryApi { if (valueType === 'field') { if (_fieldType && value !== undefined && value !== null) - point.field(_fieldType as any, name, value) + point.field(name, value, _fieldType as PointFieldType) } else if (valueType === 'tag') { point.tag(name, value) } else if (valueType === 'timestamp') { From 72c853537c469f253e78924ecfd1e880efa8df9b Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 09/45] docs: fix README lint, env variables --- README.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2797c9ad..c85c9fbe 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= ``` @@ -155,7 +155,6 @@ for await (const row of queryPointsResult) { } ``` - ## Examples For more advanced usage, see [examples](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/examples/README.md). From 9795860162ddcf9ef740fac4aa2b3d2ece20790a Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 10/45] test: InfluxDBClient argument options --- packages/client/test/unit/Influxdb.test.ts | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/packages/client/test/unit/Influxdb.test.ts b/packages/client/test/unit/Influxdb.test.ts index 779d4bfa..83d7b6be 100644 --- a/packages/client/test/unit/Influxdb.test.ts +++ b/packages/client/test/unit/Influxdb.test.ts @@ -1,7 +1,58 @@ 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 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 + }) + + 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')) + }) + describe('constructor', () => { it('is created from configuration with host', () => { expect( From c649740bd33427d7067054fd796a126ad8afaf65 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 11/45] docs: changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) 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 From 464941f57244da1529beb9b88a9547bbf7257f03 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 12/45] fix: md linter disable MD024 --- .markdownlint.yml | 1 + 1 file changed, 1 insertion(+) 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" ] }, From 862abcb168f813a7db858a50a01d7be09c68f365 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:01:40 +0200 Subject: [PATCH 13/45] fix: query Points without metadata --- packages/client/src/impl/QueryApiImpl.ts | 28 +++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index 3fb79090..9b283030 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' @@ -96,8 +96,30 @@ export default class QueryApiImpl implements QueryApi { const columnSchema = batch.schema.fields[columnIndex] const name = columnSchema.name const value = batch.getChildAt(columnIndex)?.get(rowIndex) - const type = columnSchema.metadata.get('iox::column::type') as string - const [, , valueType, _fieldType] = type.split('::') + 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' + ) { + point.measurement(value) + continue + } + + if (!metaType) { + if (name === 'time' && arrowTypeId === ArrowType.Timestamp) { + point.timestamp(value) + } else { + point.field(name, value) + } + + continue + } + + const [, , valueType, _fieldType] = metaType.split('::') if (valueType === 'field') { if (_fieldType && value !== undefined && value !== null) From 3d6ef099579ba9e66a96e9e215d32083b0174be3 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 5 Sep 2023 12:38:28 +0200 Subject: [PATCH 14/45] feat: PointValues object (first iteration) --- packages/client/src/Point.ts | 139 ++++++------ packages/client/src/PointValues.ts | 338 +++++++++++++++++++++++++++++ 2 files changed, 405 insertions(+), 72 deletions(-) create mode 100644 packages/client/src/PointValues.ts diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index fc8acd84..8a703ed6 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -2,39 +2,27 @@ 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 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 fieldEntryToLPString = (fe: FieldEntry): string => { - switch (fe[0]) { +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(fe[1]) + return escape.quoted(value as string) case 'boolean': - return fe[1] ? 'T' : 'F' + return value ? 'T' : 'F' case 'float': - return `${fe[1]}` + return `${value}` case 'integer': - return `${fe[1]}i` + return `${value}i` case 'uinteger': - return `${fe[1]}u` + return `${value}u` } } @@ -51,18 +39,29 @@ const inferType = ( * Point defines values of a single measurement. */ export class Point { - private _name: string - private _time: string | number | Date | undefined - private _tags: {[key: string]: string} = {} - private _fields: {[key: string]: FieldEntry} = {} + 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 + 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 measurementName - the measurement name + */ + constructor(values: PointValues) + constructor(arg0?: PointValues | string) { + if (arg0 instanceof PointValues) { + this._values = arg0 + } else { + this._values = new PointValues() + } + + if (typeof arg0 === 'string') this._values.measurement(arg0) } /** @@ -72,10 +71,14 @@ export class Point { * @returns this */ public measurement(name: string): Point { - this._name = name + this._values.measurement(name) return this } + public getMeasurement(): string { + return this._values.getMeasurement() as NonNullable> + } + /** * Adds a tag. The caller has to ensure that both name and value are not empty * and do not end with backslash. @@ -85,7 +88,7 @@ export class Point { * @returns this */ public tag(name: string, value: string): Point { - this._tags[name] = value + this._values.tag(name, value) return this } @@ -97,7 +100,7 @@ export class Point { * @returns this */ public booleanField(name: string, value: boolean | any): Point { - this._fields[name] = ['boolean', !!value] + this._values.booleanField(name, value) return this } @@ -119,7 +122,7 @@ export class Point { if (isNaN(val) || val <= -9223372036854776e3 || val >= 9223372036854776e3) { throw new Error(`invalid integer value for field '${name}': '${value}'!`) } - this._fields[name] = ['integer', Math.floor(val)] + this._values.intField(name, Math.floor(val)) return this } @@ -136,8 +139,8 @@ export class Point { 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 { + this._values.uintField(name, Math.floor(value as number)) + } else { const strVal = String(value) for (let i = 0; i < strVal.length; i++) { const code = strVal.charCodeAt(i) @@ -156,7 +159,7 @@ export class Point { `uint value for field '${name}' out of range: ${strVal}` ) } - this._fields[name] = ['uinteger', +strVal] + this._values.uintField(name, +strVal) } return this } @@ -180,7 +183,7 @@ export class Point { throw new Error(`invalid float value for field '${name}': '${value}'!`) } - this._fields[name] = ['float', val] + this._values.floatField(name, val) return this } @@ -194,8 +197,8 @@ export class Point { public stringField(name: string, value: string | any): Point { if (value !== null && value !== undefined) { if (typeof value !== 'string') value = String(value) - this._fields[name] = ['string', value] - } + this._values.stringField(name, value) + } return this } @@ -284,14 +287,11 @@ export class Point { 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 Error( - `field ${name} of type ${actualType} doesn't match expected type ${type}!` - ) - return value + return this._values.getField(name, type as any) + } + + public getFieldType(name: string): PointFieldType | undefined { + return this._values.getFieldType(name) } /** @@ -313,7 +313,7 @@ export class Point { * @returns this */ public timestamp(value: Date | number | string | undefined): Point { - this._time = value + this._values.timestamp(value) return this } @@ -326,33 +326,35 @@ export class Point { 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 fieldEntry = this._fields[x] - const lpStringValue = fieldEntryToLPString(fieldEntry) + .forEach((name) => { + if (name) { + const type = this._values.getFieldType(name) + const value = this._values.getField(name) + if (!type || !value) return; + const lpStringValue = fieldToLPString(type, value) if (fieldsLine.length > 0) fieldsLine += ',' - fieldsLine += `${escape.tag(x)}=${lpStringValue}` + fieldsLine += `${escape.tag(name)}=${lpStringValue}` } }) if (fieldsLine.length === 0) return undefined // no fields present let tagsLine = '' - const tags = this._tags - Object.keys(tags) + const tagNames = this._values.getTagNames() + tagNames .sort() .forEach((x) => { if (x) { - const val = tags[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) @@ -362,7 +364,7 @@ export class Point { time = convertTimePrecision(time) } - return `${escape.measurement(this._name)}${tagsLine} ${fieldsLine}${ + return `${escape.measurement(this.getMeasurement())}${tagsLine} ${fieldsLine}${ time !== undefined ? ` ${time}` : '' }` } @@ -373,13 +375,6 @@ export class Point { } copy(): Point { - const copy = new Point() - 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 + return new Point(this._values.copy()) } } diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts new file mode 100644 index 00000000..15e70bf1 --- /dev/null +++ b/packages/client/src/PointValues.ts @@ -0,0 +1,338 @@ +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 +} + +/** + * 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() {} + + /** + * Sets point's measurement. + * + * @param name - measurement name + * @returns this + */ + public measurement(name: string): PointValues { + this._name = name + return this + } + + getMeasurement(): string | undefined { + return this._name + } + + /** + * Adds 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): PointValues { + this._tags[name] = value + return this + } + + public getTag(name: string): string | undefined { + return this._tags[name]; + } + + public getTagNames(): string[] { + return Object.keys(this._tags) + } + + /** + * Adds a boolean field. + * + * @param field - field name + * @param value - field value + * @returns this + */ + public booleanField(name: string, value: boolean | any): PointValues { + this._fields[name] = ['boolean', !!value] + return this + } + + /** + * Adds 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): 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 + } + + /** + * Adds 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): 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 + } + + /** + * Adds a number field. + * + * @param name - field name + * @param value - field value + * @returns this + * @throws NaN/Infinity/-Infinity is supplied + */ + public floatField(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 + } + + /** + * Adds a string field. + * + * @param name - field name + * @param value - field value + * @returns this + */ + public stringField(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 + } + + /** + * Adds field based on provided type. + * + * @param name - field name + * @param value - field value + * @param type - field type + * @returns this + */ + public field(name: string, value: any, type?: PointFieldType): PointValues { + const inferedType = type ?? inferType(value) + switch (inferedType) { + case 'string': + return this.stringField(name, value) + case 'boolean': + return this.booleanField(name, value) + case 'float': + return this.floatField(name, value) + case 'integer': + return this.intField(name, value) + case 'uinteger': + return this.uintField(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 name - field name + * @param value - field value + * @returns this + */ + public fields(fields: {[key: string]: number | boolean | string}): PointValues { + for (const [name, value] of Object.entries(fields)) { + this.field(name, value) + } + return this + } + + /** + * Get field of numeric type. + * + * @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. + * + * @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 name - field name + * @param type - field boolean type + * @throws Field type doesn't match actual type + * @returns this + */ + 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 { + const fieldEntry = this._fields[name] + if (!fieldEntry) return undefined + const [actualType, value] = fieldEntry + if (type !== undefined && type !== actualType) + throw new Error( + `field ${name} of type ${actualType} doesn't match expected type ${type}!` + ) + return value + } + + public getFieldType(name: string): PointFieldType | undefined { + const fieldEntry = this._fields[name] + if (!fieldEntry) return undefined + return fieldEntry[0] + } + + public getFieldNames(): string[] { + return Object.keys(this._fields) + } + + /** + * 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 timestamp(value: Date | number | string | undefined): PointValues { + this._time = value + return this + } + + public getTimestamp(): Date | number | string | undefined { + return this._time; + } + + public asPoint(): Point { + return new Point(this); + } + + 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 + } +} From 763fea83f1a05faf58ef7d93b8d4498d39dbf6cf Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:37:00 +0200 Subject: [PATCH 15/45] refactor: PointValues query, private Point cons --- packages/client/src/InfluxDBClient.ts | 4 +- packages/client/src/Point.ts | 71 +++++++++++-------- packages/client/src/PointValues.ts | 12 ++-- packages/client/src/QueryApi.ts | 6 +- packages/client/src/impl/QueryApiImpl.ts | 6 +- packages/client/test/integration/e2e.test.ts | 5 +- packages/client/test/unit/Write.test.ts | 34 ++++++--- .../client/test/unit/util/generics.test.ts | 12 ++-- packages/client/test/unit/util/point.test.ts | 48 ++++++------- 9 files changed, 114 insertions(+), 84 deletions(-) diff --git a/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index 29e314b0..3b0153aa 100644 --- a/packages/client/src/InfluxDBClient.ts +++ b/packages/client/src/InfluxDBClient.ts @@ -6,7 +6,7 @@ import {ClientOptions, QueryType, WriteOptions} from './options' import {IllegalArgumentError} from './errors' import {WritableData, writableDataToLineProtocol} from './util/generics' import {throwReturn} from './util/common' -import {Point} from './Point' +import {PointValues} from './PointValues' const argumentErrorMessage = `\ Please specify the 'database' as a method parameter or use default configuration \ @@ -80,7 +80,7 @@ export default class InfluxDBClient { database: string, queryType: QueryType, measurement: string - ): AsyncGenerator { + ): AsyncGenerator { const points = this._queryApi.queryPoints( query, database ?? diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 8a703ed6..d87c4265 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -2,16 +2,16 @@ import {TimeConverter} from './WriteApi' import {convertTimeToNanos, convertTime} from './util/time' import {escape} from './util/escape' import {WritePrecision} from './options' -import { PointFieldType, PointValues } from "./PointValues" +import {PointFieldType, PointValues} from './PointValues' 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 => { + (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) @@ -46,15 +46,15 @@ export class Point { * * @param measurementName - the measurement name */ - constructor(measurementName: string) + 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 measurementName - the measurement name */ - constructor(values: PointValues) - constructor(arg0?: PointValues | string) { + private constructor(values: PointValues) + private constructor(arg0?: PointValues | string) { if (arg0 instanceof PointValues) { this._values = arg0 } else { @@ -64,6 +64,14 @@ export class Point { if (typeof arg0 === 'string') this._values.measurement(arg0) } + public static measurement(name: string): Point { + return new Point(name) + } + + public static fromValues(values: PointValues): Point { + return new Point(values) + } + /** * Sets point's measurement. * @@ -76,7 +84,9 @@ export class Point { } public getMeasurement(): string { - return this._values.getMeasurement() as NonNullable> + return this._values.getMeasurement() as NonNullable< + ReturnType + > } /** @@ -140,7 +150,7 @@ export class Point { throw new Error(`uint value for field '${name}' out of range: ${value}`) } this._values.uintField(name, Math.floor(value as number)) - } else { + } else { const strVal = String(value) for (let i = 0; i < strVal.length; i++) { const code = strVal.charCodeAt(i) @@ -197,8 +207,8 @@ export class Point { public stringField(name: string, value: string | any): Point { if (value !== null && value !== undefined) { if (typeof value !== 'string') value = String(value) - this._values.stringField(name, value) - } + this._values.stringField(name, value) + } return this } @@ -328,13 +338,14 @@ export class Point { ): string | undefined { if (!this._values.getMeasurement()) return undefined let fieldsLine = '' - this._values.getFieldNames() + this._values + .getFieldNames() .sort() .forEach((name) => { if (name) { const type = this._values.getFieldType(name) const value = this._values.getField(name) - if (!type || !value) return; + if (type === undefined || value === undefined) return const lpStringValue = fieldToLPString(type, value) if (fieldsLine.length > 0) fieldsLine += ',' fieldsLine += `${escape.tag(name)}=${lpStringValue}` @@ -343,18 +354,16 @@ export class Point { if (fieldsLine.length === 0) return undefined // no fields present let tagsLine = '' 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)}` - } + 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._values.getTimestamp(); + } + }) + let time = this._values.getTimestamp() if (!convertTimePrecision) { time = convertTimeToNanos(time) @@ -364,9 +373,9 @@ export class Point { time = convertTimePrecision(time) } - return `${escape.measurement(this.getMeasurement())}${tagsLine} ${fieldsLine}${ - time !== undefined ? ` ${time}` : '' - }` + return `${escape.measurement( + this.getMeasurement() + )}${tagsLine} ${fieldsLine}${time !== undefined ? ` ${time}` : ''}` } toString(): string { diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index 15e70bf1..8ef8be20 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -1,4 +1,4 @@ -import { Point } from "./Point" +import {Point} from './Point' export type PointFieldType = | 'float' @@ -72,7 +72,7 @@ export class PointValues { } public getTag(name: string): string | undefined { - return this._tags[name]; + return this._tags[name] } public getTagNames(): string[] { @@ -226,7 +226,9 @@ export class PointValues { * @param value - field value * @returns this */ - public fields(fields: {[key: string]: number | boolean | string}): PointValues { + public fields(fields: { + [key: string]: number | boolean | string + }): PointValues { for (const [name, value] of Object.entries(fields)) { this.field(name, value) } @@ -318,11 +320,11 @@ export class PointValues { } public getTimestamp(): Date | number | string | undefined { - return this._time; + return this._time } public asPoint(): Point { - return new Point(this); + return Point.fromValues(this) } copy(): PointValues { diff --git a/packages/client/src/QueryApi.ts b/packages/client/src/QueryApi.ts index 15f302ce..394937e9 100644 --- a/packages/client/src/QueryApi.ts +++ b/packages/client/src/QueryApi.ts @@ -1,4 +1,4 @@ -import {Point} from './Point' +import {PointValues} from './PointValues' import {QueryType} from './options' /** @@ -25,13 +25,13 @@ export default interface QueryApi { * @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 Point object. + * @returns An async generator that yields PointValues object. */ queryPoints( query: string, database: string, queryType: QueryType - ): AsyncGenerator + ): AsyncGenerator close(): Promise } diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index 9b283030..75372221 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -6,7 +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 {Point, PointFieldType} from '../Point' +import {PointFieldType, PointValues} from '../PointValues' export default class QueryApiImpl implements QueryApi { private _closed = false @@ -86,12 +86,12 @@ export default class QueryApiImpl implements QueryApi { query: string, database: string, queryType: QueryType - ): AsyncGenerator { + ): AsyncGenerator { const batches = this._queryRawBatches(query, database, queryType) for await (const batch of batches) { for (let rowIndex = 0; rowIndex < batch.numRows; rowIndex++) { - const point = new Point() + const point = new PointValues() for (let columnIndex = 0; columnIndex < batch.numCols; columnIndex++) { const columnSchema = batch.schema.fields[columnIndex] const name = columnSchema.name diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index 5d5e4f7e..9918db96 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/PointValues' const getEnvVariables = () => { const { @@ -42,7 +43,7 @@ describe('e2e test', () => { const avg1 = getRandomInt(110, 500) const max1 = getRandomInt(900, 1000) - const point = new Point('stat') + const point = Point.measurement('stat') .tag('unit', 'temperature') .floatField('avg', avg1) .floatField('max', max1) @@ -76,7 +77,7 @@ describe('e2e test', () => { const dataPoints = client.queryPoints(query, database, queryType, 'stat') - let pointRow: IteratorResult + let pointRow: IteratorResult pointRow = await dataPoints.next() expect(pointRow.done).to.equal(false) diff --git a/packages/client/test/unit/Write.test.ts b/packages/client/test/unit/Write.test.ts index 91978728..461b4538 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').floatField('value', 1), + DATABASE + ) ) await rejects( - subject.write([new Point('test').floatField('value', 1)], DATABASE) + subject.write( + [Point.measurement('test').floatField('value', 1)], + DATABASE + ) ) }) }) @@ -129,7 +135,7 @@ describe('Write', () => { } }) .persist() - const point = new Point('test') + const point = Point.measurement('test') .tag('t', ' ') .floatField('value', 1) .timestamp('') @@ -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,10 +169,12 @@ 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') + Point.measurement('test').floatField('value', 1).timestamp('1'), + Point.measurement('test').floatField('value', 2).timestamp(2.1), + Point.measurement('test') + .floatField('value', 3) + .timestamp(new Date(3)), + Point.measurement('test') .floatField('value', 4) .timestamp(false as any as string), // server decides what to do with such values ] @@ -224,7 +232,10 @@ describe('Write', () => { return [204, '', {}] }) .persist() - await subject.write(new Point('test').floatField('value', 1), DATABASE) + await subject.write( + Point.measurement('test').floatField('value', 1), + DATABASE + ) expect(logs.error).has.length(0) expect(logs.warn).has.length(0) expect(authorization).equals(`Token customToken`) @@ -241,7 +252,10 @@ describe('Write', () => { return [204, '', {}] }) .persist() - await subject.write(new Point('test').floatField('value', 1), DATABASE) + await subject.write( + Point.measurement('test').floatField('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 43ab0ce1..a5207de2 100644 --- a/packages/client/test/unit/util/generics.test.ts +++ b/packages/client/test/unit/util/generics.test.ts @@ -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').floatField('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').floatField('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') + .floatField('blah', 456.7) + .timestamp(date) + const point3 = Point.measurement('test') + .floatField('blah', 789.8) + .timestamp('') const input: WritableData = [point1, point2, point3] const output = writableDataToLineProtocol(input) expect(output.length).to.equal(3) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index 54ed4647..db6d3484 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -3,8 +3,7 @@ import {Point, convertTime} from '../../../src' describe('point', () => { it('creates point with various fields', () => { - const point = new Point() - .measurement('blah') + const point = Point.measurement('blah') .booleanField('truthy', true) .booleanField('falsy', false) .intField('intFromString', '20') @@ -18,32 +17,32 @@ describe('point', () => { it('fails on invalid fields', () => { expect(() => { - new Point().intField('fails', NaN) + Point.measurement('a').intField('fails', NaN) }).to.throw(`invalid integer value for field 'fails': 'NaN'`) expect(() => { - new Point().intField('fails', Infinity) + Point.measurement('a').intField('fails', Infinity) }).to.throw(`invalid integer value for field 'fails': 'Infinity'!`) expect(() => { - new Point().intField('fails', 9223372036854776e3) + Point.measurement('a').intField('fails', 9223372036854776e3) }).to.throw( `invalid integer value for field 'fails': '9223372036854776000'!` ) expect(() => { - new Point().floatField('fails', Infinity) + Point.measurement('a').floatField('fails', Infinity) }).to.throw(`invalid float value for field 'fails': 'Infinity'!`) expect(() => { - new Point().uintField('fails', NaN) + Point.measurement('a').uintField('fails', NaN) }).to.throw(`uint value for field 'fails' out of range: NaN`) expect(() => { - new Point().uintField('fails', -1) + Point.measurement('a').uintField('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').uintField('fails', Number.MAX_SAFE_INTEGER + 10) }).to.throw( `uint value for field 'fails' out of range: ${ Number.MAX_SAFE_INTEGER + 10 @@ -51,18 +50,18 @@ describe('point', () => { ) expect(() => { - new Point().uintField('fails', '10a8') + Point.measurement('a').uintField('fails', '10a8') }).to.throw(`uint value has an unsupported character at pos 2: 10a8`) expect(() => { - new Point().uintField('fails', '18446744073709551616') + Point.measurement('a').uintField('fails', '18446744073709551616') }).to.throw( `uint value for field 'fails' out of range: 18446744073709551616` ) }) it('infers type when no type supported', () => { - const point = new Point('a') + const point = Point.measurement('a') .fields({ float: 20.3, float2: 20, @@ -78,15 +77,14 @@ describe('point', () => { it('throws when invalid type for method field is provided', () => { expect(() => { - new Point().field('errorlike', undefined, 'bad-type' as any) + Point.measurement('a').field('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 = new Point() - .measurement('blah') + const point = Point.measurement('blah') .field('truthy', true, 'boolean') .field('falsy', false, 'boolean') .field('intFromString', '20', 'integer') @@ -99,7 +97,7 @@ describe('point', () => { }) it('creates point with uint fields', () => { - const point = new Point('a') + const point = Point.measurement('a') .uintField('floored', 10.88) .uintField('fromString', '789654123') .timestamp('') @@ -109,7 +107,7 @@ describe('point', () => { }) it('returns field of with getField and throws if type not match', () => { - const point = new Point('a').fields({ + const point = Point.measurement('a').fields({ float: 20.3, float2: 20, string: 'text', @@ -131,8 +129,7 @@ describe('point', () => { }) it('creates deep copy of point', () => { - const point = new Point() - .measurement('measure1') + const point = Point.measurement('measure1') .booleanField('truthy', true) .booleanField('falsy', false) .intField('intFromString', '20') @@ -156,20 +153,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').floatField('b', 1).timestamp('') 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').floatField('b', 1).timestamp(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').floatField('b', 1).timestamp(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').floatField('b', 1) expect(p.toLineProtocol(precision)).satisfies((x: string) => { return x.startsWith('a b=1') }, `does not start with 'a b=1'`) @@ -178,7 +175,10 @@ 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') + .floatField('b', 1) + .tag('c', 'd') + .timestamp('') expect(p.toLineProtocol()).equals(p.toString()) }) }) From 3878460c26979a6495678bcdd9f84dcd40b21934 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Wed, 6 Sep 2023 11:47:38 +0200 Subject: [PATCH 16/45] refactor: reworked Point api --- examples/basic/src/index.ts | 18 +- examples/browser/src/main.ts | 16 +- packages/client/src/InfluxDBClient.ts | 2 +- packages/client/src/Point.ts | 251 ++++++++---------- packages/client/src/PointValues.ts | 218 ++++++++------- packages/client/src/impl/QueryApiImpl.ts | 16 +- packages/client/test/integration/e2e.test.ts | 8 +- packages/client/test/unit/Write.test.ts | 30 ++- .../client/test/unit/util/generics.test.ts | 12 +- packages/client/test/unit/util/point.test.ts | 86 +++--- 10 files changed, 333 insertions(+), 324 deletions(-) diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 58171000..9af928c8 100644 --- a/examples/basic/src/index.ts +++ b/examples/basic/src/index.ts @@ -24,23 +24,26 @@ 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 point as template with anonymous fields object const pointTemplate = Object.freeze( - new Point('stat').tag('unit', 'temperature') + Point.measurement('stat').setTag('unit', 'temperature') ) const sensorData = { avg: 28, max: 40.3, } - const p2 = pointTemplate.copy().fields(sensorData).timestamp(new Date()) + const p2 = pointTemplate + .copy() + .setFields(sensorData) + .setTimestamp(new Date()) await client.write(p2, database) @@ -78,7 +81,6 @@ async function main() { 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()}`) } } catch (err) { console.error(err) 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/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index 3b0153aa..d2eae5c2 100644 --- a/packages/client/src/InfluxDBClient.ts +++ b/packages/client/src/InfluxDBClient.ts @@ -90,7 +90,7 @@ export default class InfluxDBClient { ) for await (const point of points) { - yield point.measurement(measurement) + yield point.setMeasurement(measurement) } } diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index d87c4265..8933cdbe 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -26,15 +26,6 @@ const fieldToLPString: { } } -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 -} - /** * Point defines values of a single measurement. */ @@ -61,7 +52,7 @@ export class Point { this._values = new PointValues() } - if (typeof arg0 === 'string') this._values.measurement(arg0) + if (typeof arg0 === 'string') this._values.setMeasurement(arg0) } public static measurement(name: string): Point { @@ -72,21 +63,50 @@ export class Point { return new Point(values) } + public getMeasurement(): string { + return this._values.getMeasurement() as string + } + /** * Sets point's measurement. * * @param name - measurement name * @returns this */ - public measurement(name: string): Point { - this._values.measurement(name) + public setMeasurement(name: string): Point { + this._values.setMeasurement(name) return this } - public getMeasurement(): string { - return this._values.getMeasurement() as NonNullable< - ReturnType - > + 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 + } + + public getTag(name: string): string | undefined { + return this._values.getTag(name) } /** @@ -97,22 +117,35 @@ export class Point { * @param value - tag value * @returns this */ - public tag(name: string, value: string): Point { - this._values.tag(name, value) + public setTag(name: string, value: string): Point { + this._values.setTag(name, value) return this } + public removeTag(name: string): Point { + this._values.removeTag(name) + return this + } + + public getTagNames(): string[] { + return this._values.getTagNames() + } + /** - * Adds a boolean field. + * Adds 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._values.booleanField(name, value) + public setFloatField(name: string, value: number | any): Point { + this._values.setFloatField(name, value) return this } + public getIntField(name: string): number | undefined { + return this._values.getIntField(name) + } /** * Adds an integer field. @@ -122,19 +155,13 @@ export class Point { * @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._values.intField(name, Math.floor(val)) + public setIntField(name: string, value: number | any): Point { + this._values.setIntField(name, value) return this } + public getUintField(name: string): number | undefined { + return this._values.getUintField(name) + } /** * Adds an unsigned integer field. @@ -144,57 +171,12 @@ export class Point { * @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._values.uintField(name, 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._values.uintField(name, +strVal) - } + public setUintField(name: string, value: number | any): Point { + this._values.setUintField(name, value) return this } - - /** - * Adds a number 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._values.floatField(name, val) - return this + public getStringField(name: string): string | undefined { + return this._values.getStringField(name) } /** @@ -204,55 +186,23 @@ export class Point { * @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._values.stringField(name, value) - } + public setStringField(name: string, value: string | any): Point { + this._values.setStringField(name, value) return this } - - /** - * Adds field based on provided type. - * - * @param name - field name - * @param value - field value - * @param type - field type - * @returns this - */ - public field(name: string, value: any, type?: PointFieldType): Point { - const inferedType = type ?? inferType(value) - switch (inferedType) { - case 'string': - return this.stringField(name, value) - case 'boolean': - return this.booleanField(name, value) - case 'float': - return this.floatField(name, value) - case 'integer': - return this.intField(name, value) - case 'uinteger': - return this.uintField(name, value) - case undefined: - return this - default: - throw new Error( - `invalid field type for field '${name}': type -> ${type}, value -> ${value}!` - ) - } + public getBooleanField(name: string): boolean | undefined { + return this._values.getBooleanField(name) } /** - * Add fields according to their type. All numeric type is considered float + * Adds a boolean field. * - * @param name - field name + * @param field - field name * @param value - field value * @returns this */ - public fields(fields: {[key: string]: number | boolean | string}): Point { - for (const [name, value] of Object.entries(fields)) { - this.field(name, value) - } + public setBooleanField(name: string, value: boolean | any): Point { + this._values.setBooleanField(name, value) return this } @@ -305,28 +255,47 @@ export class Point { } /** - * 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. + * Adds field based on provided type. * - * Note that InfluxDB requires the timestamp to fit into int64 data 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 value - point time + * @param name - field name + * @param value - field value * @returns this */ - public timestamp(value: Date | number | string | undefined): Point { - this._values.timestamp(value) + public setFields(fields: {[key: string]: number | boolean | string}): Point { + this._values.setFields(fields) return this } + public removeField(name: string): Point { + this._values.removeField(name) + return this + } + + public getFieldNames(): string[] { + return this._values.getFieldNames() + } + + public hasFields(): boolean { + return this._values.hasFields() + } + + 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, @@ -348,7 +317,7 @@ export class Point { if (type === undefined || value === undefined) return const lpStringValue = fieldToLPString(type, value) if (fieldsLine.length > 0) fieldsLine += ',' - fieldsLine += `${escape.tag(name)}=${lpStringValue}` + fieldsLine += `${escape.setTag(name)}=${lpStringValue}` } }) if (fieldsLine.length === 0) return undefined // no fields present @@ -359,7 +328,7 @@ export class Point { const val = this._values.getTag(x) if (val) { tagsLine += ',' - tagsLine += `${escape.tag(x)}=${escape.tag(val)}` + tagsLine += `${escape.setTag(x)}=${escape.setTag(val)}` } } }) @@ -382,8 +351,4 @@ export class Point { const line = this.toLineProtocol(undefined) return line ? line : `invalid point: ${JSON.stringify(this, undefined)}` } - - copy(): Point { - return new Point(this._values.copy()) - } } diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index 8ef8be20..f5923ec1 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -43,19 +43,46 @@ export class PointValues { */ constructor() {} + getMeasurement(): string | undefined { + return this._name + } + /** * Sets point's measurement. * * @param name - measurement name * @returns this */ - public measurement(name: string): PointValues { + public setMeasurement(name: string): PointValues { this._name = name return this } - getMeasurement(): string | undefined { - return this._name + 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 } /** @@ -66,7 +93,7 @@ export class PointValues { * @param value - tag value * @returns this */ - public tag(name: string, value: string): PointValues { + public setTag(name: string, value: string): PointValues { this._tags[name] = value return this } @@ -75,22 +102,46 @@ export class PointValues { return this._tags[name] } + public removeTag(name: string): PointValues { + delete this._tags[name] + return this + } + public getTagNames(): string[] { return Object.keys(this._tags) } + public getFloatField(name: string): number | undefined { + return this.getField(name, 'float') + } + /** - * Adds a boolean field. + * Adds 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): PointValues { - this._fields[name] = ['boolean', !!value] + 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 } + public getIntField(name: string): number | undefined { + return this.getField(name, 'integer') + } + /** * Adds an integer field. * @@ -99,7 +150,7 @@ export class PointValues { * @returns this * @throws NaN or out of int64 range value is supplied */ - public intField(name: string, value: number | any): PointValues { + public setIntField(name: string, value: number | any): PointValues { let val: number if (typeof value === 'number') { val = value @@ -113,6 +164,10 @@ export class PointValues { return this } + public getUintField(name: string): number | undefined { + return this.getField(name, 'uinteger') + } + /** * Adds an unsigned integer field. * @@ -121,7 +176,7 @@ export class PointValues { * @returns this * @throws NaN out of range value is supplied */ - public uintField(name: string, value: number | any): PointValues { + public setUintField(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}`) @@ -151,27 +206,8 @@ export class PointValues { return this } - /** - * Adds a number field. - * - * @param name - field name - * @param value - field value - * @returns this - * @throws NaN/Infinity/-Infinity is supplied - */ - public floatField(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 + public getStringField(name: string): string | undefined { + return this.getField(name, 'string') } /** @@ -181,7 +217,7 @@ export class PointValues { * @param value - field value * @returns this */ - public stringField(name: string, value: string | any): PointValues { + 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] @@ -189,49 +225,19 @@ export class PointValues { return this } - /** - * Adds field based on provided type. - * - * @param name - field name - * @param value - field value - * @param type - field type - * @returns this - */ - public field(name: string, value: any, type?: PointFieldType): PointValues { - const inferedType = type ?? inferType(value) - switch (inferedType) { - case 'string': - return this.stringField(name, value) - case 'boolean': - return this.booleanField(name, value) - case 'float': - return this.floatField(name, value) - case 'integer': - return this.intField(name, value) - case 'uinteger': - return this.uintField(name, value) - case undefined: - return this - default: - throw new Error( - `invalid field type for field '${name}': type -> ${type}, value -> ${value}!` - ) - } + public getBooleanField(name: string): boolean | undefined { + return this.getField(name, 'boolean') } /** - * Add fields according to their type. All numeric type is considered float + * Adds a boolean field. * - * @param name - field name + * @param field - field name * @param value - field value * @returns this */ - public fields(fields: { - [key: string]: number | boolean | string - }): PointValues { - for (const [name, value] of Object.entries(fields)) { - this.field(name, value) - } + public setBooleanField(name: string, value: boolean | any): PointValues { + this._fields[name] = ['boolean', !!value] return this } @@ -292,39 +298,67 @@ export class PointValues { return fieldEntry[0] } - public getFieldNames(): string[] { - return Object.keys(this._fields) + /** + * Adds 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.setIntField(name, value) + case 'uinteger': + return this.setUintField(name, value) + case undefined: + return this + default: + throw new Error( + `invalid field type for field '${name}': type -> ${type}, value -> ${value}!` + ) + } } /** - * 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. + * Add fields according to their type. All numeric type is considered float * - * @param value - point time + * @param name - field name + * @param value - field value * @returns this */ - public timestamp(value: Date | number | string | undefined): PointValues { - this._time = value + public setFields(fields: { + [key: string]: number | boolean | string + }): PointValues { + for (const [name, value] of Object.entries(fields)) { + this.setField(name, value) + } return this } - public getTimestamp(): Date | number | string | undefined { - return this._time + public removeField(name: string): PointValues { + delete this._fields[name] + return this } - public asPoint(): Point { - return Point.fromValues(this) + public getFieldNames(): string[] { + return Object.keys(this._fields) + } + + public hasFields(): boolean { + return this.getFieldNames().length > 0 } copy(): PointValues { @@ -337,4 +371,8 @@ export class PointValues { ) return copy } + + public asPoint(): Point { + return Point.fromValues(this) + } } diff --git a/packages/client/src/impl/QueryApiImpl.ts b/packages/client/src/impl/QueryApiImpl.ts index 75372221..cd2927c9 100644 --- a/packages/client/src/impl/QueryApiImpl.ts +++ b/packages/client/src/impl/QueryApiImpl.ts @@ -91,7 +91,7 @@ export default class QueryApiImpl implements QueryApi { for await (const batch of batches) { for (let rowIndex = 0; rowIndex < batch.numRows; rowIndex++) { - const point = new PointValues() + const values = new PointValues() for (let columnIndex = 0; columnIndex < batch.numCols; columnIndex++) { const columnSchema = batch.schema.fields[columnIndex] const name = columnSchema.name @@ -105,15 +105,15 @@ export default class QueryApiImpl implements QueryApi { (name === 'measurement' || name == 'iox::measurement') && typeof value === 'string' ) { - point.measurement(value) + values.setMeasurement(value) continue } if (!metaType) { if (name === 'time' && arrowTypeId === ArrowType.Timestamp) { - point.timestamp(value) + values.setTimestamp(value) } else { - point.field(name, value) + values.setField(name, value) } continue @@ -123,15 +123,15 @@ export default class QueryApiImpl implements QueryApi { if (valueType === 'field') { if (_fieldType && value !== undefined && value !== null) - point.field(name, value, _fieldType as PointFieldType) + values.setField(name, value, _fieldType as PointFieldType) } else if (valueType === 'tag') { - point.tag(name, value) + values.setTag(name, value) } else if (valueType === 'timestamp') { - point.timestamp(value) + values.setTimestamp(value) } } - yield point + yield values } } } diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index 9918db96..67022af1 100644 --- a/packages/client/test/integration/e2e.test.ts +++ b/packages/client/test/integration/e2e.test.ts @@ -44,10 +44,10 @@ describe('e2e test', () => { const max1 = getRandomInt(900, 1000) const point = Point.measurement('stat') - .tag('unit', 'temperature') - .floatField('avg', avg1) - .floatField('max', max1) - .intField('testId', testId) + .setTag('unit', 'temperature') + .setFloatField('avg', avg1) + .setFloatField('max', max1) + .setIntField('testId', testId) await client.write(point, database) const query = ` diff --git a/packages/client/test/unit/Write.test.ts b/packages/client/test/unit/Write.test.ts index 461b4538..d120dc33 100644 --- a/packages/client/test/unit/Write.test.ts +++ b/packages/client/test/unit/Write.test.ts @@ -72,13 +72,13 @@ describe('Write', () => { await rejects(subject.write(['text value=1', 'text value=2'], DATABASE)) await rejects( subject.write( - Point.measurement('test').floatField('value', 1), + Point.measurement('test').setFloatField('value', 1), DATABASE ) ) await rejects( subject.write( - [Point.measurement('test').floatField('value', 1)], + [Point.measurement('test').setFloatField('value', 1)], DATABASE ) ) @@ -136,9 +136,9 @@ describe('Write', () => { }) .persist() const point = Point.measurement('test') - .tag('t', ' ') - .floatField('value', 1) - .timestamp('') + .setTag('t', ' ') + .setFloatField('value', 1) + .setTimestamp('') failNextRequest = true await subject @@ -169,14 +169,18 @@ describe('Write', () => { expect(logs.warn).has.length(0) const points = [ - Point.measurement('test').floatField('value', 1).timestamp('1'), - Point.measurement('test').floatField('value', 2).timestamp(2.1), Point.measurement('test') - .floatField('value', 3) - .timestamp(new Date(3)), + .setFloatField('value', 1) + .setTimestamp('1'), Point.measurement('test') - .floatField('value', 4) - .timestamp(false as any as string), // server decides what to do with such values + .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) @@ -233,7 +237,7 @@ describe('Write', () => { }) .persist() await subject.write( - Point.measurement('test').floatField('value', 1), + Point.measurement('test').setFloatField('value', 1), DATABASE ) expect(logs.error).has.length(0) @@ -253,7 +257,7 @@ describe('Write', () => { }) .persist() await subject.write( - Point.measurement('test').floatField('value', 1), + Point.measurement('test').setFloatField('value', 1), DATABASE ) await subject.close() diff --git a/packages/client/test/unit/util/generics.test.ts b/packages/client/test/unit/util/generics.test.ts index a5207de2..c5415320 100644 --- a/packages/client/test/unit/util/generics.test.ts +++ b/packages/client/test/unit/util/generics.test.ts @@ -22,7 +22,7 @@ describe('writableDataToLineProtocol', () => { }) it('should convert single Point to line protocol', () => { - const point = Point.measurement('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,14 +31,14 @@ describe('writableDataToLineProtocol', () => { }) it('should convert array-like Point to line protocol', () => { - const point1 = Point.measurement('test').floatField('blah', 123.6) + const point1 = Point.measurement('test').setFloatField('blah', 123.6) const date = Date.now() const point2 = Point.measurement('test') - .floatField('blah', 456.7) - .timestamp(date) + .setFloatField('blah', 456.7) + .setTimestamp(date) const point3 = Point.measurement('test') - .floatField('blah', 789.8) - .timestamp('') + .setFloatField('blah', 789.8) + .setTimestamp('') const input: WritableData = [point1, point2, point3] const output = writableDataToLineProtocol(input) expect(output.length).to.equal(3) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index db6d3484..66c039f2 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -4,12 +4,12 @@ import {Point, convertTime} from '../../../src' describe('point', () => { it('creates point with various fields', () => { const point = Point.measurement('blah') - .booleanField('truthy', true) - .booleanField('falsy', false) - .intField('intFromString', '20') - .floatField('floatFromString', '60.3') - .stringField('str', 'abc') - .timestamp('') + .setBooleanField('truthy', true) + .setBooleanField('falsy', false) + .setIntField('intFromString', '20') + .setFloatField('floatFromString', '60.3') + .setStringField('str', 'abc') + .setTimestamp('') expect(point.toLineProtocol()).to.equal( 'blah falsy=F,floatFromString=60.3,intFromString=20i,str="abc",truthy=T' ) @@ -17,32 +17,32 @@ describe('point', () => { it('fails on invalid fields', () => { expect(() => { - Point.measurement('a').intField('fails', NaN) + Point.measurement('a').setIntField('fails', NaN) }).to.throw(`invalid integer value for field 'fails': 'NaN'`) expect(() => { - Point.measurement('a').intField('fails', Infinity) + Point.measurement('a').setIntField('fails', Infinity) }).to.throw(`invalid integer value for field 'fails': 'Infinity'!`) expect(() => { - Point.measurement('a').intField('fails', 9223372036854776e3) + Point.measurement('a').setIntField('fails', 9223372036854776e3) }).to.throw( `invalid integer value for field 'fails': '9223372036854776000'!` ) expect(() => { - Point.measurement('a').floatField('fails', Infinity) + Point.measurement('a').setFloatField('fails', Infinity) }).to.throw(`invalid float value for field 'fails': 'Infinity'!`) expect(() => { - Point.measurement('a').uintField('fails', NaN) + Point.measurement('a').setUintField('fails', NaN) }).to.throw(`uint value for field 'fails' out of range: NaN`) expect(() => { - Point.measurement('a').uintField('fails', -1) + Point.measurement('a').setUintField('fails', -1) }).to.throw(`uint value for field 'fails' out of range: -1`) expect(() => { - Point.measurement('a').uintField('fails', Number.MAX_SAFE_INTEGER + 10) + Point.measurement('a').setUintField('fails', Number.MAX_SAFE_INTEGER + 10) }).to.throw( `uint value for field 'fails' out of range: ${ Number.MAX_SAFE_INTEGER + 10 @@ -50,11 +50,11 @@ describe('point', () => { ) expect(() => { - Point.measurement('a').uintField('fails', '10a8') + Point.measurement('a').setUintField('fails', '10a8') }).to.throw(`uint value has an unsupported character at pos 2: 10a8`) expect(() => { - Point.measurement('a').uintField('fails', '18446744073709551616') + Point.measurement('a').setUintField('fails', '18446744073709551616') }).to.throw( `uint value for field 'fails' out of range: 18446744073709551616` ) @@ -62,14 +62,14 @@ describe('point', () => { it('infers type when no type supported', () => { const point = Point.measurement('a') - .fields({ + .setFields({ float: 20.3, float2: 20, string: 'text', bool: true, nothing: undefined as any, }) - .timestamp('') + .setTimestamp('') expect(point.toLineProtocol()).to.equal( 'a bool=T,float=20.3,float2=20,string="text"' ) @@ -77,7 +77,7 @@ describe('point', () => { it('throws when invalid type for method field is provided', () => { expect(() => { - Point.measurement('a').field('errorlike', undefined, 'bad-type' as any) + Point.measurement('a').setField('errorlike', undefined, 'bad-type' as any) }).to.throw( `invalid field type for field 'errorlike': type -> bad-type, value -> undefined!` ) @@ -85,12 +85,12 @@ describe('point', () => { it('adds field using field method', () => { const point = Point.measurement('blah') - .field('truthy', true, 'boolean') - .field('falsy', false, 'boolean') - .field('intFromString', '20', 'integer') - .field('floatFromString', '60.3', 'float') - .field('str', 'abc', 'string') - .timestamp('') + .setField('truthy', true, 'boolean') + .setField('falsy', false, 'boolean') + .setField('intFromString', '20', 'integer') + .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' ) @@ -98,16 +98,16 @@ describe('point', () => { it('creates point with uint fields', () => { const point = Point.measurement('a') - .uintField('floored', 10.88) - .uintField('fromString', '789654123') - .timestamp('') + .setUintField('floored', 10.88) + .setUintField('fromString', '789654123') + .setTimestamp('') expect(point.toLineProtocol()).to.equal( 'a floored=10u,fromString=789654123u' ) }) it('returns field of with getField and throws if type not match', () => { - const point = Point.measurement('a').fields({ + const point = Point.measurement('a').setFields({ float: 20.3, float2: 20, string: 'text', @@ -130,19 +130,19 @@ describe('point', () => { it('creates deep copy of point', () => { const point = Point.measurement('measure1') - .booleanField('truthy', true) - .booleanField('falsy', false) - .intField('intFromString', '20') - .uintField('intFromString', '20') - .floatField('floatFromString', '60.3') - .stringField('str', 'abc') - .timestamp('') + .setBooleanField('truthy', true) + .setBooleanField('falsy', false) + .setIntField('intFromString', '20') + .setUintField('intFromString', '20') + .setFloatField('floatFromString', '60.3') + .setStringField('str', 'abc') + .setTimestamp('') const copy = point.copy() expect(copy.toLineProtocol()).to.equal(point.toLineProtocol()) - copy.intField('truthy', 1) + copy.setIntField('truthy', 1) expect(copy.toLineProtocol()).to.not.equal(point.toLineProtocol()) }) @@ -153,20 +153,20 @@ describe('point', () => { convertTime(value, precision) it('converts empty string to no timestamp', () => { - const p = Point.measurement('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 = Point.measurement('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 = Point.measurement('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 = Point.measurement('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'`) @@ -176,9 +176,9 @@ describe('point', () => { }) it('toString() works same as toLineProtocol()', () => { const p = Point.measurement('a') - .floatField('b', 1) - .tag('c', 'd') - .timestamp('') + .setFloatField('b', 1) + .setTag('c', 'd') + .setTimestamp('') expect(p.toLineProtocol()).equals(p.toString()) }) }) From e9f681741ed99aa19acf6f43b61f4646a0da42f6 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:26:41 +0200 Subject: [PATCH 17/45] fix: renaming error --- packages/client/src/Point.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 8933cdbe..0a023e0c 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -317,7 +317,7 @@ export class Point { if (type === undefined || value === undefined) return const lpStringValue = fieldToLPString(type, value) if (fieldsLine.length > 0) fieldsLine += ',' - fieldsLine += `${escape.setTag(name)}=${lpStringValue}` + fieldsLine += `${escape.tag(name)}=${lpStringValue}` } }) if (fieldsLine.length === 0) return undefined // no fields present @@ -328,7 +328,7 @@ export class Point { const val = this._values.getTag(x) if (val) { tagsLine += ',' - tagsLine += `${escape.setTag(x)}=${escape.setTag(val)}` + tagsLine += `${escape.tag(x)}=${escape.tag(val)}` } } }) From 2e9577449688e061562745a8789f8fce85ba9ccd Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:26:59 +0200 Subject: [PATCH 18/45] docs: Point values comments --- packages/client/src/PointValues.ts | 128 +++++++++++++++++++++++++++-- 1 file changed, 122 insertions(+), 6 deletions(-) diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index f5923ec1..60b1e65d 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -29,6 +29,21 @@ const inferType = ( 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. */ @@ -43,6 +58,11 @@ export class PointValues { */ constructor() {} + /** + * Get measurement name. Can be undefined if not set. + * + * @return measurement name or undefined + */ getMeasurement(): string | undefined { return this._name } @@ -58,6 +78,11 @@ export class PointValues { return this } + /** + * Get timestamp. Can be undefined if not set. + * + * @return timestamp or undefined + */ public getTimestamp(): Date | number | string | undefined { return this._time } @@ -98,19 +123,45 @@ export class PointValues { 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] } + /** + * 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 {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') } @@ -164,6 +215,15 @@ export class PointValues { 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 {GetFieldTypeMissmatchError} Actual type of field doesn't match uint type. + * @returns The uint field value or undefined. + */ public getUintField(name: string): number | undefined { return this.getField(name, 'uinteger') } @@ -206,6 +266,15 @@ export class PointValues { 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 {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') } @@ -225,6 +294,15 @@ export class PointValues { 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 {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') } @@ -243,10 +321,12 @@ export class PointValues { /** * 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 Field type doesn't match actual type + * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match provided numeric type. * @returns this */ public getField( @@ -255,24 +335,29 @@ export class PointValues { ): 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 Field type doesn't match actual type + * @throws {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 Field type doesn't match actual type + * @throws {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 @@ -286,12 +371,17 @@ export class PointValues { if (!fieldEntry) return undefined const [actualType, value] = fieldEntry if (type !== undefined && type !== actualType) - throw new Error( - `field ${name} of type ${actualType} doesn't match expected type ${type}!` - ) + throw new GetFieldTypeMissmatchError(name, actualType, type) 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 @@ -348,19 +438,40 @@ export class PointValues { 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 @@ -372,6 +483,11 @@ export class PointValues { return copy } + /** + * Creates new Point with this as values. + * + * @returns Point from this values. + */ public asPoint(): Point { return Point.fromValues(this) } From c8f3a18e956e341a1f605ba277b9b222a54ce8ae Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:09:16 +0200 Subject: [PATCH 19/45] refactor: rename int to integer --- packages/client/src/Point.ts | 21 ++++++----- packages/client/src/PointValues.ts | 38 ++++++++++---------- packages/client/test/integration/e2e.test.ts | 2 +- packages/client/test/unit/util/point.test.ts | 28 +++++++-------- 4 files changed, 48 insertions(+), 41 deletions(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 0a023e0c..9ef721b6 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -131,6 +131,10 @@ export class Point { return this._values.getTagNames() } + public getFloatField(name: string): number | undefined { + return this._values.getFloatField(name) + } + /** * Adds a number field. * @@ -143,8 +147,9 @@ export class Point { this._values.setFloatField(name, value) return this } - public getIntField(name: string): number | undefined { - return this._values.getIntField(name) + + public getIntegerField(name: string): number | undefined { + return this._values.getIntegerField(name) } /** @@ -155,12 +160,12 @@ export class Point { * @returns this * @throws NaN or out of int64 range value is supplied */ - public setIntField(name: string, value: number | any): Point { - this._values.setIntField(name, value) + public setIntegerField(name: string, value: number | any): Point { + this._values.setIntegerField(name, value) return this } - public getUintField(name: string): number | undefined { - return this._values.getUintField(name) + public getUintegerField(name: string): number | undefined { + return this._values.getUintegerField(name) } /** @@ -171,8 +176,8 @@ export class Point { * @returns this * @throws NaN out of range value is supplied */ - public setUintField(name: string, value: number | any): Point { - this._values.setUintField(name, value) + public setUintegerField(name: string, value: number | any): Point { + this._values.setUintegerField(name, value) return this } public getStringField(name: string): string | undefined { diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index 60b1e65d..02b71f04 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -110,6 +110,16 @@ export class PointValues { 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] + } + /** * Adds a tag. The caller has to ensure that both name and value are not empty * and do not end with backslash. @@ -123,16 +133,6 @@ export class PointValues { 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] - } - /** * Removes a tag with the specified name if it exists; otherwise, it does nothing. * @@ -189,7 +189,7 @@ export class PointValues { return this } - public getIntField(name: string): number | undefined { + public getIntegerField(name: string): number | undefined { return this.getField(name, 'integer') } @@ -201,7 +201,7 @@ export class PointValues { * @returns this * @throws NaN or out of int64 range value is supplied */ - public setIntField(name: string, value: number | any): PointValues { + public setIntegerField(name: string, value: number | any): PointValues { let val: number if (typeof value === 'number') { val = value @@ -224,7 +224,7 @@ export class PointValues { * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match uint type. * @returns The uint field value or undefined. */ - public getUintField(name: string): number | undefined { + public getUintegerField(name: string): number | undefined { return this.getField(name, 'uinteger') } @@ -236,7 +236,7 @@ export class PointValues { * @returns this * @throws NaN out of range value is supplied */ - public setUintField(name: string, value: number | any): PointValues { + 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}`) @@ -410,9 +410,9 @@ export class PointValues { case 'float': return this.setFloatField(name, value) case 'integer': - return this.setIntField(name, value) + return this.setIntegerField(name, value) case 'uinteger': - return this.setUintField(name, value) + return this.setUintegerField(name, value) case undefined: return this default: @@ -488,7 +488,9 @@ export class PointValues { * * @returns Point from this values. */ - public asPoint(): Point { - return Point.fromValues(this) + public asPoint(measurement?: string): Point { + return Point.fromValues( + measurement ? this.setMeasurement(measurement) : this + ) } } diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index 67022af1..c24b3a2c 100644 --- a/packages/client/test/integration/e2e.test.ts +++ b/packages/client/test/integration/e2e.test.ts @@ -47,7 +47,7 @@ describe('e2e test', () => { .setTag('unit', 'temperature') .setFloatField('avg', avg1) .setFloatField('max', max1) - .setIntField('testId', testId) + .setIntegerField('testId', testId) await client.write(point, database) const query = ` diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index 66c039f2..bd49233f 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -6,7 +6,7 @@ describe('point', () => { const point = Point.measurement('blah') .setBooleanField('truthy', true) .setBooleanField('falsy', false) - .setIntField('intFromString', '20') + .setIntegerField('intFromString', '20') .setFloatField('floatFromString', '60.3') .setStringField('str', 'abc') .setTimestamp('') @@ -17,15 +17,15 @@ describe('point', () => { it('fails on invalid fields', () => { expect(() => { - Point.measurement('a').setIntField('fails', NaN) + Point.measurement('a').setIntegerField('fails', NaN) }).to.throw(`invalid integer value for field 'fails': 'NaN'`) expect(() => { - Point.measurement('a').setIntField('fails', Infinity) + Point.measurement('a').setIntegerField('fails', Infinity) }).to.throw(`invalid integer value for field 'fails': 'Infinity'!`) expect(() => { - Point.measurement('a').setIntField('fails', 9223372036854776e3) + Point.measurement('a').setIntegerField('fails', 9223372036854776e3) }).to.throw( `invalid integer value for field 'fails': '9223372036854776000'!` ) @@ -34,15 +34,15 @@ describe('point', () => { }).to.throw(`invalid float value for field 'fails': 'Infinity'!`) expect(() => { - Point.measurement('a').setUintField('fails', NaN) + Point.measurement('a').setUintegerField('fails', NaN) }).to.throw(`uint value for field 'fails' out of range: NaN`) expect(() => { - Point.measurement('a').setUintField('fails', -1) + Point.measurement('a').setUintegerField('fails', -1) }).to.throw(`uint value for field 'fails' out of range: -1`) expect(() => { - Point.measurement('a').setUintField('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,11 +50,11 @@ describe('point', () => { ) expect(() => { - Point.measurement('a').setUintField('fails', '10a8') + Point.measurement('a').setUintegerField('fails', '10a8') }).to.throw(`uint value has an unsupported character at pos 2: 10a8`) expect(() => { - Point.measurement('a').setUintField('fails', '18446744073709551616') + Point.measurement('a').setUintegerField('fails', '18446744073709551616') }).to.throw( `uint value for field 'fails' out of range: 18446744073709551616` ) @@ -98,8 +98,8 @@ describe('point', () => { it('creates point with uint fields', () => { const point = Point.measurement('a') - .setUintField('floored', 10.88) - .setUintField('fromString', '789654123') + .setUintegerField('floored', 10.88) + .setUintegerField('fromString', '789654123') .setTimestamp('') expect(point.toLineProtocol()).to.equal( 'a floored=10u,fromString=789654123u' @@ -132,8 +132,8 @@ describe('point', () => { const point = Point.measurement('measure1') .setBooleanField('truthy', true) .setBooleanField('falsy', false) - .setIntField('intFromString', '20') - .setUintField('intFromString', '20') + .setIntegerField('intFromString', '20') + .setUintegerField('intFromString', '20') .setFloatField('floatFromString', '60.3') .setStringField('str', 'abc') .setTimestamp('') @@ -142,7 +142,7 @@ describe('point', () => { expect(copy.toLineProtocol()).to.equal(point.toLineProtocol()) - copy.setIntField('truthy', 1) + copy.setIntegerField('truthy', 1) expect(copy.toLineProtocol()).to.not.equal(point.toLineProtocol()) }) From 52a608c8ba8a3b2f9411c1fe3d81b6a864e6ceee Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:12:37 +0200 Subject: [PATCH 20/45] feat: fromValues throws when measurement missing --- packages/client/src/Point.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 9ef721b6..200534e8 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -60,6 +60,9 @@ export class Point { } 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) } From 6daa3b6979e71aa690561668ac07b43912520fd7 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:30:43 +0200 Subject: [PATCH 21/45] docs: Point comments --- packages/client/src/Point.ts | 121 ++++++++++++++++++++++++++++- packages/client/src/PointValues.ts | 9 +++ 2 files changed, 129 insertions(+), 1 deletion(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 200534e8..face7d76 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -55,10 +55,24 @@ export class Point { 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 {Error} missing measurement + * @return 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!') @@ -66,6 +80,11 @@ export class Point { return new Point(values) } + /** + * Get measurement name. + * + * @return measurement name + */ public getMeasurement(): string { return this._values.getMeasurement() as string } @@ -77,10 +96,17 @@ export class Point { * @returns this */ public setMeasurement(name: string): Point { - this._values.setMeasurement(name) + if (name !== '') { + this._values.setMeasurement(name) + } return this } + /** + * Get timestamp. Can be undefined if not set. + * + * @return timestamp or undefined + */ public getTimestamp(): Date | number | string | undefined { return this._values.getTimestamp() } @@ -108,6 +134,12 @@ export class Point { 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) } @@ -125,15 +157,35 @@ export class Point { 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): 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 {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) } @@ -151,6 +203,15 @@ export class Point { 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 {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) } @@ -167,6 +228,16 @@ export class Point { this._values.setIntegerField(name, value) 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 {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) } @@ -183,6 +254,16 @@ export class Point { this._values.setUintegerField(name, value) 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 {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) } @@ -198,6 +279,16 @@ export class Point { this._values.setStringField(name, 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 {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) } @@ -258,6 +349,13 @@ export class Point { 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) } @@ -287,19 +385,40 @@ export class Point { 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()) } diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index 02b71f04..54b8bd9f 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -189,6 +189,15 @@ export class PointValues { 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 {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') } From f368bf486ff9624aa39b3f8857f8fd1994a35c62 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 12 Sep 2023 14:59:53 +0200 Subject: [PATCH 22/45] feat: downsampling example --- examples/basic/src/index.ts | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 9af928c8..1c3d4bb1 100644 --- a/examples/basic/src/index.ts +++ b/examples/basic/src/index.ts @@ -70,17 +70,36 @@ async function main() { console.log(`max is ${row.max}`) } - // Execute query again as points + 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 DESC;` + + // Execute downsampling query into pointValues const queryPointsResult = client.queryPoints( - query, + downsamplingQuery, database, - queryType, - 'stat' + queryType ) for await (const row of queryPointsResult) { console.log(`avg is ${row.getField('avg', 'float')}`) console.log(`max is ${row.getField('max', 'float')}`) + + // write back downsampled point + const point = row.asPoint('stat_downsampled') + const windowStart = point.getFloatField('window_start') as number + point.setTimestamp(windowStart) + + await client.write(point, database, undefined, { + precision: 'ms', + }) } } catch (err) { console.error(err) From 8238186f9d2d59213207012f3b5f006d3ebb615b Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:00:26 +0200 Subject: [PATCH 23/45] fix: minor fixes --- packages/client/src/InfluxDBClient.ts | 5 ++--- packages/client/src/Point.ts | 2 +- packages/client/src/PointValues.ts | 2 +- packages/client/test/integration/e2e.test.ts | 2 +- packages/client/test/unit/util/point.test.ts | 5 ++++- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index d2eae5c2..fd3a7cc6 100644 --- a/packages/client/src/InfluxDBClient.ts +++ b/packages/client/src/InfluxDBClient.ts @@ -78,8 +78,7 @@ export default class InfluxDBClient { async *queryPoints( query: string, database: string, - queryType: QueryType, - measurement: string + queryType: QueryType ): AsyncGenerator { const points = this._queryApi.queryPoints( query, @@ -90,7 +89,7 @@ export default class InfluxDBClient { ) for await (const point of points) { - yield point.setMeasurement(measurement) + yield point } } diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index face7d76..d9c9c6de 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -75,7 +75,7 @@ export class Point { */ public static fromValues(values: PointValues): Point { if (!values.getMeasurement() || values.getMeasurement() === '') { - throw new Error('cannot convert values to point without measurement set!') + throw new Error('Cannot convert values to point without measurement set!') } return new Point(values) } diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index 54b8bd9f..ee69c66f 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -380,7 +380,7 @@ export class PointValues { if (!fieldEntry) return undefined const [actualType, value] = fieldEntry if (type !== undefined && type !== actualType) - throw new GetFieldTypeMissmatchError(name, actualType, type) + throw new GetFieldTypeMissmatchError(name, type, actualType) return value } diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index c24b3a2c..8591f18e 100644 --- a/packages/client/test/integration/e2e.test.ts +++ b/packages/client/test/integration/e2e.test.ts @@ -75,7 +75,7 @@ describe('e2e test', () => { row = await data.next() expect(row.done).to.equal(true) - const dataPoints = client.queryPoints(query, database, queryType, 'stat') + const dataPoints = client.queryPoints(query, database, queryType) let pointRow: IteratorResult pointRow = await dataPoints.next() diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index bd49233f..15d600f1 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -42,7 +42,10 @@ describe('point', () => { }).to.throw(`uint value for field 'fails' out of range: -1`) expect(() => { - Point.measurement('a').setUintegerField('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 From f55cb9612aff05b8bd64556e4b2aa4cab0f50255 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:23:38 +0200 Subject: [PATCH 24/45] docs: use set instead of add --- packages/client/src/Point.ts | 14 +++++++------- packages/client/src/PointValues.ts | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index d9c9c6de..700849da 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -145,7 +145,7 @@ export class Point { } /** - * Adds a tag. The caller has to ensure that both name and value are not empty + * 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 @@ -191,7 +191,7 @@ export class Point { } /** - * Adds a number field. + * Sets a number field. * * @param name - field name * @param value - field value @@ -217,7 +217,7 @@ export class Point { } /** - * Adds an integer field. + * Sets an integer field. * * @param name - field name * @param value - field value @@ -243,7 +243,7 @@ export class Point { } /** - * Adds an unsigned integer field. + * Sets an unsigned integer field. * * @param name - field name * @param value - field value @@ -269,7 +269,7 @@ export class Point { } /** - * Adds a string field. + * Sets a string field. * * @param name - field name * @param value - field value @@ -294,7 +294,7 @@ export class Point { } /** - * Adds a boolean field. + * Sets a boolean field. * * @param field - field name * @param value - field value @@ -361,7 +361,7 @@ export class Point { } /** - * Adds field based on provided type. + * Sets field based on provided type. * * @param name - field name * @param value - field value diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index ee69c66f..b581413a 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -121,7 +121,7 @@ export class PointValues { } /** - * Adds a tag. The caller has to ensure that both name and value are not empty + * 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 @@ -167,7 +167,7 @@ export class PointValues { } /** - * Adds a number field. + * Sets a number field. * * @param name - field name * @param value - field value @@ -203,7 +203,7 @@ export class PointValues { } /** - * Adds an integer field. + * Sets an integer field. * * @param name - field name * @param value - field value @@ -238,7 +238,7 @@ export class PointValues { } /** - * Adds an unsigned integer field. + * Sets an unsigned integer field. * * @param name - field name * @param value - field value @@ -289,7 +289,7 @@ export class PointValues { } /** - * Adds a string field. + * Sets a string field. * * @param name - field name * @param value - field value @@ -317,7 +317,7 @@ export class PointValues { } /** - * Adds a boolean field. + * Sets a boolean field. * * @param field - field name * @param value - field value @@ -398,7 +398,7 @@ export class PointValues { } /** - * Adds field based on provided type. + * Sets field based on provided type. * * @param name - field name * @param value - field value From 138ff063eca6b3495bbce05a422dd15509a83403 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:33:10 +0200 Subject: [PATCH 25/45] docs: fix --- packages/client/src/Point.ts | 3 +-- packages/client/src/PointValues.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 700849da..0ab8d8fe 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -376,8 +376,7 @@ export class Point { /** * Add fields according to their type. All numeric type is considered float * - * @param name - field name - * @param value - field value + * @param fields - name-value map * @returns this */ public setFields(fields: {[key: string]: number | boolean | string}): Point { diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index b581413a..eeabaee7 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -434,8 +434,7 @@ export class PointValues { /** * Add fields according to their type. All numeric type is considered float * - * @param name - field name - * @param value - field value + * @param fields - name-value map * @returns this */ public setFields(fields: { From 5e1f77546b54e87cfcfaac2db5856d1dc7190d74 Mon Sep 17 00:00:00 2001 From: Sciator <39964450+Sciator@users.noreply.github.com> Date: Thu, 14 Sep 2023 11:00:59 +0200 Subject: [PATCH 26/45] fix: apidoc --- packages/client/src/Point.ts | 18 +++++++++--------- packages/client/src/PointValues.ts | 20 ++++++++++---------- packages/client/src/index.ts | 1 + 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index 0ab8d8fe..e57bdae6 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -70,8 +70,8 @@ export class Point { * Can throw error if measurement missing. * * @param values - point values object with measurement - * @throws {Error} missing measurement - * @return new point from values + * @throws missing measurement + * @returns new point from values */ public static fromValues(values: PointValues): Point { if (!values.getMeasurement() || values.getMeasurement() === '') { @@ -83,7 +83,7 @@ export class Point { /** * Get measurement name. * - * @return measurement name + * @returns measurement name */ public getMeasurement(): string { return this._values.getMeasurement() as string @@ -105,7 +105,7 @@ export class Point { /** * Get timestamp. Can be undefined if not set. * - * @return timestamp or undefined + * @returns timestamp or undefined */ public getTimestamp(): Date | number | string | undefined { return this._values.getTimestamp() @@ -183,7 +183,7 @@ export class Point { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match float type. + * @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 { @@ -209,7 +209,7 @@ export class Point { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match integer type. + * @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 { @@ -235,7 +235,7 @@ export class Point { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match uint type. + * @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 { @@ -261,7 +261,7 @@ export class Point { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match string type. + * @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 { @@ -286,7 +286,7 @@ export class Point { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match boolean type. + * @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 { diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index eeabaee7..abc8533d 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -61,7 +61,7 @@ export class PointValues { /** * Get measurement name. Can be undefined if not set. * - * @return measurement name or undefined + * @returns measurement name or undefined */ getMeasurement(): string | undefined { return this._name @@ -81,7 +81,7 @@ export class PointValues { /** * Get timestamp. Can be undefined if not set. * - * @return timestamp or undefined + * @returns timestamp or undefined */ public getTimestamp(): Date | number | string | undefined { return this._time @@ -159,7 +159,7 @@ export class PointValues { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match float type. + * @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 { @@ -195,7 +195,7 @@ export class PointValues { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match integer type. + * @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 { @@ -230,7 +230,7 @@ export class PointValues { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match uint type. + * @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 { @@ -281,7 +281,7 @@ export class PointValues { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match string type. + * @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 { @@ -309,7 +309,7 @@ export class PointValues { * If the field is not present, returns undefined. * * @param name - field name - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match boolean type. + * @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 { @@ -335,7 +335,7 @@ export class PointValues { * * @param name - field name * @param type - field numeric type - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match provided numeric type. + * @throws {@link GetFieldTypeMissmatchError} Actual type of field doesn't match provided numeric type. * @returns this */ public getField( @@ -349,7 +349,7 @@ export class PointValues { * * @param name - field name * @param type - field string type - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match provided '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 @@ -360,7 +360,7 @@ export class PointValues { * * @param name - field name * @param type - field boolean type - * @throws {GetFieldTypeMissmatchError} Actual type of field doesn't match provided '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 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' From d42c665d8b5576ed892758bad195be7127674aca Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 09:57:48 +0200 Subject: [PATCH 27/45] docs: correct name for type --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c85c9fbe..a0209aae 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ for await (const row of queryResult) { } ``` -or use typesafe Point structure with `client.queryPoints` +or use typesafe `PointValues` structure with `client.queryPoints` ```ts const queryPointsResult = client.queryPoints( From 175164ae676bcbffb5bdfa631de7208cbc1c391a Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 10:05:06 +0200 Subject: [PATCH 28/45] docs: add documentation --- packages/client/src/InfluxDBClient.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index fd3a7cc6..4fe80edd 100644 --- a/packages/client/src/InfluxDBClient.ts +++ b/packages/client/src/InfluxDBClient.ts @@ -45,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, @@ -61,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, @@ -75,6 +90,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 PointValues object. + */ async *queryPoints( query: string, database: string, @@ -93,6 +116,9 @@ export default class InfluxDBClient { } } + /** + * Closes the client and all its resources (connections, ...) + */ async close(): Promise { await this._writeApi.close() await this._queryApi.close() From a89ba34b7385e1deee65f7003e439bbc99889a7c Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 10:12:25 +0200 Subject: [PATCH 29/45] fix: correct field name --- packages/client/src/Point.ts | 6 +++--- packages/client/src/PointValues.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/client/src/Point.ts b/packages/client/src/Point.ts index e57bdae6..e2b84978 100644 --- a/packages/client/src/Point.ts +++ b/packages/client/src/Point.ts @@ -42,7 +42,7 @@ export class Point { * Create a new Point with given values. * After creating Point, it's values shouldn't be modified directly by PointValues object. * - * @param measurementName - the measurement name + * @param values - point values */ private constructor(values: PointValues) private constructor(arg0?: PointValues | string) { @@ -296,7 +296,7 @@ export class Point { /** * Sets a boolean field. * - * @param field - field name + * @param name - field name * @param value - field value * @returns this */ @@ -424,7 +424,7 @@ export class Point { /** * 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 */ diff --git a/packages/client/src/PointValues.ts b/packages/client/src/PointValues.ts index abc8533d..4adc7934 100644 --- a/packages/client/src/PointValues.ts +++ b/packages/client/src/PointValues.ts @@ -319,7 +319,7 @@ export class PointValues { /** * Sets a boolean field. * - * @param field - field name + * @param name - field name * @param value - field value * @returns this */ From 1dbeeb97708a856d71eea3d514c46eb245f64bfa Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 10:21:45 +0200 Subject: [PATCH 30/45] chore: test converting point values to point --- packages/client/test/unit/util/point.test.ts | 28 +++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index 15d600f1..ea2f70dc 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -1,5 +1,5 @@ import {expect} from 'chai' -import {Point, convertTime} from '../../../src' +import {Point, PointValues, convertTime} from '../../../src' describe('point', () => { it('creates point with various fields', () => { @@ -185,4 +185,30 @@ describe('point', () => { expect(p.toLineProtocol()).equals(p.toString()) }) }) + + 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 ('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!` + ) + + }) + }) }) From 7559f37cdbb0eadd5781629bd32abdf86d1a4437 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 10:53:31 +0200 Subject: [PATCH 31/45] fix: code style --- packages/client/test/unit/util/point.test.ts | 33 +++++++++----------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index ea2f70dc..31fa016e 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -187,28 +187,25 @@ describe('point', () => { }) describe('point values', () => { - it ('convert point values to point', () => { + 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); + .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 ('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('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!`) }) }) }) From 802ad9f4020e595bf8fabb3466cb6525f4f8a138 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 11:00:34 +0200 Subject: [PATCH 32/45] chore: test point values --- packages/client/test/unit/util/point.test.ts | 33 +++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index 31fa016e..ececa45d 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -196,7 +196,15 @@ describe('point', () => { 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) + const p = v.asPoint() + expect('a,c=d b=1 150').equals(p.toString()) + }) it('convert point values to point with undefined measurement', () => { const v = new PointValues() .setMeasurement('') @@ -207,5 +215,28 @@ describe('point', () => { 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()) + }) }) }) From 9d28e6978043cdc4d9d647089e972d99a33311b3 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 13:33:53 +0200 Subject: [PATCH 33/45] chore: test point values --- packages/client/test/unit/util/point.test.ts | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index ececa45d..c4e1b165 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -238,5 +238,28 @@ describe('point', () => { 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) + .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')) + }) }) }) From b05ab2baa2463d3d8cc1c5cb11f97879b3e7d22e Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 13:42:41 +0200 Subject: [PATCH 34/45] chore: test point values --- packages/client/test/unit/util/point.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index c4e1b165..a6300b71 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -202,8 +202,10 @@ describe('point', () => { .setField('b', 1) .setTag('c', 'd') .setTimestamp(150) - const p = v.asPoint() + 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() @@ -255,11 +257,23 @@ describe('point', () => { .setField('c', 'xyz') .setField('d', false) .setField('e', 3.45) + .setUintegerField('f', 8) + .setStringField('g', 88) .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')) + }) }) From 29ed88364c5f321f1630957dcf28d6642257ac79 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Mon, 18 Sep 2023 13:58:26 +0200 Subject: [PATCH 35/45] chore: test point values --- packages/client/src/InfluxDBClient.ts | 4 ++-- packages/client/test/integration/e2e.test.ts | 2 +- packages/client/test/unit/Influxdb.test.ts | 10 ++++++++++ packages/client/test/unit/util/point.test.ts | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index 4fe80edd..64f8384f 100644 --- a/packages/client/src/InfluxDBClient.ts +++ b/packages/client/src/InfluxDBClient.ts @@ -100,8 +100,8 @@ export default class InfluxDBClient { */ async *queryPoints( query: string, - database: string, - queryType: QueryType + database?: string, + queryType: QueryType = 'sql' ): AsyncGenerator { const points = this._queryApi.queryPoints( query, diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index 8591f18e..40593301 100644 --- a/packages/client/test/integration/e2e.test.ts +++ b/packages/client/test/integration/e2e.test.ts @@ -1,7 +1,7 @@ import {expect} from 'chai' import {InfluxDBClient, Point} from '../../src' import {rejects} from 'assert' -import {PointValues} from '../../src/PointValues' +import {PointValues} from '../../src' const getEnvVariables = () => { const { diff --git a/packages/client/test/unit/Influxdb.test.ts b/packages/client/test/unit/Influxdb.test.ts index 83d7b6be..bd294ff8 100644 --- a/packages/client/test/unit/Influxdb.test.ts +++ b/packages/client/test/unit/Influxdb.test.ts @@ -20,6 +20,7 @@ describe('InfluxDB', () => { 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'] @@ -39,6 +40,15 @@ describe('InfluxDB', () => { client.query(query, 'another') expect(queryStub.calledOnceWith(query, 'another', 'sql')).to.be.true + + // queryPoints + client.queryPoints(query).next() + + expect(queryPointsStub.calledOnceWith(query, database, 'sql')).to.be.true + queryPointsStub.resetHistory() + + client.queryPoints(query, 'another').next() + expect(queryPointsStub.calledOnceWith(query, 'another', 'sql')).to.be.true }) it('throws when no database provided', async () => { diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index a6300b71..d91310bd 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -259,6 +259,7 @@ describe('point', () => { .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')) From 095a51c756f7693c129de0d6694cf5b5fdc523a4 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 09:15:29 +0200 Subject: [PATCH 36/45] chore: simplify queryPoints --- packages/client/src/InfluxDBClient.ts | 8 ++------ packages/client/test/unit/util/point.test.ts | 1 + 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/client/src/InfluxDBClient.ts b/packages/client/src/InfluxDBClient.ts index 64f8384f..9fa2a0bb 100644 --- a/packages/client/src/InfluxDBClient.ts +++ b/packages/client/src/InfluxDBClient.ts @@ -98,22 +98,18 @@ export default class InfluxDBClient { * @param queryType - The type of query (default: 'sql'). * @returns An async generator that yields PointValues object. */ - async *queryPoints( + queryPoints( query: string, database?: string, queryType: QueryType = 'sql' ): AsyncGenerator { - const points = this._queryApi.queryPoints( + return this._queryApi.queryPoints( query, database ?? this._options.database ?? throwReturn(new Error(argumentErrorMessage)), queryType ) - - for await (const point of points) { - yield point - } } /** diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index d91310bd..a4f56fc3 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -4,6 +4,7 @@ import {Point, PointValues, convertTime} from '../../../src' describe('point', () => { it('creates point with various fields', () => { const point = Point.measurement('blah') + .setMeasurement('') .setBooleanField('truthy', true) .setBooleanField('falsy', false) .setIntegerField('intFromString', '20') From 04b97fe06174ac8b20f65fdbfb2735e5403c65f3 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 09:23:42 +0200 Subject: [PATCH 37/45] fix: tests --- packages/client/test/unit/Influxdb.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client/test/unit/Influxdb.test.ts b/packages/client/test/unit/Influxdb.test.ts index bd294ff8..3c9ede16 100644 --- a/packages/client/test/unit/Influxdb.test.ts +++ b/packages/client/test/unit/Influxdb.test.ts @@ -42,12 +42,12 @@ describe('InfluxDB', () => { expect(queryStub.calledOnceWith(query, 'another', 'sql')).to.be.true // queryPoints - client.queryPoints(query).next() + client.queryPoints(query) expect(queryPointsStub.calledOnceWith(query, database, 'sql')).to.be.true queryPointsStub.resetHistory() - client.queryPoints(query, 'another').next() + client.queryPoints(query, 'another') expect(queryPointsStub.calledOnceWith(query, 'another', 'sql')).to.be.true }) From dc6ab3e66d3839d8a3f2d0f730ea178275f816fb Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 11:58:24 +0200 Subject: [PATCH 38/45] chore: test point --- packages/client/test/unit/Influxdb.test.ts | 12 +++++ packages/client/test/unit/util/point.test.ts | 47 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/packages/client/test/unit/Influxdb.test.ts b/packages/client/test/unit/Influxdb.test.ts index 3c9ede16..86989b41 100644 --- a/packages/client/test/unit/Influxdb.test.ts +++ b/packages/client/test/unit/Influxdb.test.ts @@ -63,6 +63,18 @@ 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/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index a4f56fc3..3fcd561b 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -151,6 +151,53 @@ describe('point', () => { 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')) + }) + + 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', () => { const precision = 'ms' const clinetConvertTime = (value: Parameters[0]) => From 452236883feb767331e2d054d0719adc1939f630 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 12:07:05 +0200 Subject: [PATCH 39/45] chore: test point --- packages/client/test/unit/util/point.test.ts | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index 3fcd561b..de7ef1be 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -180,6 +180,35 @@ describe('point', () => { 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', () => { From 6fec0145f7c83cb7dc11eb27dbfb97fcea49ad6c Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 12:27:22 +0200 Subject: [PATCH 40/45] chore: test aggregation --- packages/client/test/integration/e2e.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/client/test/integration/e2e.test.ts b/packages/client/test/integration/e2e.test.ts index 40593301..11895c3c 100644 --- a/packages/client/test/integration/e2e.test.ts +++ b/packages/client/test/integration/e2e.test.ts @@ -75,7 +75,7 @@ describe('e2e test', () => { row = await data.next() expect(row.done).to.equal(true) - const dataPoints = client.queryPoints(query, database, queryType) + let dataPoints = client.queryPoints(query, database, queryType) let pointRow: IteratorResult pointRow = await dataPoints.next() @@ -87,6 +87,23 @@ describe('e2e test', () => { 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()) }) From 47b6e77c53053d45a46916ba362fc4da7c3561c5 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 12:39:35 +0200 Subject: [PATCH 41/45] chore: test aggregation --- packages/client/test/unit/util/point.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/client/test/unit/util/point.test.ts b/packages/client/test/unit/util/point.test.ts index de7ef1be..29e6fbf8 100644 --- a/packages/client/test/unit/util/point.test.ts +++ b/packages/client/test/unit/util/point.test.ts @@ -92,11 +92,12 @@ describe('point', () => { .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' + 'blah falsy=F,floatFromString=60.3,intFromString=20i,str="abc",truthy=T,uintFromString=30u' ) }) @@ -261,6 +262,13 @@ describe('point', () => { .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', () => { From a6c665549fe943ed54345e278edee03147a91a92 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 13:24:26 +0200 Subject: [PATCH 42/45] feat: use separate directory for downsampling data --- examples/README.md | 3 +- examples/basic/README.md | 2 +- examples/downsampling/README.md | 32 +++++++++++ examples/downsampling/package.json | 17 ++++++ examples/downsampling/src/index.ts | 84 +++++++++++++++++++++++++++++ examples/downsampling/tsconfig.json | 11 ++++ 6 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 examples/downsampling/README.md create mode 100644 examples/downsampling/package.json create mode 100644 examples/downsampling/src/index.ts create mode 100644 examples/downsampling/tsconfig.json 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/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"] +} From 2152181fa84d047f3d61688d688d0ca164bed037 Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 13:24:54 +0200 Subject: [PATCH 43/45] feat: use separate directory for downsampling data --- examples/downsampling/yarn.lock | 547 ++++++++++++++++++++++++++++++++ 1 file changed, 547 insertions(+) create mode 100644 examples/downsampling/yarn.lock 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== From 57217da9ca3d63a5db5bec55a182eeca24237eee Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 13:27:06 +0200 Subject: [PATCH 44/45] feat: use separate directory for downsampling data --- examples/basic/src/index.ts | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 1c3d4bb1..26fbeea6 100644 --- a/examples/basic/src/index.ts +++ b/examples/basic/src/index.ts @@ -70,20 +70,9 @@ async function main() { console.log(`max is ${row.max}`) } - 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 DESC;` - - // Execute downsampling query into pointValues + // Execute query again as points const queryPointsResult = client.queryPoints( - downsamplingQuery, + query, database, queryType ) @@ -91,15 +80,6 @@ async function main() { for await (const row of queryPointsResult) { console.log(`avg is ${row.getField('avg', 'float')}`) console.log(`max is ${row.getField('max', 'float')}`) - - // write back downsampled point - const point = row.asPoint('stat_downsampled') - const windowStart = point.getFloatField('window_start') as number - point.setTimestamp(windowStart) - - await client.write(point, database, undefined, { - precision: 'ms', - }) } } catch (err) { console.error(err) From 37399b32f9c97b22e26f10c9ec8259d9da30b33a Mon Sep 17 00:00:00 2001 From: Jakub Bednar Date: Tue, 19 Sep 2023 13:31:31 +0200 Subject: [PATCH 45/45] feat: use separate directory for downsampling data --- examples/basic/src/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/basic/src/index.ts b/examples/basic/src/index.ts index 26fbeea6..2369e5a2 100644 --- a/examples/basic/src/index.ts +++ b/examples/basic/src/index.ts @@ -71,11 +71,7 @@ async function main() { } // Execute query again as points - const queryPointsResult = client.queryPoints( - query, - database, - queryType - ) + const queryPointsResult = client.queryPoints(query, database, queryType) for await (const row of queryPointsResult) { console.log(`avg is ${row.getField('avg', 'float')}`)