├── .prettierrc ├── .gitignore ├── .npmignore ├── scripts ├── generateDocs.sh └── generateCerts.sh ├── .gitmodules ├── docs ├── .nojekyll ├── assets │ ├── hierarchy.js │ ├── navigation.js │ ├── highlight.css │ ├── icons.svg │ └── icons.js ├── hierarchy.html ├── types │ ├── TimestampUnit.html │ ├── Logger.html │ └── ExtraOptions.html └── functions │ ├── bigintToTwosComplementBytes.html │ ├── createTransport.html │ └── createBuffer.html ├── eslint.config.mjs ├── tsconfig.json ├── typedoc.json ├── src ├── index.ts ├── logging.ts ├── buffer │ ├── bufferv1.ts │ ├── bufferv3.ts │ ├── bufferv2.ts │ └── index.ts ├── transport │ ├── index.ts │ ├── http │ │ ├── base.ts │ │ ├── undici.ts │ │ └── stdlib.ts │ └── tcp.ts ├── validation.ts └── utils.ts ├── examples ├── basic.js ├── auth.js ├── auth_tls.js ├── auth_tcp.js ├── auth_tls_tcp.js ├── arrays.ts └── workers.ts ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── test ├── utils.decimal.test.ts ├── certs │ ├── client │ │ ├── client.csr │ │ ├── client.crt │ │ └── client.key │ ├── server │ │ ├── server.csr │ │ ├── server.crt │ │ └── server.key │ └── ca │ │ ├── ca.crt │ │ └── ca.key ├── util │ ├── proxy.ts │ ├── mockproxy.ts │ ├── proxyfunctions.ts │ └── mockhttp.ts ├── testapp.ts └── logging.test.ts ├── examples.manifest.yaml ├── package.json ├── CONTRIBUTING.md ├── CLAUDE.md └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .idea 4 | *.iml 5 | .DS_Store 6 | dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .idea 4 | *.iml 5 | .DS_Store 6 | test 7 | notes.md -------------------------------------------------------------------------------- /scripts/generateDocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | jsdoc index.js src/sender.js src/row.js src/timestamp.js README.md -d docs 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "questdb-client-test"] 2 | path = questdb-client-test 3 | url = https://github.com/questdb/questdb-client-test.git 4 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from '@eslint/js'; 4 | import tseslint from 'typescript-eslint'; 5 | 6 | export default tseslint.config( 7 | eslint.configs.recommended, 8 | tseslint.configs.recommended, 9 | ); -------------------------------------------------------------------------------- /docs/assets/hierarchy.js: -------------------------------------------------------------------------------- 1 | window.hierarchyData = "eJx1jkEOgjAURO8y6ypIkUivITvComk/obG0pK0r0rsbNBqicfWT+TPzZkXwPkWInrd8YAg0WlLJeBchVvCWb8fJmSBwJacpdEG6uPiQwHAzTkNU54bhHiwEjEsURqkoFl/u45RmCwZlZYwQSFEftvjhE9mek7E6kIPo67IZMkNdNrsFnVp+8afq8sY/yykWe99f8EvIOT8AighX2g==" -------------------------------------------------------------------------------- /docs/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "eJyF0s1OwzAMAOB3ybmiNGIDehxC4oDEgYwL4hBSt4tofpR4YhPi3ZHoYUmaptfY/mTHfv8hCCckLXkF3YHbHfse3FtDKmI5HkhLxMi9B1/H8asDqpFU5EvqjrQNvfut8hJdkWhJEg44wpR5cfqjFiiN9nUYj53tzUJDF0ZqBNdzkXQUO3SzDaBnMwwhgWcLvp5ek7rr+9tmQ4PaxxM6/mL/G0+FMLbmTL3OoPhzs1ZuS0tAqfIJ0TLHtbfG4RyIwiVnrzspZEFKEtanyVizLS9w8aKnw8pw6e0taNH5MVH6rjBampBJBR65snstMT2gKLh2QZ9ykBqZYd/GPxhlR1CgcXdG8LlBC+mzoT/+AEllex0=" -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "compilerOptions": { 4 | "moduleResolution": "bundler", 5 | "module": "ESNext", 6 | "declaration": true, 7 | "target": "ESNext", 8 | "lib": [ 9 | "es2020", 10 | "esnext" 11 | ], 12 | // Types should go into this directory 13 | // Go to .js file when using IDE functions like "Go to Definition" in VSCode 14 | "declarationMap": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://typedoc.org/schema.json", 3 | "entryPoints": ["./src/index.ts"], 4 | "out": "docs", 5 | "name": "QuestDB Node.js Client", 6 | "readme": "./README.md", 7 | "tsconfig": "./tsconfig.json", 8 | "exclude": ["**/test/**/*", "**/examples/**/*", "**/node_modules/**/*"], 9 | "excludePrivate": true, 10 | "excludeProtected": false, 11 | "excludeInternal": true, 12 | "includeVersion": true, 13 | "disableSources": false, 14 | "searchInComments": true, 15 | "navigationLinks": { 16 | "GitHub": "https://github.com/questdb/nodejs-questdb-client", 17 | "QuestDB": "https://questdb.io" 18 | }, 19 | "categorizeByGroup": true, 20 | "sort": ["source-order"], 21 | "visibilityFilters": { 22 | "protected": true, 23 | "private": false, 24 | "inherited": true, 25 | "external": false 26 | } 27 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A Node.js client for QuestDB. 3 | * @packageDocumentation 4 | */ 5 | 6 | export { Sender } from "./sender"; 7 | export { SenderOptions } from "./options"; 8 | export type { ExtraOptions } from "./options"; 9 | export type { TimestampUnit } from "./utils"; 10 | export type { SenderBuffer } from "./buffer"; 11 | export { createBuffer } from "./buffer"; 12 | export { SenderBufferV1 } from "./buffer/bufferv1"; 13 | export { SenderBufferV2 } from "./buffer/bufferv2"; 14 | export type { SenderTransport } from "./transport"; 15 | export { createTransport } from "./transport"; 16 | export { TcpTransport } from "./transport/tcp"; 17 | export { HttpTransport } from "./transport/http/stdlib"; 18 | export { UndiciTransport } from "./transport/http/undici"; 19 | export type { Logger } from "./logging"; 20 | export { bigintToTwosComplementBytes } from "./utils"; 21 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | const { Sender } = require("@questdb/nodejs-client"); 2 | 3 | async function run() { 4 | // create a sender using HTTP protocol 5 | const sender = await Sender.fromConfig("http::addr=127.0.0.1:9000"); 6 | 7 | // add rows to the buffer of the sender 8 | await sender 9 | .table("trades") 10 | .symbol("symbol", "BTC-USD") 11 | .symbol("side", "sell") 12 | .floatColumn("price", 39269.98) 13 | .floatColumn("amount", 0.011) 14 | .at(Date.now(), "ms"); 15 | 16 | // flush the buffer of the sender, sending the data to QuestDB 17 | // the buffer is cleared after the data is sent, and the sender is ready to accept new data 18 | await sender.flush(); 19 | 20 | // close the connection after all rows ingested 21 | // unflushed data will be lost 22 | await sender.close(); 23 | } 24 | 25 | run().then(console.log).catch(console.error); 26 | -------------------------------------------------------------------------------- /examples/auth.js: -------------------------------------------------------------------------------- 1 | const { Sender } = require("@questdb/nodejs-client"); 2 | 3 | async function run() { 4 | // authentication details 5 | const USER = "admin"; 6 | const PWD = "quest"; 7 | 8 | // pass the authentication details to the sender 9 | // for secure connection use 'https' protocol instead of 'http' 10 | const sender = await Sender.fromConfig( 11 | `http::addr=127.0.0.1:9000;username=${USER};password=${PWD}` 12 | ); 13 | 14 | // add rows to the buffer of the sender 15 | await sender 16 | .table("trades") 17 | .symbol("symbol", "ETH-USD") 18 | .symbol("side", "sell") 19 | .floatColumn("price", 2615.54) 20 | .floatColumn("amount", 0.00044) 21 | .at(Date.now(), "ms"); 22 | 23 | // flush the buffer of the sender, sending the data to QuestDB 24 | await sender.flush(); 25 | 26 | // close the connection after all rows ingested 27 | await sender.close(); 28 | } 29 | 30 | run().catch(console.error); -------------------------------------------------------------------------------- /examples/auth_tls.js: -------------------------------------------------------------------------------- 1 | const { Sender } = require("@questdb/nodejs-client"); 2 | 3 | async function run() { 4 | // authentication details 5 | const USER = "admin"; 6 | const PWD = "quest"; 7 | 8 | // pass the authentication details to the sender 9 | // for secure connection use 'https' protocol instead of 'http' 10 | const sender = await Sender.fromConfig( 11 | `https::addr=127.0.0.1:9000;username=${USER};password=${PWD}` 12 | ); 13 | 14 | // add rows to the buffer of the sender 15 | await sender 16 | .table("trades") 17 | .symbol("symbol", "ETH-USD") 18 | .symbol("side", "sell") 19 | .floatColumn("price", 2615.54) 20 | .floatColumn("amount", 0.00044) 21 | .at(Date.now(), "ms"); 22 | 23 | // flush the buffer of the sender, sending the data to QuestDB 24 | await sender.flush(); 25 | 26 | // close the connection after all rows ingested 27 | await sender.close(); 28 | } 29 | 30 | run().catch(console.error); -------------------------------------------------------------------------------- /examples/auth_tcp.js: -------------------------------------------------------------------------------- 1 | const { Sender } = require("@questdb/nodejs-client"); 2 | 3 | async function run() { 4 | // authentication details 5 | const CLIENT_ID = "admin"; 6 | const PRIVATE_KEY = "ZRxmCOQBpZoj2fZ-lEtqzVDkCre_ouF3ePpaQNDwoQk"; 7 | 8 | // pass the authentication details to the sender 9 | const sender = await Sender.fromConfig( 10 | `tcp::addr=127.0.0.1:9009;username=${CLIENT_ID};token=${PRIVATE_KEY}` 11 | ); 12 | await sender.connect(); 13 | 14 | // add rows to the buffer of the sender 15 | await sender 16 | .table("trades") 17 | .symbol("symbol", "BTC-USD") 18 | .symbol("side", "sell") 19 | .floatColumn("price", 39269.98) 20 | .floatColumn("amount", 0.001) 21 | .at(Date.now(), "ms"); 22 | 23 | // flush the buffer of the sender, sending the data to QuestDB 24 | await sender.flush(); 25 | 26 | // close the connection after all rows ingested 27 | await sender.close(); 28 | } 29 | 30 | run().catch(console.error); -------------------------------------------------------------------------------- /examples/auth_tls_tcp.js: -------------------------------------------------------------------------------- 1 | const { Sender } = require("@questdb/nodejs-client"); 2 | 3 | async function run() { 4 | // authentication details 5 | const CLIENT_ID = "admin"; 6 | const PRIVATE_KEY = "ZRxmCOQBpZoj2fZ-lEtqzVDkCre_ouF3ePpaQNDwoQk"; 7 | 8 | // pass the authentication details to the sender 9 | const sender = await Sender.fromConfig( 10 | `tcps::addr=127.0.0.1:9009;username=${CLIENT_ID};token=${PRIVATE_KEY}` 11 | ); 12 | await sender.connect(); 13 | 14 | // add rows to the buffer of the sender 15 | await sender 16 | .table("trades") 17 | .symbol("symbol", "BTC-USD") 18 | .symbol("side", "sell") 19 | .floatColumn("price", 39269.98) 20 | .floatColumn("amount", 0.001) 21 | .at(Date.now(), "ms"); 22 | 23 | // flush the buffer of the sender, sending the data to QuestDB 24 | await sender.flush(); 25 | 26 | // close the connection after all rows ingested 27 | await sender.close(); 28 | } 29 | 30 | run().catch(console.error); -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | schedule: 9 | - cron: '15 2,10,18 * * *' 10 | 11 | jobs: 12 | test: 13 | name: Build with Node.js ${{ matrix.node-version }} 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [20, 22, latest] 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | with: 22 | submodules: recursive 23 | 24 | - name: Setup node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - uses: pnpm/action-setup@v4 30 | with: 31 | version: 9 32 | run_install: true 33 | 34 | - name: Linting 35 | run: pnpm eslint 36 | 37 | - name: Type-checking 38 | run: pnpm typecheck 39 | 40 | - name: Tests 41 | run: pnpm test 42 | -------------------------------------------------------------------------------- /test/utils.decimal.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { describe, it, expect } from "vitest"; 3 | import { bigintToTwosComplementBytes } from "../src/utils"; 4 | 5 | describe("bigintToTwosComplementBytes", () => { 6 | it("encodes zero as a single zero byte", () => { 7 | expect(bigintToTwosComplementBytes(0n)).toEqual([0x00]); 8 | }); 9 | 10 | it("encodes positive values without unnecessary sign-extension", () => { 11 | expect(bigintToTwosComplementBytes(1n)).toEqual([0x01]); 12 | expect(bigintToTwosComplementBytes(123456n)).toEqual([0x01, 0xe2, 0x40]); 13 | }); 14 | 15 | it("adds a leading zero when the positive sign bit would be set", () => { 16 | expect(bigintToTwosComplementBytes(255n)).toEqual([0x00, 0xff]); 17 | }); 18 | 19 | it("encodes negative values with two's complement sign extension", () => { 20 | expect(bigintToTwosComplementBytes(-1n)).toEqual([0xff]); 21 | expect(bigintToTwosComplementBytes(-10n)).toEqual([0xff, 0xf6]); 22 | expect(bigintToTwosComplementBytes(-256n)).toEqual([0xff, 0x00]); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | test: 8 | name: Publish 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | with: 14 | submodules: recursive 15 | 16 | - name: Setup node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 22 20 | 21 | - uses: pnpm/action-setup@v4 22 | with: 23 | version: 9 24 | run_install: true 25 | 26 | - name: Linting 27 | run: pnpm eslint 28 | 29 | - name: Type-checking 30 | run: pnpm typecheck 31 | 32 | - name: Tests 33 | run: pnpm test 34 | 35 | - name: Build 36 | run: pnpm build 37 | 38 | - name: Check for build artifacts 39 | run: | 40 | [ -f dist/cjs/index.js ] || (echo "CJS build missing" && exit 1) 41 | [ -f dist/es/index.mjs ] || (echo "ESM build missing" && exit 1) 42 | 43 | - name: Publish 44 | uses: JS-DevTools/npm-publish@v3 45 | with: 46 | token: ${{ secrets.CI_TOKEN }} 47 | access: public 48 | strategy: all 49 | package: package.json 50 | -------------------------------------------------------------------------------- /examples.manifest.yaml: -------------------------------------------------------------------------------- 1 | - name: ilp 2 | lang: javascript 3 | path: examples/basic.js 4 | header: |- 5 | NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client). 6 | - name: ilp-auth 7 | lang: javascript 8 | path: examples/auth.js 9 | header: |- 10 | NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client). 11 | auth: 12 | kid: testapp 13 | d: 9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8 14 | x: aultdA0PjhD_cWViqKKyL5chm6H1n-BiZBo_48T-uqc 15 | y: __ptaol41JWSpTTL525yVEfzmY8A6Vi_QrW1FjKcHMg 16 | addr: 17 | host: localhost 18 | port: 9009 19 | - name: ilp-auth-tls 20 | lang: javascript 21 | path: examples/auth_tls.js 22 | header: |- 23 | NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client). 24 | auth: 25 | kid: testapp 26 | d: 9b9x5WhJywDEuo1KGQWSPNxtX-6X6R2BRCKhYMMY6n8 27 | x: aultdA0PjhD_cWViqKKyL5chm6H1n-BiZBo_48T-uqc 28 | y: __ptaol41JWSpTTL525yVEfzmY8A6Vi_QrW1FjKcHMg 29 | addr: 30 | host: localhost 31 | port: 9009 32 | - name: ilp-from-conf 33 | lang: javascript 34 | path: examples/basic.js 35 | header: |- 36 | NodeJS client library [repo](https://github.com/questdb/nodejs-questdb-client). 37 | conf: http::addr=localhost:9000 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@questdb/nodejs-client", 3 | "version": "4.2.0", 4 | "description": "QuestDB Node.js Client", 5 | "scripts": { 6 | "test": "vitest", 7 | "build": "bunchee", 8 | "eslint": "eslint src/**", 9 | "typecheck": "tsc --noEmit", 10 | "format": "prettier --write '{src,test}/**/*.{ts,js,json}'", 11 | "docs": "typedoc --out docs src/index.ts", 12 | "preview:docs": "serve docs" 13 | }, 14 | "files": [ 15 | "dist/cjs", 16 | "dist/es" 17 | ], 18 | "main": "dist/cjs/index.js", 19 | "module": "dist/es/index.mjs", 20 | "types": "dist/cjs/index.d.ts", 21 | "exports": { 22 | "import": { 23 | "types": "./dist/es/index.d.mts", 24 | "default": "./dist/es/index.mjs" 25 | }, 26 | "require": { 27 | "types": "./dist/cjs/index.d.ts", 28 | "default": "./dist/cjs/index.js" 29 | } 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+ssh://git@github.com/questdb/nodejs-questdb-client.git" 34 | }, 35 | "keywords": [ 36 | "QuestDB" 37 | ], 38 | "author": "QuestDB", 39 | "license": "Apache-2.0", 40 | "homepage": "https://questdb.github.io/nodejs-questdb-client", 41 | "devDependencies": { 42 | "@eslint/js": "^9.16.0", 43 | "@microsoft/tsdoc": "^0.15.1", 44 | "@types/node": "^22.15.17", 45 | "bunchee": "^6.5.1", 46 | "eslint": "^9.26.0", 47 | "prettier": "^3.5.3", 48 | "serve": "^14.2.4", 49 | "testcontainers": "^10.25.0", 50 | "typedoc": "^0.28.9", 51 | "typescript": "^5.7.2", 52 | "typescript-eslint": "^8.32.0", 53 | "vitest": "^3.1.3" 54 | }, 55 | "dependencies": { 56 | "undici": "^7.8.0" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/logging.ts: -------------------------------------------------------------------------------- 1 | // Log level configuration with console methods and criticality levels.
2 | // Higher criticality values indicate more important messages. 3 | const LOG_LEVELS = { 4 | error: { log: console.error, criticality: 3 }, 5 | warn: { log: console.warn, criticality: 2 }, 6 | info: { log: console.info, criticality: 1 }, 7 | debug: { log: console.debug, criticality: 0 }, 8 | }; 9 | 10 | // Default logging criticality level. Messages with criticality below this level are ignored. 11 | const DEFAULT_CRITICALITY = LOG_LEVELS.info.criticality; 12 | 13 | /** 14 | * Logger function type definition. 15 | * 16 | * @param {'error'|'warn'|'info'|'debug'} level - The log level for the message 17 | * @param {string | Error} message - The message to log, either a string or Error object 18 | */ 19 | type Logger = ( 20 | level: "error" | "warn" | "info" | "debug", 21 | message: string | Error, 22 | ) => void; 23 | 24 | /** 25 | * Simple logger to write log messages to the console.
26 | * Supported logging levels are `error`, `warn`, `info` and `debug`.
27 | * Throws an error if logging level is invalid. 28 | * 29 | * @param {'error'|'warn'|'info'|'debug'} level - The log level for the message 30 | * @param {string | Error} message - The message to log, either a string or Error object 31 | */ 32 | function log( 33 | level: "error" | "warn" | "info" | "debug", 34 | message: string | Error, 35 | ): void { 36 | const logLevel = LOG_LEVELS[level]; 37 | if (!logLevel) { 38 | throw new Error(`Invalid log level: '${level}'`); 39 | } 40 | if (logLevel.criticality >= DEFAULT_CRITICALITY) { 41 | logLevel.log(message); 42 | } 43 | } 44 | 45 | export { log, Logger }; 46 | -------------------------------------------------------------------------------- /test/certs/client/client.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEkzCCAnsCAQAwTjELMAkGA1UEBhMCR0IxCzAJBgNVBAgMAkVOMQowCAYDVQQH 3 | DAEuMRUwEwYDVQQKDAxRVUVTVCBDTElFTlQxDzANBgNVBAMMBkNMSUVOVDCCAiIw 4 | DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALPlJZ3ouHfnrPi9II1vSFaqS4ry 5 | xh9yND1c2A+CB0lfbfoDpK8onBjgPkv3JnebesoLjb2jH+KVnhlr24zt6NbAJWru 6 | ASx3tMI12O5yyFjErH57AC9OMcNx2kNBurIgrAiORCgvw7paNtPZfl+YEYV1uGpN 7 | 5egdHrcUjYq3TmeZiplAdUpgdyugdu7mv8osI2vmbWJdnWVzDR+e5wfBhg2Sxc/G 8 | t7uAINmwMnRWy84VtS+uC37Sv0egFY3XRPgQVo0iBash/cckNz3lLmOKSN8qYLHW 9 | BdHQ0x8loIOwmcCi3gJbiEXH6fiCJRbNqa5Gj5izSuP+LWGhIf2QY+vVars5/7ye 10 | M4vb5jW/8yq4IO6PxANNWyy955HPnk5r8giiOTbr1DX/d6LOFYrGYZzYQKEBil/E 11 | IPvbWv9YYdl8ZHGTMrfat8gtVJHc+yxyZ8P7vLzVGS9CZi4JMGtoU9lHe0NcheIe 12 | ig7tVDeFa48SaXkwjZSve3b8eNIgKJkJahePFlgluGAClxEGsXkNfZAbqKR93Kx7 13 | 1o8aFAcm8kEl7XMm5MLWOtC6onabIx4mtRyuM257DetIkTIp1GUZdg8gRJJq5GMq 14 | 9G1JzO3ApOJaUVAZSKhGZZPpdBi06m9zx/M4B2UTetPPGCXDLuIh5ITBqmPIKYjf 15 | ov/+l/BF4K73JMMNAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAgEArKuTy4wboqjP 16 | LyEwfnzgQbEKtwhWpiUt2Yl6YVhHIZwaZZEQe8Xjcp5z0HtRVACN4wIfHBX35Ajj 17 | xacRQBqwUtNsDeljGI4QEMM2hThLMMfpE0I0N8y/JE+3FYj285IcjnhPjEMoiGZN 18 | nQ71VntAXCFJ8CqrOZk3GF0bhLfgzkfSUExLypVQkDMRvraQiQOr73+HjiWISNMl 19 | 2Qqqz87kN0krINwL2p0LVJWSTFe+AQW4hcCZnRe/lGEVcmALyryci6WRhGWnpiK+ 20 | cmsjlKvgoQbBuOXHn67zJ3j/sz4X24lX+vCo3H0LAL/Q6kOnaX38q+/tEzJh8pNg 21 | fsIQSFPAbonSg2pWuFlVe2h96i0kjTsoTH2BnufnN47gJI3eANZ6gChkfmD0C1G8 22 | s1J+qj5BWU/4gx6u1YQuUctbIK8rtdGq1/l9Wem49Hosbzti1dE1bhLTqLS7rRoX 23 | uSUudK+fM+glcQHRRdhc0OYdO01wXhrzioKNl4hjMK89MI4aZ4kw1nD+pMXVxBcI 24 | HmoiSi99r2O6TBO0tAhLgQA2hQNLOATiaf1LYatRNeS1CCW30EB7sBGluqUrmBhu 25 | MJ0Go7wBGy8Pu/EyXU2DgEeCBtRFk0/UV9zofAWSRomLpDJGi/CjgEF/G/ZWWbsr 26 | gij2bm8G++O8o09ENalOmeVu3wtHkAU= 27 | -----END CERTIFICATE REQUEST----- 28 | -------------------------------------------------------------------------------- /test/util/proxy.ts: -------------------------------------------------------------------------------- 1 | import net, { Socket } from "node:net"; 2 | import tls from "node:tls"; 3 | import { write, listen, shutdown, connect, close } from "./proxyfunctions"; 4 | 5 | // handles only a single client 6 | // client -> server (Proxy) -> remote (QuestDB) 7 | // client <- server (Proxy) <- remote (QuestDB) 8 | class Proxy { 9 | client: Socket; 10 | remote: Socket; 11 | server: net.Server | tls.Server; 12 | 13 | constructor() { 14 | this.remote = new Socket(); 15 | 16 | this.remote.on("data", async (data: string) => { 17 | console.info(`received from remote, forwarding to client: ${data}`); 18 | await write(this.client, data); 19 | }); 20 | 21 | this.remote.on("close", () => { 22 | console.info("remote connection closed"); 23 | }); 24 | 25 | this.remote.on("error", (err: Error) => { 26 | console.error(`remote connection: ${err}`); 27 | }); 28 | } 29 | 30 | async start( 31 | listenPort: number, 32 | remotePort: number, 33 | remoteHost: string, 34 | tlsOptions: Record, 35 | ) { 36 | return new Promise((resolve) => { 37 | this.remote.on("ready", async () => { 38 | console.info("remote connection ready"); 39 | await listen( 40 | this, 41 | listenPort, 42 | async (data: string) => { 43 | console.info(`received from client, forwarding to remote: ${data}`); 44 | await write(this.remote, data); 45 | }, 46 | tlsOptions, 47 | ); 48 | resolve(); 49 | }); 50 | 51 | connect(this, remotePort, remoteHost); 52 | }); 53 | } 54 | 55 | async stop() { 56 | await shutdown(this, async () => await close()); 57 | } 58 | } 59 | 60 | export { Proxy }; 61 | -------------------------------------------------------------------------------- /test/util/mockproxy.ts: -------------------------------------------------------------------------------- 1 | import net, { Socket } from "node:net"; 2 | import tls from "node:tls"; 3 | import { write, listen, shutdown } from "./proxyfunctions"; 4 | 5 | const CHALLENGE_LENGTH = 512; 6 | 7 | type MockConfig = { 8 | auth?: boolean; 9 | assertions?: boolean; 10 | }; 11 | 12 | class MockProxy { 13 | mockConfig: MockConfig; 14 | dataSentToRemote: string[]; 15 | hasSentChallenge: boolean; 16 | client: Socket; 17 | server: net.Server | tls.Server; 18 | 19 | constructor(mockConfig: MockConfig) { 20 | if (!mockConfig) { 21 | throw new Error("Missing mock config"); 22 | } 23 | this.mockConfig = mockConfig; 24 | this.dataSentToRemote = []; 25 | } 26 | 27 | async start(listenPort: number, tlsOptions?: tls.TlsOptions) { 28 | await listen( 29 | this, 30 | listenPort, 31 | async (data) => { 32 | console.info(`received from client: ${data}`); 33 | if (this.mockConfig.assertions) { 34 | this.dataSentToRemote.push(data.toString()); 35 | } 36 | if (this.mockConfig.auth && !this.hasSentChallenge) { 37 | await write(this.client, mockChallenge()); 38 | this.hasSentChallenge = true; 39 | } 40 | }, 41 | tlsOptions, 42 | ); 43 | } 44 | 45 | async stop() { 46 | await shutdown(this); 47 | } 48 | 49 | getDataSentToRemote() { 50 | if (!this.mockConfig.assertions) { 51 | throw new Error("Should be called only when assertions switched on"); 52 | } 53 | return this.dataSentToRemote; 54 | } 55 | } 56 | 57 | function mockChallenge() { 58 | let challenge = ""; 59 | for (let i = 0; i < CHALLENGE_LENGTH - 1; i++) { 60 | challenge += "a"; 61 | } 62 | return challenge + "\n"; 63 | } 64 | 65 | export { MockProxy }; 66 | -------------------------------------------------------------------------------- /test/certs/server/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIEwzCCAqsCAQAwUTELMAkGA1UEBhMCR0IxCzAJBgNVBAgMAkVOMQowCAYDVQQH 3 | DAEuMRUwEwYDVQQKDAxRVUVTVCBTRVJWRVIxEjAQBgNVBAMMCWxvY2FsaG9zdDCC 4 | AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALmv+IbHqlrkIi3V0apF79mz 5 | a0pJnPuiS2nZnO2OAX2wTOxS3RdfeU+1liyjGuzz02t44cURRAjmBEpm32X5yzDe 6 | JiXwEYZZDAOWBl+3NYfPGzqxhZvrd4ccnNt3R1nNzPTa1NvjofpzOnmVUBv2d84G 7 | jVgmaNtiJBRQ6BY9d/zEkxPDcqEa39VR2bM9rE+ZHhfrvSiQq4LyJpV5KG7E7yyc 8 | izvWfDYeiqyAZJ0sOp86UYINh8rcI5x7WY1N/GMjDk7tpR5RJG0srS9IaaMJzSHE 9 | zqLFfPbFihqgc/aJOYe2gUNSEPd5YpZFp/1ZTO8XT6k2gRWxhJ9FBRWY9T8zsoLv 10 | QZyF1AdjtmOi6LV13u+9ZjIlVcNNiEe2Z3Wwyo4rb0S4KEDmQYrHSEZK2znDrKda 11 | UsQ2S2ZwPQQkrpuwkqXgDvKrJoyAxcByCHIq+p87Uy4YSL4+JWT244eBqcpYprlq 12 | 7ouTGikATkR/N+yAqYPgksanaHqARAdJRBzJFIFBHJ44spQ/gBjYEUbHybRLXZpz 13 | wyLK4d8N970AIhUFNnMBsDlQ8+AxtEkb21hjoZZIJSMnhMRCNh1YCREb3TNl4Q+n 14 | B+Fumxua87aZhHcse/J7e7Aa3DYGTXdoZqv8PsQ5LNe/CfgOytQT1/oGSgurRyau 15 | wX25zuL3EEWZapHCVWkvAgMBAAGgLTArBgkqhkiG9w0BCQ4xHjAcMBoGA1UdEQQT 16 | MBGCCWxvY2FsaG9zdIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAe+58NJDrYTHY 17 | 5OUDI/mKc/zJdhvcyr2H0DTSi8KyrhunsjF4O4msZ8D9vKxMMuLdJJdGLXh9PkW/ 18 | 7k5AjfPLx/SjNS3mxtsaXIzNCCWrtKgW566CwPVVtOb1z9ie/2jqyuhiDQW5aX8t 19 | m+JxpH6x3xBFjckomEWf8X8qM/Jx5EiZaKB+VF1bGuZstS4Ln8yyQ16bU5FcKCFL 20 | GKmFKQbB09AA4dLfZC4zQj83f1nUzdalv4glVSVIg/XmOQW6D/H2Doxqn/Qi9lja 21 | JxYUl0ELqbk8KbduQ/b9I4DpYJ2j33+Jeyc0uEHqacQdlaGLanUodyWaHHOgkJey 22 | FvP3T2r3ZOQvQ3rUDlwVmiN3cGpXTz/Q+1r6E3QXd9erMuRPxB6dD3Xs14bttQ/d 23 | Kqy1MekfSKSm4C8IsGgq6m21jh571jWTiZwS8e8shME+eRH5+8XTI6SI0y9RmGuZ 24 | lkJY3hvdF7Whfd2hP9ANs2YX4hBFlYmUGXuU0ddqVYGn/8nelkT3OlpUu3aEIdah 25 | Sg563rJ3O8guM2ysuEE0//K/pyMAhAFbTS4LVnzwZX4jtlvvyWAv+MJNbXarAEvK 26 | LpZclhhg+iJJns1uaGlkTyBBl2jTWQzP6jfFzIj+5SjT2S4eZLkuKPnRbUAPeQQo 27 | UFzcOSVdiSEp17xhNrADQ148T6cYpfw= 28 | -----END CERTIFICATE REQUEST----- 29 | -------------------------------------------------------------------------------- /examples/arrays.ts: -------------------------------------------------------------------------------- 1 | import { Sender } from "@questdb/nodejs-client"; 2 | 3 | async function run() { 4 | // create a sender 5 | const sender = await Sender.fromConfig('http::addr=localhost:9000'); 6 | 7 | // order book snapshots to ingest 8 | const orderBooks = [ 9 | { 10 | symbol: 'BTC-USD', 11 | exchange: 'Coinbase', 12 | timestamp: Date.now(), 13 | bidPrices: [50100.25, 50100.20, 50100.15, 50100.10, 50100.05], 14 | bidSizes: [0.5, 1.2, 2.1, 0.8, 3.5], 15 | askPrices: [50100.30, 50100.35, 50100.40, 50100.45, 50100.50], 16 | askSizes: [0.6, 1.5, 1.8, 2.2, 4.0] 17 | }, 18 | { 19 | symbol: 'ETH-USD', 20 | exchange: 'Coinbase', 21 | timestamp: Date.now(), 22 | bidPrices: [2850.50, 2850.45, 2850.40, 2850.35, 2850.30], 23 | bidSizes: [5.0, 8.2, 12.5, 6.8, 15.0], 24 | askPrices: [2850.55, 2850.60, 2850.65, 2850.70, 2850.75], 25 | askSizes: [4.5, 7.8, 10.2, 8.5, 20.0] 26 | } 27 | ]; 28 | 29 | try { 30 | // add rows to the buffer of the sender 31 | for (const orderBook of orderBooks) { 32 | await sender 33 | .table('order_book_l2') 34 | .symbol('symbol', orderBook.symbol) 35 | .symbol('exchange', orderBook.exchange) 36 | .arrayColumn('bid_prices', orderBook.bidPrices) 37 | .arrayColumn('bid_sizes', orderBook.bidSizes) 38 | .arrayColumn('ask_prices', orderBook.askPrices) 39 | .arrayColumn('ask_sizes', orderBook.askSizes) 40 | .at(orderBook.timestamp, 'ms'); 41 | } 42 | 43 | // flush the buffer of the sender, sending the data to QuestDB 44 | // the buffer is cleared after the data is sent, and the sender is ready to accept new data 45 | await sender.flush(); 46 | } finally { 47 | // close the connection after all rows ingested 48 | await sender.close(); 49 | } 50 | } 51 | 52 | run().then(console.log).catch(console.error); 53 | -------------------------------------------------------------------------------- /test/certs/client/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFDTCCAvUCAQEwDQYJKoZIhvcNAQELBQAwSTELMAkGA1UEBhMCR0IxCzAJBgNV 3 | BAgMAkVOMQowCAYDVQQHDAEuMREwDwYDVQQKDAhRVUVTVCBDQTEOMAwGA1UEAwwF 4 | UVVFU1QwIBcNMjMxMDA5MDY1MjE4WhgPMjA1MTAyMjQwNjUyMThaME4xCzAJBgNV 5 | BAYTAkdCMQswCQYDVQQIDAJFTjEKMAgGA1UEBwwBLjEVMBMGA1UECgwMUVVFU1Qg 6 | Q0xJRU5UMQ8wDQYDVQQDDAZDTElFTlQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw 7 | ggIKAoICAQCz5SWd6Lh356z4vSCNb0hWqkuK8sYfcjQ9XNgPggdJX236A6SvKJwY 8 | 4D5L9yZ3m3rKC429ox/ilZ4Za9uM7ejWwCVq7gEsd7TCNdjucshYxKx+ewAvTjHD 9 | cdpDQbqyIKwIjkQoL8O6WjbT2X5fmBGFdbhqTeXoHR63FI2Kt05nmYqZQHVKYHcr 10 | oHbu5r/KLCNr5m1iXZ1lcw0fnucHwYYNksXPxre7gCDZsDJ0VsvOFbUvrgt+0r9H 11 | oBWN10T4EFaNIgWrIf3HJDc95S5jikjfKmCx1gXR0NMfJaCDsJnAot4CW4hFx+n4 12 | giUWzamuRo+Ys0rj/i1hoSH9kGPr1Wq7Of+8njOL2+Y1v/MquCDuj8QDTVssveeR 13 | z55Oa/IIojk269Q1/3eizhWKxmGc2EChAYpfxCD721r/WGHZfGRxkzK32rfILVSR 14 | 3PsscmfD+7y81RkvQmYuCTBraFPZR3tDXIXiHooO7VQ3hWuPEml5MI2Ur3t2/HjS 15 | ICiZCWoXjxZYJbhgApcRBrF5DX2QG6ikfdyse9aPGhQHJvJBJe1zJuTC1jrQuqJ2 16 | myMeJrUcrjNuew3rSJEyKdRlGXYPIESSauRjKvRtScztwKTiWlFQGUioRmWT6XQY 17 | tOpvc8fzOAdlE3rTzxglwy7iIeSEwapjyCmI36L//pfwReCu9yTDDQIDAQABMA0G 18 | CSqGSIb3DQEBCwUAA4ICAQBS1opGX8iQVlcdBpZXIm5t6vW0htKA/g3ml6/RVHZ4 19 | 8Ml8Tv5mrpfJ0Qvtrmx9QwKbgwAVf3dSZFtyrrhKtD5VugAueCaFvEs4LuqrFrxq 20 | PMDwR8SDqoXJTxAetQ/RT107CiPncBt5WAfw95xphaqmiGPm3KMBmao+JyGzAz2L 21 | zc009+sKeF2nMtnSvemLa6NofMGxdrjJRwKiGp998Cs2CixuLCpNj8E5SiQnoyYy 22 | CLAaRCyu51ybolcie29uCIAhR/X+79hE3Xk6lPfU8jRTyAWR4LIs2yuSeTb8Wddk 23 | EWrZC5VnPcM83McgXexBOyTBp17KWQianyAm3HFENlB8+Z9IWkLMqmcmZ5k3IsFY 24 | fdQV5ETm2YzUyuY8Vc/39JRX0UnBzmrsEb58Z9g28MalFqG7YQO/9ztpvJBRttBa 25 | xMfz1DSuV6I4fcjDNcFSM6I3RBECDtZm+P8jVdkteujeVomlebNEKU5vi1bQTXXs 26 | gs2+MPvin5naxQ9UXk4TjCouIX03PyQ5iVdYDWW1XHVjqRAl8YCRa74C1bqfL/UM 27 | AcH7OByvKAlmzUKGU3G+XZkQZmfj1ev46kEd5Mfnk1yk+KKmHDhGW9bey9qKRwjB 28 | uhE5JmBjPvoldEa41RVCZ8HXieOrezEiSwtVBP/lD04yyn45hEs08rQzp3+nmOnP 29 | Hg== 30 | -----END CERTIFICATE----- 31 | -------------------------------------------------------------------------------- /test/certs/ca/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | z2hyreerpV/A 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /scripts/generateCerts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if test $# -ne 2 3 | then 4 | echo "Wrong number of arguments" 5 | echo "Usage: generateCerts.sh " 6 | exit 1 7 | fi 8 | 9 | ROOTPATH="$1" 10 | PASSWORD=$2 11 | RSABITS=4096 12 | 13 | # make directories to work from 14 | mkdir -p $ROOTPATH/certs/{server,client,ca} 15 | 16 | PATH_CA=$ROOTPATH/certs/ca 17 | PATH_SERVER=$ROOTPATH/certs/server 18 | PATH_CLIENT=$ROOTPATH/certs/client 19 | 20 | ###### 21 | # CA # 22 | ###### 23 | 24 | # Generate CA key 25 | openssl genrsa -passout pass:$PASSWORD -out $PATH_CA/ca.key $RSABITS 26 | 27 | # Create CA cert 28 | openssl req -new -x509 -days 10000 -key $PATH_CA/ca.key -out $PATH_CA/ca.crt -passin pass:$PASSWORD -subj "/C=GB/ST=EN/L=./O=QUEST CA/CN=QUEST" 29 | 30 | ########## 31 | # SERVER # 32 | ########## 33 | 34 | # Generate server key 35 | openssl genrsa -out $PATH_SERVER/server.key $RSABITS 36 | 37 | # Generate server cert 38 | openssl req -new -key $PATH_SERVER/server.key -out $PATH_SERVER/server.csr -passout pass:$PASSWORD -subj "/C=GB/ST=EN/L=./O=QUEST SERVER/CN=localhost" \ 39 | -reqexts SAN -extensions SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:localhost,IP:127.0.0.1")) 40 | 41 | # Sign server cert with CA 42 | openssl x509 -req -days 10000 -set_serial 01 \ 43 | -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1") \ 44 | -passin pass:$PASSWORD -in $PATH_SERVER/server.csr -CA $PATH_CA/ca.crt -CAkey $PATH_CA/ca.key -out $PATH_SERVER/server.crt 45 | 46 | ########## 47 | # CLIENT # 48 | ########## 49 | 50 | # Generate client key 51 | openssl genrsa -out $PATH_CLIENT/client.key $RSABITS 52 | 53 | # Generate client cert 54 | openssl req -new -key $PATH_CLIENT/client.key -out $PATH_CLIENT/client.csr -passout pass:$PASSWORD -subj "/C=GB/ST=EN/L=./O=QUEST CLIENT/CN=CLIENT" 55 | 56 | # Sign client cert with CA 57 | openssl x509 -req -days 10000 -set_serial 01 \ 58 | -passin pass:$PASSWORD -in $PATH_CLIENT/client.csr -CA $PATH_CA/ca.crt -CAkey $PATH_CA/ca.key -out $PATH_CLIENT/client.crt 59 | 60 | exit 0 61 | -------------------------------------------------------------------------------- /test/certs/server/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 31 | C+ksVr7TuiBe 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /test/testapp.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "node:fs"; 2 | 3 | import { Proxy } from "./util/proxy"; 4 | import { Sender, SenderOptions } from "../src"; 5 | 6 | const PROXY_PORT = 9099; 7 | const PORT = 9009; 8 | const HOST = "localhost"; 9 | 10 | const senderOptions: SenderOptions = { 11 | protocol: "tcps", 12 | host: HOST, 13 | port: PROXY_PORT, 14 | addr: "localhost", 15 | tls_ca: readFileSync("certs/ca/ca.crt"), // necessary only if the server uses self-signed certificate 16 | }; 17 | 18 | const proxyTLS = { 19 | key: readFileSync("certs/server/server.key"), 20 | cert: readFileSync("certs/server/server.crt"), 21 | ca: readFileSync("certs/ca/ca.crt"), // authority chain for the clients 22 | }; 23 | 24 | async function run() { 25 | const proxy = new Proxy(); 26 | await proxy.start(PROXY_PORT, PORT, HOST, proxyTLS); 27 | 28 | const sender = new Sender(senderOptions); //with authentication 29 | const connected = await sender.connect(); //connection through proxy with encryption 30 | if (connected) { 31 | await sender 32 | .table("test") 33 | .symbol("location", "emea") 34 | .symbol("city", "budapest") 35 | .stringColumn("hoppa", "hello") 36 | .stringColumn("hippi", "hello") 37 | .stringColumn("hippo", "haho") 38 | .floatColumn("temperature", 14.1) 39 | .intColumn("intcol", 56) 40 | .timestampColumn("tscol", Date.now(), "ms") 41 | .atNow(); 42 | await sender 43 | .table("test") 44 | .symbol("location", "asia") 45 | .symbol("city", "singapore") 46 | .stringColumn("hoppa", "hi") 47 | .stringColumn("hopp", "hello") 48 | .stringColumn("hippo", "huhu") 49 | .floatColumn("temperature", 7.1) 50 | .at(1658484765000555000n, "ns"); 51 | await sender.flush(); 52 | 53 | await sender 54 | .table("test") 55 | .symbol("location", "emea") 56 | .symbol("city", "miskolc") 57 | .stringColumn("hoppa", "hello") 58 | .stringColumn("hippi", "hello") 59 | .stringColumn("hippo", "lalalala") 60 | .floatColumn("temperature", 13.1) 61 | .intColumn("intcol", 333) 62 | .atNow(); 63 | await sender.flush(); 64 | } 65 | await sender.close(); 66 | 67 | await proxy.stop(); 68 | } 69 | 70 | run().catch(console.error); 71 | -------------------------------------------------------------------------------- /test/util/proxyfunctions.ts: -------------------------------------------------------------------------------- 1 | import net, { Socket } from "node:net"; 2 | import tls, { TLSSocket } from "node:tls"; 3 | import { Proxy } from "./proxy"; 4 | import { MockProxy } from "./mockproxy"; 5 | 6 | const LOCALHOST = "localhost"; 7 | 8 | async function write(socket: Socket, data: string) { 9 | return new Promise((resolve, reject) => { 10 | socket.write(data, "utf8", (err: Error) => (err ? reject(err) : resolve())); 11 | }); 12 | } 13 | 14 | async function listen( 15 | proxy: Proxy | MockProxy, 16 | listenPort: number, 17 | dataHandler: (data: string) => void, 18 | tlsOptions: tls.TlsOptions, 19 | ) { 20 | return new Promise((resolve) => { 21 | const clientConnHandler = (client: Socket | TLSSocket) => { 22 | console.info("client connected"); 23 | if (proxy.client) { 24 | console.error("There is already a client connected"); 25 | process.exit(1); 26 | } 27 | proxy.client = client; 28 | 29 | client.on("data", dataHandler); 30 | }; 31 | 32 | proxy.server = tlsOptions 33 | ? tls.createServer(tlsOptions, clientConnHandler) 34 | : net.createServer(clientConnHandler); 35 | 36 | proxy.server.on("error", (err) => { 37 | console.error(`server error: ${err}`); 38 | }); 39 | 40 | proxy.server.listen(listenPort, LOCALHOST, () => { 41 | console.info(`listening for clients on ${listenPort}`); 42 | resolve(); 43 | }); 44 | }); 45 | } 46 | 47 | async function shutdown( 48 | proxy: Proxy | MockProxy, 49 | onServerClose = async () => {}, 50 | ) { 51 | console.info("closing proxy"); 52 | return new Promise((resolve) => { 53 | proxy.server.close(async () => { 54 | await onServerClose(); 55 | resolve(); 56 | }); 57 | }); 58 | } 59 | 60 | async function connect(proxy: Proxy, remotePort: number, remoteHost: string) { 61 | console.info(`opening remote connection to ${remoteHost}:${remotePort}`); 62 | return new Promise((resolve) => { 63 | proxy.remote.connect(remotePort, remoteHost, () => resolve()); 64 | }); 65 | } 66 | 67 | async function close() { 68 | console.info("closing remote connection"); 69 | return new Promise((resolve) => { 70 | resolve(); 71 | }); 72 | } 73 | 74 | export { write, listen, shutdown, connect, close }; 75 | -------------------------------------------------------------------------------- /src/buffer/bufferv1.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { SenderOptions } from "../options"; 3 | import { SenderBuffer } from "./index"; 4 | import { SenderBufferBase } from "./base"; 5 | import { timestampToMicros, timestampToNanos, TimestampUnit } from "../utils"; 6 | 7 | /** 8 | * Buffer implementation for protocol version 1.
9 | * Sends floating point numbers in their text form. 10 | */ 11 | class SenderBufferV1 extends SenderBufferBase { 12 | /** 13 | * Creates a new SenderBufferV1 instance. 14 | * 15 | * @param {SenderOptions} options - Sender configuration object.
16 | * See SenderOptions documentation for detailed description of configuration options. 17 | */ 18 | constructor(options: SenderOptions) { 19 | super(options); 20 | } 21 | 22 | /** 23 | * Writes a 64-bit floating point value into the buffer using v1 serialization (text format).
24 | * Use it to insert into DOUBLE or FLOAT database columns. 25 | * 26 | * @param {string} name - Column name. 27 | * @param {number} value - Column value, accepts only number values. 28 | * @return {Sender} Returns with a reference to this sender. 29 | */ 30 | floatColumn(name: string, value: number): SenderBuffer { 31 | this.writeColumn( 32 | name, 33 | value, 34 | () => { 35 | const valueStr = value.toString(); 36 | this.checkCapacity([valueStr]); 37 | this.write(valueStr); 38 | }, 39 | "number", 40 | ); 41 | return this; 42 | } 43 | 44 | protected writeTimestamp( 45 | timestamp: number | bigint, 46 | unit: TimestampUnit = "us", 47 | designated: boolean, 48 | ): void { 49 | const biValue = BigInt(timestamp); 50 | const timestampValue = designated 51 | ? timestampToNanos(biValue, unit) 52 | : timestampToMicros(biValue, unit); 53 | const timestampStr = timestampValue.toString(); 54 | this.checkCapacity([timestampStr], 2); 55 | this.write(timestampStr); 56 | if (!designated) { 57 | this.write("t"); 58 | } 59 | } 60 | 61 | /** 62 | * Array columns are not supported in protocol v1. 63 | * 64 | * @throws Error indicating arrays are not supported in v1 65 | */ 66 | arrayColumn(): SenderBuffer { 67 | throw new Error("Arrays are not supported in protocol v1"); 68 | } 69 | } 70 | 71 | export { SenderBufferV1 }; 72 | -------------------------------------------------------------------------------- /src/transport/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Buffer } from "node:buffer"; 3 | 4 | import { SenderOptions, HTTP, HTTPS, TCP, TCPS } from "../options"; 5 | import { UndiciTransport } from "./http/undici"; 6 | import { TcpTransport } from "./tcp"; 7 | import { HttpTransport } from "./http/stdlib"; 8 | 9 | /** 10 | * Interface for QuestDB transport implementations.
11 | * Defines the contract for different transport protocols (HTTP/HTTPS/TCP/TCPS). 12 | */ 13 | interface SenderTransport { 14 | /** 15 | * Establishes a connection to the database server. 16 | * Should not be called on HTTP transports. 17 | * @returns Promise resolving to true if connection is successful 18 | */ 19 | connect(): Promise; 20 | 21 | /** 22 | * Sends the data to the database server. 23 | * @param data - Buffer containing the data to send 24 | * @returns Promise resolving to true if data was sent successfully 25 | */ 26 | send(data: Buffer): Promise; 27 | 28 | /** 29 | * Closes the connection to the database server. 30 | * Should not be called on HTTP transports. 31 | * @returns Promise that resolves when the connection is closed 32 | */ 33 | close(): Promise; 34 | 35 | /** 36 | * Gets the default number of rows that trigger auto-flush for this transport. 37 | * @returns Default auto-flush row count 38 | */ 39 | getDefaultAutoFlushRows(): number; 40 | } 41 | 42 | /** 43 | * Factory function to create appropriate transport instance based on configuration. 44 | * @param options - Sender configuration options including protocol and connection details 45 | * @returns Transport instance appropriate for the specified protocol 46 | * @throws Error if protocol or host options are missing or invalid 47 | */ 48 | function createTransport(options: SenderOptions): SenderTransport { 49 | if (!options || !options.protocol) { 50 | throw new Error("The 'protocol' option is mandatory"); 51 | } 52 | if (!options.host) { 53 | throw new Error("The 'host' option is mandatory"); 54 | } 55 | 56 | switch (options.protocol) { 57 | case HTTP: 58 | case HTTPS: 59 | return options.stdlib_http 60 | ? new HttpTransport(options) 61 | : new UndiciTransport(options); 62 | case TCP: 63 | case TCPS: 64 | return new TcpTransport(options); 65 | default: 66 | throw new Error(`Invalid protocol: '${options.protocol}'`); 67 | } 68 | } 69 | 70 | export { SenderTransport, createTransport }; 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to nodejs-questdb-client 2 | 3 | Thank you for your interest in contributing to nodejs-questdb-client! This document provides guidelines and instructions for contributing to the project. 4 | 5 | ## Development Setup 6 | 7 | 1. Fork and clone the repository: 8 | ```bash 9 | git clone https://github.com/YOUR_USERNAME/nodejs-questdb-client.git 10 | cd nodejs-questdb-client 11 | ``` 12 | 13 | 2. Install dependencies: 14 | ```bash 15 | pnpm install 16 | ``` 17 | 18 | 19 | ## Running Tests 20 | 21 | The project uses Vitest for testing. Tests are located in the `test` directory. 22 | 23 | 1. Run tests in watch mode during development: 24 | ```bash 25 | pnpm run test 26 | ``` 27 | 28 | ### Test Requirements 29 | 30 | - Some tests use mock servers and certificates located in the `test/certs` directory 31 | 32 | > You can generate the certificates by running the `generateCerts.sh` script in the `scripts` directory. The script requires two arguments: the output directory and the password for the certificates. 33 | `./scripts/generateCerts.sh . questdbPwd123` 34 | 35 | 36 | ## Code Style and Quality 37 | 38 | 1. The project uses TypeScript. Make sure your code is properly typed. 39 | 40 | 2. Format your code using Prettier 41 | 42 | 3. Lint your code: 43 | ```bash 44 | pnpm run lint 45 | ``` 46 | 47 | 4. Fix linting issues: 48 | ```bash 49 | pnpm run lint --fix 50 | ``` 51 | 52 | ## Making Changes 53 | 54 | 1. Create a new branch for your changes: 55 | ```bash 56 | git checkout -b feature/your-feature-name 57 | ``` 58 | 59 | 2. Make your changes and commit them with clear, descriptive commit messages: 60 | ```bash 61 | git add . 62 | git commit -m "feat: add new feature" 63 | ``` 64 | 65 | We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages. 66 | 67 | 3. Push your changes to your fork: 68 | ```bash 69 | git push origin feature/your-feature-name 70 | ``` 71 | 72 | 4. Create a Pull Request from your fork to the main repository. 73 | 74 | ## Pull Request Guidelines 75 | 76 | 1. Make sure all tests pass 77 | 2. Update documentation if needed 78 | 3. Add tests for new features 79 | 4. Keep PRs focused - one feature or bug fix per PR 80 | 5. Link any related issues in the PR description 81 | 82 | ## Documentation 83 | 84 | - Update the README.md if you're adding new features or changing existing ones 85 | - Add JSDoc comments for new public APIs 86 | - Include examples in the documentation when appropriate 87 | 88 | ## Need Help? 89 | 90 | If you have questions or need help, you can: 91 | - Open an issue with your question 92 | - Join our community discussions (if available) 93 | 94 | ## License 95 | 96 | By contributing to nodejs-questdb-client, you agree that your contributions will be licensed under the project's license. 97 | 98 | -------------------------------------------------------------------------------- /examples/workers.ts: -------------------------------------------------------------------------------- 1 | import { Sender } from "@questdb/nodejs-client"; 2 | import { Worker, isMainThread, parentPort, workerData } from "worker_threads"; 3 | 4 | // fake venue 5 | // generates random prices and amounts for a ticker for max 5 seconds, then the feed closes 6 | function* venue(ticker) { 7 | let end = false; 8 | setTimeout(() => { 9 | end = true; 10 | }, rndInt(5000)); 11 | while (!end) { 12 | yield { ticker, price: Math.random(), amount: Math.random() }; 13 | } 14 | } 15 | 16 | // market data feed simulator 17 | // uses the fake venue to deliver price and amount updates to the feed handler (onTick() callback) 18 | async function subscribe(ticker, onTick) { 19 | const feed = venue(workerData.ticker); 20 | let tick; 21 | while ((tick = feed.next().value)) { 22 | await onTick(tick); 23 | await sleep(rndInt(30)); 24 | } 25 | } 26 | 27 | async function run() { 28 | if (isMainThread) { 29 | const tickers = ["ETH-USD", "BTC-USD", "SOL-USD", "DOGE-USD"]; 30 | // main thread to start a worker thread for each ticker 31 | for (let ticker of tickers) { 32 | new Worker(__filename, { workerData: { ticker: ticker } }) 33 | .on("error", (err) => { 34 | throw err; 35 | }) 36 | .on("exit", () => { 37 | console.log(`${ticker} thread exiting...`); 38 | }) 39 | .on("message", (msg) => { 40 | console.log(`Ingested ${msg.count} prices for ticker ${msg.ticker}`); 41 | }); 42 | } 43 | } else { 44 | // it is important that each worker has a dedicated sender object 45 | // threads cannot share the sender because they would write into the same buffer 46 | const sender = await Sender.fromConfig("http::addr=127.0.0.1:9000"); 47 | 48 | // subscribe for the market data of the ticker assigned to the worker 49 | // ingest each price update into the database using the sender 50 | let count = 0; 51 | await subscribe(workerData.ticker, async (tick) => { 52 | await sender 53 | .table("trades") 54 | .symbol("symbol", tick.ticker) 55 | .symbol("side", "sell") 56 | .floatColumn("price", tick.price) 57 | .floatColumn("amount", tick.amount) 58 | .at(Date.now(), "ms"); 59 | await sender.flush(); 60 | count++; 61 | }); 62 | 63 | // let the main thread know how many prices were ingested 64 | parentPort.postMessage({ ticker: workerData.ticker, count }); 65 | 66 | // close the connection to the database 67 | await sender.close(); 68 | } 69 | } 70 | 71 | function sleep(ms: number) { 72 | return new Promise((resolve) => setTimeout(resolve, ms)); 73 | } 74 | 75 | function rndInt(limit: number) { 76 | return Math.floor(Math.random() * limit + 1); 77 | } 78 | 79 | run().then(console.log).catch(console.error); 80 | -------------------------------------------------------------------------------- /test/logging.test.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { 3 | describe, 4 | it, 5 | beforeAll, 6 | afterAll, 7 | afterEach, 8 | expect, 9 | vi, 10 | } from "vitest"; 11 | 12 | import { Logger } from "../src"; 13 | 14 | describe("Default logging suite", function () { 15 | const error = vi.spyOn(console, "error").mockImplementation(() => {}); 16 | const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); 17 | const info = vi.spyOn(console, "info").mockImplementation(() => {}); 18 | const debug = vi.spyOn(console, "debug").mockImplementation(() => {}); 19 | let log: Logger; 20 | 21 | beforeAll(async () => { 22 | log = (await import("../src/logging")).log; 23 | }); 24 | 25 | afterAll(() => { 26 | error.mockReset(); 27 | warn.mockReset(); 28 | info.mockReset(); 29 | debug.mockReset(); 30 | }); 31 | 32 | afterEach(() => { 33 | error.mockClear(); 34 | warn.mockClear(); 35 | info.mockClear(); 36 | debug.mockClear(); 37 | }); 38 | 39 | it("can log error level messages", function () { 40 | const testMessage = "ERROR ERROR ERROR"; 41 | log("error", testMessage); 42 | expect(error).toHaveBeenCalledTimes(1); 43 | expect(warn).toHaveBeenCalledTimes(0); 44 | expect(info).toHaveBeenCalledTimes(0); 45 | expect(debug).toHaveBeenCalledTimes(0); 46 | expect(error).toHaveBeenCalledWith(testMessage); 47 | }); 48 | 49 | it("can log warn level messages", function () { 50 | const testMessage = "WARN WARN WARN"; 51 | log("warn", testMessage); 52 | expect(error).toHaveBeenCalledTimes(0); 53 | expect(warn).toHaveBeenCalledTimes(1); 54 | expect(info).toHaveBeenCalledTimes(0); 55 | expect(debug).toHaveBeenCalledTimes(0); 56 | expect(warn).toHaveBeenCalledWith(testMessage); 57 | }); 58 | 59 | it("can log info level messages", function () { 60 | const testMessage = "INFO INFO INFO"; 61 | log("info", testMessage); 62 | expect(error).toHaveBeenCalledTimes(0); 63 | expect(warn).toHaveBeenCalledTimes(0); 64 | expect(info).toHaveBeenCalledTimes(1); 65 | expect(debug).toHaveBeenCalledTimes(0); 66 | expect(info).toHaveBeenCalledWith(testMessage); 67 | }); 68 | 69 | it("cannot log debug level messages, default logging level is 'info'", function () { 70 | const testMessage = "DEBUG DEBUG DEBUG"; 71 | log("debug", testMessage); 72 | expect(error).toHaveBeenCalledTimes(0); 73 | expect(warn).toHaveBeenCalledTimes(0); 74 | expect(info).toHaveBeenCalledTimes(0); 75 | expect(debug).toHaveBeenCalledTimes(0); 76 | }); 77 | 78 | it("throws exception if log level is not supported", function () { 79 | // @ts-expect-error - Testing invalid log level 80 | expect(() => log("trace", "TRACE TRACE TRACE")).toThrow( 81 | "Invalid log level: 'trace'", 82 | ); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #008000; 3 | --dark-hl-0: #6A9955; 4 | --light-hl-1: #795E26; 5 | --dark-hl-1: #DCDCAA; 6 | --light-hl-2: #000000; 7 | --dark-hl-2: #D4D4D4; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #AF00DB; 13 | --dark-hl-5: #C586C0; 14 | --light-hl-6: #001080; 15 | --dark-hl-6: #9CDCFE; 16 | --light-hl-7: #0070C1; 17 | --dark-hl-7: #4FC1FF; 18 | --light-hl-8: #098658; 19 | --dark-hl-8: #B5CEA8; 20 | --light-hl-9: #000000FF; 21 | --dark-hl-9: #D4D4D4; 22 | --light-hl-10: #267F99; 23 | --dark-hl-10: #4EC9B0; 24 | --light-code-background: #FFFFFF; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | :root[data-theme='light'] { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | :root[data-theme='dark'] { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /test/certs/ca/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | 6SgC6R52uGXg+tLpvqDtihdP3bo/ 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/certs/client/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | jPCrahxxpLUIKlGTlJC5GSFomGua8UI= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /test/certs/server/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | WhntPvCJT8HT6ciI0Z3NkOCXNsK93ro= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is the QuestDB Node.js client library (@questdb/nodejs-client) that provides data ingestion capabilities to QuestDB databases. The client supports multiple transport protocols (HTTP/HTTPS, TCP/TCPS) and authentication methods. 8 | 9 | ## Development Commands 10 | 11 | ### Build 12 | ```bash 13 | pnpm build # Build the library using bunchee (produces both ESM and CJS outputs) 14 | ``` 15 | 16 | ### Testing 17 | ```bash 18 | pnpm test # Run all tests using Vitest 19 | ``` 20 | 21 | ### Code Quality 22 | ```bash 23 | pnpm eslint # Run ESLint on all source files 24 | pnpm typecheck # Run TypeScript type checking without emitting files 25 | pnpm format # Format code using Prettier 26 | ``` 27 | 28 | ### Documentation 29 | ```bash 30 | pnpm docs # Build JSDoc documentation 31 | pnpm preview:docs # Preview generated documentation locally 32 | ``` 33 | 34 | ## Architecture 35 | 36 | ### Core Components 37 | 38 | 1. **Sender** (`src/sender.ts`): Main API class that orchestrates data ingestion. Handles auto-flushing, connection management, and provides the builder pattern API for constructing rows. 39 | 40 | 2. **Transport Layer** (`src/transport/`): 41 | - `http/undici.ts`: Default HTTP transport using Undici library for high performance 42 | - `http/stdlib.ts`: Alternative HTTP transport using Node.js built-in modules 43 | - `tcp.ts`: TCP/TCPS transport for persistent connections with JWK authentication 44 | - Protocol negotiation and retry logic for HTTP transports 45 | 46 | 3. **Buffer System** (`src/buffer/`): 47 | - `bufferv1.ts`: Text-based protocol (version 1) for backward compatibility 48 | - `bufferv2.ts`: Binary protocol (version 2) with double encoding and array support 49 | - Dynamic buffer resizing and row-level transaction support 50 | 51 | 4. **Configuration** (`src/options.ts`): Comprehensive options parsing from connection strings with validation and deprecation handling. 52 | 53 | ### Protocol Versions 54 | 55 | - **Version 1**: Text-based serialization, compatible with older QuestDB versions 56 | - **Version 2**: Binary encoding for doubles, supports array columns, better performance 57 | - **Auto-negotiation**: HTTP transport can automatically detect and use the best protocol version 58 | 59 | ### Key Design Patterns 60 | 61 | - Builder pattern for row construction with method chaining 62 | - Factory methods (`Sender.fromConfig()`, `Sender.fromEnv()`) for validated initialization 63 | - Abstract base classes for transport and buffer implementations 64 | - Automatic buffer management with configurable auto-flush behavior 65 | 66 | ## Testing Strategy 67 | 68 | Tests are organized by component: 69 | - `sender.config.test.ts`: Configuration parsing and validation 70 | - `sender.buffer.test.ts`: Buffer operations and protocol serialization 71 | - `sender.transport.test.ts`: Transport layer functionality 72 | - `sender.integration.test.ts`: End-to-end integration tests with QuestDB 73 | 74 | Integration tests use TestContainers to spin up QuestDB instances for realistic testing. 75 | 76 | ## Important Implementation Notes 77 | 78 | - The client requires Node.js v20+ for Undici support 79 | - Authentication is handled differently per transport (Basic/Bearer for HTTP, JWK for TCP) 80 | - Buffer automatically resizes up to `max_buf_size` (default 100MB) 81 | - Auto-flush triggers based on row count or time interval 82 | - Each worker thread needs its own Sender instance (buffers cannot be shared) 83 | - Protocol version 2 is recommended for new implementations with array column support -------------------------------------------------------------------------------- /test/util/mockhttp.ts: -------------------------------------------------------------------------------- 1 | import http from "node:http"; 2 | import https from "node:https"; 3 | 4 | type MockConfig = { 5 | responseDelays?: number[]; 6 | responseCodes?: number[]; 7 | username?: string; 8 | password?: string; 9 | token?: string; 10 | settings?: { 11 | config?: { "line.proto.support.versions"?: number[] }; 12 | }; 13 | }; 14 | 15 | class MockHttp { 16 | server: http.Server | https.Server; 17 | mockConfig: MockConfig; 18 | numOfRequests: number; 19 | 20 | constructor() { 21 | this.reset(); 22 | } 23 | 24 | reset(mockConfig: MockConfig = {}) { 25 | if (!mockConfig.settings) { 26 | mockConfig.settings = { 27 | config: { "line.proto.support.versions": [1, 2, 3] }, 28 | }; 29 | } 30 | 31 | this.mockConfig = mockConfig; 32 | this.numOfRequests = 0; 33 | } 34 | 35 | async start( 36 | listenPort: number, 37 | secure: boolean = false, 38 | options?: Record, 39 | ): Promise { 40 | const serverCreator = secure ? https.createServer : http.createServer; 41 | // @ts-expect-error - Testing different options, so typing is not important 42 | this.server = serverCreator( 43 | options, 44 | (req: http.IncomingMessage, res: http.ServerResponse) => { 45 | const { url, method } = req; 46 | if (url.startsWith("/write") && method === "POST") { 47 | const authFailed = checkAuthHeader(this.mockConfig, req); 48 | 49 | const body: Uint8Array[] = []; 50 | req.on("data", (chunk: Uint8Array) => { 51 | body.push(chunk); 52 | }); 53 | 54 | req.on("end", async () => { 55 | console.info(`Received data: ${Buffer.concat(body)}`); 56 | this.numOfRequests++; 57 | 58 | const delay = 59 | this.mockConfig.responseDelays && 60 | this.mockConfig.responseDelays.length > 0 61 | ? this.mockConfig.responseDelays.pop() 62 | : undefined; 63 | if (delay) { 64 | await sleep(delay); 65 | } 66 | 67 | const responseCode = authFailed 68 | ? 401 69 | : this.mockConfig.responseCodes && 70 | this.mockConfig.responseCodes.length > 0 71 | ? this.mockConfig.responseCodes.pop() 72 | : 204; 73 | res.writeHead(responseCode); 74 | res.end(); 75 | }); 76 | } else if (url === "/settings" && method === "GET") { 77 | const settingsStr = JSON.stringify(this.mockConfig.settings); 78 | console.info(`Settings reply: ${settingsStr}`); 79 | res.writeHead(200, { "Content-Type": "application/json" }); 80 | res.end(settingsStr); 81 | return; 82 | } else { 83 | console.info(`No handler for: ${method} ${url}`); 84 | res.writeHead(404, { "Content-Type": "text/plain" }); 85 | res.end("Not found"); 86 | } 87 | }, 88 | ); 89 | 90 | return new Promise((resolve, reject) => { 91 | this.server.listen(listenPort, () => { 92 | console.info(`Server is running on port ${listenPort}`); 93 | resolve(true); 94 | }); 95 | 96 | this.server.on("error", (e) => { 97 | console.error(`server error: ${e}`); 98 | reject(e); 99 | }); 100 | }); 101 | } 102 | 103 | async stop() { 104 | if (this.server) { 105 | this.server.close(); 106 | } 107 | } 108 | } 109 | 110 | function checkAuthHeader(mockConfig: MockConfig, req: http.IncomingMessage) { 111 | let authFailed = false; 112 | const header = (req.headers.authorization || "").split(/\s+/); 113 | switch (header[0]) { 114 | case "Basic": { 115 | const auth = Buffer.from(header[1], "base64").toString().split(/:/); 116 | if (mockConfig.username !== auth[0] || mockConfig.password !== auth[1]) { 117 | authFailed = true; 118 | } 119 | break; 120 | } 121 | case "Bearer": 122 | if (mockConfig.token !== header[1]) { 123 | authFailed = true; 124 | } 125 | break; 126 | default: 127 | if (mockConfig.username || mockConfig.password || mockConfig.token) { 128 | authFailed = true; 129 | } 130 | } 131 | return authFailed; 132 | } 133 | 134 | function sleep(ms: number) { 135 | return new Promise((resolve) => setTimeout(resolve, ms)); 136 | } 137 | 138 | export { MockHttp }; 139 | -------------------------------------------------------------------------------- /src/validation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Validates a table name. 3 | * 4 | * @param {string} name - The table name to validate. 5 | * @param {number} maxNameLength - The maximum length of table names. 6 | * @throws Error if table name is invalid. 7 | */ 8 | function validateTableName(name: string, maxNameLength: number): void { 9 | const len = name.length; 10 | if (len > maxNameLength) { 11 | throw new Error(`Table name is too long, max length is ${maxNameLength}`); 12 | } 13 | if (len === 0) { 14 | throw new Error("Empty string is not allowed as table name"); 15 | } 16 | for (let i = 0; i < len; i++) { 17 | const ch = name[i]; 18 | switch (ch) { 19 | case ".": 20 | if (i === 0 || i === len - 1 || name[i - 1] === ".") 21 | // single dot is allowed in the middle only 22 | // starting with a dot hides directory in Linux 23 | // ending with a dot can be trimmed by some Windows versions / file systems 24 | // double or triple dot looks suspicious 25 | // single dot allowed as compatibility, 26 | // when someone uploads 'file_name.csv' the file name used as the table name 27 | throw new Error( 28 | "Table name cannot start or end with a dot, and only a single dot allowed", 29 | ); 30 | break; 31 | case "?": 32 | case ",": 33 | case "'": 34 | case '"': 35 | case "\\": 36 | case "/": 37 | case ":": 38 | case ")": 39 | case "(": 40 | case "+": 41 | case "*": 42 | case "%": 43 | case "~": 44 | case "\u0000": 45 | case "\u0001": 46 | case "\u0002": 47 | case "\u0003": 48 | case "\u0004": 49 | case "\u0005": 50 | case "\u0006": 51 | case "\u0007": 52 | case "\u0008": 53 | case "\u0009": // control characters, except \n. 54 | case "\u000B": // new line allowed for compatibility, there are tests to make sure it works 55 | case "\u000c": 56 | case "\r": 57 | case "\n": 58 | case "\u000e": 59 | case "\u000f": 60 | case "\u007f": 61 | case "\ufeff": // UTF-8 BOM (Byte Order Mark) can appear at the beginning of a character stream 62 | throw new Error(`Invalid character in table name: ${ch}`); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * Validates a column name. 69 | * 70 | * @param {string} name - The column name to validate. 71 | * @param {number} maxNameLength - The maximum length of column names. 72 | * @throws Error if column name is invalid. 73 | */ 74 | function validateColumnName(name: string, maxNameLength: number): void { 75 | const len = name.length; 76 | if (len > maxNameLength) { 77 | throw new Error(`Column name is too long, max length is ${maxNameLength}`); 78 | } 79 | if (len === 0) { 80 | throw new Error("Empty string is not allowed as column name"); 81 | } 82 | for (const ch of name) { 83 | switch (ch) { 84 | case "?": 85 | case ".": 86 | case ",": 87 | case "'": 88 | case '"': 89 | case "\\": 90 | case "/": 91 | case ":": 92 | case ")": 93 | case "(": 94 | case "+": 95 | case "-": 96 | case "*": 97 | case "%": 98 | case "~": 99 | case "\u0000": 100 | case "\u0001": 101 | case "\u0002": 102 | case "\u0003": 103 | case "\u0004": 104 | case "\u0005": 105 | case "\u0006": 106 | case "\u0007": 107 | case "\u0008": 108 | case "\u0009": // control characters, except \n 109 | case "\u000B": 110 | case "\u000c": 111 | case "\r": 112 | case "\n": 113 | case "\u000e": 114 | case "\u000f": 115 | case "\u007f": 116 | case "\ufeff": // UTF-8 BOM (Byte Order Mark) can appear at the beginning of a character stream 117 | throw new Error(`Invalid character in column name: ${ch}`); 118 | } 119 | } 120 | } 121 | 122 | /** 123 | * Validates a decimal text. 124 | * 125 | * This is a partial validation to catch obvious errors early. 126 | * We only accept numeric digits, signs, decimal point (.), exponent (e, E), and NaN/Infinity. 127 | * 128 | * @param {string} value - The decimal text to validate. 129 | * @throws Error if decimal text is invalid. 130 | */ 131 | function validateDecimalText(value: string): void { 132 | if (value.length === 0) { 133 | throw new Error("Decimal text cannot be empty"); 134 | } 135 | const decimalRegex = 136 | /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$|^[-+]?Infinity$|^NaN$/; 137 | if (!decimalRegex.test(value)) { 138 | throw new Error(`Invalid decimal text: ${value}`); 139 | } 140 | } 141 | 142 | export { validateTableName, validateColumnName, validateDecimalText }; 143 | -------------------------------------------------------------------------------- /src/buffer/bufferv3.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { SenderOptions } from "../options"; 3 | import { SenderBuffer } from "./index"; 4 | import { bigintToTwosComplementBytes } from "../utils"; 5 | import { SenderBufferV2 } from "./bufferv2"; 6 | import { validateDecimalText } from "../validation"; 7 | 8 | // Entity type constants for protocol v3. 9 | const ENTITY_TYPE_DECIMAL: number = 23; 10 | 11 | // ASCII code for equals sign used in binary protocol. 12 | const EQUALS_SIGN: number = "=".charCodeAt(0); 13 | 14 | /** 15 | * Buffer implementation for protocol version 3. 16 | * 17 | * Provides support for decimals. 18 | */ 19 | class SenderBufferV3 extends SenderBufferV2 { 20 | /** 21 | * Creates a new SenderBufferV3 instance. 22 | * 23 | * @param {SenderOptions} options - Sender configuration object. 24 | * 25 | * See SenderOptions documentation for detailed description of configuration options. 26 | */ 27 | constructor(options: SenderOptions) { 28 | super(options); 29 | } 30 | 31 | /** 32 | * Writes a decimal value into the buffer using its text format. 33 | * 34 | * Use it to insert into DECIMAL database columns. 35 | * 36 | * @param {string} name - Column name. 37 | * @param {string | number} value - The decimal value to write. 38 | * - Accepts either a `number` or a `string` containing a valid decimal representation. 39 | * - String values should follow standard decimal notation (e.g., `"123.45"` or `"-0.001"`). 40 | * @returns {Sender} Returns with a reference to this buffer. 41 | * @throws Error If decimals are not supported by the buffer implementation, or validation fails. 42 | * Possible validation errors: 43 | * - The provided string is not a valid decimal representation. 44 | */ 45 | decimalColumnText(name: string, value: string | number): SenderBuffer { 46 | let str = ""; 47 | if (typeof value === "string") { 48 | validateDecimalText(value); 49 | str = value; 50 | } else if (typeof value === "number") { 51 | str = value.toString(); 52 | } else { 53 | throw new TypeError(`Invalid decimal value type: ${typeof value}`); 54 | } 55 | this.writeColumn(name, str, () => { 56 | this.checkCapacity([str], 1); 57 | this.write(str); 58 | this.write("d"); 59 | }); 60 | return this; 61 | } 62 | 63 | /** 64 | * Writes a decimal value into the buffer using its binary format. 65 | * 66 | * Use it to insert into DECIMAL database columns. 67 | * 68 | * @param {string} name - Column name. 69 | * @param {bigint | Int8Array} unscaled - The unscaled integer portion of the decimal value. 70 | * - If a `bigint` is provided, it will be converted automatically. 71 | * - If an `Int8Array` is provided, it must contain the two’s complement representation 72 | * of the unscaled value in **big-endian** byte order. 73 | * - An empty `Int8Array` represents a `NULL` value. 74 | * @param {number} scale - The number of fractional digits (the scale) of the decimal value. 75 | * @returns {SenderBuffer} Returns with a reference to this buffer. 76 | * @throws {Error} If decimals are not supported by the buffer implementation, or validation fails. 77 | * Possible validation errors: 78 | * - `unscaled` length is not between 0 and 32 bytes. 79 | * - `scale` is not between 0 and 76. 80 | * - `unscaled` contains invalid bytes. 81 | */ 82 | decimalColumn( 83 | name: string, 84 | unscaled: bigint | Int8Array, 85 | scale: number, 86 | ): SenderBuffer { 87 | if (scale < 0 || scale > 76) { 88 | throw new RangeError("Scale must be between 0 and 76"); 89 | } 90 | let arr: number[]; 91 | if (typeof unscaled === "bigint") { 92 | arr = bigintToTwosComplementBytes(unscaled); 93 | } else if (unscaled instanceof Int8Array) { 94 | arr = Array.from(unscaled); 95 | } else { 96 | throw new TypeError( 97 | `Invalid unscaled value type: ${typeof unscaled}, expected Int8Array or bigint`, 98 | ); 99 | } 100 | if (arr.length > 32) { 101 | throw new RangeError( 102 | "Unscaled value length must be between 0 and 32 bytes", 103 | ); 104 | } 105 | this.writeColumn(name, unscaled, () => { 106 | this.checkCapacity([], 4 + arr.length); 107 | this.writeByte(EQUALS_SIGN); 108 | this.writeByte(ENTITY_TYPE_DECIMAL); 109 | this.writeByte(scale); 110 | this.writeByte(arr.length); 111 | for (let i = 0; i < arr.length; i++) { 112 | let byte = arr[i]; 113 | if (byte > 255 || byte < -128) { 114 | throw new RangeError( 115 | `Unscaled value contains invalid byte [index=${i}, value=${byte}]`, 116 | ); 117 | } 118 | if (byte > 127) { 119 | byte -= 256; 120 | } 121 | this.writeByte(byte); 122 | } 123 | }); 124 | return this; 125 | } 126 | } 127 | 128 | export { SenderBufferV3 }; 129 | -------------------------------------------------------------------------------- /src/transport/http/base.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { readFileSync } from "node:fs"; 3 | import { Buffer } from "node:buffer"; 4 | 5 | import { log, Logger } from "../../logging"; 6 | import { SenderOptions, HTTP, HTTPS } from "../../options"; 7 | import { SenderTransport } from "../index"; 8 | import { isBoolean, isInteger } from "../../utils"; 9 | 10 | // HTTP status code for successful request with no content. 11 | const HTTP_NO_CONTENT = 204; 12 | 13 | // Default number of rows that trigger auto-flush for HTTP transport. 14 | const DEFAULT_HTTP_AUTO_FLUSH_ROWS = 75000; 15 | 16 | // Default minimum throughput for HTTP requests (100 KB/sec). 17 | const DEFAULT_REQUEST_MIN_THROUGHPUT = 102400; 18 | 19 | // Default request timeout in milliseconds (10 seconds). 20 | const DEFAULT_REQUEST_TIMEOUT = 10000; 21 | 22 | // Default retry timeout in milliseconds (10 seconds). 23 | const DEFAULT_RETRY_TIMEOUT = 10000; 24 | 25 | // HTTP status codes that should trigger request retries. 26 | // Includes server errors and gateway timeouts that may be transient. 27 | const RETRIABLE_STATUS_CODES = [500, 503, 504, 507, 509, 523, 524, 529, 599]; 28 | 29 | /** 30 | * Abstract base class for HTTP-based transport implementations.
31 | * Provides common configuration and functionality for HTTP and HTTPS protocols. 32 | */ 33 | abstract class HttpTransportBase implements SenderTransport { 34 | protected readonly secure: boolean; 35 | protected readonly host: string; 36 | protected readonly port: number; 37 | 38 | protected readonly username: string; 39 | protected readonly password: string; 40 | protected readonly token: string; 41 | 42 | protected readonly tlsVerify: boolean; 43 | protected readonly tlsCA: Buffer; 44 | 45 | protected readonly requestMinThroughput: number; 46 | protected readonly requestTimeout: number; 47 | protected readonly retryTimeout: number; 48 | 49 | protected readonly log: Logger; 50 | 51 | /** 52 | * Creates a new HttpTransportBase instance. 53 | * 54 | * @param {SenderOptions} options - Sender configuration options including connection and authentication details 55 | * @throws Error if required protocol or host options are missing 56 | */ 57 | protected constructor(options: SenderOptions) { 58 | if (!options || !options.protocol) { 59 | throw new Error("The 'protocol' option is mandatory"); 60 | } 61 | if (!options.host) { 62 | throw new Error("The 'host' option is mandatory"); 63 | } 64 | this.log = typeof options.log === "function" ? options.log : log; 65 | 66 | this.tlsVerify = isBoolean(options.tls_verify) ? options.tls_verify : true; 67 | this.tlsCA = options.tls_ca ? readFileSync(options.tls_ca) : undefined; 68 | 69 | this.username = options.username; 70 | this.password = options.password; 71 | this.token = options.token; 72 | if (!options.port) { 73 | options.port = 9000; 74 | } 75 | 76 | this.host = options.host; 77 | this.port = options.port; 78 | 79 | this.requestMinThroughput = isInteger(options.request_min_throughput, 0) 80 | ? options.request_min_throughput 81 | : DEFAULT_REQUEST_MIN_THROUGHPUT; 82 | this.requestTimeout = isInteger(options.request_timeout, 1) 83 | ? options.request_timeout 84 | : DEFAULT_REQUEST_TIMEOUT; 85 | this.retryTimeout = isInteger(options.retry_timeout, 0) 86 | ? options.retry_timeout 87 | : DEFAULT_RETRY_TIMEOUT; 88 | 89 | switch (options.protocol) { 90 | case HTTP: 91 | this.secure = false; 92 | break; 93 | case HTTPS: 94 | this.secure = true; 95 | break; 96 | default: 97 | throw new Error( 98 | "The 'protocol' has to be 'http' or 'https' for the HTTP transport", 99 | ); 100 | } 101 | } 102 | 103 | /** 104 | * HTTP transport does not require explicit connection establishment. 105 | * @throws Error indicating connect is not required for HTTP transport 106 | */ 107 | connect(): Promise { 108 | throw new Error("'connect()' is not required for HTTP transport"); 109 | } 110 | 111 | /** 112 | * HTTP transport does not require explicit connection closure. 113 | * @returns Promise that resolves immediately 114 | */ 115 | async close(): Promise {} 116 | 117 | /** 118 | * Gets the default auto-flush row count for HTTP transport. 119 | * @returns {number} Default number of rows that trigger auto-flush 120 | */ 121 | getDefaultAutoFlushRows(): number { 122 | return DEFAULT_HTTP_AUTO_FLUSH_ROWS; 123 | } 124 | 125 | /** 126 | * Sends data to the QuestDB server via HTTP. 127 | * Must be implemented by concrete HTTP transport classes. 128 | * @param {Buffer} data - Buffer containing the data to send 129 | * @returns Promise resolving to true if data was sent successfully 130 | */ 131 | abstract send(data: Buffer): Promise; 132 | } 133 | 134 | export { 135 | HttpTransportBase, 136 | RETRIABLE_STATUS_CODES, 137 | HTTP_NO_CONTENT, 138 | DEFAULT_REQUEST_TIMEOUT, 139 | }; 140 | -------------------------------------------------------------------------------- /docs/hierarchy.html: -------------------------------------------------------------------------------- 1 | QuestDB Node.js Client - v4.2.0

QuestDB Node.js Client - v4.2.0

Hierarchy Summary

2 | -------------------------------------------------------------------------------- /docs/types/TimestampUnit.html: -------------------------------------------------------------------------------- 1 | TimestampUnit | QuestDB Node.js Client - v4.2.0

Type Alias TimestampUnit

TimestampUnit: "ns" | "us" | "ms"

Supported timestamp units for QuestDB operations.

2 |
3 | -------------------------------------------------------------------------------- /src/buffer/bufferv2.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { SenderOptions } from "../options"; 3 | import { SenderBuffer } from "./index"; 4 | import { SenderBufferBase } from "./base"; 5 | import { 6 | ArrayPrimitive, 7 | getDimensions, 8 | timestampToMicros, 9 | TimestampUnit, 10 | validateArray, 11 | } from "../utils"; 12 | 13 | // Column type constants for protocol v2. 14 | const COLUMN_TYPE_DOUBLE: number = 10; 15 | const COLUMN_TYPE_NULL: number = 33; 16 | 17 | // Entity type constants for protocol v2. 18 | const ENTITY_TYPE_ARRAY: number = 14; 19 | const ENTITY_TYPE_DOUBLE: number = 16; 20 | 21 | // ASCII code for equals sign used in binary protocol. 22 | const EQUALS_SIGN: number = "=".charCodeAt(0); 23 | 24 | /** 25 | * Buffer implementation for protocol version 2.
26 | * Sends floating point numbers in binary form, and provides support for arrays. 27 | */ 28 | class SenderBufferV2 extends SenderBufferBase { 29 | /** 30 | * Creates a new SenderBufferV2 instance. 31 | * 32 | * @param {SenderOptions} options - Sender configuration object.
33 | * See SenderOptions documentation for detailed description of configuration options. 34 | */ 35 | constructor(options: SenderOptions) { 36 | super(options); 37 | } 38 | 39 | /** 40 | * Writes a 64-bit floating point value into the buffer using v2 serialization (binary format).
41 | * Use it to insert into DOUBLE or FLOAT database columns. 42 | * 43 | * @param {string} name - Column name. 44 | * @param {number} value - Column value, accepts only number values. 45 | * @returns {Sender} Returns with a reference to this buffer. 46 | */ 47 | floatColumn(name: string, value: number): SenderBuffer { 48 | this.writeColumn( 49 | name, 50 | value, 51 | () => { 52 | this.checkCapacity([], 10); 53 | this.writeByte(EQUALS_SIGN); 54 | this.writeByte(ENTITY_TYPE_DOUBLE); 55 | this.writeDouble(value); 56 | }, 57 | "number", 58 | ); 59 | return this; 60 | } 61 | 62 | protected writeTimestamp( 63 | value: number | bigint, 64 | unit: TimestampUnit = "us", 65 | ): void { 66 | let biValue: bigint; 67 | let suffix: string; 68 | switch (unit) { 69 | case "ns": 70 | biValue = BigInt(value); 71 | suffix = "n"; 72 | break; 73 | default: 74 | biValue = timestampToMicros(BigInt(value), unit); 75 | suffix = "t"; 76 | } 77 | 78 | const timestampStr = biValue.toString(); 79 | this.checkCapacity([timestampStr], 2); 80 | this.write(timestampStr); 81 | this.write(suffix); 82 | } 83 | 84 | /** 85 | * Write an array column with its values into the buffer using v2 format. 86 | * 87 | * @param {string} name - Column name 88 | * @param {unknown[]} value - Array values to write (currently supports double arrays) 89 | * @returns {Sender} Returns with a reference to this buffer. 90 | * @throws Error if array validation fails: 91 | * - value is not an array 92 | * - or the shape of the array is irregular: the length of sub-arrays are different 93 | * - or the array is not homogeneous: its elements are not all the same type 94 | */ 95 | arrayColumn(name: string, value: unknown[]): SenderBuffer { 96 | const dimensions = getDimensions(value); 97 | const type = validateArray(value, dimensions); 98 | // only number arrays and NULL supported for now 99 | if (type !== "number" && type !== null) { 100 | throw new Error(`Unsupported array type [type=${type}]`); 101 | } 102 | 103 | this.writeColumn(name, value, () => { 104 | this.checkCapacity([], 3); 105 | this.writeByte(EQUALS_SIGN); 106 | this.writeByte(ENTITY_TYPE_ARRAY); 107 | 108 | if (!value) { 109 | this.writeByte(COLUMN_TYPE_NULL); 110 | } else { 111 | this.writeByte(COLUMN_TYPE_DOUBLE); 112 | this.writeArray(value, dimensions, type); 113 | } 114 | }); 115 | return this; 116 | } 117 | 118 | private writeArray( 119 | arr: unknown[], 120 | dimensions: number[], 121 | type: ArrayPrimitive, 122 | ) { 123 | this.checkCapacity([], 1 + dimensions.length * 4); 124 | this.writeByte(dimensions.length); 125 | for (let i = 0; i < dimensions.length; i++) { 126 | this.writeInt(dimensions[i]); 127 | } 128 | 129 | this.checkCapacity([], SenderBufferV2.arraySize(dimensions, type)); 130 | this.writeArrayValues(arr, dimensions); 131 | } 132 | 133 | private writeArrayValues(arr: unknown[], dimensions: number[]) { 134 | if (Array.isArray(arr[0])) { 135 | for (let i = 0; i < arr.length; i++) { 136 | this.writeArrayValues(arr[i] as unknown[], dimensions); 137 | } 138 | } else { 139 | const type = arr[0] !== undefined ? typeof arr[0] : null; 140 | switch (type) { 141 | case "number": 142 | for (let i = 0; i < arr.length; i++) { 143 | this.position = this.buffer.writeDoubleLE( 144 | arr[i] as number, 145 | this.position, 146 | ); 147 | } 148 | break; 149 | case null: 150 | // empty array 151 | break; 152 | default: 153 | throw new Error(`Unsupported array type [type=${type}]`); 154 | } 155 | } 156 | } 157 | 158 | private static arraySize(dimensions: number[], type: ArrayPrimitive): number { 159 | let numOfElements = 1; 160 | for (let i = 0; i < dimensions.length; i++) { 161 | numOfElements *= dimensions[i]; 162 | } 163 | 164 | switch (type) { 165 | case "number": 166 | return numOfElements * 8; 167 | case "boolean": 168 | return numOfElements; 169 | case "string": 170 | // in case of string[] capacity check is done separately for each array element 171 | return 0; 172 | case null: 173 | // empty array 174 | return 0; 175 | default: 176 | throw new Error(`Unsupported array type [type=${type}]`); 177 | } 178 | } 179 | } 180 | 181 | export { SenderBufferV2 }; 182 | -------------------------------------------------------------------------------- /src/transport/http/undici.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Buffer } from "node:buffer"; 3 | import { Agent, RetryAgent } from "undici"; 4 | import Dispatcher from "undici/types/dispatcher"; 5 | 6 | import { SenderOptions, HTTP, HTTPS } from "../../options"; 7 | import { 8 | HttpTransportBase, 9 | RETRIABLE_STATUS_CODES, 10 | HTTP_NO_CONTENT, 11 | } from "./base"; 12 | 13 | /** 14 | * Default HTTP options for the Undici agent. 15 | * Configures keep-alive connections with 60-second timeout and single request pipelining. 16 | */ 17 | const DEFAULT_HTTP_OPTIONS: Agent.Options = { 18 | connect: { 19 | keepAlive: true, 20 | }, 21 | pipelining: 1, 22 | keepAliveTimeout: 60000, // 1 minute 23 | }; 24 | 25 | /** 26 | * HTTP transport implementation using the Undici library.
27 | * Provides high-performance HTTP requests with connection pooling and retry logic.
28 | * Supports both HTTP and HTTPS protocols with configurable authentication. 29 | */ 30 | class UndiciTransport extends HttpTransportBase { 31 | private static DEFAULT_HTTP_AGENT: Agent; 32 | 33 | private readonly agent: Dispatcher; 34 | private readonly dispatcher: RetryAgent; 35 | 36 | /** 37 | * Creates a new UndiciTransport instance. 38 | * 39 | * @param options - Sender configuration object containing connection and retry settings 40 | * @throws Error if the protocol is not 'http' or 'https' 41 | */ 42 | constructor(options: SenderOptions) { 43 | super(options); 44 | 45 | switch (options.protocol) { 46 | case HTTP: 47 | this.agent = 48 | options.agent instanceof Agent 49 | ? options.agent 50 | : UndiciTransport.getDefaultHttpAgent(); 51 | break; 52 | case HTTPS: 53 | if (options.agent instanceof Agent) { 54 | this.agent = options.agent; 55 | } else { 56 | // Create a new agent with instance-specific TLS options 57 | this.agent = new Agent({ 58 | ...DEFAULT_HTTP_OPTIONS, 59 | connect: { 60 | ...DEFAULT_HTTP_OPTIONS.connect, 61 | requestCert: this.tlsVerify, 62 | rejectUnauthorized: this.tlsVerify, 63 | ca: this.tlsCA, 64 | }, 65 | }); 66 | } 67 | break; 68 | default: 69 | throw new Error( 70 | "The 'protocol' has to be 'http' or 'https' for the Undici HTTP transport", 71 | ); 72 | } 73 | 74 | this.dispatcher = new RetryAgent(this.agent, { 75 | maxRetries: Infinity, 76 | minTimeout: 10, 77 | maxTimeout: 1000, 78 | timeoutFactor: 2, 79 | retryAfter: true, 80 | methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"], 81 | statusCodes: RETRIABLE_STATUS_CODES, 82 | errorCodes: [ 83 | "ECONNRESET", 84 | "EAI_AGAIN", 85 | "ECONNREFUSED", 86 | "ETIMEDOUT", 87 | "EPIPE", 88 | "UND_ERR_CONNECT_TIMEOUT", 89 | "UND_ERR_HEADERS_TIMEOUT", 90 | "UND_ERR_BODY_TIMEOUT", 91 | ], 92 | }); 93 | } 94 | 95 | /** 96 | * Sends data to QuestDB using HTTP POST. 97 | * 98 | * @param {Buffer} data - Buffer containing the data to send 99 | * @returns Promise resolving to true if data was sent successfully 100 | * @throws Error if request fails after all retries or times out 101 | */ 102 | async send(data: Buffer): Promise { 103 | const headers: Record = {}; 104 | if (this.token) { 105 | headers["Authorization"] = `Bearer ${this.token}`; 106 | } else if (this.username && this.password) { 107 | headers["Authorization"] = 108 | `Basic ${Buffer.from(`${this.username}:${this.password}`).toString("base64")}`; 109 | } 110 | 111 | const controller = new AbortController(); 112 | const { signal } = controller; 113 | const timeoutId = setTimeout(() => controller.abort(), this.retryTimeout); 114 | 115 | let responseData: Dispatcher.ResponseData; 116 | try { 117 | const timeoutMillis = 118 | (data.length / this.requestMinThroughput) * 1000 + this.requestTimeout; 119 | responseData = await this.dispatcher.request({ 120 | origin: `${this.secure ? "https" : "http"}://${this.host}:${this.port}`, 121 | path: "/write?precision=n", 122 | method: "POST", 123 | headers, 124 | body: data, 125 | headersTimeout: this.requestTimeout, 126 | bodyTimeout: timeoutMillis, 127 | signal, 128 | }); 129 | } catch (err) { 130 | if (err.name === "AbortError") { 131 | throw new Error( 132 | "HTTP request timeout, no response from server in time", 133 | ); 134 | } else { 135 | throw err; 136 | } 137 | } finally { 138 | clearTimeout(timeoutId); 139 | } 140 | 141 | const { statusCode } = responseData; 142 | const body = await responseData.body.arrayBuffer(); 143 | if (statusCode === HTTP_NO_CONTENT) { 144 | if (body.byteLength > 0) { 145 | const message = Buffer.from(body).toString(); 146 | const logMessage = 147 | message.length < 256 148 | ? message 149 | : `${message.substring(0, 256)}... (truncated, full length=${message.length})`; 150 | this.log("warn", `Unexpected message from server: ${logMessage}`); 151 | } 152 | return true; 153 | } else { 154 | throw new Error( 155 | `HTTP request failed, statusCode=${statusCode}, error=${Buffer.from(body).toString()}`, 156 | ); 157 | } 158 | } 159 | 160 | /** 161 | * @ignore 162 | * Gets or creates the default HTTP agent with standard configuration. 163 | * Uses a singleton pattern to reuse the same agent across instances. 164 | * @returns The default Undici agent instance 165 | */ 166 | private static getDefaultHttpAgent(): Agent { 167 | if (!UndiciTransport.DEFAULT_HTTP_AGENT) { 168 | UndiciTransport.DEFAULT_HTTP_AGENT = new Agent(DEFAULT_HTTP_OPTIONS); 169 | } 170 | return UndiciTransport.DEFAULT_HTTP_AGENT; 171 | } 172 | } 173 | 174 | export { UndiciTransport }; 175 | -------------------------------------------------------------------------------- /docs/functions/bigintToTwosComplementBytes.html: -------------------------------------------------------------------------------- 1 | bigintToTwosComplementBytes | QuestDB Node.js Client - v4.2.0

Function bigintToTwosComplementBytes

  • Converts a bigint into a two's complement big-endian byte array. 2 | Produces the minimal-width representation that preserves the sign.

    3 |

    Parameters

    • value: bigint

      The value to serialise

      4 |

    Returns number[]

    Byte array in big-endian order

    5 |
6 | -------------------------------------------------------------------------------- /docs/functions/createTransport.html: -------------------------------------------------------------------------------- 1 | createTransport | QuestDB Node.js Client - v4.2.0

Function createTransport

  • Factory function to create appropriate transport instance based on configuration.

    2 |

    Parameters

    • options: SenderOptions

      Sender configuration options including protocol and connection details

      3 |

    Returns SenderTransport

    Transport instance appropriate for the specified protocol

    4 |

    Error if protocol or host options are missing or invalid

    5 |
6 | -------------------------------------------------------------------------------- /docs/functions/createBuffer.html: -------------------------------------------------------------------------------- 1 | createBuffer | QuestDB Node.js Client - v4.2.0

Function createBuffer

  • Factory function to create a SenderBuffer instance based on the protocol version.

    2 |

    Parameters

    • options: SenderOptions

      Sender configuration object. 3 | See SenderOptions documentation for detailed description of configuration options.

      4 |

    Returns SenderBuffer

    A SenderBuffer instance appropriate for the specified protocol version

    5 |

    Error if protocol version is not specified or is unsupported

    6 |
7 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from "undici"; 2 | 3 | /** 4 | * Primitive types for QuestDB arrays.
5 | * Currently only number arrays are supported by the server. 6 | */ 7 | type ArrayPrimitive = "number" | "boolean" | "string" | null; 8 | 9 | /** 10 | * Supported timestamp units for QuestDB operations. 11 | */ 12 | type TimestampUnit = "ns" | "us" | "ms"; 13 | 14 | /** 15 | * Type guard to check if a value is a boolean. 16 | * @param {unknown} value - The value to check 17 | * @returns True if the value is a boolean, false otherwise 18 | */ 19 | function isBoolean(value: unknown): value is boolean { 20 | return typeof value === "boolean"; 21 | } 22 | 23 | /** 24 | * Type guard to check if a value is an integer within specified bounds. 25 | * @param {unknown} value - The value to check 26 | * @param {number} lowerBound - The minimum allowed value (inclusive) 27 | * @returns True if the value is an integer >= lowerBound, false otherwise 28 | */ 29 | function isInteger(value: unknown, lowerBound: number): value is number { 30 | return ( 31 | typeof value === "number" && Number.isInteger(value) && value >= lowerBound 32 | ); 33 | } 34 | 35 | /** 36 | * Converts a timestamp from the specified unit to microseconds. 37 | * @param {bigint} timestamp - The timestamp value as a bigint 38 | * @param {TimestampUnit} unit - The source timestamp unit 39 | * @returns The timestamp converted to microseconds 40 | * @throws Error if the timestamp unit is unknown 41 | */ 42 | function timestampToMicros(timestamp: bigint, unit: TimestampUnit): bigint { 43 | switch (unit) { 44 | case "ns": 45 | return timestamp / 1000n; 46 | case "us": 47 | return timestamp; 48 | case "ms": 49 | return timestamp * 1000n; 50 | default: 51 | throw new Error(`Unknown timestamp unit: ${unit}`); 52 | } 53 | } 54 | 55 | /** 56 | * Converts a timestamp from the specified unit to nanoseconds. 57 | * @param {bigint} timestamp - The timestamp value as a bigint 58 | * @param {TimestampUnit} unit - The source timestamp unit 59 | * @returns The timestamp converted to nanoseconds 60 | * @throws Error if the timestamp unit is unknown 61 | */ 62 | function timestampToNanos(timestamp: bigint, unit: TimestampUnit): bigint { 63 | switch (unit) { 64 | case "ns": 65 | return timestamp; 66 | case "us": 67 | return timestamp * 1000n; 68 | case "ms": 69 | return timestamp * 1000_000n; 70 | default: 71 | throw new Error(`Unknown timestamp unit: ${unit}`); 72 | } 73 | } 74 | 75 | /** 76 | * Analyzes the dimensions of a nested array structure. 77 | * @param {unknown} data - The array to analyze 78 | * @returns Array of dimension sizes at each nesting level 79 | * @throws Error if any dimension has zero length 80 | */ 81 | function getDimensions(data: unknown): number[] { 82 | const dimensions: number[] = []; 83 | while (Array.isArray(data)) { 84 | dimensions.push(data.length); 85 | data = data[0]; 86 | } 87 | return dimensions; 88 | } 89 | 90 | /** 91 | * Validates an array structure.
92 | * Validation fails if: 93 | * - data is not an array 94 | * - the array is irregular: the length of its sub-arrays are different 95 | * - the array is not homogenous: the array contains mixed types 96 | * @param {unknown[]} data - The array to validate 97 | * @param {number[]} dimensions - The shape of the array 98 | * @returns The primitive type of the array's elements 99 | * @throws Error if the validation fails 100 | */ 101 | function validateArray(data: unknown[], dimensions: number[]): ArrayPrimitive { 102 | if (data === null || data === undefined) { 103 | return null; 104 | } 105 | if (!Array.isArray(data)) { 106 | throw new Error( 107 | `The value must be an array [value=${JSON.stringify(data)}, type=${typeof data}]`, 108 | ); 109 | } 110 | 111 | let expectedType: ArrayPrimitive = null; 112 | 113 | function checkArray( 114 | array: unknown[], 115 | depth: number = 0, 116 | path: string = "", 117 | ): void { 118 | const expectedLength = dimensions[depth]; 119 | if (array.length !== expectedLength) { 120 | throw new Error( 121 | `Lengths of sub-arrays do not match [expected=${expectedLength}, actual=${array.length}, dimensions=[${dimensions}], path=${path}]`, 122 | ); 123 | } 124 | 125 | if (depth < dimensions.length - 1) { 126 | // intermediate level, expecting arrays 127 | for (let i = 0; i < array.length; i++) { 128 | if (!Array.isArray(array[i])) { 129 | throw new Error( 130 | `Mixed types found [expected=array, current=${typeof array[i]}, path=${path}[${i}]]`, 131 | ); 132 | } 133 | checkArray(array[i] as unknown[], depth + 1, `${path}[${i}]`); 134 | } 135 | } else { 136 | // leaf level, expecting primitives 137 | if (expectedType === null && array[0] !== undefined) { 138 | expectedType = typeof array[0] as ArrayPrimitive; 139 | } 140 | 141 | for (let i = 0; i < array.length; i++) { 142 | const currentType = typeof array[i] as ArrayPrimitive; 143 | if (currentType !== expectedType) { 144 | throw new Error( 145 | expectedType !== null 146 | ? `Mixed types found [expected=${expectedType}, current=${currentType}, path=${path}[${i}]]` 147 | : `Unsupported array type [type=${currentType}]`, 148 | ); 149 | } 150 | } 151 | } 152 | } 153 | 154 | checkArray(data); 155 | return expectedType; 156 | } 157 | 158 | /** 159 | * Fetches JSON data from a URL. 160 | * @template T - The expected type of the JSON response 161 | * @param {string} url - The URL to fetch from 162 | * @param {Agent} agent - HTTP agent to be used for the request 163 | * @param {number} timeout - Request timeout, query will be aborted if not finished in time 164 | * @returns Promise resolving to the parsed JSON data 165 | * @throws Error if the request fails or returns a non-OK status 166 | */ 167 | async function fetchJson( 168 | url: string, 169 | timeout: number, 170 | agent: Agent, 171 | ): Promise { 172 | const controller = new AbortController(); 173 | const { signal } = controller; 174 | const timeoutId = setTimeout(() => controller.abort(), timeout); 175 | 176 | let response: globalThis.Response; 177 | try { 178 | response = await fetch(url, { 179 | dispatcher: agent, 180 | signal, 181 | }); 182 | } catch (error) { 183 | throw new Error(`Failed to load ${url} [error=${error}]`); 184 | } finally { 185 | clearTimeout(timeoutId); 186 | } 187 | 188 | if (!response.ok) { 189 | throw new Error( 190 | `Failed to load ${url} [statusCode=${response.status} (${response.statusText})]`, 191 | ); 192 | } 193 | return (await response.json()) as T; 194 | } 195 | 196 | /** 197 | * Converts a bigint into a two's complement big-endian byte array. 198 | * Produces the minimal-width representation that preserves the sign. 199 | * @param {bigint} value - The value to serialise 200 | * @returns {number[]} Byte array in big-endian order 201 | */ 202 | function bigintToTwosComplementBytes(value: bigint): number[] { 203 | if (value === 0n) { 204 | return [0]; 205 | } 206 | 207 | const bytes: number[] = []; 208 | const byteMask = 0xffn; 209 | 210 | if (value > 0n) { 211 | let tmp = value; 212 | while (tmp > 0n) { 213 | bytes.unshift(Number(tmp & byteMask)); 214 | tmp >>= 8n; 215 | } 216 | if (bytes[0] & 0x80) { 217 | bytes.unshift(0); 218 | } 219 | return bytes; 220 | } 221 | 222 | let tmp = value; 223 | while (tmp < -1n) { 224 | bytes.unshift(Number(tmp & byteMask)); 225 | tmp >>= 8n; 226 | } 227 | bytes.unshift(Number(tmp & byteMask)); 228 | if (!(bytes[0] & 0x80)) { 229 | bytes.unshift(0xff); 230 | } 231 | return bytes; 232 | } 233 | 234 | export { 235 | isBoolean, 236 | isInteger, 237 | timestampToMicros, 238 | timestampToNanos, 239 | TimestampUnit, 240 | fetchJson, 241 | getDimensions, 242 | validateArray, 243 | ArrayPrimitive, 244 | bigintToTwosComplementBytes, 245 | }; 246 | -------------------------------------------------------------------------------- /src/transport/http/stdlib.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import * as http from "http"; 3 | import * as https from "https"; 4 | import { Buffer } from "node:buffer"; 5 | 6 | import { SenderOptions, HTTP, HTTPS } from "../../options"; 7 | import { 8 | HttpTransportBase, 9 | RETRIABLE_STATUS_CODES, 10 | HTTP_NO_CONTENT, 11 | } from "./base"; 12 | 13 | /** 14 | * Default configuration for HTTP agents. 15 | * - Persistent connections with 1 minute idle timeout 16 | * - Maximum of 256 open connections (matching server default) 17 | */ 18 | const DEFAULT_HTTP_AGENT_CONFIG = { 19 | maxSockets: 256, 20 | keepAlive: true, 21 | timeout: 60000, // 1 min 22 | }; 23 | 24 | /** 25 | * HTTP transport implementation using Node.js built-in http/https modules.
26 | * Supports both HTTP and HTTPS protocols with configurable authentication. 27 | */ 28 | class HttpTransport extends HttpTransportBase { 29 | private static DEFAULT_HTTP_AGENT: http.Agent; 30 | private static DEFAULT_HTTPS_AGENT: https.Agent; 31 | 32 | private readonly agent: http.Agent | https.Agent; 33 | 34 | /** 35 | * Creates a new HttpTransport instance using Node.js HTTP modules. 36 | * 37 | * @param {SenderOptions} options - Sender configuration object containing connection details 38 | * @throws Error if the protocol is not 'http' or 'https' 39 | */ 40 | constructor(options: SenderOptions) { 41 | super(options); 42 | 43 | switch (options.protocol) { 44 | case HTTP: 45 | this.agent = 46 | options.agent instanceof http.Agent 47 | ? options.agent 48 | : HttpTransport.getDefaultHttpAgent(); 49 | break; 50 | case HTTPS: 51 | this.agent = 52 | options.agent instanceof https.Agent 53 | ? options.agent 54 | : HttpTransport.getDefaultHttpsAgent(); 55 | break; 56 | default: 57 | throw new Error( 58 | "The 'protocol' has to be 'http' or 'https' for the HTTP transport", 59 | ); 60 | } 61 | } 62 | 63 | /** 64 | * Sends data to QuestDB using HTTP POST. 65 | * 66 | * @param {Buffer} data - Buffer containing the data to send 67 | * @param {number} retryBegin - Internal parameter for tracking retry start time 68 | * @param {number} retryInterval - Internal parameter for tracking retry intervals 69 | * @returns Promise resolving to true if data was sent successfully 70 | * @throws Error if request fails after all retries or times out 71 | */ 72 | send( 73 | data: Buffer, 74 | retryBegin: number = -1, 75 | retryInterval: number = -1, 76 | ): Promise { 77 | const request = this.secure ? https.request : http.request; 78 | 79 | const timeoutMillis = 80 | (data.length / this.requestMinThroughput) * 1000 + this.requestTimeout; 81 | const options = this.createRequestOptions(timeoutMillis); 82 | 83 | return new Promise((resolve, reject) => { 84 | let statusCode = -1; 85 | const req = request(options, (response) => { 86 | statusCode = response.statusCode; 87 | 88 | const body = []; 89 | response 90 | .on("data", (chunk) => { 91 | body.push(chunk); 92 | }) 93 | .on("error", (err) => { 94 | this.log("error", `resp err=${err}`); 95 | }); 96 | 97 | if (statusCode === HTTP_NO_CONTENT) { 98 | response.on("end", () => { 99 | if (body.length > 0) { 100 | const message = Buffer.concat(body).toString(); 101 | const logMessage = 102 | message.length < 256 103 | ? message 104 | : `${message.substring(0, 256)}... (truncated, full length=${message.length})`; 105 | this.log("warn", `Unexpected message from server: ${logMessage}`); 106 | } 107 | resolve(true); 108 | }); 109 | } else { 110 | req.destroy( 111 | new Error( 112 | `HTTP request failed, statusCode=${statusCode}, error=${Buffer.concat(body)}`, 113 | ), 114 | ); 115 | } 116 | }); 117 | 118 | if (this.token) { 119 | req.setHeader("Authorization", `Bearer ${this.token}`); 120 | } else if (this.username && this.password) { 121 | req.setHeader( 122 | "Authorization", 123 | `Basic ${Buffer.from(`${this.username}:${this.password}`).toString("base64")}`, 124 | ); 125 | } 126 | 127 | req.on("timeout", () => { 128 | // set a retryable error code 129 | statusCode = 524; 130 | req.destroy( 131 | new Error("HTTP request timeout, no response from server in time"), 132 | ); 133 | }); 134 | req.on("error", (err) => { 135 | // if the error is thrown while the request is sent, statusCode is -1 => no retry 136 | // request timeout comes through with statusCode 524 => retry 137 | // if the error is thrown while the response is processed, the statusCode is taken from the response => retry depends on statusCode 138 | if (isRetryable(statusCode) && this.retryTimeout > 0) { 139 | if (retryBegin < 0) { 140 | retryBegin = Date.now(); 141 | retryInterval = 10; 142 | } else { 143 | const elapsed = Date.now() - retryBegin; 144 | if (elapsed > this.retryTimeout) { 145 | reject(err); 146 | return; 147 | } 148 | } 149 | const jitter = Math.floor(Math.random() * 10) - 5; 150 | setTimeout(() => { 151 | retryInterval = Math.min(retryInterval * 2, 1000); 152 | this.send(data, retryBegin, retryInterval) 153 | .then(() => resolve(true)) 154 | .catch((e) => reject(e)); 155 | }, retryInterval + jitter); 156 | } else { 157 | reject(err); 158 | } 159 | }); 160 | req.write(data, (err) => (err ? reject(err) : () => {})); 161 | req.end(); 162 | }); 163 | } 164 | 165 | /** 166 | * @ignore 167 | * Creates HTTP request options based on configuration. 168 | * 169 | * @param {number} timeoutMillis - Request timeout in milliseconds 170 | * @returns HTTP or HTTPS request options object 171 | */ 172 | private createRequestOptions( 173 | timeoutMillis: number, 174 | ): http.RequestOptions | https.RequestOptions { 175 | return { 176 | hostname: this.host, 177 | port: this.port, 178 | agent: this.agent, 179 | path: "/write?precision=n", 180 | method: "POST", 181 | timeout: timeoutMillis, 182 | rejectUnauthorized: this.secure && this.tlsVerify, 183 | ca: this.secure ? this.tlsCA : undefined, 184 | }; 185 | } 186 | 187 | /** 188 | * @ignore 189 | * Gets or creates the default HTTP agent with standard configuration. 190 | * Uses a singleton pattern to reuse the same agent across instances. 191 | * @returns The default HTTP agent instance 192 | */ 193 | private static getDefaultHttpAgent(): http.Agent { 194 | if (!HttpTransport.DEFAULT_HTTP_AGENT) { 195 | HttpTransport.DEFAULT_HTTP_AGENT = new http.Agent( 196 | DEFAULT_HTTP_AGENT_CONFIG, 197 | ); 198 | } 199 | return HttpTransport.DEFAULT_HTTP_AGENT; 200 | } 201 | 202 | /** 203 | * @ignore 204 | * Gets or creates the default HTTPS agent with standard configuration. 205 | * Uses a singleton pattern to reuse the same agent across instances. 206 | * @returns The default HTTPS agent instance 207 | */ 208 | private static getDefaultHttpsAgent(): https.Agent { 209 | if (!HttpTransport.DEFAULT_HTTPS_AGENT) { 210 | HttpTransport.DEFAULT_HTTPS_AGENT = new https.Agent( 211 | DEFAULT_HTTP_AGENT_CONFIG, 212 | ); 213 | } 214 | return HttpTransport.DEFAULT_HTTPS_AGENT; 215 | } 216 | } 217 | 218 | /** 219 | * @ignore 220 | * Determines if an HTTP status code should trigger a retry. 221 | * @param {number} statusCode - HTTP status code to check 222 | * @returns True if the status code indicates a retryable error 223 | */ 224 | function isRetryable(statusCode: number): boolean { 225 | return RETRIABLE_STATUS_CODES.includes(statusCode); 226 | } 227 | 228 | export { HttpTransport, HttpTransportBase }; 229 | -------------------------------------------------------------------------------- /docs/types/Logger.html: -------------------------------------------------------------------------------- 1 | Logger | QuestDB Node.js Client - v4.2.0

Type Alias Logger

Logger: (
    level: "error" | "warn" | "info" | "debug",
    message: string | Error,
) => void

Logger function type definition.

2 |

Type declaration

    • (level: "error" | "warn" | "info" | "debug", message: string | Error): void
    • Parameters

      • level: "error" | "warn" | "info" | "debug"

        The log level for the message

        3 |
      • message: string | Error

        The message to log, either a string or Error object

        4 |

      Returns void

5 | -------------------------------------------------------------------------------- /docs/types/ExtraOptions.html: -------------------------------------------------------------------------------- 1 | ExtraOptions | QuestDB Node.js Client - v4.2.0

Type Alias ExtraOptions

type ExtraOptions = {
    log?: Logger;
    agent?: Agent | http.Agent | https.Agent;
}
Index

Properties

log? 2 | agent? 3 |

Properties

log?: Logger
agent?: Agent | http.Agent | https.Agent
4 | -------------------------------------------------------------------------------- /src/buffer/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Buffer } from "node:buffer"; 3 | 4 | import { 5 | SenderOptions, 6 | PROTOCOL_VERSION_V1, 7 | PROTOCOL_VERSION_V2, 8 | PROTOCOL_VERSION_AUTO, 9 | PROTOCOL_VERSION_V3, 10 | } from "../options"; 11 | import { TimestampUnit } from "../utils"; 12 | import { SenderBufferV1 } from "./bufferv1"; 13 | import { SenderBufferV2 } from "./bufferv2"; 14 | import { SenderBufferV3 } from "./bufferv3"; 15 | 16 | // Default initial buffer size in bytes (64 KB). 17 | const DEFAULT_BUFFER_SIZE = 65536; // 64 KB 18 | 19 | // Default maximum buffer size in bytes (100 MB). 20 | const DEFAULT_MAX_BUFFER_SIZE = 104857600; // 100 MB 21 | 22 | /** 23 | * Factory function to create a SenderBuffer instance based on the protocol version. 24 | * @param options - Sender configuration object. 25 | * See {@link SenderOptions} documentation for detailed description of configuration options. 26 | * @returns A SenderBuffer instance appropriate for the specified protocol version 27 | * @throws Error if protocol version is not specified or is unsupported 28 | */ 29 | function createBuffer(options: SenderOptions): SenderBuffer { 30 | switch (options.protocol_version) { 31 | case PROTOCOL_VERSION_V3: 32 | return new SenderBufferV3(options); 33 | case PROTOCOL_VERSION_V2: 34 | return new SenderBufferV2(options); 35 | case PROTOCOL_VERSION_V1: 36 | return new SenderBufferV1(options); 37 | case PROTOCOL_VERSION_AUTO: 38 | case undefined: 39 | case null: 40 | case "": 41 | throw new Error( 42 | "Provide the 'protocol_version' option, or call 'await SenderOptions.resolveAuto(options)' first", 43 | ); 44 | default: 45 | throw new Error( 46 | `Unsupported protocol version: ${options.protocol_version}`, 47 | ); 48 | } 49 | } 50 | 51 | /** 52 | * Buffer used by the Sender for data serialization.
53 | * Provides methods for writing different data types into the buffer. 54 | */ 55 | interface SenderBuffer { 56 | /** 57 | * Resets the buffer, data sitting in the buffer will be lost. 58 | * In other words it clears the buffer, and sets the writing position to the beginning of the buffer. 59 | * @returns Returns with a reference to this buffer. 60 | */ 61 | reset(): SenderBuffer; 62 | 63 | /** 64 | * Returns a cropped buffer, or null if there is nothing to send. 65 | * The returned buffer is backed by this buffer instance, meaning the view can change as the buffer is mutated. 66 | * Used only in tests to assert the buffer's content. 67 | * @param pos - Optional position parameter 68 | * @returns A view of the buffer 69 | */ 70 | toBufferView(pos?: number): Buffer; 71 | 72 | /** 73 | * Returns a cropped buffer ready to send to the server, or null if there is nothing to send. 74 | * The returned buffer is a copy of this buffer. 75 | * It also compacts the buffer. 76 | * @param pos - Optional position parameter 77 | * @returns A copy of the buffer ready to send, or null 78 | */ 79 | toBufferNew(pos?: number): Buffer | null; 80 | 81 | /** 82 | * Writes the table name into the buffer. 83 | * @param table - Table name. 84 | * @returns Returns with a reference to this buffer. 85 | */ 86 | table(table: string): SenderBuffer; 87 | 88 | /** 89 | * Writes a symbol name and value into the buffer. 90 | * Use it to insert into SYMBOL columns. 91 | * @param name - Symbol name. 92 | * @param value - Symbol value, toString() is called to extract the actual symbol value from the parameter. 93 | * @returns Returns with a reference to this buffer. 94 | */ 95 | symbol(name: string, value: unknown): SenderBuffer; 96 | 97 | /** 98 | * Writes a string column with its value into the buffer. 99 | * Use it to insert into VARCHAR and STRING columns. 100 | * @param name - Column name. 101 | * @param value - Column value, accepts only string values. 102 | * @returns Returns with a reference to this buffer. 103 | */ 104 | stringColumn(name: string, value: string): SenderBuffer; 105 | 106 | /** 107 | * Writes a boolean column with its value into the buffer. 108 | * Use it to insert into BOOLEAN columns. 109 | * @param name - Column name. 110 | * @param value - Column value, accepts only boolean values. 111 | * @returns Returns with a reference to this buffer. 112 | */ 113 | booleanColumn(name: string, value: boolean): SenderBuffer; 114 | 115 | /** 116 | * Writes a 64-bit floating point value into the buffer. 117 | * Use it to insert into DOUBLE or FLOAT database columns. 118 | * @param name - Column name. 119 | * @param value - Column value, accepts only number values. 120 | * @returns Returns with a reference to this buffer. 121 | */ 122 | floatColumn(name: string, value: number): SenderBuffer; 123 | 124 | /** 125 | * Writes an array column with its values into the buffer. 126 | * @param name - Column name 127 | * @param value - Array values to write (currently supports double arrays) 128 | * @returns Returns with a reference to this buffer. 129 | * @throws Error if arrays are not supported by the buffer implementation, or array validation fails: 130 | * - value is not an array 131 | * - or the shape of the array is irregular: the length of sub-arrays are different 132 | * - or the array is not homogeneous: its elements are not all the same type 133 | */ 134 | arrayColumn(name: string, value: unknown[]): SenderBuffer; 135 | 136 | /** 137 | * Writes a 64-bit signed integer into the buffer. 138 | * Use it to insert into LONG, INT, SHORT and BYTE columns. 139 | * @param name - Column name. 140 | * @param value - Column value, accepts only number values. 141 | * @returns Returns with a reference to this buffer. 142 | * @throws Error if the value is not an integer 143 | */ 144 | intColumn(name: string, value: number): SenderBuffer; 145 | 146 | /** 147 | * Writes a timestamp column and its value into the buffer. 148 | * 149 | * Use this method to insert data into `TIMESTAMP` or `TIMESTAMP_NS` columns. 150 | * 151 | * **Precision rules**: 152 | * - **Protocol v2 and higher:** 153 | * Timestamps passed with unit `'ns'` (nanoseconds) are sent with full nanosecond precision. 154 | * All other timestamps are sent with microsecond precision. 155 | * - **Protocol v1:** 156 | * Always uses microsecond precision, even if the timestamp is specified in nanoseconds. 157 | * 158 | * @param {string} name - The column name. 159 | * @param {number | bigint} value - The epoch timestamp. Must be an integer or a `BigInt`. 160 | * @param {'ns' | 'us' | 'ms'} [unit='us'] - The time unit of the timestamp. 161 | * Supported values: 162 | * - `'ns'` — nanoseconds (requires `BigInt`) 163 | * - `'us'` — microseconds *(default)* 164 | * - `'ms'` — milliseconds 165 | * 166 | * @returns {SenderBuffer} Returns with a reference to this buffer. 167 | * 168 | * @throws {Error} If `value` is not an integer or `BigInt`. 169 | * @throws {Error} If `unit` is `'ns'` but `value` is not a `BigInt`. 170 | */ 171 | timestampColumn( 172 | name: string, 173 | value: number | bigint, 174 | unit: TimestampUnit, 175 | ): SenderBuffer; 176 | 177 | /** 178 | * Writes a decimal value into the buffer using its text format. 179 | * 180 | * Use it to insert into DECIMAL database columns. 181 | * 182 | * @param {string} name - Column name. 183 | * @param {string | number} value - The decimal value to write. 184 | * - Accepts either a `number` or a `string` containing a valid decimal representation. 185 | * - String values should follow standard decimal notation (e.g., `"123.45"` or `"-0.001"`). 186 | * @returns {Sender} Returns with a reference to this buffer. 187 | * @throws Error If decimals are not supported by the buffer implementation, or validation fails. 188 | * Possible validation errors: 189 | * - The provided string is not a valid decimal representation. 190 | */ 191 | decimalColumnText(name: string, value: string | number): SenderBuffer; 192 | 193 | /** 194 | * Writes a decimal value into the buffer using its binary format. 195 | * 196 | * Use it to insert into DECIMAL database columns. 197 | * 198 | * @param {string} name - Column name. 199 | * @param {bigint | Int8Array} unscaled - The unscaled integer portion of the decimal value. 200 | * - If a `bigint` is provided, it will be converted automatically. 201 | * - If an `Int8Array` is provided, it must contain the two’s complement representation 202 | * of the unscaled value in **big-endian** byte order. 203 | * - An empty `Int8Array` represents a `NULL` value. 204 | * @param {number} scale - The number of fractional digits (the scale) of the decimal value. 205 | * @returns {SenderBuffer} Returns with a reference to this buffer. 206 | * @throws {Error} If decimals are not supported by the buffer implementation, or validation fails. 207 | * Possible validation errors: 208 | * - `unscaled` length is not between 0 and 32 bytes. 209 | * - `scale` is not between 0 and 76. 210 | * - `unscaled` contains invalid bytes. 211 | */ 212 | decimalColumn( 213 | name: string, 214 | unscaled: bigint | Int8Array, 215 | scale: number, 216 | ): SenderBuffer; 217 | 218 | /** 219 | * Closes the row after writing the designated timestamp into the buffer. 220 | * 221 | * **Precision rules**: 222 | * - **Protocol v2 and higher:** 223 | * Timestamps passed with unit `'ns'` (nanoseconds) are sent with full nanosecond precision. 224 | * All other timestamps are sent with microsecond precision. 225 | * - **Protocol v1:** 226 | * Always uses microsecond precision, even if the timestamp is specified in nanoseconds. 227 | * 228 | * @param {number | bigint} timestamp - Designated epoch timestamp. Must be an integer or a `BigInt`. 229 | * @param {'ns' | 'us' | 'ms'} [unit='us'] - The time unit of the timestamp. 230 | * Supported values: 231 | * - `'ns'` — nanoseconds (requires `BigInt`) 232 | * - `'us'` — microseconds *(default)* 233 | * - `'ms'` — milliseconds 234 | * 235 | * @returns {SenderBuffer} Returns with a reference to this buffer. 236 | * 237 | * @throws {Error} If `value` is not an integer or `BigInt`. 238 | * @throws {Error} If `unit` is `'ns'` but `value` is not a `BigInt`. 239 | */ 240 | at(timestamp: number | bigint, unit: TimestampUnit): void; 241 | 242 | /** 243 | * Closes the row without writing designated timestamp into the buffer. 244 | * Designated timestamp will be populated by the server on this record. 245 | */ 246 | atNow(): void; 247 | 248 | /** 249 | * Returns the current position of the buffer. 250 | * New data will be written into the buffer starting from this position. 251 | * @returns The current write position in the buffer 252 | */ 253 | currentPosition(): number; 254 | } 255 | 256 | export { 257 | SenderBuffer, 258 | createBuffer, 259 | DEFAULT_BUFFER_SIZE, 260 | DEFAULT_MAX_BUFFER_SIZE, 261 | }; 262 | -------------------------------------------------------------------------------- /src/transport/tcp.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { readFileSync } from "node:fs"; 3 | import { Buffer } from "node:buffer"; 4 | import net from "node:net"; 5 | import tls from "node:tls"; 6 | import crypto from "node:crypto"; 7 | 8 | import { log, Logger } from "../logging"; 9 | import { SenderOptions, TCP, TCPS } from "../options"; 10 | import { SenderTransport } from "./index"; 11 | import { isBoolean } from "../utils"; 12 | 13 | // Default number of rows that trigger auto-flush for TCP transport. 14 | const DEFAULT_TCP_AUTO_FLUSH_ROWS = 600; 15 | 16 | // Arbitrary public key, used to construct valid JWK tokens. 17 | // These are not used for actual authentication, only required for crypto API compatibility. 18 | const PUBLIC_KEY = { 19 | x: "aultdA0PjhD_cWViqKKyL5chm6H1n-BiZBo_48T-uqc", 20 | y: "__ptaol41JWSpTTL525yVEfzmY8A6Vi_QrW1FjKcHMg", 21 | }; 22 | 23 | // New Line character 24 | const NEWLINE = 10; 25 | 26 | /** 27 | * TCP transport implementation.
28 | * Supports both plain TCP or secure TLS-encrypted connections with configurable JWK token authentication. 29 | */ 30 | class TcpTransport implements SenderTransport { 31 | private readonly secure: boolean; 32 | private readonly host: string; 33 | private readonly port: number; 34 | 35 | private socket: net.Socket | tls.TLSSocket; 36 | 37 | private readonly tlsVerify: boolean; 38 | private readonly tlsCA: Buffer; 39 | 40 | private readonly log: Logger; 41 | private readonly jwk: Record; 42 | 43 | /** 44 | * Creates a new TcpTransport instance. 45 | * 46 | * @param {SenderOptions} options - Sender configuration object containing connection and authentication details 47 | * @throws Error if required options are missing or protocol is not 'tcp' or 'tcps' 48 | */ 49 | constructor(options: SenderOptions) { 50 | if (!options || !options.protocol) { 51 | throw new Error("The 'protocol' option is mandatory"); 52 | } 53 | if (!options.host) { 54 | throw new Error("The 'host' option is mandatory"); 55 | } 56 | this.log = typeof options.log === "function" ? options.log : log; 57 | 58 | this.tlsVerify = isBoolean(options.tls_verify) ? options.tls_verify : true; 59 | this.tlsCA = options.tls_ca ? readFileSync(options.tls_ca) : undefined; 60 | 61 | this.host = options.host; 62 | this.port = options.port; 63 | 64 | switch (options.protocol) { 65 | case TCP: 66 | this.secure = false; 67 | break; 68 | case TCPS: 69 | this.secure = true; 70 | break; 71 | default: 72 | throw new Error( 73 | "The 'protocol' has to be 'tcp' or 'tcps' for the TCP transport", 74 | ); 75 | } 76 | 77 | if (!options.auth && !options.jwk) { 78 | constructAuth(options); 79 | } 80 | this.jwk = constructJwk(options); 81 | if (!options.port) { 82 | options.port = 9009; 83 | } 84 | } 85 | 86 | /** 87 | * Creates a TCP connection to the database. 88 | * @returns Promise resolving to true if the connection is established successfully 89 | * @throws Error if connection fails or authentication is rejected 90 | */ 91 | connect(): Promise { 92 | const connOptions: net.NetConnectOpts | tls.ConnectionOptions = { 93 | host: this.host, 94 | port: this.port, 95 | ca: this.tlsCA, 96 | }; 97 | 98 | return new Promise((resolve, reject) => { 99 | if (this.socket) { 100 | throw new Error("Sender connected already"); 101 | } 102 | 103 | let authenticated: boolean = false; 104 | let data: Buffer; 105 | 106 | this.socket = !this.secure 107 | ? net.connect(connOptions as net.NetConnectOpts) 108 | : tls.connect(connOptions as tls.ConnectionOptions, () => { 109 | if (authenticated) { 110 | resolve(true); 111 | } 112 | }); 113 | this.socket.setKeepAlive(true); 114 | 115 | this.socket 116 | .on("data", async (raw) => { 117 | data = !data ? raw : Buffer.concat([data, raw]); 118 | if (!authenticated) { 119 | authenticated = await this.authenticate(data); 120 | if (authenticated) { 121 | resolve(true); 122 | } 123 | } else { 124 | this.log("warn", `Received unexpected data: ${data}`); 125 | } 126 | }) 127 | .on("ready", async () => { 128 | this.log( 129 | "info", 130 | `Successfully connected to ${connOptions.host}:${connOptions.port}`, 131 | ); 132 | if (this.jwk) { 133 | this.log( 134 | "info", 135 | `Authenticating with ${connOptions.host}:${connOptions.port}`, 136 | ); 137 | this.socket.write(`${this.jwk.kid}\n`, (err) => { 138 | if (err) { 139 | this.log( 140 | "error", 141 | `Failed to send authentication: ${err.message}`, 142 | ); 143 | reject(err); 144 | } 145 | }); 146 | } else { 147 | authenticated = true; 148 | if (!this.secure || !this.tlsVerify) { 149 | resolve(true); 150 | } 151 | } 152 | }) 153 | .on("error", (err: Error & { code: string }) => { 154 | this.log("error", err); 155 | if ( 156 | this.tlsVerify || 157 | !err.code || 158 | err.code !== "SELF_SIGNED_CERT_IN_CHAIN" 159 | ) { 160 | reject(err); 161 | } else { 162 | // Warn about accepting self-signed certificate 163 | this.log( 164 | "warn", 165 | "Accepting self-signed certificate. This is insecure and should only be used in development environments.", 166 | ); 167 | } 168 | }); 169 | }); 170 | } 171 | 172 | /** 173 | * Sends data over the established TCP connection. 174 | * @param {Buffer} data - Buffer containing the data to send 175 | * @returns Promise resolving to true if data was sent successfully 176 | * @throws Error if the data could not be written to the socket 177 | */ 178 | send(data: Buffer): Promise { 179 | if (!this.socket || this.socket.destroyed) { 180 | throw new Error("TCP transport is not connected"); 181 | } 182 | return new Promise((resolve, reject) => { 183 | this.socket.write(data, (err: Error) => { 184 | if (err) { 185 | reject(err); 186 | } else { 187 | resolve(true); 188 | } 189 | }); 190 | }); 191 | } 192 | 193 | /** 194 | * Closes the TCP connection to the database. 195 | */ 196 | async close(): Promise { 197 | if (this.socket) { 198 | const address = this.socket.remoteAddress; 199 | const port = this.socket.remotePort; 200 | this.socket.destroy(); 201 | this.socket = undefined; 202 | this.log("info", `Connection to ${address}:${port} is closed`); 203 | } 204 | } 205 | 206 | /** 207 | * Gets the default auto-flush row count for TCP transport. 208 | * @returns Default number of rows that trigger auto-flush 209 | */ 210 | getDefaultAutoFlushRows(): number { 211 | return DEFAULT_TCP_AUTO_FLUSH_ROWS; 212 | } 213 | 214 | /** 215 | * @ignore 216 | * Handles the JWK token authentication challenge-response flow. 217 | * @param {Buffer} challenge - Challenge buffer received from the server 218 | * @returns Promise resolving to true if authentication is successful 219 | */ 220 | private async authenticate(challenge: Buffer): Promise { 221 | // Check for trailing \n which ends the challenge 222 | if (challenge.subarray(-1).readInt8() === NEWLINE) { 223 | const keyObject = crypto.createPrivateKey({ 224 | key: this.jwk, 225 | format: "jwk", 226 | }); 227 | const signature = crypto.sign( 228 | "RSA-SHA256", 229 | challenge.subarray(0, challenge.length - 1), 230 | keyObject, 231 | ); 232 | 233 | return new Promise((resolve, reject) => { 234 | this.socket.write( 235 | `${Buffer.from(signature).toString("base64")}\n`, 236 | (err: Error) => { 237 | if (err) { 238 | reject(err); 239 | } else { 240 | resolve(true); 241 | } 242 | }, 243 | ); 244 | }); 245 | } 246 | return false; 247 | } 248 | } 249 | 250 | /** 251 | * @ignore 252 | * Constructs authentication configuration from username/token options. 253 | * @param {SenderOptions} options - Sender options that may contain authentication details 254 | * @throws Error if username or token is missing when authentication is intended 255 | */ 256 | function constructAuth(options: SenderOptions): void { 257 | if (!options.username && !options.token && !options.password) { 258 | // no intention to authenticate 259 | return; 260 | } 261 | if (!options.username || !options.token) { 262 | throw new Error( 263 | `TCP transport requires a username and a private key for authentication, please, specify the 'username' and 'token' config options`, 264 | ); 265 | } 266 | 267 | options.auth = { 268 | keyId: options.username, 269 | token: options.token, 270 | }; 271 | } 272 | 273 | /** 274 | * @ignore 275 | * Constructs a JWK (JSON Web Key) object for cryptographic authentication. 276 | * @param {SenderOptions} options - Sender options containing authentication configuration 277 | * @returns JWK object with key ID, private key, and public key coordinates 278 | * @throws Error if required authentication properties are missing or invalid 279 | */ 280 | function constructJwk(options: SenderOptions): Record { 281 | if (options.auth) { 282 | if (!options.auth.keyId) { 283 | throw new Error( 284 | `Missing username, please, specify the 'keyId' property of the 'auth' config option. For example: new Sender({protocol: 'tcp', host: 'host', auth: {keyId: 'username', token: 'private key'}})`, 285 | ); 286 | } 287 | if (typeof options.auth.keyId !== "string") { 288 | throw new Error( 289 | `Please, specify the 'keyId' property of the 'auth' config option as a string. For example: new Sender({protocol: 'tcp', host: 'host', auth: {keyId: 'username', token: 'private key'}})`, 290 | ); 291 | } 292 | if (!options.auth.token) { 293 | throw new Error( 294 | `Missing private key, please, specify the 'token' property of the 'auth' config option. For example: new Sender({protocol: 'tcp', host: 'host', auth: {keyId: 'username', token: 'private key'}})`, 295 | ); 296 | } 297 | if (typeof options.auth.token !== "string") { 298 | throw new Error( 299 | `Please, specify the 'token' property of the 'auth' config option as a string. For example: new Sender({protocol: 'tcp', host: 'host', auth: {keyId: 'username', token: 'private key'}})`, 300 | ); 301 | } 302 | 303 | return { 304 | kid: options.auth.keyId, 305 | d: options.auth.token, 306 | ...PUBLIC_KEY, 307 | kty: "EC", 308 | crv: "P-256", 309 | }; 310 | } else { 311 | return options.jwk; 312 | } 313 | } 314 | 315 | export { TcpTransport }; 316 | -------------------------------------------------------------------------------- /docs/assets/icons.svg: -------------------------------------------------------------------------------- 1 | MMNEPVFCICPMFPCPTTAAATR -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ```shell 4 | # With npm 5 | npm i -s @questdb/nodejs-client 6 | 7 | # With yarn 8 | yarn add @questdb/nodejs-client 9 | 10 | # With pnpm 11 | pnpm add @questdb/nodejs-client 12 | ``` 13 | 14 | ## Compatibility table 15 | 16 | | QuestDB client version | Supported Node.js versions | Default HTTP Agent | 17 | |------------------------|----------------------------|---------------------| 18 | | ^4.0.0 | v20 and above | Undici Http Agent | 19 | | ^3.0.0 | v16 and above | Standard Http Agent | 20 | 21 | The current version of the client requires Node.js v20 or newer version. 22 | Versions up to and including 3.0.0 are compatible with Node.js v16 and above. 23 | 24 | The Undici HTTP agent was introduced in 4.0.0, and it is the default HTTP transport. 25 | The standard HTTP/HTTPS modules of Node.js are still supported for backwards compatibility. 26 | Use the stdlib_http option to switch to the standard HTTP/HTTPS modules. 27 | 28 | ## Configuration options 29 | 30 | Detailed description of the client's configuration options can be found in 31 | the {@link SenderOptions} documentation. 32 | 33 | ## Examples 34 | 35 | The examples below demonstrate how to use the client.
36 | For more details, please, check the {@link Sender}'s documentation. 37 | 38 | ### Basic API usage 39 | 40 | ```typescript 41 | import { Sender } from "@questdb/nodejs-client"; 42 | 43 | async function run() { 44 | // create a sender using HTTP protocol 45 | const sender = await Sender.fromConfig("http::addr=127.0.0.1:9000"); 46 | 47 | // add rows to the buffer of the sender 48 | await sender 49 | .table("trades") 50 | .symbol("symbol", "BTC-USD") 51 | .symbol("side", "sell") 52 | .floatColumn("price", 39269.98) 53 | .floatColumn("amount", 0.011) 54 | .at(Date.now(), "ms"); 55 | 56 | // flush the buffer of the sender, sending the data to QuestDB 57 | // the buffer is cleared after the data is sent, and the sender is ready to accept new data 58 | await sender.flush(); 59 | 60 | // close the connection after all rows ingested 61 | // unflushed data will be lost 62 | await sender.close(); 63 | } 64 | 65 | run().then(console.log).catch(console.error); 66 | ``` 67 | 68 | ### Authentication and secure connection 69 | 70 | #### Username and password authentication with HTTP transport 71 | 72 | ```typescript 73 | import { Sender } from "@questdb/nodejs-client"; 74 | 75 | async function run() { 76 | // authentication details 77 | const USER = "admin"; 78 | const PWD = "quest"; 79 | 80 | // pass the authentication details to the sender 81 | // for secure connection use 'https' protocol instead of 'http' 82 | const sender = await Sender.fromConfig( 83 | `http::addr=127.0.0.1:9000;username=${USER};password=${PWD}` 84 | ); 85 | 86 | // add rows to the buffer of the sender 87 | await sender 88 | .table("trades") 89 | .symbol("symbol", "ETH-USD") 90 | .symbol("side", "sell") 91 | .floatColumn("price", 2615.54) 92 | .floatColumn("amount", 0.00044) 93 | .at(Date.now(), "ms"); 94 | 95 | // flush the buffer of the sender, sending the data to QuestDB 96 | await sender.flush(); 97 | 98 | // close the connection after all rows ingested 99 | await sender.close(); 100 | } 101 | 102 | run().catch(console.error); 103 | ``` 104 | 105 | #### REST token authentication with HTTP transport 106 | 107 | ```typescript 108 | import { Sender } from "@questdb/nodejs-client"; 109 | 110 | async function run() { 111 | // authentication details 112 | const TOKEN = "Xyvd3er6GF87ysaHk"; 113 | 114 | // pass the authentication details to the sender 115 | // for secure connection use 'https' protocol instead of 'http' 116 | const sender = await Sender.fromConfig( 117 | `http::addr=127.0.0.1:9000;token=${TOKEN}` 118 | ); 119 | 120 | // add rows to the buffer of the sender 121 | await sender 122 | .table("trades") 123 | .symbol("symbol", "ETH-USD") 124 | .symbol("side", "sell") 125 | .floatColumn("price", 2615.54) 126 | .floatColumn("amount", 0.00044) 127 | .at(Date.now(), "ms"); 128 | 129 | // flush the buffer of the sender, sending the data to QuestDB 130 | await sender.flush(); 131 | 132 | // close the connection after all rows ingested 133 | await sender.close(); 134 | } 135 | 136 | run().catch(console.error); 137 | ``` 138 | 139 | #### JWK token authentication with TCP transport 140 | 141 | ```typescript 142 | import { Sender } from "@questdb/nodejs-client"; 143 | 144 | async function run() { 145 | // authentication details 146 | const CLIENT_ID = "admin"; 147 | const PRIVATE_KEY = "ZRxmCOQBpZoj2fZ-lEtqzVDkCre_ouF3ePpaQNDwoQk"; 148 | 149 | // pass the authentication details to the sender 150 | const sender = await Sender.fromConfig( 151 | `tcp::addr=127.0.0.1:9009;username=${CLIENT_ID};token=${PRIVATE_KEY}` 152 | ); 153 | await sender.connect(); 154 | 155 | // add rows to the buffer of the sender 156 | await sender 157 | .table("trades") 158 | .symbol("symbol", "BTC-USD") 159 | .symbol("side", "sell") 160 | .floatColumn("price", 39269.98) 161 | .floatColumn("amount", 0.001) 162 | .at(Date.now(), "ms"); 163 | 164 | // flush the buffer of the sender, sending the data to QuestDB 165 | await sender.flush(); 166 | 167 | // close the connection after all rows ingested 168 | await sender.close(); 169 | } 170 | 171 | run().catch(console.error); 172 | ``` 173 | 174 | ### Array usage example 175 | 176 | ```typescript 177 | import { Sender } from "@questdb/nodejs-client"; 178 | 179 | async function run() { 180 | // create a sender 181 | const sender = await Sender.fromConfig('http::addr=localhost:9000'); 182 | 183 | // order book snapshots to ingest 184 | const orderBooks = [ 185 | { 186 | symbol: 'BTC-USD', 187 | exchange: 'Coinbase', 188 | timestamp: Date.now(), 189 | bidPrices: [50100.25, 50100.20, 50100.15, 50100.10, 50100.05], 190 | bidSizes: [0.5, 1.2, 2.1, 0.8, 3.5], 191 | askPrices: [50100.30, 50100.35, 50100.40, 50100.45, 50100.50], 192 | askSizes: [0.6, 1.5, 1.8, 2.2, 4.0] 193 | }, 194 | { 195 | symbol: 'ETH-USD', 196 | exchange: 'Coinbase', 197 | timestamp: Date.now(), 198 | bidPrices: [2850.50, 2850.45, 2850.40, 2850.35, 2850.30], 199 | bidSizes: [5.0, 8.2, 12.5, 6.8, 15.0], 200 | askPrices: [2850.55, 2850.60, 2850.65, 2850.70, 2850.75], 201 | askSizes: [4.5, 7.8, 10.2, 8.5, 20.0] 202 | } 203 | ]; 204 | 205 | try { 206 | // add rows to the buffer of the sender 207 | for (const orderBook of orderBooks) { 208 | await sender 209 | .table('order_book_l2') 210 | .symbol('symbol', orderBook.symbol) 211 | .symbol('exchange', orderBook.exchange) 212 | .arrayColumn('bid_prices', orderBook.bidPrices) 213 | .arrayColumn('bid_sizes', orderBook.bidSizes) 214 | .arrayColumn('ask_prices', orderBook.askPrices) 215 | .arrayColumn('ask_sizes', orderBook.askSizes) 216 | .at(orderBook.timestamp, 'ms'); 217 | } 218 | 219 | // flush the buffer of the sender, sending the data to QuestDB 220 | // the buffer is cleared after the data is sent, and the sender is ready to accept new data 221 | await sender.flush(); 222 | } finally { 223 | // close the connection after all rows ingested 224 | await sender.close(); 225 | } 226 | } 227 | 228 | run().then(console.log).catch(console.error); 229 | ``` 230 | 231 | ### Worker threads example 232 | 233 | ```typescript 234 | import { Sender } from "@questdb/nodejs-client"; 235 | import { Worker, isMainThread, parentPort, workerData } from "worker_threads"; 236 | 237 | // fake venue 238 | // generates random prices and amounts for a ticker for max 5 seconds, then the feed closes 239 | function* venue(ticker) { 240 | let end = false; 241 | setTimeout(() => { 242 | end = true; 243 | }, rndInt(5000)); 244 | while (!end) { 245 | yield { ticker, price: Math.random(), amount: Math.random() }; 246 | } 247 | } 248 | 249 | // market data feed simulator 250 | // uses the fake venue to deliver price and amount updates to the feed handler (onTick() callback) 251 | async function subscribe(ticker, onTick) { 252 | const feed = venue(workerData.ticker); 253 | let tick; 254 | while ((tick = feed.next().value)) { 255 | await onTick(tick); 256 | await sleep(rndInt(30)); 257 | } 258 | } 259 | 260 | async function run() { 261 | if (isMainThread) { 262 | const tickers = ["ETH-USD", "BTC-USD", "SOL-USD", "DOGE-USD"]; 263 | // main thread to start a worker thread for each ticker 264 | for (let ticker of tickers) { 265 | new Worker(__filename, { workerData: { ticker: ticker } }) 266 | .on("error", (err) => { 267 | throw err; 268 | }) 269 | .on("exit", () => { 270 | console.log(`${ticker} thread exiting...`); 271 | }) 272 | .on("message", (msg) => { 273 | console.log(`Ingested ${msg.count} prices for ticker ${msg.ticker}`); 274 | }); 275 | } 276 | } else { 277 | // it is important that each worker has a dedicated sender object 278 | // threads cannot share the sender because they would write into the same buffer 279 | const sender = await Sender.fromConfig("http::addr=127.0.0.1:9000"); 280 | 281 | // subscribe for the market data of the ticker assigned to the worker 282 | // ingest each price update into the database using the sender 283 | let count = 0; 284 | await subscribe(workerData.ticker, async (tick) => { 285 | await sender 286 | .table("trades") 287 | .symbol("symbol", tick.ticker) 288 | .symbol("side", "sell") 289 | .floatColumn("price", tick.price) 290 | .floatColumn("amount", tick.amount) 291 | .at(Date.now(), "ms"); 292 | await sender.flush(); 293 | count++; 294 | }); 295 | 296 | // let the main thread know how many prices were ingested 297 | parentPort.postMessage({ ticker: workerData.ticker, count }); 298 | 299 | // close the connection to the database 300 | await sender.close(); 301 | } 302 | } 303 | 304 | function sleep(ms: number) { 305 | return new Promise((resolve) => setTimeout(resolve, ms)); 306 | } 307 | 308 | function rndInt(limit: number) { 309 | return Math.floor(Math.random() * limit + 1); 310 | } 311 | 312 | run().then(console.log).catch(console.error); 313 | ``` 314 | 315 | ### Decimal usage example 316 | 317 | Since v9.2.0, QuestDB supports the `DECIMAL` data type. 318 | Decimals can be ingested with ILP protocol v3 using either textual or binary representation. 319 | 320 | #### Textual representation 321 | 322 | ```typescript 323 | import { Sender } from "@questdb/nodejs-client"; 324 | 325 | async function runDecimals() { 326 | const sender = await Sender.fromConfig( 327 | "http::addr=127.0.0.1:9000;protocol_version=3", 328 | ); 329 | 330 | await sender 331 | .table("fx") 332 | // textual form keeps the literal and its exact scale, 333 | // resulting in ILP line: fx mid=1.234500d 334 | .decimalColumnText("mid", "1.234500") 335 | .atNow(); 336 | 337 | await sender.flush(); 338 | await sender.close(); 339 | } 340 | 341 | runDecimals().catch(console.error); 342 | ``` 343 | 344 | #### Binary representation 345 | 346 | It is recommended to use the binary representation for better ingestion performance and reduced payload size (for bigger decimal values). 347 | 348 | ```typescript 349 | import { Sender } from "@questdb/nodejs-client"; 350 | 351 | async function runDecimals() { 352 | const sender = await Sender.fromConfig( 353 | "http::addr=127.0.0.1:9000;protocol_version=3", 354 | ); 355 | 356 | await sender 357 | .table("fx") 358 | // decimal value is sent in its binary form 359 | // 123.456 = 123456 * 10^-3 360 | .decimalColumn("mid", 123456n, 3) 361 | .atNow(); 362 | 363 | await sender.flush(); 364 | await sender.close(); 365 | } 366 | 367 | runDecimals().catch(console.error); 368 | ``` 369 | 370 | ## Community 371 | 372 | If you need help, have additional questions or want to provide feedback, you 373 | may find us on our [Community Forum](https://community.questdb.io/). 374 | 375 | You can also [sign up to our mailing list](https://questdb.io/contributors/) 376 | to get notified of new releases. 377 | -------------------------------------------------------------------------------- /docs/assets/icons.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | addIcons(); 3 | function addIcons() { 4 | if (document.readyState === "loading") return document.addEventListener("DOMContentLoaded", addIcons); 5 | const svg = document.body.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg")); 6 | svg.innerHTML = `MMNEPVFCICPMFPCPTTAAATR`; 7 | svg.style.display = "none"; 8 | if (location.protocol === "file:") updateUseElements(); 9 | } 10 | 11 | function updateUseElements() { 12 | document.querySelectorAll("use").forEach(el => { 13 | if (el.getAttribute("href").includes("#icon-")) { 14 | el.setAttribute("href", el.getAttribute("href").replace(/.*#/, "#")); 15 | } 16 | }); 17 | } 18 | })() --------------------------------------------------------------------------------