├── .circleci ├── config.yml ├── deploy-nightly-version.sh └── setup-npmjs.sh ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ ├── FEATURE_REQUEST.yml │ └── SUPPORT.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── linter.yml │ └── semantic.yml ├── .gitignore ├── .markdownlint.yml ├── .npmignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── eslint.config.mjs ├── examples ├── README.md ├── basic │ ├── README.md │ ├── package.json │ ├── src │ │ ├── client.ts │ │ ├── index.ts │ │ └── writeRetry.ts │ └── tsconfig.json ├── browser │ ├── .gitignore │ ├── README.md │ ├── docker-compose.yml │ ├── envoy.yaml │ ├── favicon.ico │ ├── index.html │ ├── package.json │ ├── src │ │ ├── exampleQueries.ts │ │ ├── influx-logo.png │ │ ├── main.ts │ │ ├── view.ts │ │ └── vite-env.d.ts │ ├── style.css │ ├── tsconfig.json │ └── vite.config.ts └── downsampling │ ├── README.md │ ├── package.json │ ├── src │ └── index.ts │ └── tsconfig.json ├── js_logo.png ├── lerna.json ├── package.json ├── packages ├── .eslintrc.json ├── client-browser │ ├── .npmignore │ └── package.json ├── client │ ├── .mocharc.json │ ├── .npmignore │ ├── .nycrc │ ├── api-extractor.json │ ├── package.json │ ├── src │ │ ├── InfluxDBClient.ts │ │ ├── Point.ts │ │ ├── PointValues.ts │ │ ├── QueryApi.ts │ │ ├── WriteApi.ts │ │ ├── errors.ts │ │ ├── generated │ │ │ └── flight │ │ │ │ ├── Flight.client.ts │ │ │ │ ├── Flight.ts │ │ │ │ └── google │ │ │ │ └── protobuf │ │ │ │ └── timestamp.ts │ │ ├── impl │ │ │ ├── QueryApiImpl.ts │ │ │ ├── WriteApiImpl.ts │ │ │ ├── browser │ │ │ │ ├── .eslintrc.json │ │ │ │ ├── FetchTransport.ts │ │ │ │ ├── eslint.config.mjs │ │ │ │ ├── index.ts │ │ │ │ ├── rpc.ts │ │ │ │ └── tsconfig.json │ │ │ ├── completeCommunicationObserver.ts │ │ │ ├── implSelector.ts │ │ │ ├── node │ │ │ │ ├── NodeHttpTransport.ts │ │ │ │ ├── index.ts │ │ │ │ └── rpc.ts │ │ │ └── version.ts │ │ ├── index.ts │ │ ├── options.ts │ │ ├── results │ │ │ ├── Cancellable.ts │ │ │ ├── CommunicationObserver.ts │ │ │ ├── chunkCombiner.ts │ │ │ └── index.ts │ │ ├── transport.ts │ │ └── util │ │ │ ├── TypeCasting.ts │ │ │ ├── common.ts │ │ │ ├── escape.ts │ │ │ ├── fixUrl.ts │ │ │ ├── generics.ts │ │ │ ├── logger.ts │ │ │ ├── sql.ts │ │ │ └── time.ts │ ├── test │ │ ├── .eslintrc.json │ │ ├── TestServer.ts │ │ ├── eslint.config.mjs │ │ ├── integration │ │ │ ├── e2e.test.ts │ │ │ └── queryAPI.test.ts │ │ ├── unit │ │ │ ├── Influxdb.test.ts │ │ │ ├── Query.test.ts │ │ │ ├── Write.test.ts │ │ │ ├── errors.test.ts │ │ │ ├── impl │ │ │ │ ├── browser │ │ │ │ │ ├── FetchTransport.test.ts │ │ │ │ │ └── emulateBrowser.ts │ │ │ │ └── node │ │ │ │ │ └── NodeHttpTransport.test.ts │ │ │ ├── options.test.ts │ │ │ ├── results │ │ │ │ └── chunkCombiner.test.ts │ │ │ └── util │ │ │ │ ├── common.test.ts │ │ │ │ ├── generics.test.ts │ │ │ │ ├── logger.test.ts │ │ │ │ ├── point.test.ts │ │ │ │ ├── time.test.ts │ │ │ │ ├── typeCasting.test.ts │ │ │ │ ├── utils.test.ts │ │ │ │ └── waitForCondition.ts │ │ └── util.ts │ ├── tsconfig.json │ ├── tsup.config.browser.ts │ └── tsup.config.ts └── eslint.config.mjs ├── scripts ├── change-version.js ├── cp.js ├── esbuild-gzip-js.ts ├── generate-flight.js ├── gh-pages_config.yml └── require-yarn.js ├── tsconfig.base.json ├── typedoc.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | init-dependencies: 5 | steps: 6 | - restore_cache: 7 | name: Restore Yarn Package Cache 8 | keys: 9 | - yarn-packages-v2-{{ checksum "yarn.lock" }} 10 | - run: 11 | name: Install Dependencies 12 | command: yarn install --frozen-lockfile 13 | - save_cache: 14 | name: Save Yarn Package Cache 15 | key: yarn-packages-v2-{{ checksum "yarn.lock" }} 16 | paths: 17 | - ~/.cache/yarn 18 | 19 | jobs: 20 | tests: 21 | parameters: 22 | image: 23 | type: string 24 | default: &default-image 'cimg/node:18.19' 25 | docker: 26 | - image: << parameters.image >> 27 | steps: 28 | - checkout 29 | - init-dependencies 30 | - run: 31 | name: Run tests 32 | command: | 33 | yarn flight:test 34 | yarn build 35 | yarn test:ci 36 | yarn typedoc 37 | - store_test_results: 38 | path: ./reports 39 | 40 | generate-flight: 41 | docker: 42 | - image: *default-image 43 | steps: 44 | - checkout 45 | - init-dependencies 46 | - run: 47 | name: Generate Flight Client 48 | command: | 49 | yarn flight 50 | coverage: 51 | parameters: 52 | docker: 53 | - image: *default-image 54 | steps: 55 | - checkout 56 | - init-dependencies 57 | - run: 58 | name: Runs tests with coverage 59 | command: | 60 | yarn --version 61 | yarn flight:test 62 | yarn run coverage:ci 63 | - run: 64 | name: Report test results to codecov 65 | command: | 66 | curl -Os https://uploader.codecov.io/latest/linux/codecov 67 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM 68 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig 69 | curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import 70 | gpgv codecov.SHA256SUM.sig codecov.SHA256SUM 71 | shasum -a 256 -c codecov.SHA256SUM 72 | chmod +x ./codecov 73 | ./codecov 74 | - store_artifacts: 75 | path: ./packages/client/coverage 76 | 77 | deploy-preview: 78 | parameters: 79 | docker: 80 | - image: *default-image 81 | steps: 82 | - run: 83 | name: Early return if this build is from a forked repository 84 | command: | 85 | if [[ $CIRCLE_PROJECT_USERNAME != "InfluxCommunity" ]]; then 86 | echo "Nothing to do for forked repositories, so marking this step successful" 87 | circleci step halt 88 | fi 89 | - checkout 90 | - init-dependencies 91 | - run: 92 | name: Setup npmjs 93 | command: bash .circleci/setup-npmjs.sh 94 | - run: 95 | name: Build & Deploy nightly version 96 | command: bash .circleci/deploy-nightly-version.sh 97 | 98 | workflows: 99 | build: 100 | jobs: 101 | - tests: 102 | name: 'tests-node-18' 103 | filters: 104 | branches: 105 | ignore: gh-pages 106 | - tests: 107 | name: 'tests-node-21' 108 | image: 'cimg/node:21.4' 109 | filters: 110 | branches: 111 | ignore: gh-pages 112 | - tests: 113 | name: 'tests-node-20' 114 | image: 'cimg/node:20.9.0' 115 | filters: 116 | branches: 117 | ignore: gh-pages 118 | - tests: 119 | name: 'tests-node-22' 120 | image: 'cimg/node:22.12.0' 121 | filters: 122 | branches: 123 | ignore: gh-pages 124 | - coverage: 125 | filters: 126 | branches: 127 | ignore: gh-pages 128 | - generate-flight: 129 | filters: 130 | branches: 131 | ignore: gh-pages 132 | - deploy-preview: 133 | requires: 134 | - tests-node-18 135 | - coverage 136 | filters: 137 | branches: 138 | only: main 139 | -------------------------------------------------------------------------------- /.circleci/deploy-nightly-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | SCRIPT_PATH="$( cd "$(dirname "$0")" || exit ; pwd -P )" 6 | 7 | # Update Version 8 | VERSION=$(head -n 1 'packages/client/src/impl/version.ts' | sed 's/[^0-9.]*//g' | awk -F. '{$2+=1; OFS="."; print $1"."$2"."$3; exit}') 9 | sed -i -e "s/CLIENT_LIB_VERSION = '.*'/CLIENT_LIB_VERSION = '${VERSION}.nightly'/" packages/client/src/impl/version.ts 10 | yarn lerna version "$VERSION"-nightly."$CIRCLE_BUILD_NUM" --no-git-tag-version --yes 11 | git config user.name "CircleCI Builder" 12 | git config user.email "noreply@influxdata.com" 13 | git commit -am "chore(release): prepare to release influxdb3-js-${VERSION}.nightly" 14 | 15 | # Build Core 16 | cd "${SCRIPT_PATH}"/.. 17 | yarn build 18 | 19 | # Publish 20 | yarn lerna publish --canary from-package --no-git-tag-version --force-publish --preid nightly --yes 21 | -------------------------------------------------------------------------------- /.circleci/setup-npmjs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 4 | chmod 0600 ~/.npmrc 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | "ecmaFeatures": {} 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint/eslint-plugin", 9 | "prettier" 10 | ], 11 | "env": { 12 | "node": true, 13 | "es6": true, 14 | "mocha": true 15 | }, 16 | "extends": [ 17 | "eslint:recommended", 18 | "plugin:@typescript-eslint/recommended", 19 | "plugin:prettier/recommended" 20 | ], 21 | "rules": { 22 | "no-console": "off", 23 | "@typescript-eslint/naming-convention": [ 24 | "error", 25 | { 26 | "selector": "variable", 27 | "format": ["camelCase", "UPPER_CASE"], 28 | "filter": { 29 | "regex": "^DEFAULT_|^Log$", 30 | "match": false 31 | }, 32 | "leadingUnderscore": "allow", 33 | "trailingUnderscore": "allow" 34 | }, 35 | { 36 | "selector": "typeLike", 37 | "format": ["PascalCase"] 38 | } 39 | ], 40 | "@typescript-eslint/no-explicit-any": "off", 41 | "@typescript-eslint/no-unused-vars": [ 42 | "error", 43 | {"varsIgnorePattern": "^_", "argsIgnorePattern": "^_"} 44 | ], 45 | "@typescript-eslint/no-empty-function": "off", 46 | "@typescript-eslint/no-empty-interface": "off", 47 | "prefer-template": "error", 48 | "@typescript-eslint/explicit-module-boundary-types": [ 49 | "error", 50 | { 51 | "allowArgumentsExplicitlyTypedAsAny": true 52 | } 53 | ], 54 | "@typescript-eslint/no-non-null-assertion": "error" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Rush's JSON config files use JavaScript-style code comments. The rule below prevents pedantic 2 | # syntax highlighters such as GitHub's from highlighting these comments as errors. Your text editor 3 | # may also require a special configuration to allow comments in JSON. 4 | # 5 | # For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088 6 | # 7 | *.json linguist-language=JSON-with-Comments -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report to help us improve 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking time to fill out this bug report! We reserve this repository issues for bugs with reproducible problems. 9 | Please redirect any questions about the client usage to our [Community Slack](https://app.slack.com/huddle/TH8RGQX5Z/C02UDUPLQKA) or [Community Page](https://community.influxdata.com/) we have a lot of talented community members there who could help answer your question more quickly. 10 | 11 | * Please add a :+1: or comment on a similar existing bug report instead of opening a new one. 12 | * Please check whether the bug can be reproduced with the latest release. 13 | - type: textarea 14 | id: specifications 15 | attributes: 16 | label: Specifications 17 | description: Describe the steps to reproduce the bug. 18 | value: | 19 | * Client Version: 20 | * InfluxDB Version: 21 | * Platform: 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: reproduce 26 | attributes: 27 | label: Code sample to reproduce problem 28 | description: Provide a code sample that reproduces the problem 29 | value: | 30 | ```csharp 31 | ``` 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: expected-behavior 36 | attributes: 37 | label: Expected behavior 38 | description: Describe what you expected to happen when you performed the above steps. 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: actual-behavior 43 | attributes: 44 | label: Actual behavior 45 | description: Describe what actually happened when you performed the above steps. 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: additional-info 50 | attributes: 51 | label: Additional info 52 | description: Include gist of relevant config, logs, etc. 53 | validations: 54 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Create a feature request to make client more awesome 3 | labels: ["feature request"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking time to share with us this feature request! Please describe why you would like this feature to be added to the client, how you plan to use it to make your life better. 9 | - type: textarea 10 | id: use-case 11 | attributes: 12 | label: Use Case 13 | description: Describe how you plan to use this feature. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: expected-behavior 18 | attributes: 19 | label: Expected behavior 20 | description: Describe what you expected to happen when you performed the above steps. 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: actual-behavior 25 | attributes: 26 | label: Actual behavior 27 | description: Describe what actually happened when you performed the above steps. 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: additional-info 32 | attributes: 33 | label: Additional info 34 | description: Include gist of relevant config, logs, etc. 35 | validations: 36 | required: false 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT.yml: -------------------------------------------------------------------------------- 1 | name: Support request 2 | description: Open a support request 3 | labels: ["support"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | WOAHH, hold up. This isn't the best place for support questions. 9 | You can get a faster response on slack or forums: 10 | 11 | Please redirect any QUESTIONS about Telegraf usage to 12 | - InfluxData Slack Channel: https://app.slack.com/huddle/TH8RGQX5Z/C02UDUPLQKA 13 | - InfluxData Community Site: https://community.influxdata.com 14 | 15 | - type: textarea 16 | attributes: 17 | label: "Please direct all support questions to slack or the forums. Thank you." 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Closes # 2 | 3 | ## Proposed Changes 4 | 5 | _Briefly describe your proposed changes:_ 6 | 7 | ## Checklist 8 | 9 | 10 | 11 | - [ ] CHANGELOG.md updated 12 | - [ ] Rebased/mergeable 13 | - [ ] A test has been added if appropriate 14 | - [ ] Tests pass 15 | - [ ] Commit messages are [conventional](https://www.conventionalcommits.org/en/v1.0.0/) 16 | - [ ] Sign [CLA](https://www.influxdata.com/legal/cla/) (if not already signed) 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | CodeQL-Build: 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | security-events: write 15 | actions: read 16 | contents: read 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Initialize CodeQL 23 | uses: github/codeql-action/init@v2 24 | with: 25 | languages: javascript 26 | 27 | - name: Autobuild 28 | uses: github/codeql-action/autobuild@v2 29 | 30 | - name: Perform CodeQL Analysis 31 | uses: github/codeql-action/analyze@v2 32 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: "Lint Code Base" 2 | 3 | on: 4 | push: 5 | branches-ignore: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | permissions: 12 | contents: read 13 | packages: read 14 | statuses: write 15 | 16 | name: Lint Code Base 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Lint Code Base 26 | uses: github/super-linter@v4.9.2 27 | env: 28 | VALIDATE_ALL_CODEBASE: false 29 | DEFAULT_BRANCH: main 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | LINTER_RULES_PATH: '.' 32 | MARKDOWN_CONFIG_FILE: .markdownlint.yml 33 | VALIDATE_MARKDOWN: true 34 | VALIDATE_BASH: true 35 | VALIDATE_TYPESCRIPT_ES: true 36 | TYPESCRIPT_ES_CONFIG_FILE: .eslintrc.json 37 | TYPESCRIPT_STANDARD_TSCONFIG_FILE: tsconfig.json 38 | VALIDATE_GITHUB_ACTIONS: true 39 | FILTER_REGEX_EXCLUDE: scripts/.*|tsup\.config\.ts|tsup.config.browser.ts|packages/client/src/generated/flight/.* 40 | -------------------------------------------------------------------------------- /.github/workflows/semantic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Semantic PR and Commit Messages" 3 | 4 | on: 5 | pull_request: 6 | types: [opened, reopened, synchronize, edited] 7 | branches: 8 | - main 9 | 10 | jobs: 11 | semantic: 12 | uses: influxdata/validate-semantic-github-messages/.github/workflows/semantic.yml@main 13 | with: 14 | CHECK_PR_TITLE_OR_ONE_COMMIT: true 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | dist 4 | coverage 5 | doc 6 | .rpt2_cache 7 | .DS_Store 8 | .nyc_output 9 | *.lcov 10 | reports 11 | yarn-error.log 12 | .idea/ 13 | /temp 14 | /docs 15 | flightgen 16 | packages/client/README.md 17 | examples/*/.env 18 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD024": false, 4 | "MD033": { 5 | "allowed_elements": [ "a", "img", "p", "details", "summary" ] 6 | }, 7 | "MD041": false, 8 | } 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-js/b28765273b04231b2666dbe98631f104d36425ab/.npmignore -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.d.ts -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "trailingComma": "es5", 5 | "bracketSpacing": false 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.2.0 [unreleased] 2 | 3 | ### Features 4 | 5 | 1. [#570](https://github.com/InfluxCommunity/influxdb3-js/pull/570): Fixing the bug that makes query results duplicate rows [#553](https://github.com/InfluxCommunity/influxdb3-js/issues/553). 6 | 7 | ## 1.1.0 [2025-03-26] 8 | 9 | ### Features 10 | 11 | 1. [#545](https://github.com/InfluxCommunity/influxdb3-js/pull/545): Sets the correct versions for the client-browser package. 12 | 13 | ## 1.0.0 [2025-01-22] 14 | 15 | ### Features 16 | 17 | 1. [#491](https://github.com/InfluxCommunity/influxdb3-js/pull/491): Respect iox::column_type::field metadata when 18 | mapping query results into values. 19 | - iox::column_type::field::integer: => number 20 | - iox::column_type::field::uinteger: => number 21 | - iox::column_type::field::float: => number 22 | - iox::column_type::field::string: => string 23 | - iox::column_type::field::boolean: => boolean 24 | 1. [499](https://github.com/InfluxCommunity/influxdb3-js/pull/499): Migrate to new doc library 25 | 26 | ## 0.12.0 [2024-10-22] 27 | 28 | ### Bugfix 29 | 30 | 1. [437](https://github.com/InfluxCommunity/influxdb3-js/pull/437): Simplify iterating over Arrow's batches in `QueryAPI` 31 | 32 | ## 0.11.0 [2024-09-13] 33 | 34 | ### Features 35 | 36 | 1. [410](https://github.com/InfluxCommunity/influxdb3-js/pull/410): Accepts HTTP responses with 2xx status codes as a success for writes. 37 | 38 | ## 0.10.0 [2024-08-12] 39 | 40 | ### Features 41 | 42 | 1. [369](https://github.com/InfluxCommunity/influxdb3-js/pull/369): Propagates headers from HTTP response to HttpError when an error is returned from the server. 43 | 1. [377](https://github.com/InfluxCommunity/influxdb3-js/pull/377): Add InfluxDB Edge (OSS) authentication support. 44 | 45 | ### Bugfix 46 | 47 | 1. [376](https://github.com/InfluxCommunity/influxdb3-js/pull/376): Handle InfluxDB Edge (OSS) errors better. 48 | 49 | ## 0.9.0 [2024-06-24] 50 | 51 | ### Features 52 | 53 | 1. [319](https://github.com/InfluxCommunity/influxdb3-js/pull/319): Adds standard `user-agent` header to calls. 54 | 55 | ## 0.8.0 [2024-04-16] 56 | 57 | ### Breaking Changes 58 | 59 | 1. [293](https://github.com/InfluxCommunity/influxdb3-js/pull/293): The Query API now uses a `QueryOptions` structure in `client.query()` methods. The `queryType` and `queryParams` values are now wrapped inside of it. QueryOptions also support adding custom headers. Query parameters are changed from type `Map` to type `Record`. 60 | 61 | ### Features 62 | 63 | 1. [293](https://github.com/InfluxCommunity/influxdb3-js/pull/293): `QueryOptions` also support adding custom headers. 64 | 65 | ## 0.7.0 [2024-03-01] 66 | 67 | ### Features 68 | 69 | 1. [#256](https://github.com/InfluxCommunity/influxdb3-js/pull/256): Adds support for named query parameters 70 | 71 | ## 0.6.0 [2024-01-30] 72 | 73 | ### Bugfix 74 | 75 | 1. [#221](https://github.com/InfluxCommunity/influxdb3-js/issues/221): Client options processing 76 | 77 | ## 0.5.0 [2023-12-05] 78 | 79 | ### Features 80 | 81 | 1. [#183](https://github.com/InfluxCommunity/influxdb3-js/pull/183): Default Tags for Writes 82 | 83 | ## 0.4.1 [2023-11-16] 84 | 85 | ### Bugfix 86 | 87 | 1. [#164](https://github.com/InfluxCommunity/influxdb3-js/issues/164): Query infinite wait state 88 | 89 | ## 0.4.0 [2023-11-03] 90 | 91 | ### Features 92 | 93 | 1. [#157](https://github.com/InfluxCommunity/influxdb3-js/pull/157): Add client instantiation from connection string and environment variables 94 | 95 | ## 0.3.1 [2023-10-06] 96 | 97 | Fixed package distribution files. The distribution files were not being included in the npm package. 98 | 99 | ## 0.3.0 [2023-10-02] 100 | 101 | ### Features 102 | 103 | 1. [#89](https://github.com/InfluxCommunity/influxdb3-js/pull/89): Add structured query support 104 | 105 | ### Docs 106 | 107 | 1. [#89](https://github.com/InfluxCommunity/influxdb3-js/pull/89): Add downsampling example 108 | 109 | ## 0.2.0 [2023-08-11] 110 | 111 | ### Features 112 | 113 | 1. [#52](https://github.com/InfluxCommunity/influxdb3-js/pull/52): Add support for browser environment 114 | 115 | ### Docs 116 | 117 | 1. [#52](https://github.com/InfluxCommunity/influxdb3-js/pull/52): Improve examples 118 | 119 | ## 0.1.0 [2023-06-29] 120 | 121 | - initial release of new client version 122 | - write using v2 api 123 | - query using FlightSQL 124 | - query using InfluxQl 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 InfluxData Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # **Releasing a new version** 2 | # Ensure that: 3 | # - You have administrator access to this repo on GitHub 4 | # - You have permissions to publish to the [influxdata](https://www.npmjs.com/org/influxdata) organization on npm 5 | # - You are on `main` and the working tree is clean 6 | # Then run the publish target with VERSION specified: 7 | # ``` 8 | # make publish VERSION=1.8.0 9 | # ``` 10 | .DEFAULT_GOAL := help 11 | 12 | help: 13 | @echo "Please use \`make ' where is one of" 14 | @echo "" 15 | @echo " publish to publish packages to specified version by VERSION property. make publish VERSION=1.1.0" 16 | @echo "" 17 | 18 | publish: 19 | $(if $(VERSION),,$(error VERSION is not defined. Pass via "make publish VERSION=1.1.0")) 20 | git checkout main 21 | git pull 22 | yarn install --frozen-lockfile 23 | node scripts/change-version.js 24 | yarn run build 25 | yarn run test:unit 26 | @echo "Publishing $(VERSION)..." 27 | git commit -am "chore(release): prepare to release influxdb3-js-$(VERSION)" 28 | npx lerna publish $(VERSION) 29 | @echo "Publish successful" 30 | @echo "" 31 | @echo "Next steps:" 32 | @echo " - publish updated API documentation by: \"yarn typedoc && yarn typedoc:gh-pages\"" 33 | @echo " - add new version to CHANGELOG.md" 34 | @echo " - push changes to repository by : \"git commit -am 'chore(release): prepare to next development iteration [skip CI]' && git push\"" 35 | @echo "" 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | JavaScript Logo 3 |

4 |

5 | 6 | NuGet Badge 7 | 8 | 9 | CodeQL analysis 10 | 11 | 12 | Lint Code Base 13 | 14 | 15 | CircleCI 16 | 17 | 18 | Code Cov 19 | 20 | 21 | Community Slack 22 | 23 |

24 | 25 | # InfluxDB 3 JavaScript Client 26 | 27 | The JavaScript Client that provides a simple and convenient way to interact with InfluxDB 3. 28 | This package supports both writing data to InfluxDB and querying data using the FlightSQL client, 29 | which allows you to execute SQL queries against InfluxDB IOx. 30 | 31 | We offer this [Getting Started: InfluxDB 3.0 Javascript Client Library](https://www.youtube.com/watch?v=vT5OLqTrJJg) video to learn more about the library and see code examples. 32 | 33 | ## Installation 34 | 35 | To write or query InfluxDB 3, add `@influxdata/influxdb3-client` as a dependency to your project using your favorite package manager. 36 | 37 | ```sh 38 | npm install --save @influxdata/influxdb3-client 39 | yarn add @influxdata/influxdb3-client 40 | pnpm add @influxdata/influxdb3-client 41 | ``` 42 | 43 | If you target Node.js, use [@influxdata/influxdb3-client](./packages/client/README.md). 44 | It provides main (CJS), module (ESM), and browser (UMD) exports. 45 | 46 | ## Usage 47 | 48 | Set environment variables: 49 | 50 | - `INFLUX_HOST` - InfluxDB address, eg. *`https://us-east-1-1.aws.cloud2.influxdata.com/`* 51 | - `INFLUX_TOKEN` - access token 52 | - `INFLUX_DATABASE` - database (bucket) name, eg. *`my-database`* 53 | 54 |
55 | linux/macos 56 | 57 | ```sh 58 | export INFLUX_HOST="" 59 | export INFLUX_DATABASE="" 60 | export INFLUX_TOKEN="" 61 | ``` 62 | 63 |
64 | 65 |
66 | windows 67 | 68 | ### powershell 69 | 70 | ```powershell 71 | $env:INFLUX_HOST = "" 72 | $env:INFLUX_DATABASE = "" 73 | $env:INFLUX_TOKEN = "" 74 | ``` 75 | 76 | ### cmd 77 | 78 | ```console 79 | set INFLUX_HOST= 80 | set INFLUX_DATABASE= 81 | set INFLUX_TOKEN= 82 | ``` 83 | 84 |
85 | 86 | ### Create a client 87 | 88 | To get started with influxdb client import `@influxdata/influxdb3-client` package. 89 | 90 | ```ts 91 | import {InfluxDBClient, Point} from '@influxdata/influxdb3-client' 92 | ``` 93 | 94 | Assign values for environment variables, and then instantiate `InfluxDBClient` inside of an asynchronous function. 95 | Please note that token is a mandatory parameter. 96 | Make sure to `close` the client when it's no longer needed for writing or querying. 97 | 98 | ```ts 99 | const host = process.env.INFLUX_HOST 100 | const token = process.env.INFLUX_TOKEN 101 | const database = process.env.INFLUX_DATABASE 102 | 103 | async function main() { 104 | const client = new InfluxDBClient({host, token, database}) 105 | 106 | // code goes here 107 | 108 | client.close() 109 | } 110 | 111 | main() 112 | ``` 113 | 114 | You can also use a provided no argument constructor for `InfluxDBClient` instantiation using environment variables: 115 | 116 | ```ts 117 | 118 | async function main() { 119 | const client = new InfluxDBClient() 120 | 121 | // code goes here 122 | 123 | client.close() 124 | } 125 | 126 | main() 127 | ``` 128 | 129 | You can also instantiate `InfluxDBClient` with a connection string: 130 | 131 | ```ts 132 | 133 | async function main() { 134 | const client = new InfluxDBClient('https://us-east-1-1.aws.cloud2.influxdata.com/?token=my-token&database=my-database') 135 | 136 | // code goes here 137 | 138 | client.close() 139 | } 140 | 141 | main() 142 | ``` 143 | 144 | ### Write data 145 | 146 | To write data to InfluxDB, call `client.write` with data in [line-protocol](https://docs.influxdata.com/influxdb/cloud-serverless/reference/syntax/line-protocol/) format and the database (or bucket) name. 147 | 148 | ```ts 149 | const line = `stat,unit=temperature avg=20.5,max=45.0` 150 | await client.write(line, database) 151 | ``` 152 | 153 | ### Query data 154 | 155 | To query data stored in InfluxDB, call `client.query` with an SQL query and the database (or bucket) name. To change to using InfluxQL add a QueryOptions object with the type 'influxql' (e.g. `client.query(query, database, { type: 'influxql'})`). 156 | 157 | ```ts 158 | // Execute query 159 | const query = ` 160 | SELECT * 161 | FROM "stat" 162 | WHERE 163 | time >= now() - interval '5 minute' 164 | AND 165 | "unit" IN ('temperature') 166 | ` 167 | const queryResult = await client.query(query, database) 168 | 169 | for await (const row of queryResult) { 170 | console.log(`avg is ${row.avg}`) 171 | console.log(`max is ${row.max}`) 172 | } 173 | ``` 174 | 175 | or use a typesafe `PointValues` structure with `client.queryPoints` 176 | 177 | ```ts 178 | const queryPointsResult = client.queryPoints( 179 | query, 180 | database, 181 | queryOptions 182 | ) 183 | 184 | for await (const row of queryPointsResult) { 185 | console.log(`avg is ${row.getField('avg', 'float')}`) 186 | console.log(`max is ${row.getField('max', 'float')}`) 187 | console.log(`lp: ${row.asPoint('stat').toLineProtocol()}`) 188 | } 189 | ``` 190 | 191 | ## Examples 192 | 193 | For more advanced usage, see [examples](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/examples/README.md). 194 | 195 | ## Feedback 196 | 197 | If you need help, please use our [Community Slack](https://app.slack.com/huddle/TH8RGQX5Z/C02UDUPLQKA) 198 | or [Community Page](https://community.influxdata.com/). 199 | 200 | New features and bugs can be reported on GitHub: 201 | 202 | ## Contribution 203 | 204 | To contribute to this project, fork the GitHub repository and send a pull request based on the `main` branch. 205 | 206 | ## Development 207 | 208 | ### Update the Flight Client 209 | 210 | For now, we're responsible for generating the Flight client. However, its Protobuf interfaces may undergo changes over time. 211 | 212 | To regenerate the Flight client, use the `yarn flight` command to execute the provided script. The script will clone the Flight Protobuf repository and generate new TypeScript files in `./src/generated/flight`. 213 | 214 | ### Generate files for mock server 215 | 216 | To generate files needed for the mock server used in some tests, run the `yarn flight:test` command. 217 | 218 | ## License 219 | 220 | The InfluxDB 3 JavaScript Client is released under the [MIT License](https://opensource.org/licenses/MIT). 221 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslintEslintPlugin from "@typescript-eslint/eslint-plugin"; 2 | import prettier from "eslint-plugin-prettier"; 3 | import globals from "globals"; 4 | import tsParser from "@typescript-eslint/parser"; 5 | import path from "node:path"; 6 | import { fileURLToPath } from "node:url"; 7 | import js from "@eslint/js"; 8 | import { FlatCompat } from "@eslint/eslintrc"; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | const compat = new FlatCompat({ 13 | baseDirectory: __dirname, 14 | recommendedConfig: js.configs.recommended, 15 | allConfig: js.configs.all 16 | }); 17 | 18 | export default [...compat.extends( 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:prettier/recommended", 22 | ), { 23 | plugins: { 24 | "@typescript-eslint": typescriptEslintEslintPlugin, 25 | prettier, 26 | }, 27 | 28 | languageOptions: { 29 | globals: { 30 | ...globals.node, 31 | ...globals.mocha, 32 | }, 33 | 34 | parser: tsParser, 35 | ecmaVersion: 5, 36 | sourceType: "module", 37 | 38 | parserOptions: { 39 | ecmaFeatures: {}, 40 | }, 41 | }, 42 | 43 | rules: { 44 | "no-console": "off", 45 | 46 | "@typescript-eslint/naming-convention": ["error", { 47 | selector: "variable", 48 | format: ["camelCase", "UPPER_CASE"], 49 | 50 | filter: { 51 | regex: "^DEFAULT_|^Log$", 52 | match: false, 53 | }, 54 | 55 | leadingUnderscore: "allow", 56 | trailingUnderscore: "allow", 57 | }, { 58 | selector: "typeLike", 59 | format: ["PascalCase"], 60 | }], 61 | 62 | "@typescript-eslint/no-explicit-any": "off", 63 | 64 | "@typescript-eslint/no-unused-vars": ["error", { 65 | varsIgnorePattern: "^_", 66 | argsIgnorePattern: "^_", 67 | }], 68 | 69 | "@typescript-eslint/no-empty-function": "off", 70 | "@typescript-eslint/no-empty-interface": "off", 71 | "prefer-template": "error", 72 | 73 | "@typescript-eslint/explicit-module-boundary-types": ["error", { 74 | allowArgumentsExplicitlyTypedAsAny: true, 75 | }], 76 | 77 | "@typescript-eslint/no-non-null-assertion": "error", 78 | }, 79 | }]; -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Examples 2 | 3 | - [Instantiate the client](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/examples/basic/src/client.ts) - This example shows various ways to instantiate the client 4 | - [IOxExample](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/examples/basic/README.md) - How to use write and query data from InfluxDB IOx 5 | - [Downsampling](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/downsampling/basic/README.md) - How to use queries to structure data for downsampling 6 | 7 | ### Browser 8 | 9 | - [Browser](https://github.com/InfluxCommunity/influxdb3-js/blob/HEAD/examples/browser/README.md) - Try influxdb in browser with example queries 10 | -------------------------------------------------------------------------------- /examples/basic/README.md: -------------------------------------------------------------------------------- 1 | ## Basic Example 2 | 3 | - [IOxExample](./src/index.ts) - How to use write and query data from InfluxDB IOx 4 | - [IOxRetryExample](./src/writeRetry.ts) - How to use write and then retry if the server returns status `429 - Too many requests` or `503 - Temporarily unavailable`. 5 | 6 | ## prerequisites 7 | 8 | - `node` and `yarn` installed 9 | 10 | - build influxdb-client: *(in project root directory)* 11 | - run `yarn install` 12 | - run `yarn build` 13 | 14 | ## Usage 15 | 16 | set environment variables. 17 | 18 | - `INFLUX_HOST` region of your influxdb cloud e.g. *`https://us-east-1-1.aws.cloud2.influxdata.com/`* 19 | - `INFLUX_TOKEN` read/write token generated in cloud 20 | - `INFLUX_DATABASE` name of database e.g .*`my-database`* 21 | 22 | For simplicity, you can use dotenv library to load environment variables in this example. Create `.env` file and paste your variables as follows: 23 | 24 | ```conf 25 | INFLUX_HOST="" 26 | INFLUX_DATABASE="" 27 | INFLUX_TOKEN="" 28 | ``` 29 | 30 | ### Run examples 31 | 32 | #### Basic 33 | 34 | - run `yarn install` 35 | - run `yarn dev` 36 | 37 | #### Write Retry 38 | 39 | - run `yarn install` 40 | - run `yarn retry` 41 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "influxdb3-client-example-basic", 3 | "main": "index.js", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "dev": "ts-node -r dotenv/config ./src/index.ts", 8 | "retry": "ts-node -r dotenv/config src/writeRetry.ts" 9 | }, 10 | "dependencies": { 11 | "@influxdata/influxdb3-client": "link:../../packages/client" 12 | }, 13 | "devDependencies": { 14 | "dotenv": "^16.3.1", 15 | "ts-node": "^10.9.1", 16 | "typescript": "^5.1.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/basic/src/client.ts: -------------------------------------------------------------------------------- 1 | import {InfluxDBClient} from '@influxdata/influxdb3-client' 2 | 3 | /** 4 | * This example shows various ways to instantiate the client. 5 | * Make sure the following environment variables are set: INFLUX_HOST, INFLUX_TOKEN 6 | */ 7 | 8 | /* eslint-disable no-console */ 9 | async function main() { 10 | let client: InfluxDBClient 11 | 12 | // Create a new client using ClientOptions 13 | client = new InfluxDBClient({ 14 | host: 'http://localhost:8086', 15 | token: 'my-token', 16 | }) 17 | await client.close() 18 | 19 | // Create a new client using connection string 20 | client = new InfluxDBClient('http://localhost:8086?token=my-token') 21 | await client.close() 22 | 23 | // Create a new client using environment variables 24 | client = new InfluxDBClient() 25 | await client.close() 26 | } 27 | 28 | main() 29 | -------------------------------------------------------------------------------- /examples/basic/src/index.ts: -------------------------------------------------------------------------------- 1 | import {InfluxDBClient, Point} from '@influxdata/influxdb3-client' 2 | 3 | type Defined = Exclude 4 | 5 | /* allows to throw error as expression */ 6 | const throwReturn = (err: Error): Defined => { 7 | throw err 8 | } 9 | 10 | /* get environment value or throw error if missing */ 11 | const getEnv = (variableName: string): string => 12 | process.env[variableName] ?? 13 | throwReturn(new Error(`missing ${variableName} environment variable`)) 14 | 15 | /* eslint-disable no-console */ 16 | async function main() { 17 | // Use environment variables to initialize client 18 | const host = getEnv('INFLUX_HOST') 19 | const token = getEnv('INFLUX_TOKEN') 20 | const database = getEnv('INFLUX_DATABASE') 21 | const measurement = 'demoBasic' 22 | 23 | // Create a new client using an InfluxDB server base URL and an authentication token 24 | const client = new InfluxDBClient({host, token}) 25 | 26 | try { 27 | // Write point 28 | const p = Point.measurement(measurement) 29 | .setTag('unit', 'temperature') 30 | .setFloatField('avg', 24.5) 31 | .setFloatField('max', 45.0) 32 | .setTimestamp(new Date()) 33 | await client.write(p, database) 34 | 35 | // Write point as template with anonymous fields object 36 | const pointTemplate = Object.freeze( 37 | Point.measurement(measurement).setTag('unit', 'temperature') 38 | ) 39 | 40 | const sensorData = { 41 | avg: 28, 42 | max: 40.3, 43 | } 44 | const p2 = pointTemplate 45 | .copy() 46 | .setFields(sensorData) 47 | .setTimestamp(new Date()) 48 | 49 | await client.write(p2, database) 50 | 51 | // Or write directly line protocol 52 | const lp = `${measurement},unit=temperature avg=20.5,max=43.0` 53 | await client.write(lp, database) 54 | 55 | // Prepare flightsql query 56 | const query = ` 57 | SELECT * 58 | FROM "${measurement}" 59 | WHERE 60 | time >= now() - interval '5 minute' 61 | AND 62 | "unit" IN ('temperature') 63 | ` 64 | 65 | // Execute query 66 | // This query type can either be 'sql' or 'influxql' 67 | const queryResult = client.query(query, database, {type: 'sql'}) 68 | 69 | for await (const row of queryResult) { 70 | console.log(`avg is ${row.avg}`) 71 | console.log(`max is ${row.max}`) 72 | } 73 | 74 | // Execute query again as points 75 | // can also rely on default queryOptions { type: 'sql } 76 | const queryPointsResult = client.queryPoints(query, database) 77 | 78 | for await (const row of queryPointsResult) { 79 | console.log(`avg is ${row.getField('avg', 'float')}`) 80 | console.log(`max is ${row.getField('max', 'float')}`) 81 | } 82 | } catch (err) { 83 | console.error(err) 84 | } finally { 85 | await client.close() 86 | } 87 | } 88 | 89 | main() 90 | -------------------------------------------------------------------------------- /examples/basic/src/writeRetry.ts: -------------------------------------------------------------------------------- 1 | import {InfluxDBClient, Point, HttpError} from '@influxdata/influxdb3-client' 2 | 3 | /** 4 | * This example seeks to exceed the write limit of a standard unpaid 5 | * influxdb account. Run it from the command line a couple of times in 6 | * succession (e.g. `yarn retry`). The first run should succeed, but 7 | * the second run should exceed the max write limit and the server should 8 | * return 429 with a `retry-after` header. 9 | */ 10 | async function main() { 11 | const host = getEnv('INFLUX_HOST') 12 | const token = getEnv('INFLUX_TOKEN') 13 | const database = getEnv('INFLUX_DATABASE') 14 | 15 | const client = new InfluxDBClient({host, token}) 16 | 17 | const points: Point[] = [] 18 | const now = new Date() 19 | for (let i = 100000; i > -1; i--) { 20 | const d: Date = new Date(now.getTime() - 100 * i) 21 | points[i] = UserMemUsage.RandomUserMemUsage().toPoint(d) 22 | } 23 | await writeData(client, points, database, 1) 24 | } 25 | 26 | /** 27 | * Returns the retry interval in milliseconds 28 | * 29 | * @param retry string or string[] from `retry-after` header 30 | */ 31 | function getRetryInterval(retry: string | string[]): number { 32 | const token = retry.toString() 33 | let result: number = parseInt(token) 34 | if (isNaN(result)) { 35 | const now: Date = new Date() 36 | const exp: Date = new Date(token) 37 | if (isNaN(exp.valueOf())) { 38 | throw new Error(`Failed to parse retry value: ${retry}`) 39 | } 40 | result = exp.getTime() - now.getTime() 41 | } else { 42 | result *= 1000 43 | } 44 | return result 45 | } 46 | 47 | let initialRetry: number 48 | 49 | /** 50 | * Helper function to leverage `retry-after` in the event a HttpError 51 | * is returned. 52 | * 53 | * @param client an InfluxDBClient instance 54 | * @param data data to be written 55 | * @param database target database 56 | * @param retryCount number of times to retry writing data 57 | */ 58 | async function writeData( 59 | client: InfluxDBClient, 60 | data: Point | Point[], 61 | database: string, 62 | retryCount = 0 63 | ) { 64 | if (initialRetry == null) { 65 | initialRetry = retryCount 66 | } 67 | try { 68 | await client.write(data, database) 69 | console.log(`Write success on try ${1 + initialRetry - retryCount}`) 70 | } catch (error: any) { 71 | if ( 72 | error instanceof HttpError && 73 | error.headers && 74 | error?.headers['retry-after'] 75 | ) { 76 | console.log( 77 | `WARNING[${new Date()}]: Caught error ${JSON.stringify(error)}` 78 | ) 79 | if (retryCount > 0) { 80 | // add an extra second to be sure 81 | const interval = getRetryInterval(error?.headers['retry-after']) + 1000 82 | console.log(`Retrying in ${interval} ms`) 83 | setTimeout(() => { 84 | writeData(client, data, database, retryCount - 1) 85 | }, interval) 86 | } else { 87 | console.log( 88 | `ERROR[${new Date()}]: Failed to write data after ${initialRetry} attempts.` 89 | ) 90 | } 91 | } 92 | } 93 | } 94 | 95 | type Defined = Exclude 96 | 97 | /* allows to throw error as expression */ 98 | const throwReturn = (err: Error): Defined => { 99 | throw err 100 | } 101 | 102 | /* get environment value or throw error if missing */ 103 | const getEnv = (variableName: string): string => 104 | process.env[variableName] ?? 105 | throwReturn(new Error(`missing ${variableName} environment variable`)) 106 | 107 | class UserMemUsage { 108 | name: string 109 | location: string 110 | pct: number 111 | procCt: number 112 | state: string 113 | 114 | constructor( 115 | name: string, 116 | location: string, // as tag 117 | pct: number, 118 | procCt: number, // as integer 119 | state: string 120 | ) { 121 | this.name = name 122 | this.location = location 123 | this.pct = pct 124 | this.procCt = procCt 125 | this.state = state 126 | } 127 | 128 | static dieRoll(): number { 129 | return Math.floor(Math.random() * 6) + 1 130 | } 131 | 132 | static RandomUserMemUsage( 133 | name = 'userMem', 134 | location = 'hic', 135 | states = ['OK', 'WARNING', 'EXCEEDED', 'CRITICAL', 'ERROR'] 136 | ): UserMemUsage { 137 | return new UserMemUsage( 138 | name, 139 | location, 140 | Math.random() * 100, 141 | this.dieRoll() + this.dieRoll() + this.dieRoll(), 142 | states[Math.floor(Math.random() * states.length)] 143 | ) 144 | } 145 | 146 | toPoint(date: Date): Point { 147 | return Point.measurement(this.name) 148 | .setTag('location', this.location) 149 | .setFloatField('percent', this.pct) 150 | .setIntegerField('processCount', this.procCt) 151 | .setStringField('state', this.state) 152 | .setTimestamp(date) 153 | } 154 | } 155 | 156 | main().then(() => { 157 | console.log('DONE') 158 | }) 159 | -------------------------------------------------------------------------------- /examples/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["es2018"], 5 | "strict": true, 6 | "moduleResolution": "node", 7 | "esModuleInterop": true 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["*.js"] 11 | } 12 | -------------------------------------------------------------------------------- /examples/browser/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/browser/README.md: -------------------------------------------------------------------------------- 1 | ## Example for browser 2 | 3 | - [IOx inside browser example](./src/main.ts) - How to use InfluxDB IOx queries in the browser. 4 | 5 | It's highly recommended to try [basic example](../basic/README.md) first. 6 | 7 | ⚠️ The browser is a specific environment that requires an additional proxy component, which transfers our communication into a gRPC-compliant form. For more details about proxy requirements, please refer to the gRPC-Web documentation - [gRPC-Web Protocol](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md). 8 | 9 | ## prerequisites 10 | 11 | - To run this example, you'll need to have `Docker` installed alongside **Node** and **Yarn**. Check your installation with `docker --version` command. 12 | 13 | - build influxdb-client: *(in project root directory)* 14 | - run `yarn install` 15 | - run `yarn build` 16 | 17 | ## Usage 18 | 19 | To inject connection variables, we will use Vite's dotenv environment variables, which can pass our values into the browser. 20 | 21 | To get started, create a `.env.local` file and update the database and token values as follows: 22 | 23 | ```conf 24 | VITE_INFLUXDB_DATABASE= 25 | VITE_INFLUXDB_TOKEN= 26 | ``` 27 | 28 | Open the `envoy.yaml` file and find the `address: "us-east-1-1.aws.cloud2.influxdata.com"` entry. Make sure to update it with your relevant cloud URL, if it differs. 29 | 30 | ### Run example 31 | 32 | - Start docker with envoy proxy 33 | - run `docker compose up` 34 | - Execute example: 35 | - run `yarn install` (in this directory) 36 | - run `yarn dev` 37 | - Example is running at `http://localhost:5173/` *(note that port can differes, look into console for exact address)* 38 | -------------------------------------------------------------------------------- /examples/browser/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | envoy: 5 | image: envoyproxy/envoy:v1.26-latest 6 | volumes: 7 | - ./envoy.yaml:/etc/envoy/envoy.yaml # Mount the envoy.yaml file into the container 8 | ports: 9 | - "10000:10000" 10 | - "9901:9901" # Envoy admin interface port 11 | 12 | -------------------------------------------------------------------------------- /examples/browser/envoy.yaml: -------------------------------------------------------------------------------- 1 | static_resources: 2 | listeners: 3 | - name: listener_0 4 | address: 5 | socket_address: { address: 0.0.0.0, port_value: 10000 } 6 | filter_chains: 7 | - filter_chain_match: 8 | filters: 9 | - name: envoy.filters.network.http_connection_manager 10 | typed_config: 11 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager 12 | stat_prefix: ingress_http 13 | http_filters: 14 | - name: envoy.filters.http.grpc_web 15 | typed_config: 16 | "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb 17 | - name: envoy.filters.http.cors 18 | typed_config: 19 | "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors 20 | - name: envoy.filters.http.router 21 | typed_config: 22 | "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router 23 | 24 | route_config: 25 | name: local_route 26 | virtual_hosts: 27 | - name: local_service 28 | domains: ["*"] 29 | routes: 30 | - match: { prefix: "/" } 31 | route: 32 | cluster: influxdb_cluster 33 | prefix_rewrite: "/" 34 | auto_host_rewrite: true 35 | timeout: 10s 36 | cors: 37 | allow_origin_string_match: 38 | - prefix: "*" 39 | allow_methods: GET, PUT, DELETE, POST, OPTIONS 40 | 41 | clusters: 42 | - name: influxdb_cluster 43 | connect_timeout: 10s 44 | type: STRICT_DNS 45 | load_assignment: 46 | cluster_name: influxdb_cluster 47 | endpoints: 48 | - lb_endpoints: 49 | - endpoint: 50 | address: 51 | socket_address: 52 | address: "us-east-1-1.aws.cloud2.influxdata.com" 53 | port_value: 443 54 | typed_extension_protocol_options: 55 | envoy.extensions.upstreams.http.v3.HttpProtocolOptions: 56 | "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions 57 | explicit_http_config: 58 | http2_protocol_options: {} 59 | transport_socket: 60 | name: envoy.transport_sockets.tls 61 | typed_config: 62 | "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext 63 | common_tls_context: 64 | alpn_protocols: [ "h2,http/1.1" ] 65 | 66 | -------------------------------------------------------------------------------- /examples/browser/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-js/b28765273b04231b2666dbe98631f104d36425ab/examples/browser/favicon.ico -------------------------------------------------------------------------------- /examples/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | InfluxDB3 Browser example 8 | 9 | 10 | 11 | 12 |
13 |
14 |

15 | 16 | InfluxDB3 Browser Example 17 |

18 | 19 |
20 | Write 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 |
49 | 50 |
51 | 52 |
53 | Query 54 |
55 |
56 | 59 | 60 |
61 | 62 | 63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 |
71 |
72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /examples/browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "influxdb3-client-example-browser", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@influxdata/influxdb3-client-browser": "link:../../packages/client-browser" 13 | }, 14 | "devDependencies": { 15 | "typescript": "^5.0.2", 16 | "vite": "^4.5.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/browser/src/exampleQueries.ts: -------------------------------------------------------------------------------- 1 | type ExampleQuery = { 2 | name: string 3 | query: string 4 | desc: string 5 | } 6 | 7 | export const measurement = 'demoBrowser' 8 | 9 | export const EXAMPLE_QUERIES: ExampleQuery[] = [ 10 | { 11 | name: 'Raw', 12 | query: `\ 13 | SELECT 14 | "Temperature", "Humidity", "time" 15 | FROM "${measurement}" 16 | WHERE 17 | time >= now() - interval '1 hour'`, 18 | desc: `\ 19 | basic query returning data as is 20 | from past 1 hour 21 | showing fields: 22 | "Temperature", "Humidity", "time" 23 | `, 24 | }, 25 | { 26 | name: `Aggregate`, 27 | desc: `\ 28 | Analyze CO2 levels 29 | from past 1 hour 30 | calculate: 31 | average, min, max for "CO2"`, 32 | query: `\ 33 | SELECT 34 | MIN("CO2") as minCO2, AVG("CO2") as avgCO2, MAX("CO2") as maxCO2 35 | FROM "${measurement}" 36 | WHERE 37 | time >= now() - interval '1 hour';`, 38 | }, 39 | { 40 | name: `Group`, 41 | desc: `\ 42 | Analyze average Temperature 43 | per device 44 | from past 1 hour 45 | and sort by it`, 46 | query: `\ 47 | SELECT 48 | "Device", AVG("Temperature") as avgTemperature 49 | FROM "${measurement}" 50 | WHERE 51 | time >= now() - interval '1 hour' 52 | GROUP BY "Device" 53 | ORDER BY avgTemperature;`, 54 | }, 55 | { 56 | name: `Window`, 57 | desc: `\ 58 | Split data into 59 | 5 minute windows 60 | for each window calculate 61 | average Humidity`, 62 | query: `\ 63 | SELECT 64 | date_bin('5 minutes', "time") as window_start, 65 | AVG("Humidity") as avgHumidity 66 | FROM "${measurement}" 67 | WHERE 68 | "time" >= now() - interval '1 hour' 69 | GROUP BY window_start 70 | ORDER BY window_start DESC;`, 71 | }, 72 | { 73 | name: `Correlation`, 74 | desc: `\ 75 | Analyze dependency between 76 | "Humidity" and "Temperature" 77 | for each device 78 | from past 1 hour`, 79 | query: `\ 80 | SELECT 81 | "Device", 82 | CORR("Humidity", "Temperature") AS correlation 83 | FROM "${measurement}" 84 | WHERE 85 | time >= now() - interval '1 hour' 86 | GROUP BY "Device";`, 87 | }, 88 | ] 89 | -------------------------------------------------------------------------------- /examples/browser/src/influx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-js/b28765273b04231b2666dbe98631f104d36425ab/examples/browser/src/influx-logo.png -------------------------------------------------------------------------------- /examples/browser/src/main.ts: -------------------------------------------------------------------------------- 1 | import {InfluxDBClient, Point} from '@influxdata/influxdb3-client-browser' 2 | 3 | import * as view from './view' 4 | import {EXAMPLE_QUERIES, measurement} from './exampleQueries' 5 | 6 | /*********** initial values ***********/ 7 | 8 | view.generateWriteInput() 9 | 10 | view.setSelectQueryOptions(EXAMPLE_QUERIES.map((x) => x.name)) 11 | view.onSelectQueryOption((exampleQueryName) => { 12 | const found = EXAMPLE_QUERIES.find((x) => x.name === exampleQueryName) 13 | if (!found) 14 | throw new Error( 15 | `Query with name "${exampleQueryName}" not found. Available queries were: ${EXAMPLE_QUERIES.map( 16 | (query) => `"${query.name}"` 17 | ).join(', ')}` 18 | ) 19 | const {desc, query} = found 20 | view.setQueryDesc(desc) 21 | view.setQuery(query) 22 | }) 23 | view.selectQueryOption(EXAMPLE_QUERIES[0].name) 24 | 25 | /*********** helper functions ***********/ 26 | 27 | const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) 28 | 29 | const getTableHeaders = (values: Record): string[] => 30 | Object.entries(values) 31 | .filter((x) => x[1]) 32 | .map((x) => x[0]) 33 | 34 | const getRowValues = (headers: string[], values: Record): any[] => 35 | headers.map((x) => 36 | JSON.stringify(values[x], (_key, value) => 37 | typeof value === 'bigint' ? value.toString() : value 38 | ) 39 | ) 40 | 41 | /*********** Influxdb client setup ***********/ 42 | 43 | const database = import.meta.env.VITE_INFLUXDB_DATABASE 44 | const token = import.meta.env.VITE_INFLUXDB_TOKEN 45 | const host = '/influx' // vite proxy 46 | 47 | // This query type can either be 'sql' or 'influxql' 48 | 49 | const client = new InfluxDBClient({host, token}) 50 | 51 | /*********** Influxdb write ***********/ 52 | 53 | view.setOnRandomize(() => { 54 | view.generateWriteInput() 55 | }) 56 | 57 | view.setOnWrite(async () => { 58 | const data = view.getWriteInput() 59 | const p = Point.measurement(measurement) 60 | .setTag('Device', data['Device']) 61 | .setFloatField('Temperature', data['Temperature']) 62 | .setFloatField('Humidity', data['Humidity']) 63 | .setFloatField('Pressure', data['Pressure']) 64 | .setIntegerField('CO2', data['CO2']) 65 | .setIntegerField('TVOC', data['TVOC']) 66 | .setTimestamp(new Date()) 67 | 68 | try { 69 | view.setWriteInfo('writing') 70 | await client.write(p, database) 71 | view.setWriteInfo('success') 72 | } catch (e: any) { 73 | view.setWriteInfo( 74 | e?.message ?? e?.toString?.() ?? 'error! more info in console' 75 | ) 76 | throw e 77 | } 78 | }) 79 | 80 | /*********** Influxdb query ***********/ 81 | 82 | view.setOnQuery(async () => { 83 | const query = view.getQuery() 84 | // Query type can either be 'sql' or 'influxql' 85 | const queryResult = client.query(query, database, {type: 'sql'}) 86 | 87 | try { 88 | const firstRow = (await queryResult.next()).value 89 | if (firstRow) { 90 | const headers = getTableHeaders(firstRow) 91 | const getValues = getRowValues.bind(undefined, headers) 92 | 93 | view.createTable(headers) 94 | view.pushTableRow(getValues(firstRow)) 95 | 96 | for await (const row of queryResult) { 97 | await sleep(50) // simulate throttling 98 | view.pushTableRow(getValues(row)) 99 | } 100 | } 101 | } catch (e: any) { 102 | view.createTable(['error']) 103 | view.pushTableRow([ 104 | e?.message ?? e?.toString?.() ?? 'error! more info in console', 105 | ]) 106 | throw e 107 | } 108 | }) 109 | -------------------------------------------------------------------------------- /examples/browser/src/view.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | 3 | /*********** write ***********/ 4 | 5 | const writeButtonElement: HTMLButtonElement = 6 | document.querySelector('#writeButton')! 7 | 8 | export const setOnWrite = (callback: () => void): void => { 9 | writeButtonElement.addEventListener('click', callback) 10 | } 11 | 12 | const randomizeButtonElement: HTMLButtonElement = 13 | document.querySelector('#randomizeButton')! 14 | 15 | export const setOnRandomize = (callback: () => void): void => { 16 | randomizeButtonElement.addEventListener('click', callback) 17 | } 18 | 19 | const writeInfoElement: HTMLSpanElement = document.querySelector('#writeInfo')! 20 | 21 | export const setWriteInfo = (message: string): void => { 22 | writeInfoElement.textContent = message 23 | } 24 | 25 | /*********** WriteData ***********/ 26 | 27 | export type WriteData = { 28 | Device: string 29 | Temperature: number 30 | Humidity: number 31 | Pressure: number 32 | CO2: number 33 | TVOC: number 34 | } 35 | 36 | const writeInputElements = Array.from( 37 | document.querySelectorAll('#writeInputsRow input') 38 | ) as HTMLInputElement[] 39 | 40 | export const getWriteInput = (): WriteData => { 41 | const res = {} as WriteData 42 | for (const input of writeInputElements) { 43 | const name = input.id as keyof WriteData 44 | const value = input.value 45 | 46 | if (name === 'Device') { 47 | res[name] = value 48 | } else { 49 | res[name] = parseFloat(value) 50 | } 51 | } 52 | 53 | return res 54 | } 55 | 56 | const DAY_MILLIS = 24 * 60 * 60 * 1000 57 | 58 | /** 59 | * Generates measurement values for a specific time. 60 | * @see {@link https://github.com/bonitoo-io/iot-center-v2} 61 | */ 62 | const generateValue = ( 63 | period: number, 64 | min: number, 65 | max: number, 66 | time: number 67 | ): number => { 68 | const dif = max - min 69 | const periodValue = 70 | (dif / 4) * 71 | Math.sin((((time / DAY_MILLIS) % period) / period) * 2 * Math.PI) 72 | const dayValue = 73 | (dif / 4) * 74 | Math.sin(((time % DAY_MILLIS) / DAY_MILLIS) * 2 * Math.PI - Math.PI / 2) 75 | return ( 76 | Math.trunc((min + dif / 2 + periodValue + dayValue + Math.random()) * 10) / 77 | 10 78 | ) 79 | } 80 | 81 | const getRandomChar = (value: number, letters: number) => 82 | String.fromCharCode(65 + Math.floor(value % letters)) 83 | 84 | const generateData = (time: number): WriteData => ({ 85 | Device: `browser-${getRandomChar(time, 7)}`, 86 | Temperature: generateValue(30, 0, 40, time), 87 | Humidity: generateValue(90, 0, 99, time), 88 | Pressure: generateValue(20, 970, 1050, time), 89 | CO2: Math.trunc(generateValue(1, 400, 3000, time)), 90 | TVOC: Math.trunc(generateValue(1, 250, 2000, time)), 91 | }) 92 | export const generateWriteInput = (time?: number): void => { 93 | const data = generateData((time ?? Date.now()) * 10_000) 94 | 95 | for (const input of writeInputElements) { 96 | const name = input.id as keyof WriteData 97 | input.value = data[name].toString() 98 | } 99 | } 100 | 101 | /*********** query ***********/ 102 | 103 | const queryElement: HTMLTextAreaElement = document.querySelector('#query')! 104 | const queryButtonElement: HTMLButtonElement = 105 | document.querySelector('#queryButton')! 106 | const querySelectElement: HTMLSelectElement = 107 | document.querySelector('#querySelect')! 108 | const queryDescElement: HTMLTextAreaElement = 109 | document.querySelector('#queryDesc')! 110 | 111 | export const selectQueryOption = (option: string): void => { 112 | querySelectElement.value = option 113 | const e = new Event('change') 114 | querySelectElement.dispatchEvent(e) 115 | } 116 | 117 | export const setSelectQueryOptions = (options: string[]): void => { 118 | for (const option of options) { 119 | const optionElement = document.createElement('option') 120 | optionElement.value = option 121 | optionElement.innerText = option 122 | querySelectElement.appendChild(optionElement) 123 | } 124 | } 125 | 126 | export const onSelectQueryOption = ( 127 | callback: (value: string) => void 128 | ): void => { 129 | querySelectElement.addEventListener('change', (e) => { 130 | const selectedOption = (e.target as HTMLSelectElement).value 131 | callback(selectedOption) 132 | }) 133 | } 134 | 135 | export const setQueryDesc = (desc: string): void => { 136 | queryDescElement.value = desc 137 | } 138 | 139 | export const setQuery = (query: string): void => { 140 | queryElement.value = query 141 | } 142 | 143 | export const getQuery = (): string => queryElement.value 144 | 145 | export const setOnQuery = (callback: () => void): void => { 146 | queryButtonElement.addEventListener('click', callback) 147 | } 148 | 149 | /*********** query result table ***********/ 150 | 151 | const queryResultElement = document.querySelector('#queryResult')! 152 | 153 | let tableBody: HTMLTableSectionElement | undefined 154 | 155 | export const cleanTable = (): void => { 156 | queryResultElement.innerHTML = '' 157 | tableBody = undefined 158 | } 159 | 160 | export const createTable = (headers: string[]): void => { 161 | cleanTable() 162 | 163 | const tableHead = document.createElement('thead') 164 | const tableHeaderRow = document.createElement('tr') 165 | tableBody = document.createElement('tbody') 166 | 167 | for (const header of headers) { 168 | const headerElement = document.createElement('th') 169 | headerElement.innerText = header 170 | tableHeaderRow.appendChild(headerElement) 171 | } 172 | tableHead.appendChild(tableHeaderRow) 173 | 174 | const table = document.createElement('table') 175 | table.appendChild(tableHead) 176 | table.appendChild(tableBody) 177 | 178 | queryResultElement.appendChild(table) 179 | } 180 | 181 | export const pushTableRow = (row: string[]): void => { 182 | if (!tableBody) throw Error('create table first!') 183 | const tableRow = document.createElement('tr') 184 | for (const value of row) { 185 | const cell = document.createElement('td') 186 | cell.innerHTML = value 187 | tableRow.appendChild(cell) 188 | } 189 | 190 | tableBody.appendChild(tableRow) 191 | } 192 | -------------------------------------------------------------------------------- /examples/browser/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_INFLUXDB_DATABASE: string 5 | readonly VITE_INFLUXDB_TOKEN: string 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /examples/browser/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: Proxima Nova, Helvetica, Arial, Tahoma, Verdana, sans-serif; 4 | } 5 | 6 | input::-webkit-outer-spin-button, 7 | input::-webkit-inner-spin-button { 8 | -webkit-appearance: none; 9 | margin: 0; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | background-color: rgb(7, 7, 14); 15 | color: rgb(255, 255, 255); 16 | } 17 | 18 | input, 19 | textarea { 20 | background-color: rgb(26, 26, 42); 21 | color: rgb(255, 255, 255); 22 | } 23 | 24 | input:focus, 25 | textarea:focus { 26 | border-color: rgba(0, 163, 255, .75); 27 | box-shadow: 0 0 4px #00a3ff; 28 | outline: none; 29 | } 30 | 31 | h1, 32 | fieldset, 33 | input, 34 | textarea { 35 | border-color: rgb(26, 26, 42); 36 | border-radius: 8px; 37 | } 38 | 39 | fieldset { 40 | margin-top: 24px; 41 | } 42 | 43 | fieldset>*:nth-child(n+3) { 44 | margin-top: 16px; 45 | } 46 | 47 | textarea { 48 | padding: 8px; 49 | width: 100%; 50 | resize: vertical; 51 | display: block; 52 | } 53 | 54 | button { 55 | padding: 8px; 56 | background: #333346; 57 | color: #fff; 58 | border-width: 0; 59 | outline: none; 60 | white-space: nowrap; 61 | } 62 | 63 | button:hover, 64 | select:hover { 65 | background: #3c3c53; 66 | } 67 | 68 | input, 69 | select { 70 | width: 100px; 71 | background-color: #07070e; 72 | border: 2px solid #333346; 73 | border-radius: 2px; 74 | color: #fff; 75 | outline: none; 76 | transition: background-color .2s cubic-bezier(.18, .16, .2, 1), border-color .2s cubic-bezier(.18, .16, .2, 1), color .2s cubic-bezier(.18, .16, .2, 1), box-shadow .2s cubic-bezier(.18, .16, .2, 1), color .2s cubic-bezier(.18, .16, .2, 1); 77 | } 78 | 79 | select { 80 | background-color: #333346; 81 | } 82 | 83 | h1 { 84 | margin-top: 16px; 85 | margin-bottom: 0; 86 | padding: 16px; 87 | background-color: #07070e; 88 | background-image: linear-gradient(13.54deg, rgba(0, 163, 255, .1) 36.27%, rgba(0, 163, 255, .25) 78.76%, rgba(0, 163, 255, .15)); 89 | } 90 | 91 | table { 92 | width: 100%; 93 | border: 2px solid #333346; 94 | border-spacing: 0; 95 | border-collapse: collapse; 96 | } 97 | 98 | table tbody tr:nth-child(odd) { 99 | background-color: rgba(26, 26, 42, .7); 100 | } 101 | 102 | th { 103 | border-bottom: 2px solid #333346; 104 | padding: 8px; 105 | text-align: left; 106 | } 107 | 108 | td { 109 | padding: 8px; 110 | } 111 | 112 | :root { 113 | --scrollbar-width: 10px; 114 | } 115 | 116 | ::-webkit-scrollbar { 117 | width: var(--scrollbar-width); 118 | } 119 | 120 | ::-webkit-scrollbar-track { 121 | background: #00000044; 122 | border-radius: 8px; 123 | } 124 | 125 | ::-webkit-scrollbar-thumb { 126 | background-color: #ffffff40; 127 | border-radius: 8px; 128 | } 129 | 130 | ::-webkit-scrollbar-thumb:hover { 131 | background-color: #ffffff55; 132 | } 133 | 134 | .container-flex-end { 135 | display: flex; 136 | justify-content: flex-end; 137 | gap: 8px; 138 | } 139 | 140 | .dimm { 141 | opacity: .3; 142 | } 143 | 144 | .content-container { 145 | display: flex; 146 | justify-content: center; 147 | } 148 | 149 | .content-container>* { 150 | --scrollbar-margin: 8px; 151 | max-width: calc(800px + 2 * (var(--scrollbar-margin) + var(--scrollbar-width))); 152 | width: 100%; 153 | max-height: 100vh; 154 | overflow-y: scroll; 155 | padding: 0 var(--scrollbar-margin) 0 calc(var(--scrollbar-margin) + var(--scrollbar-width)); 156 | } 157 | 158 | .content-container>*::-webkit-scrollbar-track { 159 | background-color: unset; 160 | } 161 | 162 | .influx-logo { 163 | width: .8em; 164 | height: .8em; 165 | display: inline-block; 166 | } 167 | 168 | /* write */ 169 | 170 | .write-set tbody td input { 171 | width: 100% 172 | } 173 | 174 | #writeInfo { 175 | overflow: auto; 176 | } 177 | 178 | .write-set textarea { 179 | text-align: end; 180 | white-space: nowrap; 181 | resize: none; 182 | 183 | } 184 | 185 | /* query */ 186 | 187 | .query-entry-container { 188 | display: flex; 189 | flex-flow: row; 190 | gap: 8px; 191 | } 192 | 193 | .query-entry-container>*:nth-child(1) { 194 | flex: 1; 195 | } 196 | 197 | .query-entry-container>*:nth-child(2) { 198 | flex: 2; 199 | } 200 | 201 | .query-example-container { 202 | display: flex; 203 | gap: 8px; 204 | flex-flow: column; 205 | padding: 12px 8px 8px 8px; 206 | background-color: #1a1a2a; 207 | border-radius: 8px; 208 | } 209 | 210 | .query-example-container label { 211 | text-align: center; 212 | } 213 | 214 | .query-example-container textarea { 215 | flex-grow: 1; 216 | margin-bottom: 8px; 217 | margin-top: 8px; 218 | resize: none; 219 | height: 100%; 220 | background-color: rgb(51, 51, 70); 221 | margin-top: 8px; 222 | } -------------------------------------------------------------------------------- /examples/browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/browser/vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite' 2 | 3 | export default defineConfig({ 4 | server: { 5 | proxy: { 6 | '/influx': { 7 | target: 'http://localhost:10000/', // envoy proxy address 8 | changeOrigin: true, 9 | rewrite: (path) => path.replace(/^\/influx/, ''), 10 | }, 11 | }, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /examples/downsampling/README.md: -------------------------------------------------------------------------------- 1 | ## Downsampling Example 2 | 3 | - [index.ts](./src/index.ts) - How to use queries to structure data for downsampling 4 | 5 | ## prerequisites 6 | 7 | - `node` and `yarn` installed 8 | 9 | - build influxdb-client: *(in project root directory)* 10 | - run `yarn install` 11 | - run `yarn build` 12 | 13 | ## Usage 14 | 15 | set environment variables. 16 | 17 | - `INFLUX_HOST` region of your influxdb cloud e.g. *`https://us-east-1-1.aws.cloud2.influxdata.com/`* 18 | - `INFLUX_TOKEN` read/write token generated in cloud 19 | - `INFLUX_DATABASE` name of database e.g .*`my-database`* 20 | 21 | For simplicity, you can use dotenv library to load environment variables in this example. Create `.env` file and paste your variables as follows: 22 | 23 | ```conf 24 | INFLUX_HOST="" 25 | INFLUX_DATABASE="" 26 | INFLUX_TOKEN="" 27 | ``` 28 | 29 | ### Run example 30 | 31 | - run `yarn install` 32 | - run `yarn dev` 33 | -------------------------------------------------------------------------------- /examples/downsampling/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "influxdb3-client-example-downsampling", 3 | "main": "index.js", 4 | "license": "MIT", 5 | "private": true, 6 | "scripts": { 7 | "dev": "ts-node -r dotenv/config ./src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@influxdata/influxdb3-client": "link:../../packages/client" 11 | }, 12 | "devDependencies": { 13 | "dotenv": "^16.3.1", 14 | "ts-node": "^10.9.1", 15 | "typescript": "^5.1.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/downsampling/src/index.ts: -------------------------------------------------------------------------------- 1 | import {InfluxDBClient} from '@influxdata/influxdb3-client' 2 | 3 | /* get environment value or throw error if missing */ 4 | const getEnv = (variableName: string): string => { 5 | if (process.env[variableName] == null) 6 | throw new Error(`missing ${variableName} environment variable`) 7 | return process.env[variableName] as string 8 | } 9 | 10 | /* eslint-disable no-console */ 11 | async function main() { 12 | // 13 | // Use environment variables to initialize client 14 | // 15 | const host = getEnv('INFLUX_HOST') 16 | const token = getEnv('INFLUX_TOKEN') 17 | const database = getEnv('INFLUX_DATABASE') 18 | const measurement = 'demoDS' 19 | const measurementDownsampled = 'demoDS2' 20 | 21 | // 22 | // Create a new client using an InfluxDB server base URL and an authentication token 23 | // 24 | const client = new InfluxDBClient({host, token, database}) 25 | 26 | try { 27 | // 28 | // Write data 29 | // 30 | await client.write(`${measurement},unit=temperature avg=24.5,max=45.0`) 31 | 32 | await new Promise((resolve) => setTimeout(resolve, 1000)) 33 | await client.write(`${measurement},unit=temperature avg=28,max=40.3`) 34 | 35 | await new Promise((resolve) => setTimeout(resolve, 1000)) 36 | await client.write(`${measurement},unit=temperature avg=20.5,max=49.0`) 37 | 38 | // 39 | // Query downsampled data 40 | // 41 | const downSamplingQuery = `\ 42 | SELECT 43 | date_bin('5 minutes', "time") as window_start, 44 | AVG("avg") as avg, 45 | MAX("max") as max 46 | FROM "${measurement}" 47 | WHERE 48 | "time" >= now() - interval '1 hour' 49 | GROUP BY window_start 50 | ORDER BY window_start ASC;` 51 | 52 | // 53 | // Execute downsampling query into pointValues 54 | // 55 | const queryPointsResult = client.queryPoints(downSamplingQuery, database, { 56 | type: 'sql', 57 | }) 58 | 59 | for await (const row of queryPointsResult) { 60 | const timestamp = new Date(row.getFloatField('window_start') as number) 61 | console.log( 62 | `${timestamp.toISOString()}: avg is ${row.getField( 63 | 'avg', 64 | 'float' 65 | )}, max is ${row.getField('max', 'float')}` 66 | ) 67 | 68 | // 69 | // write back downsampled date to $measurementDownsampled measurement 70 | // 71 | const downSampledPoint = row 72 | .asPoint(measurementDownsampled) 73 | .removeField('window_start') 74 | .setTimestamp(timestamp) 75 | 76 | await client.write(downSampledPoint, database) 77 | } 78 | } catch (err) { 79 | console.error(err) 80 | } finally { 81 | await client.close() 82 | } 83 | } 84 | 85 | main() 86 | -------------------------------------------------------------------------------- /examples/downsampling/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["es2018"], 5 | "strict": true, 6 | "moduleResolution": "node", 7 | "esModuleInterop": true 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["*.js"] 11 | } 12 | -------------------------------------------------------------------------------- /js_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-js/b28765273b04231b2666dbe98631f104d36425ab/js_logo.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.0", 3 | "npmClient": "yarn", 4 | "packages": ["packages/*"], 5 | "command": { 6 | "version": { 7 | "message": "chore(release): publish %s [skip CI]" 8 | } 9 | }, 10 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "The Client that provides a simple and convenient way to interact with InfluxDB 3.", 4 | "workspaces": { 5 | "packages": [ 6 | "packages/client", 7 | "packages/client-browser" 8 | ] 9 | }, 10 | "scripts": { 11 | "typedoc": "yarn clean && yarn build && typedoc --skipErrorChecking", 12 | "typedoc:gh-pages": "yarn typedoc && gh-pages -d ./docs -m 'docs: updates documentation [skip CI]'", 13 | "preinstall": "node ./scripts/require-yarn.js", 14 | "clean": "rimraf temp docs && yarn workspaces run clean", 15 | "cp": "node ./scripts/cp.js", 16 | "build": "yarn workspaces run build", 17 | "test": "yarn --cwd packages/client build && yarn workspaces run test", 18 | "test:ci": "yarn workspaces run test:ci", 19 | "test:unit": "yarn workspaces run test:unit", 20 | "coverage": "cd packages/client && yarn build && yarn coverage", 21 | "coverage:ci": "cd packages/client && yarn build && yarn coverage:ci", 22 | "flight": "node ./scripts/generate-flight", 23 | "flight:test": "node ./scripts/generate-flight test" 24 | }, 25 | "homepage": "https://github.com/InfluxCommunity/influxdb3-js", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/InfluxCommunity/influxdb3-js" 29 | }, 30 | "keywords": [ 31 | "influxdb", 32 | "influxdata" 33 | ], 34 | "author": { 35 | "name": "InfluxData" 36 | }, 37 | "license": "MIT", 38 | "devDependencies": { 39 | "@types/node": "^22", 40 | "gh-pages": "^6.0.0", 41 | "lerna": "^8.0.0", 42 | "prettier": "^3.0.0", 43 | "rimraf": "^5.0.0", 44 | "typedoc": "^0.27.5" 45 | }, 46 | "resolutions": { 47 | "jackspeak": "2.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "no-console": "warn" 7 | }, 8 | "ignorePatterns": ["dist/*.js", "generated/"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/client-browser/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .rpt2_cache 4 | .DS_Store 5 | .nyc_output 6 | *.lcov 7 | yarn-error.log 8 | 9 | -------------------------------------------------------------------------------- /packages/client-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@influxdata/influxdb3-client-browser", 3 | "version": "1.1.0", 4 | "description": "InfluxDB 3 client for browser", 5 | "scripts": { 6 | "apidoc:extract": "echo \"Nothing to do\"", 7 | "test": "echo \"Nothing to do\"", 8 | "test:unit": "echo \"Nothing to do\"", 9 | "test:ci": "echo \"Nothing to do\"", 10 | "build": "yarn run clean && cpr ../client/dist ./dist", 11 | "clean": "rimraf dist" 12 | }, 13 | "main": "dist/index.browser.js", 14 | "module": "dist/index.browser.mjs", 15 | "module:browser": "dist/index.browser.mjs", 16 | "browser": "dist/index.browser.js", 17 | "types": "dist/index.d.ts", 18 | "exports": { 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "browser": { 22 | "import": "./dist/index.browser.mjs", 23 | "require": "./dist/index.browser.js", 24 | "script": "./dist/influxdb.js", 25 | "umd": "./dist/index.browser.js" 26 | }, 27 | "deno": "./dist/index.browser.mjs", 28 | "import": "./dist/index.browser.mjs", 29 | "require": "./dist/index.browser.js" 30 | } 31 | }, 32 | "homepage": "https://github.com/InfluxCommunity/influxdb3-js", 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/InfluxCommunity/influxdb3-js", 36 | "directory": "packages/client-browser" 37 | }, 38 | "keywords": [ 39 | "influxdb", 40 | "influxdata" 41 | ], 42 | "author": { 43 | "name": "InfluxData" 44 | }, 45 | "license": "MIT", 46 | "publishConfig": { 47 | "access": "public" 48 | }, 49 | "devDependencies": { 50 | "@influxdata/influxdb3-client": "^1.1.0", 51 | "cpr": "^3.0.1", 52 | "rimraf": "^5.0.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/client/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension": ["ts"], 3 | "require": "ts-node/register" 4 | } 5 | -------------------------------------------------------------------------------- /packages/client/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | doc 5 | .rpt2_cache 6 | .DS_Store 7 | .nyc_output 8 | *.lcov 9 | reports 10 | yarn-error.log 11 | /tsconfig.* 12 | /tsup.* 13 | /.prettierrc.json 14 | /src 15 | /test 16 | .nycrc 17 | 18 | -------------------------------------------------------------------------------- /packages/client/.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "cache": false, 3 | "check-coverage": false, 4 | "extension": [".ts"], 5 | "include": ["src/**/*.ts"], 6 | "exclude": ["src/index.ts", "src/generated"], 7 | "sourceMap": true, 8 | "reporter": ["html", "text", "text-summary"], 9 | "all": true, 10 | "instrument": true 11 | } 12 | -------------------------------------------------------------------------------- /packages/client/api-extractor.json: -------------------------------------------------------------------------------- 1 | /** 2 | * Config file for API Extractor. For more info, please visit: https://api-extractor.com 3 | */ 4 | { 5 | "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", 6 | "extends": "../../scripts/api-extractor-base.json", 7 | "mainEntryPointFilePath": "/dist/index.d.ts" 8 | } 9 | -------------------------------------------------------------------------------- /packages/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@influxdata/influxdb3-client", 3 | "version": "1.1.0", 4 | "description": "The Client that provides a simple and convenient way to interact with InfluxDB 3.", 5 | "scripts": { 6 | "build": "yarn cp ../../README.md ./README.md && yarn run clean && yarn run build:browser && yarn run build:node", 7 | "build:node": "yarn tsup", 8 | "build:browser": "yarn tsup --config ./tsup.config.browser.ts", 9 | "clean": "rimraf --glob dist build coverage .nyc_output doc *.lcov reports", 10 | "coverage": "nyc mocha --require ts-node/register 'test/**/*.test.ts' --exit", 11 | "coverage:ci": "yarn run coverage && yarn run coverage:lcov", 12 | "coverage:lcov": "yarn run --silent nyc report --reporter=text-lcov > coverage/coverage.lcov", 13 | "cp": "node ../../scripts/cp.js", 14 | "test": "yarn run lint && yarn run typecheck && yarn run test:all", 15 | "test:all": "mocha 'test/**/*.test.ts' --exit", 16 | "test:unit": "mocha 'test/unit/**/*.test.ts' --exit", 17 | "test:integration": "mocha 'test/integration/**/*.test.ts' --exit", 18 | "test:ci": "yarn run lint:ci && yarn run test:all --exit --reporter mocha-junit-reporter --reporter-options mochaFile=../../reports/core_mocha/test-results.xml", 19 | "test:watch": "mocha 'test/unit/**/*.test.ts' --watch-extensions ts --watch", 20 | "typecheck": "tsc --noEmit --pretty", 21 | "lint": "eslint src/**/*.ts test/**/*.ts", 22 | "lint:ci": "yarn run lint --format junit --output-file ../../reports/core_eslint/eslint.xml", 23 | "lint:fix": "eslint --fix src/**/*.ts" 24 | }, 25 | "main": "dist/index.js", 26 | "module": "dist/index.mjs", 27 | "module:browser": "dist/index.browser.mjs", 28 | "browser": "dist/index.browser.js", 29 | "types": "dist/index.d.ts", 30 | "exports": { 31 | ".": { 32 | "types": "./dist/index.d.ts", 33 | "browser": { 34 | "import": "./dist/index.browser.mjs", 35 | "require": "./dist/index.browser.js", 36 | "script": "./dist/influxdb.js", 37 | "default": "./dist/index.browser.js" 38 | }, 39 | "deno": "./dist/index.browser.mjs", 40 | "import": "./dist/index.mjs", 41 | "require": "./dist/index.js" 42 | } 43 | }, 44 | "homepage": "https://github.com/InfluxCommunity/influxdb3-js", 45 | "repository": { 46 | "type": "git", 47 | "url": "git+https://github.com/InfluxCommunity/influxdb3-js", 48 | "directory": "packages/client" 49 | }, 50 | "keywords": [ 51 | "influxdb", 52 | "influxdata" 53 | ], 54 | "author": { 55 | "name": "InfluxData" 56 | }, 57 | "license": "MIT", 58 | "publishConfig": { 59 | "access": "public" 60 | }, 61 | "devDependencies": { 62 | "@types/chai": "^4.2.5", 63 | "@types/mocha": "^10.0.0", 64 | "@types/sinon": "^17.0.0", 65 | "@typescript-eslint/eslint-plugin": "^8.0.0", 66 | "@typescript-eslint/parser": "^8.0.1", 67 | "chai": "^4.2.0", 68 | "esbuild": "^0.25.0", 69 | "esbuild-runner": "^2.2.1", 70 | "eslint": "^9.10.0", 71 | "eslint-config-prettier": "^10.0.1", 72 | "eslint-formatter-junit": "^8.40.0", 73 | "eslint-plugin-prettier": "^5.0.0", 74 | "follow-redirects": "^1.14.7", 75 | "mocha": "^11.0.1", 76 | "mocha-junit-reporter": "^2.0.2", 77 | "nock": "^13.3.1", 78 | "nyc": "^17.0.0", 79 | "prettier": "^3.0.0", 80 | "rimraf": "^5.0.0", 81 | "rxjs": "^7.2.0", 82 | "sinon": "^19.0.2", 83 | "ts-node": "^10.9.1", 84 | "tsup": "^8.0.1", 85 | "typescript": "^5.1.3" 86 | }, 87 | "dependencies": { 88 | "@grpc/grpc-js": "^1.9.9", 89 | "@protobuf-ts/grpc-transport": "^2.9.1", 90 | "@protobuf-ts/grpcweb-transport": "^2.9.1", 91 | "@protobuf-ts/runtime-rpc": "^2.9.1", 92 | "apache-arrow": "^19.0.0", 93 | "grpc-web": "^1.5.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /packages/client/src/InfluxDBClient.ts: -------------------------------------------------------------------------------- 1 | import WriteApi from './WriteApi' 2 | import WriteApiImpl from './impl/WriteApiImpl' 3 | import QueryApi, {QParamType} from './QueryApi' 4 | import QueryApiImpl from './impl/QueryApiImpl' 5 | import { 6 | ClientOptions, 7 | DEFAULT_ConnectionOptions, 8 | DEFAULT_QueryOptions, 9 | QueryOptions, 10 | // QueryType, 11 | WriteOptions, 12 | fromConnectionString, 13 | fromEnv, 14 | } from './options' 15 | import {IllegalArgumentError} from './errors' 16 | import {WritableData, writableDataToLineProtocol} from './util/generics' 17 | import {throwReturn} from './util/common' 18 | import {PointValues} from './PointValues' 19 | 20 | const argumentErrorMessage = `\ 21 | Please specify the 'database' as a method parameter or use default configuration \ 22 | at 'ClientOptions.database' 23 | ` 24 | 25 | /** 26 | * `InfluxDBClient` for interacting with an InfluxDB server, simplifying common operations such as writing, querying. 27 | */ 28 | export default class InfluxDBClient { 29 | private readonly _options: ClientOptions 30 | private readonly _writeApi: WriteApi 31 | private readonly _queryApi: QueryApi 32 | 33 | /** 34 | * Creates a new instance of the `InfluxDBClient` from `ClientOptions`. 35 | * @param options - client options 36 | */ 37 | constructor(options: ClientOptions) 38 | 39 | /** 40 | * Creates a new instance of the `InfluxDBClient` from connection string. 41 | * @example https://us-east-1-1.aws.cloud2.influxdata.com/?token=my-token&database=my-database 42 | * 43 | * Supported query parameters: 44 | * - token - authentication token (required) 45 | * - authScheme - token authentication scheme. Not set for Cloud access, set to 'Bearer' for Edge. 46 | * - database - database (bucket) name 47 | * - timeout - I/O timeout 48 | * - precision - timestamp precision when writing data 49 | * - gzipThreshold - payload size threshold for gzipping data 50 | * 51 | * @param connectionString - connection string 52 | */ 53 | constructor(connectionString: string) 54 | 55 | /** 56 | * Creates a new instance of the `InfluxDBClient` from environment variables. 57 | * 58 | * Supported variables: 59 | * - INFLUX_HOST - cloud/server URL (required) 60 | * - INFLUX_TOKEN - authentication token (required) 61 | * - INFLUX_AUTH_SCHEME - token authentication scheme. Not set for Cloud access, set to 'Bearer' for Edge. 62 | * - INFLUX_TIMEOUT - I/O timeout 63 | * - INFLUX_DATABASE - database (bucket) name 64 | * - INFLUX_PRECISION - timestamp precision when writing data 65 | * - INFLUX_GZIP_THRESHOLD - payload size threshold for gzipping data 66 | */ 67 | constructor() 68 | 69 | constructor(...args: Array) { 70 | let options: ClientOptions 71 | switch (args.length) { 72 | case 0: { 73 | options = fromEnv() 74 | break 75 | } 76 | case 1: { 77 | if (args[0] == null) { 78 | throw new IllegalArgumentError('No configuration specified!') 79 | } else if (typeof args[0] === 'string') { 80 | options = fromConnectionString(args[0]) 81 | } else { 82 | options = args[0] 83 | } 84 | break 85 | } 86 | default: { 87 | throw new IllegalArgumentError('Multiple arguments specified!') 88 | } 89 | } 90 | this._options = { 91 | ...DEFAULT_ConnectionOptions, 92 | ...options, 93 | } 94 | const host = this._options.host 95 | if (typeof host !== 'string') 96 | throw new IllegalArgumentError('No host specified!') 97 | if (host.endsWith('/')) 98 | this._options.host = host.substring(0, host.length - 1) 99 | if (typeof this._options.token !== 'string') 100 | throw new IllegalArgumentError('No token specified!') 101 | this._queryApi = new QueryApiImpl(this._options) 102 | this._writeApi = new WriteApiImpl(this._options) 103 | } 104 | 105 | private _mergeWriteOptions = (writeOptions?: Partial) => { 106 | const headerMerge: Record = { 107 | ...this._options.writeOptions?.headers, 108 | ...writeOptions?.headers, 109 | } 110 | const result = { 111 | ...this._options.writeOptions, 112 | ...writeOptions, 113 | } 114 | result.headers = headerMerge 115 | return result 116 | } 117 | 118 | private _mergeQueryOptions = (queryOptions?: Partial) => { 119 | const headerMerge: Record = { 120 | ...this._options.queryOptions?.headers, 121 | ...queryOptions?.headers, 122 | } 123 | const paramsMerge: Record = { 124 | ...this._options.queryOptions?.params, 125 | ...queryOptions?.params, 126 | } 127 | const result = { 128 | ...this._options.queryOptions, 129 | ...queryOptions, 130 | } 131 | result.headers = headerMerge 132 | result.params = paramsMerge 133 | return result 134 | } 135 | 136 | /** 137 | * Write data into specified database. 138 | * @param data - data to write 139 | * @param database - database to write into 140 | * @param org - organization to write into 141 | * @param writeOptions - write options 142 | */ 143 | async write( 144 | data: WritableData, 145 | database?: string, 146 | org?: string, 147 | writeOptions?: Partial 148 | ): Promise { 149 | const options = this._mergeWriteOptions(writeOptions) 150 | 151 | await this._writeApi.doWrite( 152 | writableDataToLineProtocol(data, options?.defaultTags), 153 | database ?? 154 | this._options.database ?? 155 | throwReturn(new Error(argumentErrorMessage)), 156 | org, 157 | options 158 | ) 159 | } 160 | 161 | /** 162 | * Execute a query and return the results as an async generator. 163 | * 164 | * @param query - The query string. 165 | * @param database - The name of the database to query. 166 | * @param queryOptions - The options for the query (default: \{ type: 'sql' \}). 167 | * @example 168 | * ```typescript 169 | * client.query('SELECT * from net', 'traffic_db', { 170 | * type: 'sql', 171 | * headers: { 172 | * 'channel-pref': 'eu-west-7', 173 | * 'notify': 'central', 174 | * }, 175 | * }) 176 | * ``` 177 | * @returns An async generator that yields maps of string keys to any values. 178 | */ 179 | query( 180 | query: string, 181 | database?: string, 182 | queryOptions: Partial = this._options.queryOptions ?? 183 | DEFAULT_QueryOptions 184 | ): AsyncGenerator, void, void> { 185 | const options = this._mergeQueryOptions(queryOptions) 186 | return this._queryApi.query( 187 | query, 188 | database ?? 189 | this._options.database ?? 190 | throwReturn(new Error(argumentErrorMessage)), 191 | options as QueryOptions 192 | ) 193 | } 194 | 195 | /** 196 | * Execute a query and return the results as an async generator. 197 | * 198 | * @param query - The query string. 199 | * @param database - The name of the database to query. 200 | * @param queryOptions - The type of query (default: \{type: 'sql'\}). 201 | * @example 202 | * 203 | * ```typescript 204 | * client.queryPoints('SELECT * FROM cpu', 'performance_db', { 205 | * type: 'sql', 206 | * params: {register: 'rax'}, 207 | * }) 208 | * ``` 209 | * 210 | * @returns An async generator that yields PointValues object. 211 | */ 212 | queryPoints( 213 | query: string, 214 | database?: string, 215 | queryOptions: Partial = this._options.queryOptions ?? 216 | DEFAULT_QueryOptions 217 | ): AsyncGenerator { 218 | const options = this._mergeQueryOptions(queryOptions) 219 | return this._queryApi.queryPoints( 220 | query, 221 | database ?? 222 | this._options.database ?? 223 | throwReturn(new Error(argumentErrorMessage)), 224 | options as QueryOptions 225 | ) 226 | } 227 | 228 | /** 229 | * Closes the client and all its resources (connections, ...) 230 | */ 231 | async close(): Promise { 232 | await this._writeApi.close() 233 | await this._queryApi.close() 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /packages/client/src/QueryApi.ts: -------------------------------------------------------------------------------- 1 | import {PointValues} from './PointValues' 2 | import {QueryOptions} from './options' 3 | 4 | export type QParamType = string | number | boolean 5 | 6 | /** 7 | * Asynchronous API that queries data from a database. 8 | */ 9 | export default interface QueryApi { 10 | /** 11 | * Execute a query and return the results as an async generator. 12 | * 13 | * @param query - The query string. 14 | * @param database - The name of the database to query. 15 | * @param options - options applied to the query (default: { type: 'sql'}). 16 | * @returns An async generator that yields maps of string keys to any values. 17 | */ 18 | query( 19 | query: string, 20 | database: string, 21 | options: QueryOptions 22 | ): AsyncGenerator, void, void> 23 | 24 | /** 25 | * Execute a query and return the results as an async generator. 26 | * 27 | * @param query - The query string. 28 | * @param database - The name of the database to query. 29 | * @param options - Options for the query (default: {type: 'sql'}). 30 | * @returns An async generator that yields PointValues object. 31 | */ 32 | queryPoints( 33 | query: string, 34 | database: string, 35 | options: QueryOptions 36 | ): AsyncGenerator 37 | 38 | close(): Promise 39 | } 40 | -------------------------------------------------------------------------------- /packages/client/src/WriteApi.ts: -------------------------------------------------------------------------------- 1 | import {WriteOptions} from './options' 2 | 3 | export interface TimeConverter { 4 | (value: string | number | Date | undefined): string | undefined 5 | } 6 | 7 | /** 8 | * Asynchronous API that writes time-series data into InfluxDB. 9 | * This API always sends data to InfluxDB immediately 10 | */ 11 | export default interface WriteApi { 12 | /** 13 | * Write lines of [Line Protocol](https://bit.ly/2QL99fu). 14 | * 15 | * @param lines - InfluxDB Line Protocol 16 | */ 17 | doWrite( 18 | lines: string[], 19 | bucket: string, 20 | org?: string, 21 | writeOptions?: Partial 22 | ): Promise 23 | 24 | /** 25 | * @returns completition promise 26 | */ 27 | close(): Promise 28 | } 29 | -------------------------------------------------------------------------------- /packages/client/src/errors.ts: -------------------------------------------------------------------------------- 1 | import {Headers} from './results' 2 | 3 | /** IllegalArgumentError is thrown when illegal argument is supplied. */ 4 | export class IllegalArgumentError extends Error { 5 | /* istanbul ignore next */ 6 | constructor(message: string) { 7 | super(message) 8 | this.name = 'IllegalArgumentError' 9 | Object.setPrototypeOf(this, IllegalArgumentError.prototype) 10 | } 11 | } 12 | 13 | /** 14 | * A general HTTP error. 15 | */ 16 | export class HttpError extends Error { 17 | /** application error code, when available */ 18 | public code: string | undefined 19 | /** json error response */ 20 | public json: any 21 | 22 | /* istanbul ignore next because of super() not being covered*/ 23 | constructor( 24 | readonly statusCode: number, 25 | readonly statusMessage: string | undefined, 26 | readonly body?: string, 27 | readonly contentType?: string | undefined | null, 28 | readonly headers?: Headers | null, 29 | message?: string 30 | ) { 31 | super() 32 | Object.setPrototypeOf(this, HttpError.prototype) 33 | if (message) { 34 | this.message = message 35 | } else if (body) { 36 | // Edge may not set Content-Type header 37 | if (contentType?.startsWith('application/json') || !contentType) { 38 | try { 39 | this.json = JSON.parse(body) 40 | this.message = this.json.message 41 | this.code = this.json.code 42 | if (!this.message) { 43 | interface EdgeBody { 44 | error?: string 45 | data?: { 46 | error_message?: string 47 | } 48 | } 49 | const eb: EdgeBody = this.json as EdgeBody 50 | if (eb.data?.error_message) { 51 | this.message = eb.data.error_message 52 | } else if (eb.error) { 53 | this.message = eb.error 54 | } 55 | } 56 | } catch (e) { 57 | // silently ignore, body string is still available 58 | } 59 | } 60 | } 61 | if (!this.message) { 62 | this.message = `${statusCode} ${statusMessage} : ${body}` 63 | } 64 | this.name = 'HttpError' 65 | } 66 | } 67 | 68 | /** RequestTimedOutError indicates request timeout in the communication with the server */ 69 | export class RequestTimedOutError extends Error { 70 | /* istanbul ignore next because of super() not being covered */ 71 | constructor() { 72 | super() 73 | Object.setPrototypeOf(this, RequestTimedOutError.prototype) 74 | this.name = 'RequestTimedOutError' 75 | this.message = 'Request timed out' 76 | } 77 | } 78 | 79 | /** AbortError indicates that the communication with the server was aborted */ 80 | export class AbortError extends Error { 81 | /* istanbul ignore next because of super() not being covered */ 82 | constructor() { 83 | super() 84 | this.name = 'AbortError' 85 | Object.setPrototypeOf(this, AbortError.prototype) 86 | this.message = 'Response aborted' 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/client/src/impl/QueryApiImpl.ts: -------------------------------------------------------------------------------- 1 | import {RecordBatchReader, Type as ArrowType} from 'apache-arrow' 2 | import QueryApi, {QParamType} from '../QueryApi' 3 | import {Ticket} from '../generated/flight/Flight' 4 | import {FlightServiceClient} from '../generated/flight/Flight.client' 5 | import {ConnectionOptions, QueryOptions, QueryType} from '../options' 6 | import {createInt32Uint8Array} from '../util/common' 7 | import {RpcMetadata, RpcOptions} from '@protobuf-ts/runtime-rpc' 8 | import {impl} from './implSelector' 9 | import {PointFieldType, PointValues} from '../PointValues' 10 | import {allParamsMatched, queryHasParams} from '../util/sql' 11 | import {CLIENT_LIB_USER_AGENT} from './version' 12 | import {getMappedValue} from '../util/TypeCasting' 13 | 14 | export type TicketDataType = { 15 | database: string 16 | sql_query: string 17 | query_type: QueryType 18 | params?: {[name: string]: QParamType | undefined} 19 | } 20 | 21 | export default class QueryApiImpl implements QueryApi { 22 | private _closed = false 23 | private _flightClient: FlightServiceClient 24 | private _transport: ReturnType 25 | 26 | private _defaultHeaders: Record | undefined 27 | 28 | constructor(private _options: ConnectionOptions) { 29 | const {host, queryTimeout: timeout} = this._options 30 | this._defaultHeaders = this._options.headers 31 | this._transport = impl.queryTransport({host, timeout}) 32 | this._flightClient = new FlightServiceClient(this._transport) 33 | } 34 | 35 | prepareTicket( 36 | database: string, 37 | query: string, 38 | options: QueryOptions 39 | ): Ticket { 40 | const ticketData: TicketDataType = { 41 | database: database, 42 | sql_query: query, 43 | query_type: options.type, 44 | } 45 | 46 | if (options.params) { 47 | const param: {[name: string]: QParamType | undefined} = {} 48 | for (const key of Object.keys(options.params)) { 49 | if (options.params[key]) { 50 | param[key] = options.params[key] 51 | } 52 | } 53 | ticketData['params'] = param as {[name: string]: QParamType | undefined} 54 | } 55 | 56 | return Ticket.create({ 57 | ticket: new TextEncoder().encode(JSON.stringify(ticketData)), 58 | }) 59 | } 60 | 61 | prepareMetadata(headers?: Record): RpcMetadata { 62 | const meta: RpcMetadata = { 63 | 'User-Agent': CLIENT_LIB_USER_AGENT, 64 | ...this._defaultHeaders, 65 | ...headers, 66 | } 67 | 68 | const token = this._options.token 69 | if (token) meta['authorization'] = `Bearer ${token}` 70 | 71 | return meta 72 | } 73 | 74 | private async *_queryRawBatches( 75 | query: string, 76 | database: string, 77 | options: QueryOptions 78 | ) { 79 | if (options.params && queryHasParams(query)) { 80 | allParamsMatched(query, options.params) 81 | } 82 | 83 | if (this._closed) { 84 | throw new Error('queryApi: already closed!') 85 | } 86 | const client = this._flightClient 87 | 88 | const ticket = this.prepareTicket(database, query, options) 89 | 90 | const meta = this.prepareMetadata(options.headers) 91 | const rpcOptions: RpcOptions = {meta} 92 | 93 | const flightDataStream = client.doGet(ticket, rpcOptions) 94 | 95 | const binaryStream = (async function* () { 96 | for await (const flightData of flightDataStream.responses) { 97 | // Include the length of dataHeader for the reader. 98 | yield createInt32Uint8Array(flightData.dataHeader.length) 99 | yield flightData.dataHeader 100 | // Length of dataBody is already included in dataHeader. 101 | yield flightData.dataBody 102 | } 103 | })() 104 | 105 | const reader = await RecordBatchReader.from(binaryStream) 106 | 107 | yield* reader 108 | } 109 | 110 | async *query( 111 | query: string, 112 | database: string, 113 | options: QueryOptions 114 | ): AsyncGenerator, void, void> { 115 | const batches = this._queryRawBatches(query, database, options) 116 | 117 | for await (const batch of batches) { 118 | for (const batchRow of batch) { 119 | const row: Record = {} 120 | for (const column of batch.schema.fields) { 121 | const value = batchRow[column.name] 122 | row[column.name] = getMappedValue(column, value) 123 | } 124 | yield row 125 | } 126 | } 127 | } 128 | 129 | async *queryPoints( 130 | query: string, 131 | database: string, 132 | options: QueryOptions 133 | ): AsyncGenerator { 134 | const batches = this._queryRawBatches(query, database, options) 135 | 136 | for await (const batch of batches) { 137 | for (let rowIndex = 0; rowIndex < batch.numRows; rowIndex++) { 138 | const values = new PointValues() 139 | for (let columnIndex = 0; columnIndex < batch.numCols; columnIndex++) { 140 | const columnSchema = batch.schema.fields[columnIndex] 141 | const name = columnSchema.name 142 | const value = batch.getChildAt(columnIndex)?.get(rowIndex) 143 | const arrowTypeId = columnSchema.typeId 144 | const metaType = columnSchema.metadata.get('iox::column::type') 145 | 146 | if (value === undefined || value === null) continue 147 | 148 | if ( 149 | (name === 'measurement' || name == 'iox::measurement') && 150 | typeof value === 'string' 151 | ) { 152 | values.setMeasurement(value) 153 | continue 154 | } 155 | 156 | if (!metaType) { 157 | if (name === 'time' && arrowTypeId === ArrowType.Timestamp) { 158 | values.setTimestamp(value) 159 | } else { 160 | values.setField(name, value) 161 | } 162 | 163 | continue 164 | } 165 | 166 | const [, , valueType, _fieldType] = metaType.split('::') 167 | 168 | if (valueType === 'field') { 169 | if (_fieldType && value !== undefined && value !== null) { 170 | const mappedValue = getMappedValue(columnSchema, value) 171 | values.setField(name, mappedValue, _fieldType as PointFieldType) 172 | } 173 | } else if (valueType === 'tag') { 174 | values.setTag(name, value) 175 | } else if (valueType === 'timestamp') { 176 | values.setTimestamp(value) 177 | } 178 | } 179 | 180 | yield values 181 | } 182 | } 183 | } 184 | 185 | async close(): Promise { 186 | this._closed = true 187 | this._transport.close?.() 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /packages/client/src/impl/WriteApiImpl.ts: -------------------------------------------------------------------------------- 1 | import WriteApi from '../WriteApi' 2 | import {ClientOptions, DEFAULT_WriteOptions, WriteOptions} from '../options' 3 | import {Transport} from '../transport' 4 | import {Headers} from '../results' 5 | import {Log} from '../util/logger' 6 | import {HttpError} from '../errors' 7 | import {impl} from './implSelector' 8 | 9 | export default class WriteApiImpl implements WriteApi { 10 | private _closed = false 11 | private _transport: Transport 12 | 13 | constructor(private _options: ClientOptions) { 14 | this._transport = 15 | this._options.transport ?? impl.writeTransport(this._options) 16 | this.doWrite = this.doWrite.bind(this) 17 | } 18 | 19 | private _createWritePath( 20 | bucket: string, 21 | writeOptions: WriteOptions, 22 | org?: string 23 | ) { 24 | const query: string[] = [ 25 | `bucket=${encodeURIComponent(bucket)}`, 26 | `precision=${writeOptions.precision}`, 27 | ] 28 | if (org) query.push(`org=${encodeURIComponent(org)}`) 29 | 30 | const path = `/api/v2/write?${query.join('&')}` 31 | return path 32 | } 33 | 34 | doWrite( 35 | lines: string[], 36 | bucket: string, 37 | org?: string, 38 | writeOptions?: Partial 39 | ): Promise { 40 | // eslint-disable-next-line @typescript-eslint/no-this-alias 41 | const self: WriteApiImpl = this 42 | if (self._closed) { 43 | return Promise.reject(new Error('writeApi: already closed!')) 44 | } 45 | if (lines.length <= 0 || (lines.length === 1 && lines[0] === '')) 46 | return Promise.resolve() 47 | 48 | let resolve: (value: void | PromiseLike) => void 49 | let reject: (reason?: any) => void 50 | const promise = new Promise((res, rej) => { 51 | resolve = res 52 | reject = rej 53 | }) 54 | 55 | let responseStatusCode: number | undefined 56 | let headers: Headers 57 | const callbacks = { 58 | responseStarted(_headers: Headers, statusCode?: number): void { 59 | responseStatusCode = statusCode 60 | headers = _headers 61 | }, 62 | error(error: Error): void { 63 | // ignore informational message about the state of InfluxDB 64 | // enterprise cluster, if present 65 | if ( 66 | error instanceof HttpError && 67 | error.json && 68 | typeof error.json.error === 'string' && 69 | error.json.error.includes('hinted handoff queue not empty') 70 | ) { 71 | Log.warn(`Write to InfluxDB returns: ${error.json.error}`) 72 | responseStatusCode = 204 73 | callbacks.complete() 74 | return 75 | } 76 | Log.error(`Write to InfluxDB failed.`, error) 77 | reject(error) 78 | }, 79 | complete(): void { 80 | // older implementations of transport do not report status code 81 | if ( 82 | responseStatusCode == undefined || 83 | (responseStatusCode >= 200 && responseStatusCode < 300) 84 | ) { 85 | resolve() 86 | } else { 87 | const message = `2xx HTTP response status code expected, but ${responseStatusCode} returned` 88 | const error = new HttpError( 89 | responseStatusCode, 90 | message, 91 | undefined, 92 | '0', 93 | headers 94 | ) 95 | error.message = message 96 | callbacks.error(error) 97 | } 98 | }, 99 | } 100 | 101 | const writeOptionsOrDefault: WriteOptions = { 102 | ...DEFAULT_WriteOptions, 103 | ...writeOptions, 104 | } 105 | const sendOptions = { 106 | method: 'POST', 107 | headers: { 108 | 'content-type': 'text/plain; charset=utf-8', 109 | ...writeOptions?.headers, 110 | }, 111 | gzipThreshold: writeOptionsOrDefault.gzipThreshold, 112 | } 113 | 114 | this._transport.send( 115 | this._createWritePath(bucket, writeOptionsOrDefault, org), 116 | lines.join('\n'), 117 | sendOptions, 118 | callbacks 119 | ) 120 | 121 | return promise 122 | } 123 | 124 | async close(): Promise { 125 | this._closed = true 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /packages/client/src/impl/browser/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": false, 4 | "browser": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/client/src/impl/browser/FetchTransport.ts: -------------------------------------------------------------------------------- 1 | import {Transport, SendOptions} from '../../transport' 2 | import {AbortError, HttpError} from '../../errors' 3 | import completeCommunicationObserver from '../completeCommunicationObserver' 4 | import {Log} from '../../util/logger' 5 | import { 6 | ChunkCombiner, 7 | CommunicationObserver, 8 | createTextDecoderCombiner, 9 | Headers, 10 | ResponseStartedFn, 11 | } from '../../results' 12 | import {ConnectionOptions} from '../../options' 13 | 14 | function getResponseHeaders(response: Response): Headers { 15 | const headers: Headers = {} 16 | response.headers.forEach((value: string, key: string) => { 17 | const previous = headers[key] 18 | if (previous === undefined) { 19 | headers[key] = value 20 | } else if (Array.isArray(previous)) { 21 | previous.push(value) 22 | } else { 23 | headers[key] = [previous, value] 24 | } 25 | }) 26 | return headers 27 | } 28 | 29 | /** 30 | * Transport layer that use browser fetch. 31 | */ 32 | export default class FetchTransport implements Transport { 33 | chunkCombiner: ChunkCombiner = createTextDecoderCombiner() 34 | private _defaultHeaders: {[key: string]: string} 35 | private _url: string 36 | constructor(private _connectionOptions: ConnectionOptions) { 37 | this._defaultHeaders = { 38 | 'content-type': 'application/json; charset=utf-8', 39 | // 'User-Agent': `influxdb-client-js/${CLIENT_LIB_VERSION}`, // user-agent can hardly be customized https://github.com/influxdata/influxdb-client-js/issues/262 40 | ..._connectionOptions.headers, 41 | } 42 | if (this._connectionOptions.token) { 43 | const authScheme = this._connectionOptions.authScheme ?? 'Token' 44 | this._defaultHeaders[ 45 | 'Authorization' 46 | ] = `${authScheme} ${this._connectionOptions.token}` 47 | } 48 | this._url = String(this._connectionOptions.host) 49 | if (this._url.endsWith('/')) { 50 | this._url = this._url.substring(0, this._url.length - 1) 51 | } 52 | // https://github.com/influxdata/influxdb-client-js/issues/263 53 | // don't allow /api/v2 suffix to avoid future problems 54 | if (this._url.endsWith('/api/v2')) { 55 | this._url = this._url.substring(0, this._url.length - '/api/v2'.length) 56 | Log.warn( 57 | `Please remove '/api/v2' context path from InfluxDB base url, using ${this._url} !` 58 | ) 59 | } 60 | } 61 | send( 62 | path: string, 63 | body: string, 64 | options: SendOptions, 65 | callbacks?: Partial> | undefined 66 | ): void { 67 | const observer = completeCommunicationObserver(callbacks) 68 | let cancelled = false 69 | let signal = (options as any).signal 70 | let pausePromise: Promise | undefined 71 | const resumeQuickly = () => {} 72 | let resume = resumeQuickly 73 | if (callbacks && callbacks.useCancellable) { 74 | const controller = new AbortController() 75 | if (!signal) { 76 | signal = controller.signal 77 | options = {...options, signal} 78 | } 79 | // resume data reading so that it can exit on abort signal 80 | signal.addEventListener('abort', () => { 81 | resume() 82 | }) 83 | callbacks.useCancellable({ 84 | cancel() { 85 | cancelled = true 86 | controller.abort() 87 | }, 88 | isCancelled() { 89 | return cancelled || signal.aborted 90 | }, 91 | }) 92 | } 93 | this._fetch(path, body, options) 94 | .then(async (response) => { 95 | if (callbacks?.responseStarted) { 96 | observer.responseStarted( 97 | getResponseHeaders(response), 98 | response.status 99 | ) 100 | } 101 | await this._throwOnErrorResponse(response) 102 | if (response.body) { 103 | const reader = response.body.getReader() 104 | let chunk: ReadableStreamReadResult 105 | do { 106 | if (pausePromise) { 107 | await pausePromise 108 | } 109 | if (cancelled) { 110 | break 111 | } 112 | chunk = await reader.read() 113 | if (observer.next(chunk.value) === false) { 114 | const useResume = observer.useResume 115 | if (!useResume) { 116 | const msg = 'Unable to pause, useResume is not configured!' 117 | await reader.cancel(msg) 118 | return Promise.reject(new Error(msg)) 119 | } 120 | pausePromise = new Promise((resolve) => { 121 | resume = () => { 122 | resolve() 123 | pausePromise = undefined 124 | resume = resumeQuickly 125 | } 126 | useResume(resume) 127 | }) 128 | } 129 | } while (!chunk.done) 130 | } else if (response.arrayBuffer) { 131 | const buffer = await response.arrayBuffer() 132 | observer.next(new Uint8Array(buffer)) 133 | } else { 134 | const text = await response.text() 135 | observer.next(new TextEncoder().encode(text)) 136 | } 137 | }) 138 | .catch((e) => { 139 | if (!cancelled) { 140 | observer.error(e) 141 | } 142 | }) 143 | .finally(() => observer.complete()) 144 | } 145 | 146 | private async _throwOnErrorResponse(response: Response): Promise { 147 | if (response.status >= 300) { 148 | let text = '' 149 | try { 150 | text = await response.text() 151 | if (!text) { 152 | const headerError = response.headers.get('x-influxdb-error') 153 | if (headerError) { 154 | text = headerError 155 | } 156 | } 157 | } catch (e) { 158 | Log.warn('Unable to receive error body', e) 159 | throw new HttpError( 160 | response.status, 161 | response.statusText, 162 | undefined, 163 | response.headers.get('content-type'), 164 | getResponseHeaders(response) 165 | ) 166 | } 167 | throw new HttpError( 168 | response.status, 169 | response.statusText, 170 | text, 171 | response.headers.get('content-type'), 172 | getResponseHeaders(response) 173 | ) 174 | } 175 | } 176 | 177 | async *iterate( 178 | path: string, 179 | body: string, 180 | options: SendOptions 181 | ): AsyncIterableIterator { 182 | const response = await this._fetch(path, body, options) 183 | await this._throwOnErrorResponse(response) 184 | if (response.body) { 185 | const reader = response.body.getReader() 186 | for (;;) { 187 | const {value, done} = await reader.read() 188 | if (done) { 189 | break 190 | } 191 | if (options.signal?.aborted) { 192 | await response.body.cancel() 193 | throw new AbortError() 194 | } 195 | yield value 196 | } 197 | } else if (response.arrayBuffer) { 198 | const buffer = await response.arrayBuffer() 199 | yield new Uint8Array(buffer) 200 | } else { 201 | const text = await response.text() 202 | yield new TextEncoder().encode(text) 203 | } 204 | } 205 | 206 | async request( 207 | path: string, 208 | body: any, 209 | options: SendOptions, 210 | responseStarted?: ResponseStartedFn 211 | ): Promise { 212 | const response = await this._fetch(path, body, options) 213 | const {headers} = response 214 | const responseContentType = headers.get('content-type') || '' 215 | if (responseStarted) { 216 | responseStarted(getResponseHeaders(response), response.status) 217 | } 218 | 219 | await this._throwOnErrorResponse(response) 220 | const responseType = options.headers?.accept ?? responseContentType 221 | if (responseType.includes('json')) { 222 | return await response.json() 223 | } else if ( 224 | responseType.includes('text') || 225 | responseType.startsWith('application/csv') 226 | ) { 227 | return await response.text() 228 | } 229 | } 230 | 231 | private _fetch( 232 | path: string, 233 | body: any, 234 | options: SendOptions 235 | ): Promise { 236 | const {method, headers, ...other} = options 237 | const url = `${this._url}${path}` 238 | const request: RequestInit = { 239 | method: method, 240 | body: 241 | method === 'GET' || method === 'HEAD' 242 | ? undefined 243 | : typeof body === 'string' 244 | ? body 245 | : JSON.stringify(body), 246 | headers: { 247 | ...this._defaultHeaders, 248 | ...headers, 249 | }, 250 | credentials: 'omit' as const, 251 | // override with custom transport options 252 | ...this._connectionOptions.transportOptions, 253 | // allow to specify custom options, such as signal, in SendOptions 254 | ...other, 255 | } 256 | this.requestDecorator(request, options, url) 257 | return fetch(url, request) 258 | } 259 | 260 | /** 261 | * RequestDecorator allows to modify requests before sending. 262 | * 263 | * The following example shows a function that adds gzip 264 | * compression of requests using pako.js. 265 | * 266 | * ```ts 267 | * const client = new InfluxDB({url: 'http://a'}) 268 | * client.transport.requestDecorator = function(request, options) { 269 | * const body = request.body 270 | * if ( 271 | * typeof body === 'string' && 272 | * options.gzipThreshold !== undefined && 273 | * body.length > options.gzipThreshold 274 | * ) { 275 | * request.headers['content-encoding'] = 'gzip' 276 | * request.body = pako.gzip(body) 277 | * } 278 | * } 279 | * ``` 280 | */ 281 | public requestDecorator: ( 282 | request: RequestInit, 283 | options: SendOptions, 284 | url: string 285 | ) => void = function () {} 286 | } 287 | -------------------------------------------------------------------------------- /packages/client/src/impl/browser/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | export default [{ 4 | languageOptions: { 5 | globals: { 6 | ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, "off"])), 7 | ...globals.browser, 8 | }, 9 | }, 10 | }]; -------------------------------------------------------------------------------- /packages/client/src/impl/browser/index.ts: -------------------------------------------------------------------------------- 1 | import {TargetBasedImplementation} from '../implSelector' 2 | import FetchTransport from './FetchTransport' 3 | import {createTransport} from './rpc' 4 | 5 | const implementation: TargetBasedImplementation = { 6 | writeTransport: (opts) => new FetchTransport(opts), 7 | queryTransport: createTransport, 8 | } 9 | 10 | export default implementation 11 | -------------------------------------------------------------------------------- /packages/client/src/impl/browser/rpc.ts: -------------------------------------------------------------------------------- 1 | import {GrpcWebFetchTransport} from '@protobuf-ts/grpcweb-transport' 2 | import {CreateQueryTransport} from '../implSelector' 3 | 4 | export const createTransport: CreateQueryTransport = ({host, timeout}) => { 5 | return new GrpcWebFetchTransport({baseUrl: host, timeout}) 6 | } 7 | -------------------------------------------------------------------------------- /packages/client/src/impl/browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["DOM", "es2018"] 5 | }, 6 | "include": ["*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/client/src/impl/completeCommunicationObserver.ts: -------------------------------------------------------------------------------- 1 | import {CommunicationObserver, Headers} from '../results' 2 | 3 | type CompleteObserver = Omit< 4 | Required>, 5 | 'useCancellable' | 'useResume' 6 | > & 7 | Pick, 'useResume' | 'useCancellable'> 8 | 9 | export default function completeCommunicationObserver( 10 | callbacks: Partial> = {} 11 | ): CompleteObserver { 12 | let state = 0 13 | const retVal: CompleteObserver = { 14 | next: (data: any): void | boolean => { 15 | if ( 16 | state === 0 && 17 | callbacks.next && 18 | data !== null && 19 | data !== undefined 20 | ) { 21 | return callbacks.next(data) 22 | } 23 | }, 24 | error: (error: Error): void => { 25 | /* istanbul ignore else propagate error at most once */ 26 | if (state === 0) { 27 | state = 1 28 | /* istanbul ignore else safety check */ 29 | if (callbacks.error) callbacks.error(error) 30 | } 31 | }, 32 | complete: (): void => { 33 | if (state === 0) { 34 | state = 2 35 | /* istanbul ignore else safety check */ 36 | if (callbacks.complete) callbacks.complete() 37 | } 38 | }, 39 | responseStarted: (headers: Headers, statusCode?: number): void => { 40 | if (callbacks.responseStarted) 41 | callbacks.responseStarted(headers, statusCode) 42 | }, 43 | } 44 | if (callbacks.useCancellable) { 45 | retVal.useCancellable = callbacks.useCancellable.bind(callbacks) 46 | } 47 | if (callbacks.useResume) { 48 | retVal.useResume = callbacks.useResume.bind(callbacks) 49 | } 50 | return retVal 51 | } 52 | -------------------------------------------------------------------------------- /packages/client/src/impl/implSelector.ts: -------------------------------------------------------------------------------- 1 | // this file together with tsup config helps selects correct implementations for node/browser 2 | // Don't rename this file unless you change it's name inside tsup config 3 | 4 | import {RpcTransport} from '@protobuf-ts/runtime-rpc' 5 | import {Transport} from '../transport' 6 | import {ClientOptions} from '../options' 7 | 8 | // This import path is replaced by tsup for browser. Don't change path for ./node or ./browser! 9 | export {default as impl} from './node' 10 | 11 | interface MaybeCloseable { 12 | close?(): void 13 | } 14 | 15 | export type CreateWriteTransport = (options: ClientOptions) => Transport 16 | export type CreateQueryTransport = (options: { 17 | host: string 18 | timeout?: number 19 | }) => RpcTransport & MaybeCloseable 20 | 21 | export type TargetBasedImplementation = { 22 | writeTransport: CreateWriteTransport 23 | queryTransport: CreateQueryTransport 24 | } 25 | -------------------------------------------------------------------------------- /packages/client/src/impl/node/index.ts: -------------------------------------------------------------------------------- 1 | import {TargetBasedImplementation} from '../implSelector' 2 | import NodeHttpTransport from './NodeHttpTransport' 3 | import {createTransport} from './rpc' 4 | 5 | const implementation: TargetBasedImplementation = { 6 | writeTransport: (opts) => new NodeHttpTransport(opts), 7 | queryTransport: createTransport, 8 | } 9 | 10 | export default implementation 11 | -------------------------------------------------------------------------------- /packages/client/src/impl/node/rpc.ts: -------------------------------------------------------------------------------- 1 | import {GrpcTransport} from '@protobuf-ts/grpc-transport' 2 | import {replaceURLProtocolWithPort} from '../../util/fixUrl' 3 | import {CreateQueryTransport} from '../implSelector' 4 | import * as grpc from '@grpc/grpc-js' 5 | 6 | export const createTransport: CreateQueryTransport = ({host, timeout}) => { 7 | const {url, safe} = replaceURLProtocolWithPort(host) 8 | const channelCredentials = 9 | grpc.credentials[safe ?? true ? 'createSsl' : 'createInsecure']() 10 | 11 | return new GrpcTransport({host: url, channelCredentials, timeout}) 12 | } 13 | -------------------------------------------------------------------------------- /packages/client/src/impl/version.ts: -------------------------------------------------------------------------------- 1 | export const CLIENT_LIB_VERSION = '1.1.0' 2 | export const CLIENT_LIB_USER_AGENT = `influxdb3-js/${CLIENT_LIB_VERSION}` 3 | -------------------------------------------------------------------------------- /packages/client/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors' 2 | export * from './results' 3 | export * from './options' 4 | export * from './transport' 5 | export * from './util/logger' 6 | export * from './util/escape' 7 | export * from './util/time' 8 | export * from './util/generics' 9 | export {collectAll, isNumber} from './util/common' 10 | export * from './Point' 11 | export * from './PointValues' 12 | export {default as InfluxDBClient} from './InfluxDBClient' 13 | export {TimeConverter} from './WriteApi' 14 | export {QParamType} from './QueryApi' 15 | -------------------------------------------------------------------------------- /packages/client/src/options.ts: -------------------------------------------------------------------------------- 1 | import {Transport} from './transport' 2 | import {QParamType} from './QueryApi' 3 | 4 | /** 5 | * Option for the communication with InfluxDB server. 6 | */ 7 | export interface ConnectionOptions { 8 | /** base host URL */ 9 | host: string 10 | /** authentication token */ 11 | token?: string 12 | /** token authentication scheme. Not set for Cloud access, set to 'Bearer' for Edge. */ 13 | authScheme?: string 14 | /** 15 | * socket timeout. 10000 milliseconds by default in node.js. Not applicable in browser (option is ignored). 16 | * @defaultValue 10000 17 | */ 18 | timeout?: number 19 | /** 20 | * stream timeout for query (grpc timeout). The gRPC doesn't apply the socket timeout to operations as is defined above. To successfully close a call to the gRPC endpoint, the queryTimeout must be specified. Without this timeout, a gRPC call might end up in an infinite wait state. 21 | * @defaultValue 60000 22 | */ 23 | queryTimeout?: number 24 | /** 25 | * default database for write query if not present as argument. 26 | */ 27 | database?: string 28 | /** 29 | * TransportOptions supply extra options for the transport layer, they differ between node.js and browser/deno. 30 | * Node.js transport accepts options specified in {@link https://nodejs.org/api/http.html#http_http_request_options_callback | http.request } or 31 | * {@link https://nodejs.org/api/https.html#https_https_request_options_callback | https.request }. For example, an `agent` property can be set to 32 | * {@link https://www.npmjs.com/package/proxy-http-agent | setup HTTP/HTTPS proxy }, {@link https://nodejs.org/api/tls.html#tls_tls_connect_options_callback | rejectUnauthorized } 33 | * property can disable TLS server certificate verification. Additionally, 34 | * {@link https://github.com/follow-redirects/follow-redirects | follow-redirects } property can be also specified 35 | * in order to follow redirects in node.js. 36 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch | fetch } is used under the hood in browser/deno. 37 | * For example, 38 | * {@link https://developer.mozilla.org/en-US/docs/Web/API/fetch | redirect } property can be set to 'error' to abort request if a redirect occurs. 39 | */ 40 | transportOptions?: {[key: string]: any} 41 | /** 42 | * Default HTTP headers to send with every request. 43 | */ 44 | headers?: Record 45 | /** 46 | * Full HTTP web proxy URL including schema, for example http://your-proxy:8080. 47 | */ 48 | proxyUrl?: string 49 | } 50 | 51 | /** default connection options */ 52 | export const DEFAULT_ConnectionOptions: Partial = { 53 | timeout: 10000, 54 | queryTimeout: 60000, 55 | } 56 | 57 | /** 58 | * Options used by {@link InfluxDBClient.default.write} . 59 | * 60 | * @example WriteOptions in write call 61 | * ```typescript 62 | * client 63 | * .write(point, DATABASE, 'cpu', { 64 | * headers: { 65 | * 'channel-lane': 'reserved', 66 | * 'notify-central': '30m', 67 | * }, 68 | * precision: 'ns', 69 | * gzipThreshold: 1000, 70 | * }) 71 | * ``` 72 | */ 73 | export interface WriteOptions { 74 | /** Precision to use in writes for timestamp. default ns */ 75 | precision?: WritePrecision 76 | /** HTTP headers that will be sent with every write request */ 77 | //headers?: {[key: string]: string} 78 | headers?: Record 79 | /** When specified, write bodies larger than the threshold are gzipped */ 80 | gzipThreshold?: number 81 | /** default tags 82 | * 83 | * @example Default tags using client config 84 | * ```typescript 85 | * const client = new InfluxDBClient({ 86 | * host: 'https://eu-west-1-1.aws.cloud2.influxdata.com', 87 | * writeOptions: { 88 | * defaultTags: { 89 | * device: 'nrdc-th-52-fd889e03', 90 | * }, 91 | * }, 92 | * }) 93 | * 94 | * const p = Point.measurement('measurement').setField('num', 3) 95 | * 96 | * // this will write point with device=device-a tag 97 | * await client.write(p, 'my-db') 98 | * ``` 99 | * 100 | * @example Default tags using writeOptions argument 101 | * ```typescript 102 | * const client = new InfluxDBClient({ 103 | * host: 'https://eu-west-1-1.aws.cloud2.influxdata.com', 104 | * }) 105 | * 106 | * const defaultTags = { 107 | * device: 'rpi5_0_0599e8d7', 108 | * } 109 | * 110 | * const p = Point.measurement('measurement').setField('num', 3) 111 | * 112 | * // this will write point with device=device-a tag 113 | * await client.write(p, 'my-db', undefined, {defaultTags}) 114 | * ``` 115 | */ 116 | defaultTags?: {[key: string]: string} 117 | } 118 | 119 | /** default writeOptions */ 120 | export const DEFAULT_WriteOptions: WriteOptions = { 121 | precision: 'ns', 122 | gzipThreshold: 1000, 123 | } 124 | 125 | export type QueryType = 'sql' | 'influxql' 126 | 127 | /** 128 | * Options used by {@link InfluxDBClient.default.query} and by {@link InfluxDBClient.default.queryPoints}. 129 | * 130 | * @example QueryOptions in queryCall 131 | * ```typescript 132 | * const data = client.query('SELECT * FROM drive', 'ev_onboard_45ae770c', { 133 | * type: 'sql', 134 | * headers: { 135 | * 'one-off': 'totl', // one-off query header 136 | * 'change-on': 'shift1', // over-write universal value 137 | * }, 138 | * params: { 139 | * point: 'a7', 140 | * action: 'reverse', 141 | * }, 142 | * }) 143 | * ``` 144 | */ 145 | export interface QueryOptions { 146 | /** Type of query being sent, e.g. 'sql' or 'influxql'.*/ 147 | type: QueryType 148 | /** Custom headers to add to the request.*/ 149 | headers?: Record 150 | /** Parameters to accompany a query using them.*/ 151 | params?: Record 152 | } 153 | 154 | /** Default QueryOptions */ 155 | export const DEFAULT_QueryOptions: QueryOptions = { 156 | type: 'sql', 157 | } 158 | 159 | /** 160 | * Options used by {@link InfluxDBClient} . 161 | */ 162 | export interface ClientOptions extends ConnectionOptions { 163 | /** supplies query options to be use with each and every query.*/ 164 | queryOptions?: Partial 165 | /** supplies and overrides default writing options.*/ 166 | writeOptions?: Partial 167 | /** specifies custom transport */ 168 | transport?: Transport 169 | } 170 | 171 | /** 172 | * Timestamp precision used in write operations. 173 | * See {@link https://docs.influxdata.com/influxdb/latest/api/#operation/PostWrite } 174 | */ 175 | export type WritePrecision = 'ns' | 'us' | 'ms' | 's' 176 | 177 | /** 178 | * Parses connection string into `ClientOptions`. 179 | * @param connectionString - connection string 180 | */ 181 | export function fromConnectionString(connectionString: string): ClientOptions { 182 | if (!connectionString) { 183 | throw Error('Connection string not set!') 184 | } 185 | const url = new URL(connectionString.trim(), 'http://localhost') // artificial base is ignored when url is absolute 186 | const options: ClientOptions = { 187 | host: 188 | connectionString.indexOf('://') > 0 189 | ? url.origin + url.pathname 190 | : url.pathname, 191 | } 192 | if (url.searchParams.has('token')) { 193 | options.token = url.searchParams.get('token') as string 194 | } 195 | if (url.searchParams.has('authScheme')) { 196 | options.authScheme = url.searchParams.get('authScheme') as string 197 | } 198 | if (url.searchParams.has('database')) { 199 | options.database = url.searchParams.get('database') as string 200 | } 201 | if (url.searchParams.has('timeout')) { 202 | options.timeout = parseInt(url.searchParams.get('timeout') as string) 203 | } 204 | if (url.searchParams.has('precision')) { 205 | if (!options.writeOptions) options.writeOptions = {} as WriteOptions 206 | options.writeOptions.precision = url.searchParams.get( 207 | 'precision' 208 | ) as WritePrecision 209 | } 210 | if (url.searchParams.has('gzipThreshold')) { 211 | if (!options.writeOptions) options.writeOptions = {} as WriteOptions 212 | options.writeOptions.gzipThreshold = parseInt( 213 | url.searchParams.get('gzipThreshold') as string 214 | ) 215 | } 216 | 217 | return options 218 | } 219 | 220 | /** 221 | * Creates `ClientOptions` from environment variables. 222 | */ 223 | export function fromEnv(): ClientOptions { 224 | if (!process.env.INFLUX_HOST) { 225 | throw Error('INFLUX_HOST variable not set!') 226 | } 227 | if (!process.env.INFLUX_TOKEN) { 228 | throw Error('INFLUX_TOKEN variable not set!') 229 | } 230 | const options: ClientOptions = { 231 | host: process.env.INFLUX_HOST.trim(), 232 | } 233 | if (process.env.INFLUX_TOKEN) { 234 | options.token = process.env.INFLUX_TOKEN.trim() 235 | } 236 | if (process.env.INFLUX_AUTH_SCHEME) { 237 | options.authScheme = process.env.INFLUX_AUTH_SCHEME.trim() 238 | } 239 | if (process.env.INFLUX_DATABASE) { 240 | options.database = process.env.INFLUX_DATABASE.trim() 241 | } 242 | if (process.env.INFLUX_TIMEOUT) { 243 | options.timeout = parseInt(process.env.INFLUX_TIMEOUT.trim()) 244 | } 245 | if (process.env.INFLUX_PRECISION) { 246 | if (!options.writeOptions) options.writeOptions = {} as WriteOptions 247 | options.writeOptions.precision = process.env 248 | .INFLUX_PRECISION as WritePrecision 249 | } 250 | if (process.env.INFLUX_GZIP_THRESHOLD) { 251 | if (!options.writeOptions) options.writeOptions = {} as WriteOptions 252 | options.writeOptions.gzipThreshold = parseInt( 253 | process.env.INFLUX_GZIP_THRESHOLD 254 | ) 255 | } 256 | 257 | return options 258 | } 259 | -------------------------------------------------------------------------------- /packages/client/src/results/Cancellable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows to cancel a running execution. 3 | */ 4 | export interface Cancellable { 5 | /** 6 | * Cancels execution. 7 | */ 8 | cancel(): void 9 | isCancelled(): boolean 10 | } 11 | -------------------------------------------------------------------------------- /packages/client/src/results/CommunicationObserver.ts: -------------------------------------------------------------------------------- 1 | import {Cancellable} from './Cancellable' 2 | /** 3 | * Type of HTTP headers. 4 | */ 5 | export type HttpHeaders = {[header: string]: string | string[] | undefined} 6 | export {HttpHeaders as Headers} 7 | 8 | /** 9 | * Informs about a start of response processing. 10 | * @param headers - response HTTP headers 11 | * @param statusCode - response status code 12 | */ 13 | export type ResponseStartedFn = ( 14 | headers: HttpHeaders, 15 | statusCode?: number 16 | ) => void 17 | 18 | /** 19 | * Observes communication with the server. 20 | */ 21 | export interface CommunicationObserver { 22 | /** 23 | * Data chunk received, can be called multiple times. 24 | * @param data - data 25 | * @returns when `false` value is returned and {@link CommunicationObserver.useResume} is defined, 26 | * future calls to `next` are paused until resume is called. 27 | */ 28 | next(data: T): void | boolean 29 | /** 30 | * Communication ended with an error. 31 | */ 32 | error(error: Error): void 33 | /** 34 | * Communication was successful. 35 | */ 36 | complete(): void 37 | /** 38 | * Informs about a start of response processing. 39 | */ 40 | responseStarted?: ResponseStartedFn 41 | /** 42 | * Setups cancelllable for this communication. 43 | */ 44 | useCancellable?: (cancellable: Cancellable) => void 45 | /** 46 | * Setups a callback that resumes reading of next data, it is called whenever 47 | * {@link CommunicationObserver.next} returns `false`. 48 | * 49 | * @param resume - a function that will resume reading of next data when called 50 | */ 51 | useResume?: (resume: () => void) => void 52 | } 53 | -------------------------------------------------------------------------------- /packages/client/src/results/chunkCombiner.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ChunkCombiner is a simplified platform-neutral manipulation of Uint8arrays 3 | * that allows to process text data on the fly. The implementation can be optimized 4 | * for the target platform (node vs browser). 5 | */ 6 | export interface ChunkCombiner { 7 | /** 8 | * Concatenates first and second chunk. 9 | * @param first - first chunk 10 | * @param second - second chunk 11 | * @returns first + second 12 | */ 13 | concat(first: Uint8Array, second: Uint8Array): Uint8Array 14 | 15 | /** 16 | * Converts chunk into a string. 17 | * @param chunk - chunk 18 | * @param start - start index 19 | * @param end - end index 20 | * @returns string representation of chunk slice 21 | */ 22 | toUtf8String(chunk: Uint8Array, start: number, end: number): string 23 | 24 | /** 25 | * Creates a new chunk from the supplied chunk. 26 | * @param chunk - chunk to copy 27 | * @param start - start index 28 | * @param end - end index 29 | * @returns a copy of a chunk slice 30 | */ 31 | copy(chunk: Uint8Array, start: number, end: number): Uint8Array 32 | } 33 | 34 | // TextDecoder is available since node v8.3.0 and in all modern browsers 35 | declare class TextDecoder { 36 | constructor(encoding: string) 37 | decode(chunk: Uint8Array): string 38 | } 39 | 40 | /** 41 | * Creates a chunk combiner instance that uses UTF-8 42 | * TextDecoder to decode Uint8Arrays into strings. 43 | */ 44 | export function createTextDecoderCombiner(): ChunkCombiner { 45 | const decoder = new TextDecoder('utf-8') 46 | return { 47 | concat(first: Uint8Array, second: Uint8Array): Uint8Array { 48 | const retVal = new Uint8Array(first.length + second.length) 49 | retVal.set(first) 50 | retVal.set(second, first.length) 51 | return retVal 52 | }, 53 | copy(chunk: Uint8Array, start: number, end: number): Uint8Array { 54 | const retVal = new Uint8Array(end - start) 55 | retVal.set(chunk.subarray(start, end)) 56 | return retVal 57 | }, 58 | toUtf8String(chunk: Uint8Array, start: number, end: number): string { 59 | return decoder.decode(chunk.subarray(start, end)) 60 | }, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/client/src/results/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chunkCombiner' 2 | export * from './Cancellable' 3 | export * from './CommunicationObserver' 4 | -------------------------------------------------------------------------------- /packages/client/src/transport.ts: -------------------------------------------------------------------------------- 1 | import {CommunicationObserver, ResponseStartedFn} from './results' 2 | /** 3 | * Options for sending a request message. 4 | */ 5 | export interface SendOptions { 6 | /** HTTP method (POST, PUT, GET, PATCH ...) */ 7 | method: string 8 | /** Request HTTP headers. */ 9 | headers?: {[key: string]: string} 10 | /** When specified, message body larger than the treshold is gzipped */ 11 | gzipThreshold?: number 12 | /** Abort signal */ 13 | signal?: AbortSignal 14 | } 15 | 16 | /** 17 | * Simpified platform-neutral transport layer for communication with InfluxDB. 18 | */ 19 | export interface Transport { 20 | /** 21 | * Send data to the server and receive communication events via callbacks. 22 | * 23 | * @param path - HTTP request path 24 | * @param requestBody - HTTP request body 25 | * @param options - send options 26 | * @param callbacks - communication callbacks to received data in Uint8Array 27 | */ 28 | send( 29 | path: string, 30 | requestBody: string, 31 | options: SendOptions, 32 | callbacks?: Partial> 33 | ): void 34 | 35 | /** 36 | * Sends data to the server and receives decoded result. The type of the result depends on 37 | * response's content-type (deserialized json, text). 38 | 39 | * @param path - HTTP request path 40 | * @param requestBody - request body 41 | * @param options - send options 42 | * @returns response data 43 | */ 44 | request( 45 | path: string, 46 | requestBody: any, 47 | options: SendOptions, 48 | responseStarted?: ResponseStartedFn 49 | ): Promise 50 | 51 | /** 52 | * Sends requestBody and returns response chunks in an async iterable 53 | * that can be easily consumed in an `for-await` loop. 54 | * 55 | * @param path - HTTP request path 56 | * @param requestBody - request body 57 | * @param options - send options 58 | * @returns async iterable 59 | */ 60 | iterate( 61 | path: string, 62 | requestBody: any, 63 | options: SendOptions 64 | ): AsyncIterableIterator 65 | } 66 | -------------------------------------------------------------------------------- /packages/client/src/util/TypeCasting.ts: -------------------------------------------------------------------------------- 1 | import {Field} from 'apache-arrow' 2 | import {isNumber, isUnsignedNumber} from './common' 3 | import {Type as ArrowType} from 'apache-arrow/enum' 4 | 5 | /** 6 | * Function to cast value return base on metadata from InfluxDB. 7 | * 8 | * @param field the Field object from Arrow 9 | * @param value the value to cast 10 | * @return the value with the correct type 11 | */ 12 | export function getMappedValue(field: Field, value: any): any { 13 | if (value === null || value === undefined) { 14 | return null 15 | } 16 | 17 | const metaType = field.metadata.get('iox::column::type') 18 | 19 | if (!metaType || field.typeId === ArrowType.Timestamp) { 20 | return value 21 | } 22 | 23 | const [, , valueType, _fieldType] = metaType.split('::') 24 | 25 | if (valueType === 'field') { 26 | switch (_fieldType) { 27 | case 'integer': 28 | if (isNumber(value)) { 29 | return parseInt(value) 30 | } 31 | console.warn(`Value ${value} is not an integer`) 32 | return value 33 | case 'uinteger': 34 | if (isUnsignedNumber(value)) { 35 | return parseInt(value) 36 | } 37 | console.warn(`Value ${value} is not an unsigned integer`) 38 | return value 39 | case 'float': 40 | if (isNumber(value)) { 41 | return parseFloat(value) 42 | } 43 | console.warn(`Value ${value} is not a float`) 44 | return value 45 | case 'boolean': 46 | if (typeof value === 'boolean') { 47 | return value 48 | } 49 | console.warn(`Value ${value} is not a boolean`) 50 | return value 51 | case 'string': 52 | if (typeof value === 'string') { 53 | return String(value) 54 | } 55 | console.warn(`Value ${value} is not a string`) 56 | return value 57 | default: 58 | return value 59 | } 60 | } 61 | 62 | return value 63 | } 64 | -------------------------------------------------------------------------------- /packages/client/src/util/common.ts: -------------------------------------------------------------------------------- 1 | type Defined = Exclude 2 | 3 | /** 4 | * allows to throw error as expression 5 | */ 6 | export const throwReturn = (err: Error): Defined => { 7 | throw err 8 | } 9 | 10 | export const isDefined = (value: T): value is Defined => 11 | value !== undefined 12 | 13 | export const isArrayLike = (value: any): value is ArrayLike => 14 | value instanceof Array || 15 | (value instanceof Object && 16 | typeof value.length === 'number' && 17 | (value.length === 0 || 18 | Object.getOwnPropertyNames(value).some((x) => x === '0'))) 19 | 20 | export const createInt32Uint8Array = (value: number): Uint8Array => { 21 | const bytes = new Uint8Array(4) 22 | bytes[0] = value >> (8 * 0) 23 | bytes[1] = value >> (8 * 1) 24 | bytes[2] = value >> (8 * 2) 25 | bytes[3] = value >> (8 * 3) 26 | return bytes 27 | } 28 | 29 | export const collectAll = async ( 30 | generator: AsyncGenerator 31 | ): Promise => { 32 | const results: T[] = [] 33 | for await (const value of generator) { 34 | results.push(value) 35 | } 36 | return results 37 | } 38 | 39 | /** 40 | * Check if an input value is a valid number. 41 | * 42 | * @param value - The value to check 43 | * @returns Returns true if the value is a valid number else false 44 | */ 45 | export const isNumber = (value?: number | string | null): boolean => { 46 | if (value === null || undefined) { 47 | return false 48 | } 49 | 50 | if ( 51 | typeof value === 'string' && 52 | (value === '' || value.indexOf(' ') !== -1) 53 | ) { 54 | return false 55 | } 56 | 57 | return value !== '' && !isNaN(Number(value?.toString())) 58 | } 59 | 60 | /** 61 | * Check if an input value is a valid unsigned number. 62 | * 63 | * @param value - The value to check 64 | * @returns Returns true if the value is a valid unsigned number else false 65 | */ 66 | export const isUnsignedNumber = (value?: number | string | null): boolean => { 67 | if (!isNumber(value)) { 68 | return false 69 | } 70 | 71 | if (typeof value === 'string') { 72 | return Number(value) >= 0 73 | } 74 | 75 | return typeof value === 'number' && value >= 0 76 | } 77 | -------------------------------------------------------------------------------- /packages/client/src/util/escape.ts: -------------------------------------------------------------------------------- 1 | function createEscaper( 2 | characters: string, 3 | replacements: string[] 4 | ): (value: string) => string { 5 | return function (value: string): string { 6 | let retVal = '' 7 | let from = 0 8 | let i = 0 9 | while (i < value.length) { 10 | const found = characters.indexOf(value[i]) 11 | if (found >= 0) { 12 | retVal += value.substring(from, i) 13 | retVal += replacements[found] 14 | from = i + 1 15 | } 16 | i++ 17 | } 18 | if (from == 0) { 19 | return value 20 | } else if (from < value.length) { 21 | retVal += value.substring(from, value.length) 22 | } 23 | return retVal 24 | } 25 | } 26 | function createQuotedEscaper( 27 | characters: string, 28 | replacements: string[] 29 | ): (value: string) => string { 30 | const escaper = createEscaper(characters, replacements) 31 | return (value: string): string => `"${escaper(value)}"` 32 | } 33 | 34 | /** 35 | * Provides functions escape specific parts in InfluxDB line protocol. 36 | */ 37 | export const escape = { 38 | /** 39 | * Measurement escapes measurement names. 40 | */ 41 | measurement: createEscaper(', \n\r\t', ['\\,', '\\ ', '\\n', '\\r', '\\t']), 42 | /** 43 | * Quoted escapes quoted values, such as database names. 44 | */ 45 | quoted: createQuotedEscaper('"\\', ['\\"', '\\\\']), 46 | 47 | /** 48 | * TagEscaper escapes tag keys, tag values, and field keys. 49 | */ 50 | tag: createEscaper(', =\n\r\t', ['\\,', '\\ ', '\\=', '\\n', '\\r', '\\t']), 51 | } 52 | -------------------------------------------------------------------------------- /packages/client/src/util/fixUrl.ts: -------------------------------------------------------------------------------- 1 | const HTTP_PREFIX = 'http://' 2 | const HTTPS_PREFIX = 'https://' 3 | 4 | /** 5 | * replaceURLProtocolWithPort removes the "http://" or "https://" protocol from the given URL and replaces it with the port number. 6 | * Currently, Apache Arrow does not support the "http://" or "https://" protocol in the URL, so this function is used to remove it. 7 | * If a port number is already present in the URL, only the protocol is removed. 8 | * The function also returns a boolean value indicating whether the communication is safe or unsafe. 9 | * - If the URL starts with "https://", the communication is considered safe, and the returned boolean value will be true. 10 | * - If the URL starts with "http://", the communication is considered unsafe, and the returned boolean value will be false. 11 | * - If the URL does not start with either "http://" or "https://", the returned boolean value will be undefined. 12 | * 13 | * @param url - The URL to process. 14 | * @returns An object containing the modified URL with the protocol replaced by the port and a boolean value indicating the safety of communication (true for safe, false for unsafe) or undefined if not detected. 15 | */ 16 | export const replaceURLProtocolWithPort = ( 17 | url: string 18 | ): {url: string; safe: boolean | undefined} => { 19 | url = url.replace(/\/$/, '') 20 | 21 | let safe: boolean | undefined 22 | 23 | if (url.startsWith(HTTP_PREFIX)) { 24 | url = url.slice(HTTP_PREFIX.length) 25 | safe = false 26 | 27 | if (!url.includes(':')) { 28 | url = `${url}:80` 29 | } 30 | } else if (url.startsWith(HTTPS_PREFIX)) { 31 | url = url.slice(HTTPS_PREFIX.length) 32 | safe = true 33 | 34 | if (!url.includes(':')) { 35 | url = `${url}:443` 36 | } 37 | } 38 | 39 | return {url, safe} 40 | } 41 | -------------------------------------------------------------------------------- /packages/client/src/util/generics.ts: -------------------------------------------------------------------------------- 1 | import {Point} from '../Point' 2 | import {isArrayLike, isDefined} from './common' 3 | 4 | /** 5 | * The `WritableData` type represents different types of data that can be written. 6 | * The data can either be a uniform ArrayLike collection or a single value of the following types: 7 | * 8 | * - `Point`: Represents a {@link Point} object. 9 | * 10 | * - `string`: Represents lines of the [Line Protocol](https://bit.ly/2QL99fu). 11 | */ 12 | export type WritableData = ArrayLike | ArrayLike | string | Point 13 | 14 | export const writableDataToLineProtocol = ( 15 | data: WritableData, 16 | defaultTags?: {[key: string]: string} 17 | ): string[] => { 18 | const arrayData = ( 19 | isArrayLike(data) && typeof data !== 'string' 20 | ? Array.from(data as any) 21 | : [data] 22 | ) as string[] | Point[] 23 | if (arrayData.length === 0) return [] 24 | 25 | const isLine = typeof arrayData[0] === 'string' 26 | 27 | return isLine 28 | ? (arrayData as string[]) 29 | : (arrayData as Point[]) 30 | .map((p) => p.toLineProtocol(undefined, defaultTags)) 31 | .filter(isDefined) 32 | } 33 | -------------------------------------------------------------------------------- /packages/client/src/util/logger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Logging interface. 3 | */ 4 | export interface Logger { 5 | error(message: string, err?: any): void 6 | warn(message: string, err?: any): void 7 | } 8 | 9 | /** 10 | * Logger that logs to console.out 11 | */ 12 | export const consoleLogger: Logger = { 13 | error(message, error) { 14 | // eslint-disable-next-line no-console 15 | console.error(`ERROR: ${message}`, error ? error : '') 16 | }, 17 | warn(message, error) { 18 | // eslint-disable-next-line no-console 19 | console.warn(`WARN: ${message}`, error ? error : '') 20 | }, 21 | } 22 | let provider: Logger = consoleLogger 23 | 24 | export const Log: Logger = { 25 | error(message, error) { 26 | provider.error(message, error) 27 | }, 28 | warn(message, error) { 29 | provider.warn(message, error) 30 | }, 31 | } 32 | 33 | /** 34 | * Sets custom logger. 35 | * @param logger - logger to use 36 | * @returns previous logger 37 | */ 38 | export function setLogger(logger: Logger): Logger { 39 | const previous = provider 40 | provider = logger 41 | return previous 42 | } 43 | -------------------------------------------------------------------------------- /packages/client/src/util/sql.ts: -------------------------------------------------------------------------------- 1 | import {QParamType} from '../QueryApi' 2 | import {throwReturn} from './common' 3 | 4 | const rgxParam = /\$(\w+)/g 5 | export function queryHasParams(query: string): boolean { 6 | return !!query.match(rgxParam) 7 | } 8 | 9 | export function allParamsMatched( 10 | query: string, 11 | qParams: Record 12 | ): boolean { 13 | const matches = query.match(rgxParam) 14 | 15 | if (matches) { 16 | for (const match of matches) { 17 | if (!qParams[match.trim().replace('$', '')]) { 18 | throwReturn( 19 | new Error( 20 | `No parameter matching ${match} provided in the query params map` 21 | ) 22 | ) 23 | } 24 | } 25 | } 26 | return true 27 | } 28 | -------------------------------------------------------------------------------- /packages/client/src/util/time.ts: -------------------------------------------------------------------------------- 1 | import {WritePrecision} from '../options' 2 | 3 | declare let process: any 4 | const zeroPadding = '000000000' 5 | let useHrTime = false 6 | 7 | export function useProcessHrtime(use: boolean): boolean { 8 | /* istanbul ignore else */ 9 | if (!process.env.BUILD_BROWSER) { 10 | return (useHrTime = use && process && typeof process.hrtime === 'function') 11 | } else { 12 | return false 13 | } 14 | } 15 | useProcessHrtime(true) // preffer node 16 | 17 | let startHrMillis: number | undefined = undefined 18 | let startHrTime: [number, number] | undefined = undefined 19 | let lastMillis = Date.now() 20 | let stepsInMillis = 0 21 | function nanos(): string { 22 | if (!process.env.BUILD_BROWSER && useHrTime) { 23 | const hrTime = process.hrtime() as [number, number] 24 | let millis = Date.now() 25 | if (!startHrTime) { 26 | startHrTime = hrTime 27 | startHrMillis = millis 28 | } else { 29 | hrTime[0] = hrTime[0] - startHrTime[0] 30 | hrTime[1] = hrTime[1] - startHrTime[1] 31 | // istanbul ignore next "cannot mock system clock, manually reviewed" 32 | if (hrTime[1] < 0) { 33 | hrTime[0] -= 1 34 | hrTime[1] += 1000_000_000 35 | } 36 | millis = 37 | (startHrMillis as number) + 38 | hrTime[0] * 1000 + 39 | Math.floor(hrTime[1] / 1000_000) 40 | } 41 | const nanos = String(hrTime[1] % 1000_000) 42 | return String(millis) + zeroPadding.substr(0, 6 - nanos.length) + nanos 43 | } else { 44 | const millis = Date.now() 45 | if (millis !== lastMillis) { 46 | lastMillis = millis 47 | stepsInMillis = 0 48 | } else { 49 | stepsInMillis++ 50 | } 51 | const nanos = String(stepsInMillis) 52 | return String(millis) + zeroPadding.substr(0, 6 - nanos.length) + nanos 53 | } 54 | } 55 | 56 | function micros(): string { 57 | if (!process.env.BUILD_BROWSER && useHrTime) { 58 | const hrTime = process.hrtime() as [number, number] 59 | const micros = String(Math.trunc(hrTime[1] / 1000) % 1000) 60 | return ( 61 | String(Date.now()) + zeroPadding.substr(0, 3 - micros.length) + micros 62 | ) 63 | } else { 64 | return String(Date.now()) + zeroPadding.substr(0, 3) 65 | } 66 | } 67 | function millis(): string { 68 | return String(Date.now()) 69 | } 70 | function seconds(): string { 71 | return String(Math.floor(Date.now() / 1000)) 72 | } 73 | 74 | /** 75 | * Exposes functions that creates strings that represent a timestamp that 76 | * can be used in the line protocol. Micro and nano timestamps are emulated 77 | * depending on the js platform in use. 78 | */ 79 | export const currentTime = { 80 | s: seconds as () => string, 81 | ms: millis as () => string, 82 | us: micros as () => string, 83 | ns: nanos as () => string, 84 | seconds: seconds as () => string, 85 | millis: millis as () => string, 86 | micros: micros as () => string, 87 | nanos: nanos as () => string, 88 | } 89 | 90 | /** 91 | * dateToProtocolTimestamp provides converters for JavaScript Date to InfluxDB Write Protocol Timestamp. Keys are supported precisions. 92 | */ 93 | export const dateToProtocolTimestamp = { 94 | s: (d: Date): string => `${Math.floor(d.getTime() / 1000)}`, 95 | ms: (d: Date): string => `${d.getTime()}`, 96 | us: (d: Date): string => `${d.getTime()}000`, 97 | ns: (d: Date): string => `${d.getTime()}000000`, 98 | } 99 | 100 | /** 101 | * convertTimeToNanos converts Point's timestamp to a string. 102 | * @param value - supported timestamp value 103 | * @returns line protocol value 104 | */ 105 | export function convertTimeToNanos( 106 | value: string | number | Date | undefined 107 | ): string | undefined { 108 | if (value === undefined) { 109 | return nanos() 110 | } else if (typeof value === 'string') { 111 | return value.length > 0 ? value : undefined 112 | } else if (value instanceof Date) { 113 | return `${value.getTime()}000000` 114 | } else if (typeof value === 'number') { 115 | return String(Math.floor(value)) 116 | } else { 117 | return String(value) 118 | } 119 | } 120 | 121 | export const convertTime = ( 122 | value: string | number | Date | undefined, 123 | precision: WritePrecision = 'ns' 124 | ): string | undefined => { 125 | if (value === undefined) { 126 | return currentTime[precision]() 127 | } else if (typeof value === 'string') { 128 | return value.length > 0 ? value : undefined 129 | } else if (value instanceof Date) { 130 | return dateToProtocolTimestamp[precision](value) 131 | } else if (typeof value === 'number') { 132 | return String(Math.floor(value)) 133 | } else { 134 | return String(value) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /packages/client/test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "rules": { 6 | "@typescript-eslint/no-unused-expressions": "warn" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/client/test/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | export default [{ 4 | languageOptions: { 5 | globals: { 6 | ...globals.node, 7 | }, 8 | }, 9 | 10 | rules: { 11 | "@typescript-eslint/no-unused-expressions": "warn", 12 | }, 13 | }]; -------------------------------------------------------------------------------- /packages/client/test/integration/queryAPI.test.ts: -------------------------------------------------------------------------------- 1 | import {expect, assert} from 'chai' 2 | import {InfluxDBClient, QueryOptions, DEFAULT_QueryOptions} from '../../src' 3 | import {MockService, TestServer} from '../TestServer' 4 | ;(BigInt.prototype as any).toJSON = function () { 5 | return this.toString() 6 | } 7 | 8 | const grpcVersion: string = 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports 10 | require('../../../../node_modules/@grpc/grpc-js/package.json').version 11 | 12 | const USER_AGENT = `grpc-node-js/${grpcVersion}` 13 | 14 | describe('query api tests', () => { 15 | let server: TestServer 16 | before('start server', async () => { 17 | server = new TestServer() 18 | await server.start() 19 | }) 20 | beforeEach('reset server', async () => { 21 | MockService.resetAll() 22 | }) 23 | after('stop server', async () => { 24 | await server.shutdown() 25 | }) 26 | it('sends a query', async () => { 27 | const client: InfluxDBClient = new InfluxDBClient({ 28 | host: `http://localhost:${server.port}`, 29 | token: 'TEST_TOKEN', 30 | database: 'CI_TEST', 31 | }) 32 | const query = `SELECT * FROM wumpus` 33 | const data = client.query(query, 'CI_TEST') 34 | try { 35 | await data.next() 36 | } catch (e: any) { 37 | assert.fail(`failed to get next data value from test server: ${e}`) 38 | } 39 | expect(MockService.callCount.doGet).to.equal(1) 40 | const doGetMap = MockService.callMeta.get(MockService.genCallId('doGet', 1)) 41 | expect(doGetMap?.get('user-agent')?.toString()).to.equal(USER_AGENT) 42 | expect(doGetMap?.get('authorization')?.toString()).to.equal( 43 | 'Bearer TEST_TOKEN' 44 | ) 45 | const ticket = MockService.getCallTicketDecoded( 46 | MockService.genCallId('doGet', 1) 47 | ) 48 | expect(ticket).to.deep.equal({ 49 | database: 'CI_TEST', 50 | sql_query: 'SELECT * FROM wumpus', 51 | query_type: 'sql', 52 | params: {}, 53 | }) 54 | }) 55 | it('sends a query with options', async () => { 56 | const client: InfluxDBClient = new InfluxDBClient({ 57 | host: `http://localhost:${server.port}`, 58 | token: 'TEST_TOKEN', 59 | database: 'CI_TEST', 60 | headers: { 61 | extra: 'yes', 62 | }, 63 | }) 64 | const qOpts: QueryOptions = { 65 | ...DEFAULT_QueryOptions, 66 | headers: { 67 | formula: 'x16', 68 | 'channel-pref': 'h3', 69 | }, 70 | } 71 | const query = `SELECT * FROM wumpus` 72 | const data = client.query(query, 'CI_TEST', qOpts) 73 | try { 74 | await data.next() 75 | } catch (e: any) { 76 | assert.fail(`failed to get next data value from test server: ${e}`) 77 | } 78 | expect(MockService.callCount.doGet).to.equal(1) 79 | const doGetMap = MockService.callMeta.get(MockService.genCallId('doGet', 1)) 80 | expect(doGetMap?.get('user-agent')?.toString()).to.equal(USER_AGENT) 81 | expect(doGetMap?.get('authorization')?.toString()).to.equal( 82 | 'Bearer TEST_TOKEN' 83 | ) 84 | expect(doGetMap?.get('extra')?.toString()).to.equal('yes') 85 | expect(doGetMap?.get('formula')?.toString()).to.equal('x16') 86 | expect(doGetMap?.get('channel-pref')?.toString()).to.equal('h3') 87 | const ticket = MockService.getCallTicketDecoded( 88 | MockService.genCallId('doGet', 1) 89 | ) 90 | expect(ticket).to.deep.equal({ 91 | database: 'CI_TEST', 92 | sql_query: 'SELECT * FROM wumpus', 93 | query_type: 'sql', 94 | params: {}, 95 | }) 96 | }) 97 | it('uses all header options on query', async () => { 98 | const client = new InfluxDBClient({ 99 | host: `http://localhost:${server.port}`, 100 | token: 'TEST_TOKEN', 101 | database: 'CI_TEST', 102 | headers: { 103 | extra: 'yes', // universal header - shared by write and query APIs 104 | }, 105 | queryOptions: { 106 | headers: { 107 | special: 'super', // universal query header 108 | 'change-it': 'caspar', // universal to be overwitten by call 109 | }, 110 | }, 111 | }) 112 | const data = client.query('SELECT * from wumpus', 'CI_TEST', { 113 | type: 'sql', 114 | headers: { 115 | 'one-off': 'top of the league', // one-off query header 116 | 'change-it': 'balthazar', // over-write universal value 117 | }, 118 | }) 119 | try { 120 | // N.B. remember, it's a generator and next must be called to yield response 121 | await data.next() 122 | } catch (e: any) { 123 | assert.fail(`failed to get next data value from test server: ${e}`) 124 | } 125 | expect(MockService.callCount.doGet).to.equal(1) 126 | const extra = MockService.getCallMeta( 127 | MockService.genCallId('doGet', 1), 128 | 'extra' 129 | )?.toString() 130 | const special = MockService.getCallMeta( 131 | MockService.genCallId('doGet', 1), 132 | 'special' 133 | )?.toString() 134 | const oneOff = MockService.getCallMeta( 135 | MockService.genCallId('doGet', 1), 136 | 'one-off' 137 | )?.toString() 138 | const changeIt = MockService.getCallMeta( 139 | MockService.genCallId('doGet', 1), 140 | 'change-it' 141 | )?.toString() 142 | expect(extra).to.equal('yes') 143 | expect(special).to.equal('super') 144 | expect(oneOff).to.equal('top of the league') 145 | expect(changeIt).to.equal('balthazar') 146 | }) 147 | it('sends a query with headers and params', async () => { 148 | const client: InfluxDBClient = new InfluxDBClient({ 149 | host: `http://localhost:${server.port}`, 150 | token: 'TEST_TOKEN', 151 | database: 'CI_TEST', 152 | headers: { 153 | extra: 'yes', 154 | }, 155 | queryOptions: { 156 | headers: { 157 | special: 'super', 158 | }, 159 | params: { 160 | ecrivain: 'E_ZOLA', 161 | acteur: 'R_NAVARRE', 162 | }, 163 | }, 164 | }) 165 | const query = 166 | 'SELECT * FROM wumpus WHERE "writer" = $ecrivain AND "painter" = $peintre AND "actor" = $acteur' 167 | const data = client.query(query, 'CI_TEST', { 168 | type: 'sql', 169 | headers: { 170 | 'one-off': 'top of the league', // one-off query header 171 | 'change-it': 'balthazar', // over-write universal value 172 | }, 173 | params: { 174 | peintre: 'F_LEGER', 175 | acteur: 'A_ARTAUD', 176 | }, 177 | }) 178 | try { 179 | await data.next() 180 | } catch (e: any) { 181 | assert.fail(`failed to get next data value from test server: ${e}`) 182 | } 183 | expect(MockService.callCount.doGet).to.equal(1) 184 | const ticket = MockService.getCallTicketDecoded( 185 | MockService.genCallId('doGet', 1) 186 | ) 187 | expect(ticket).to.deep.equal({ 188 | database: 'CI_TEST', 189 | sql_query: query, 190 | params: { 191 | ecrivain: 'E_ZOLA', 192 | acteur: 'A_ARTAUD', 193 | peintre: 'F_LEGER', 194 | }, 195 | query_type: 'sql', 196 | }) 197 | const extra = MockService.getCallMeta( 198 | MockService.genCallId('doGet', 1), 199 | 'extra' 200 | )?.toString() 201 | const special = MockService.getCallMeta( 202 | MockService.genCallId('doGet', 1), 203 | 'special' 204 | )?.toString() 205 | const auth = MockService.getCallMeta( 206 | MockService.genCallId('doGet', 1), 207 | 'authorization' 208 | )?.toString() 209 | const agent = MockService.getCallMeta( 210 | MockService.genCallId('doGet', 1), 211 | 'user-agent' 212 | )?.toString() 213 | expect(extra).to.equal('yes') 214 | expect(special).to.equal('super') 215 | expect(auth).to.equal('Bearer TEST_TOKEN') 216 | expect(agent).to.equal(USER_AGENT) 217 | }) 218 | it('sends a query with influxql type', async () => { 219 | const client: InfluxDBClient = new InfluxDBClient({ 220 | host: `http://localhost:${server.port}`, 221 | token: 'TEST_TOKEN', 222 | database: 'CI_TEST', 223 | queryOptions: { 224 | type: 'influxql', 225 | }, 226 | }) 227 | const data = client.query('SELECT * FROM wumpus', 'CI_TEST') 228 | try { 229 | await data.next() 230 | } catch (e: any) { 231 | assert.fail(`failed to get next data value from test server: ${e}`) 232 | } 233 | expect(MockService.callCount.doGet).to.equal(1) 234 | const ticket = MockService.getCallTicketDecoded( 235 | MockService.genCallId('doGet', 1) 236 | ) 237 | expect(ticket).to.deep.equal({ 238 | database: 'CI_TEST', 239 | sql_query: 'SELECT * FROM wumpus', 240 | query_type: 'influxql', 241 | params: {}, 242 | }) 243 | }) 244 | }) 245 | -------------------------------------------------------------------------------- /packages/client/test/unit/Query.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import QueryApiImpl, {TicketDataType} from '../../src/impl/QueryApiImpl' 3 | import {ConnectionOptions, DEFAULT_QueryOptions} from '../../src/options' 4 | import {Ticket} from '../../src/generated/flight/Flight' 5 | import {QParamType} from '../../src/QueryApi' 6 | import {allParamsMatched, queryHasParams} from '../../src/util/sql' 7 | import {RpcMetadata} from '@protobuf-ts/runtime-rpc' 8 | import {CLIENT_LIB_VERSION} from '../../src/impl/version' 9 | 10 | const testSQLTicket = { 11 | db: 'TestDB', 12 | query: 'select *', 13 | } 14 | 15 | describe('Query', () => { 16 | it('creates a basic ticket', () => { 17 | const ticketData: TicketDataType = { 18 | database: testSQLTicket.db, 19 | sql_query: testSQLTicket.query, 20 | query_type: 'sql', 21 | } 22 | 23 | const ticket = Ticket.create({ 24 | ticket: new TextEncoder().encode(JSON.stringify(ticketData)), 25 | }) 26 | 27 | expect(ticket).to.not.equal(null) 28 | 29 | const ticketDecode = JSON.parse(new TextDecoder().decode(ticket.ticket)) 30 | 31 | expect(ticketDecode).to.deep.equal(ticketData) 32 | expect(ticketDecode.database).to.equal(ticketData.database) 33 | expect(ticketDecode.sql_query).to.equal(ticketData.sql_query) 34 | expect(ticketDecode.query_type).to.equal(ticketData.query_type) 35 | }) 36 | it('creates a basic ticket with params', () => { 37 | const ticketData: TicketDataType = { 38 | database: testSQLTicket.db, 39 | sql_query: testSQLTicket.query, 40 | query_type: 'sql', 41 | } 42 | const queryParams: Map = new Map() 43 | queryParams.set('foo', 'bar') 44 | queryParams.set('fooInt', 42) 45 | queryParams.set('fooFloat', Math.PI) 46 | queryParams.set('fooTrue', true) 47 | const paramHolder: {[name: string]: QParamType | undefined} = {} 48 | for (const key of queryParams.keys()) { 49 | if (queryParams.get(key)) { 50 | paramHolder[key] = queryParams.get(key) 51 | } 52 | } 53 | ticketData['params'] = paramHolder as { 54 | [name: string]: QParamType | undefined 55 | } 56 | 57 | const ticket = Ticket.create({ 58 | ticket: new TextEncoder().encode(JSON.stringify(ticketData)), 59 | }) 60 | 61 | expect(ticket).not.to.equal(null) 62 | const ticketDecode = JSON.parse(new TextDecoder().decode(ticket.ticket)) 63 | 64 | expect(ticketDecode).to.deep.equal(ticketData) 65 | }) 66 | it('matches all params', () => { 67 | const query = 'SELECT a, b, c FROM my_table WHERE id = $id AND name=$_name' 68 | expect(queryHasParams('select * ')).to.be.false 69 | expect(queryHasParams(query)).to.be.true 70 | const queryParams: Record = {} 71 | queryParams['id'] = 42 72 | queryParams['_name'] = 'Zaphrod' 73 | expect(allParamsMatched(query, queryParams)).to.be.true 74 | }) 75 | it('throws error on missing param', () => { 76 | const query = 'SELECT a, b, c FROM my_table WHERE id = $id AND name=$_name' 77 | expect(queryHasParams(query)).to.be.true 78 | const queryParams: Record = {} 79 | queryParams['id'] = 42 80 | queryParams['_key'] = 'Zaphrod' 81 | expect(() => { 82 | allParamsMatched(query, queryParams) 83 | }).to.throw('No parameter matching $_name provided in the query params map') 84 | }) 85 | it('ignores params for query without params', () => { 86 | const query = `SELECT a, b, c FROM my_table WHERE a = '123' AND c='chat'` 87 | expect(queryHasParams(query)).to.be.false 88 | const queryParams: Record = {} 89 | queryParams['b'] = 42 90 | queryParams['c'] = 'Zaphrod' 91 | expect(allParamsMatched(query, queryParams)).to.be.true 92 | }) 93 | it('prepares a ticket', async () => { 94 | const options: ConnectionOptions = { 95 | host: 'http://localhost:8086', 96 | token: 'TEST_TOKEN', 97 | } 98 | const qApi = new QueryApiImpl(options) 99 | const ticket: Ticket = qApi.prepareTicket( 100 | 'TEST_DB', 101 | 'SELECT * FROM cpu', 102 | DEFAULT_QueryOptions 103 | ) 104 | const decoder = new TextDecoder() 105 | expect(JSON.parse(decoder.decode(ticket.ticket))).to.deep.equal({ 106 | database: 'TEST_DB', 107 | sql_query: 'SELECT * FROM cpu', 108 | query_type: 'sql', 109 | }) 110 | }) 111 | it('prepares a ticket with params', async () => { 112 | const options: ConnectionOptions = { 113 | host: 'http://localhost:8086', 114 | token: 'TEST_TOKEN', 115 | } 116 | const qApi = new QueryApiImpl(options) 117 | const ticket = qApi.prepareTicket( 118 | 'TEST_DB', 119 | 'SELECT * FROM cpu WHERE "reg" = $reg', 120 | { 121 | ...DEFAULT_QueryOptions, 122 | params: { 123 | reg: 'CX', 124 | }, 125 | } 126 | ) 127 | const decoder = new TextDecoder() 128 | expect(JSON.parse(decoder.decode(ticket.ticket))).to.deep.equal({ 129 | database: 'TEST_DB', 130 | sql_query: 'SELECT * FROM cpu WHERE "reg" = $reg', 131 | query_type: 'sql', 132 | params: { 133 | reg: 'CX', 134 | }, 135 | }) 136 | }) 137 | it('has correct default headers', async () => { 138 | const qApi = new QueryApiImpl({ 139 | host: 'http://localost:8086', 140 | token: 'TEST_TOKEN', 141 | } as ConnectionOptions) 142 | const meta: RpcMetadata = qApi.prepareMetadata() 143 | expect(meta['User-Agent']).to.equal(`influxdb3-js/${CLIENT_LIB_VERSION}`) 144 | expect(meta['authorization']).to.equal('Bearer TEST_TOKEN') 145 | }) 146 | it('sets header metadata in request', async () => { 147 | const options: ConnectionOptions = { 148 | host: 'http://localhost:8086', 149 | token: 'TEST_TOKEN', 150 | } 151 | const qApi = new QueryApiImpl(options) 152 | const testMeta: Record = { 153 | route: 'CZ66', 154 | } 155 | const meta: RpcMetadata = qApi.prepareMetadata(testMeta) 156 | expect(meta['authorization']).to.equal('Bearer TEST_TOKEN') 157 | expect(meta['route']).to.equal('CZ66') 158 | }) 159 | it('gets header metadata from config', async () => { 160 | const options: ConnectionOptions = { 161 | host: 'http://localhost:8086', 162 | token: 'TEST_TOKEN', 163 | headers: { 164 | hunter: 'Herbie Hancock', 165 | feeder: 'Jefferson Airplane', 166 | }, 167 | } 168 | const qApi = new QueryApiImpl(options) 169 | const meta: RpcMetadata = qApi.prepareMetadata() 170 | expect(meta['authorization']).to.equal('Bearer TEST_TOKEN') 171 | expect(meta['hunter']).to.equal('Herbie Hancock') 172 | expect(meta['feeder']).to.equal('Jefferson Airplane') 173 | }) 174 | it('prefers request header metadata to config', async () => { 175 | const options: ConnectionOptions = { 176 | host: 'http://localhost:8086', 177 | token: 'TEST_TOKEN', 178 | headers: { 179 | hunter: 'Maori', 180 | feeder: 'Lewis Carol', 181 | }, 182 | } 183 | const qApi = new QueryApiImpl(options) 184 | const testMeta: Record = { 185 | hunter: 'Herbie Hancock', 186 | feeder: 'Jefferson Airplane', 187 | } 188 | const meta: RpcMetadata = qApi.prepareMetadata(testMeta) 189 | expect(meta['authorization']).to.equal('Bearer TEST_TOKEN') 190 | expect(meta['hunter']).to.equal('Herbie Hancock') 191 | expect(meta['feeder']).to.equal('Jefferson Airplane') 192 | }) 193 | }) 194 | -------------------------------------------------------------------------------- /packages/client/test/unit/errors.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import * as http from 'http' 3 | import { 4 | HttpError, 5 | RequestTimedOutError, 6 | AbortError, 7 | IllegalArgumentError, 8 | } from '../../src' 9 | 10 | describe('errors', () => { 11 | describe('have standard error properties', () => { 12 | const pairs: {error: Error; name: string}[] = [ 13 | {error: new HttpError(200, 'OK'), name: 'HttpError'}, 14 | {error: new IllegalArgumentError('Not OK'), name: 'IllegalArgumentError'}, 15 | {error: new RequestTimedOutError(), name: 'RequestTimedOutError'}, 16 | {error: new AbortError(), name: 'AbortError'}, 17 | ] 18 | pairs.forEach(({error, name}) => { 19 | describe(`${name}`, () => { 20 | it('has descriptive name property', () => { 21 | expect(error.name).equals(name) 22 | }) 23 | it('has message property', () => { 24 | expect(error.message).is.not.empty 25 | }) 26 | it('has expected toString', () => { 27 | expect(error.toString()).matches(new RegExp(`^${name}:.*`)) 28 | }) 29 | }) 30 | }) 31 | }) 32 | describe('message property is defined', () => { 33 | it('verifies message properties', () => { 34 | expect(new HttpError(200, 'OK').message).is.not.empty 35 | expect(new IllegalArgumentError('Not OK').message).is.not.empty 36 | expect(new RequestTimedOutError().message).is.not.empty 37 | expect(new AbortError().message).is.not.empty 38 | }) 39 | }) 40 | describe('HttpError message property is correct', () => { 41 | it('verifies Cloud error message', () => { 42 | expect( 43 | new HttpError( 44 | 400, 45 | 'Bad Request', 46 | '{"message": "parsing failed for write_lp endpoint"}' 47 | ).message 48 | ).equals('parsing failed for write_lp endpoint') 49 | }) 50 | it('verifies Edge error without detail message', () => { 51 | expect( 52 | new HttpError( 53 | 400, 54 | 'Bad Request', 55 | '{"error": "parsing failed for write_lp endpoint"}' 56 | ).message 57 | ).equals('parsing failed for write_lp endpoint') 58 | }) 59 | it('verifies Edge error with detail message', () => { 60 | expect( 61 | new HttpError( 62 | 400, 63 | 'Bad Request', 64 | '{"error": "parsing failed for write_lp endpoint", "data": {"error_message": "invalid field value in line protocol for field \'value\' on line 0"}}' // Edge 65 | ).message 66 | ).equals( 67 | "invalid field value in line protocol for field 'value' on line 0" 68 | ) 69 | }) 70 | }) 71 | describe('http error values', () => { 72 | it('propagate headers', () => { 73 | const httpHeaders: http.IncomingHttpHeaders = { 74 | 'content-type': 'application/json', 75 | 'retry-after': '42', 76 | } 77 | const httpError: HttpError = new HttpError( 78 | 429, 79 | 'Too Many Requests', 80 | undefined, 81 | httpHeaders['content-type'], 82 | httpHeaders 83 | ) 84 | expect(httpError.headers).is.not.empty 85 | expect(httpError.contentType).equals('application/json') 86 | expect(httpError.statusMessage).equals('Too Many Requests') 87 | if (httpError.headers) { 88 | expect(httpError.headers['retry-after']).equals('42') 89 | } else { 90 | expect.fail('httpError.headers should be defined') 91 | } 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /packages/client/test/unit/impl/browser/emulateBrowser.ts: -------------------------------------------------------------------------------- 1 | import {HttpError} from '../../../../src' 2 | 3 | interface ResponseSpec { 4 | headers?: {[key: string]: string} 5 | status?: number 6 | body?: string | Uint8Array | Array 7 | } 8 | 9 | function createResponse({ 10 | headers = {}, 11 | status = 200, 12 | body = '', 13 | }: ResponseSpec): any { 14 | const retVal: any = { 15 | status, 16 | statusText: `X${status}X`, 17 | headers: { 18 | get(key: string): string | undefined { 19 | return headers[key] ?? headers[key.toLowerCase()] 20 | }, 21 | forEach(fn: (value: string, key: string) => void): void { 22 | Object.keys(headers).forEach((key: string) => { 23 | fn(headers[key], key) 24 | if (key === 'duplicate') { 25 | fn(`${headers[key]}2`, key) 26 | fn(`${headers[key]}3`, key) 27 | } 28 | }) 29 | }, 30 | }, 31 | json(): Promise { 32 | if (typeof body === 'string') { 33 | if (body === 'error') return Promise.reject(new Error('error data')) 34 | return Promise.resolve(body).then((body) => 35 | body ? JSON.parse(body) : '' 36 | ) 37 | } else { 38 | return Promise.reject(new Error(`String body expected, but ${body}`)) 39 | } 40 | }, 41 | } 42 | if (typeof body === 'string') { 43 | retVal.text = function (): Promise { 44 | if (body === 'error') return Promise.reject(new Error('error data')) 45 | return Promise.resolve(body) 46 | } 47 | } 48 | if (body instanceof Uint8Array) { 49 | retVal.arrayBuffer = function (): Promise { 50 | return Promise.resolve( 51 | body.buffer.slice(body.byteOffset, body.byteOffset + body.length) 52 | ) 53 | } 54 | } 55 | if (Array.isArray(body)) { 56 | retVal.body = { 57 | cancel() {}, 58 | getReader(): any { 59 | let position = 0 60 | return { 61 | read(): Promise { 62 | if (position < body.length) { 63 | return Promise.resolve({ 64 | value: body[position++], 65 | done: false, 66 | }) 67 | } else { 68 | return Promise.resolve({ 69 | value: undefined, 70 | done: true, 71 | }) 72 | } 73 | }, 74 | cancel(_msg = '') { 75 | /* read cancelled with an optional message*/ 76 | }, 77 | } 78 | }, 79 | } 80 | } 81 | return retVal 82 | } 83 | 84 | let beforeEmulation: 85 | | {fetch: any; abortController: any; textEncoder: any} 86 | | undefined 87 | 88 | export class AbortController { 89 | private _listeners: Array<() => void> = [] 90 | signal = { 91 | aborted: false, 92 | addEventListener: (type: string, listener: () => void): void => { 93 | this._listeners.push(listener) 94 | }, 95 | } 96 | constructor(aborted = false) { 97 | this.signal.aborted = aborted 98 | } 99 | abort(): void { 100 | this.signal.aborted = true 101 | this._listeners.forEach((x) => x()) 102 | } 103 | 104 | getSignal(): AbortSignal { 105 | return this.signal as unknown as AbortSignal 106 | } 107 | } 108 | 109 | export function emulateFetchApi( 110 | spec: ResponseSpec, 111 | onRequest?: (options: any) => void 112 | ): void { 113 | function fetch(url: string, options: any): Promise { 114 | if (onRequest) onRequest(options) 115 | return url.endsWith('error') 116 | ? Promise.reject( 117 | new HttpError(500, undefined, undefined, undefined, undefined, url) 118 | ) 119 | : Promise.resolve(createResponse(spec)) 120 | } 121 | class TextEncoder { 122 | encode(s: string): Uint8Array { 123 | return Buffer.from(s) 124 | } 125 | } 126 | const globalVars = global as any 127 | if (!beforeEmulation) { 128 | beforeEmulation = { 129 | fetch: globalVars.fetch, 130 | abortController: globalVars.AbortController, 131 | textEncoder: globalVars.TextEncoder, 132 | } 133 | } 134 | globalVars.fetch = fetch 135 | globalVars.AbortController = AbortController 136 | globalVars.TextEncoder = TextEncoder 137 | } 138 | export function removeFetchApi(): void { 139 | if (beforeEmulation) { 140 | const {fetch, abortController, textEncoder} = beforeEmulation 141 | beforeEmulation = undefined 142 | const globalVars = global as any 143 | globalVars.fetch = fetch 144 | globalVars.abortController = abortController 145 | globalVars.textEncoder = textEncoder 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/client/test/unit/options.test.ts: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon' 2 | import {expect} from 'chai' 3 | import {ClientOptions, fromConnectionString} from '../../src' 4 | 5 | describe('ClientOptions', () => { 6 | afterEach(() => { 7 | sinon.restore() 8 | }) 9 | 10 | describe('constructor with connection string', () => { 11 | it('with empty', () => { 12 | expect(() => fromConnectionString('')).to.throw( 13 | 'Connection string not set!' 14 | ) 15 | }) 16 | it('is created with relative URL with token (#213)', () => { 17 | expect( 18 | fromConnectionString('/influx?token=my-token') as ClientOptions 19 | ).to.deep.equal({ 20 | host: '/influx', 21 | token: 'my-token', 22 | }) 23 | }) 24 | it('is created with relative URL with token + whitespace around (#213)', () => { 25 | expect( 26 | fromConnectionString(' /influx?token=my-token ') as ClientOptions 27 | ).to.deep.equal({ 28 | host: '/influx', 29 | token: 'my-token', 30 | }) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /packages/client/test/unit/results/chunkCombiner.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {createTextDecoderCombiner} from '../../../src' 3 | 4 | describe('createTextDecoderCombiner', () => { 5 | const pureJsChunkCombiner = createTextDecoderCombiner() 6 | it('concatenates chunks', () => { 7 | expect( 8 | pureJsChunkCombiner.concat( 9 | Uint8Array.from([1, 2]), 10 | Uint8Array.from([3, 4]) 11 | ) 12 | ).is.deep.equal(Uint8Array.from([1, 2, 3, 4])) 13 | expect( 14 | pureJsChunkCombiner.concat(Uint8Array.from([]), Uint8Array.from([3, 4])) 15 | ).is.deep.equal(Uint8Array.from([3, 4])) 16 | }) 17 | it('copies chunks', () => { 18 | const src = Uint8Array.from([1, 2]) 19 | const copy = pureJsChunkCombiner.copy(src, 1, 2) 20 | expect(copy).is.deep.equal(Uint8Array.from([2])) 21 | src[1] = 3 22 | expect(copy[0]).is.equal(2) 23 | }) 24 | // see examples in https://en.wikipedia.org/wiki/UTF-8 25 | const chunks = [ 26 | ...[0, 1, 2, 3, 4, 5, 6, 7].map((num) => [ 27 | String.fromCharCode(num << 4), 28 | Uint8Array.from([num << 4]), 29 | ]), 30 | ['$', Uint8Array.from([0x24])], 31 | ['\u{A2}', Uint8Array.from([0xc2, 0xa2])], 32 | ['\u{4FF}', Uint8Array.from([0xd3, 0xbf])], 33 | ['\u{939}', Uint8Array.from([0xe0, 0xa4, 0xb9])], 34 | ['\u{10348}', Uint8Array.from([0xf0, 0x90, 0x8d, 0x88])], 35 | ] 36 | chunks.forEach(([str, chunk]) => { 37 | it(`utf-8 encodes chunk ${JSON.stringify(str)}`, () => { 38 | const encoded = pureJsChunkCombiner.toUtf8String( 39 | chunk as Uint8Array, 40 | 0, 41 | chunk.length 42 | ) 43 | expect(encoded).equals(str) 44 | }) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /packages/client/test/unit/util/common.test.ts: -------------------------------------------------------------------------------- 1 | import {isNumber} from '../../../src' 2 | import {expect} from 'chai' 3 | import {isUnsignedNumber} from '../../../src/util/common' 4 | 5 | describe('Test functions in common', () => { 6 | const pairs: Array<{value: any; expect: boolean}> = [ 7 | {value: 1, expect: true}, 8 | {value: -1, expect: true}, 9 | {value: -1.2, expect: true}, 10 | {value: '-1.2', expect: true}, 11 | {value: '2', expect: true}, 12 | {value: 'a', expect: false}, 13 | {value: 'true', expect: false}, 14 | {value: '', expect: false}, 15 | {value: ' ', expect: false}, 16 | {value: '32a', expect: false}, 17 | {value: '32 ', expect: false}, 18 | {value: null, expect: false}, 19 | {value: undefined, expect: false}, 20 | {value: NaN, expect: false}, 21 | ] 22 | pairs.forEach((pair) => { 23 | it(`check if ${pair.value} is a valid number`, () => { 24 | expect(isNumber(pair.value)).to.equal(pair.expect) 25 | }) 26 | }) 27 | 28 | const pairs1: Array<{value: any; expect: boolean}> = [ 29 | {value: 1, expect: true}, 30 | {value: 1.2, expect: true}, 31 | {value: '1.2', expect: true}, 32 | {value: '2', expect: true}, 33 | {value: -2.3, expect: false}, 34 | {value: '-2.3', expect: false}, 35 | {value: 'a', expect: false}, 36 | {value: 'true', expect: false}, 37 | {value: '', expect: false}, 38 | {value: ' ', expect: false}, 39 | {value: '32a', expect: false}, 40 | {value: '32 ', expect: false}, 41 | {value: null, expect: false}, 42 | {value: undefined, expect: false}, 43 | {value: NaN, expect: false}, 44 | ] 45 | 46 | pairs1.forEach((pair) => { 47 | it(`check if ${pair.value} is a valid unsigned number`, () => { 48 | expect(isUnsignedNumber(pair.value)).to.equal(pair.expect) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/client/test/unit/util/generics.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {Point} from '../../../src' 3 | import { 4 | WritableData, 5 | writableDataToLineProtocol, 6 | } from '../../../src/util/generics' 7 | 8 | describe('writableDataToLineProtocol', () => { 9 | it('should convert single line of line protocol', () => { 10 | const lp = 'measurement val=0' 11 | const output = writableDataToLineProtocol(lp) 12 | expect(output).to.deep.equal([lp]) 13 | }) 14 | 15 | it('should convert array-like lines to line protocol', () => { 16 | const lp1 = 'measurement val=0' 17 | const lp2 = 'measurement val=1' 18 | const lp3 = 'measurement val=2' 19 | const input: WritableData = {0: lp1, 1: lp2, 2: lp3, length: 3} 20 | const output = writableDataToLineProtocol(input) 21 | expect(output).to.deep.equal([lp1, lp2, lp3]) 22 | }) 23 | 24 | it('should convert single Point to line protocol', () => { 25 | const point = Point.measurement('test').setFloatField('blah', 123.6) 26 | const output = writableDataToLineProtocol(point) 27 | expect(output.length).to.equal(1) 28 | expect(output[0]).satisfies((x: string) => { 29 | return x.startsWith('test blah=123.6') 30 | }, `does not start with 'test blah=123.6'`) 31 | }) 32 | 33 | it('should convert array-like Point to line protocol', () => { 34 | const point1 = Point.measurement('test').setFloatField('blah', 123.6) 35 | const date = Date.now() 36 | const point2 = Point.measurement('test') 37 | .setFloatField('blah', 456.7) 38 | .setTimestamp(date) 39 | const point3 = Point.measurement('test') 40 | .setFloatField('blah', 789.8) 41 | .setTimestamp('') 42 | const input: WritableData = [point1, point2, point3] 43 | const output = writableDataToLineProtocol(input) 44 | expect(output.length).to.equal(3) 45 | expect(output[0]).satisfies((x: string) => { 46 | return x.startsWith('test blah=123.6') 47 | }, `does not start with 'test blah=123.6'`) 48 | expect(output[1]).to.equal(`test blah=456.7 ${date}`) 49 | expect(output[2]).to.equal('test blah=789.8') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/client/test/unit/util/logger.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {Log, setLogger, consoleLogger} from '../../../src' 3 | 4 | describe('Logger', () => { 5 | ;[{message: ' hey', error: 'you'}, {message: ' hey'}].forEach( 6 | (data) => { 7 | it(`uses custom logger's error (${Object.keys(data).length})`, () => { 8 | let args: Array | undefined 9 | setLogger({ 10 | error(message, error): void { 11 | // eslint-disable-next-line prefer-rest-params 12 | args = Array.from(arguments) 13 | consoleLogger.error(message, error) 14 | }, 15 | warn(message, error): void { 16 | consoleLogger.warn(message, error) 17 | }, 18 | }) 19 | Log.error.call(Log, data.message, data.error) 20 | expect(args).to.be.deep.equal([data.message, data.error]) 21 | }) 22 | it(`uses custom logger's warn (${Object.keys(data).length})`, () => { 23 | let args: Array | undefined 24 | setLogger({ 25 | error(message, error): void { 26 | consoleLogger.error(message, error) 27 | }, 28 | warn(message, error): void { 29 | // eslint-disable-next-line prefer-rest-params 30 | args = Array.from(arguments) 31 | consoleLogger.warn(message, error) 32 | }, 33 | }) 34 | 35 | Log.warn.call(Log, data.message, data.error) 36 | expect(args).to.be.deep.equal([data.message, data.error]) 37 | }) 38 | } 39 | ) 40 | }) 41 | -------------------------------------------------------------------------------- /packages/client/test/unit/util/time.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {convertTime, convertTimeToNanos, useProcessHrtime} from '../../../src' 3 | import sinon from 'sinon' 4 | 5 | describe('time/convertTime', () => { 6 | let hrtimeStub: sinon.SinonSpy 7 | let dateNowStub: sinon.SinonStub | undefined 8 | 9 | beforeEach(function () { 10 | hrtimeStub = sinon.spy(process, 'hrtime') 11 | }) 12 | 13 | afterEach(function () { 14 | hrtimeStub.restore() 15 | if (dateNowStub) { 16 | dateNowStub.restore() 17 | dateNowStub = undefined 18 | } 19 | useProcessHrtime(true) 20 | }) 21 | 22 | it(`it uses hrtime based on settings`, () => { 23 | useProcessHrtime(false) 24 | convertTimeToNanos(undefined) 25 | convertTime(undefined, 'us') 26 | 27 | expect(hrtimeStub.called).to.be.false 28 | 29 | useProcessHrtime(true) 30 | convertTimeToNanos(undefined) 31 | convertTime(undefined, 'us') 32 | 33 | expect(hrtimeStub.called).to.be.true 34 | }) 35 | 36 | it('uses right converter', () => { 37 | const date = new Date(1_000) 38 | 39 | expect(convertTime(date, 's')).to.equal('1') 40 | expect(convertTime(date, 'ms')).to.equal('1000') 41 | expect(convertTime(date, 'us')).to.equal('1000000') 42 | expect(convertTime(date, 'ns')).to.equal('1000000000') 43 | }) 44 | 45 | it('returns different time for ns if hrtime disabled', () => { 46 | dateNowStub = sinon.stub(Date, 'now').callsFake(() => 1_000_000_000_000) 47 | useProcessHrtime(false) 48 | const time1 = convertTime(undefined) 49 | const time2 = convertTime(undefined) 50 | expect(time1).to.not.equal(time2) 51 | }) 52 | 53 | it('returns current time if no date provided ', () => { 54 | dateNowStub = sinon.stub(Date, 'now').callsFake(() => 1_000) 55 | useProcessHrtime(false) 56 | 57 | expect(convertTime(undefined, 's')).to.equal('1') 58 | expect(convertTime(undefined, 'ms')).to.equal('1000') 59 | expect(convertTime(undefined, 'us')).to.equal('1000000') 60 | expect(convertTime(undefined, 'ns')).to.equal('1000000000') 61 | }) 62 | 63 | it('works in edge cases', () => { 64 | let time = convertTime(BigInt(10) as any) 65 | expect(time).to.equal('10') 66 | time = convertTime('10') 67 | expect(time).to.equal('10') 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /packages/client/test/unit/util/typeCasting.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Bool, 3 | Field, 4 | Float64, 5 | Int64, 6 | Timestamp, 7 | TimeUnit, 8 | Uint64, 9 | Utf8, 10 | } from 'apache-arrow' 11 | import {getMappedValue} from '../../../src/util/TypeCasting' 12 | import {expect} from 'chai' 13 | 14 | describe('Type casting test', () => { 15 | it('getMappedValue test', () => { 16 | // If pass the correct value type to getMappedValue() it will return the value with a correct type 17 | // If pass the incorrect value type to getMappedValue() it will NOT throws any error but return the passed value 18 | 19 | const fieldName = 'test' 20 | let field: Field 21 | 22 | field = generateIntField(fieldName) 23 | expect(getMappedValue(field, 1)).to.equal(1) 24 | expect(getMappedValue(field, 'a')).to.equal('a') 25 | 26 | field = generateUnsignedIntField(fieldName) 27 | expect(getMappedValue(field, 1)).to.equal(1) 28 | expect(getMappedValue(field, -1)).to.equal(-1) 29 | expect(getMappedValue(field, 'a')).to.equal('a') 30 | 31 | field = generateFloatField(fieldName) 32 | expect(getMappedValue(field, 1.1)).to.equal(1.1) 33 | expect(getMappedValue(field, 'a')).to.equal('a') 34 | 35 | field = generateBooleanField(fieldName) 36 | expect(getMappedValue(field, true)).to.equal(true) 37 | expect(getMappedValue(field, 'a')).to.equal('a') 38 | 39 | field = generateStringField(fieldName) 40 | expect(getMappedValue(field, 'a')).to.equal('a') 41 | expect(getMappedValue(field, true)).to.equal(true) 42 | 43 | field = generateTimeStamp(fieldName) 44 | const nowNanoSecond = Date.now() * 1_000_000 45 | expect(getMappedValue(field, nowNanoSecond)).to.equal(nowNanoSecond) 46 | 47 | field = generateIntFieldTestTypeMeta(fieldName) 48 | expect(getMappedValue(field, 1)).to.equal(1) 49 | 50 | // If metadata is null return the value 51 | field = new Field(fieldName, new Int64(), true, null) 52 | expect(getMappedValue(field, 1)).to.equal(1) 53 | 54 | // If value is null return null 55 | field = new Field(fieldName, new Int64(), true, null) 56 | expect(getMappedValue(field, null)).to.equal(null) 57 | }) 58 | }) 59 | 60 | function generateIntField(name: string): Field { 61 | const map = new Map() 62 | map.set('iox::column::type', 'iox::column_type::field::integer') 63 | return new Field(name, new Int64(), true, map) 64 | } 65 | 66 | function generateUnsignedIntField(name: string): Field { 67 | const map = new Map() 68 | map.set('iox::column::type', 'iox::column_type::field::uinteger') 69 | return new Field(name, new Uint64(), true, map) 70 | } 71 | 72 | function generateFloatField(name: string): Field { 73 | const map = new Map() 74 | map.set('iox::column::type', 'iox::column_type::field::float') 75 | return new Field(name, new Float64(), true, map) 76 | } 77 | 78 | function generateStringField(name: string): Field { 79 | const map = new Map() 80 | map.set('iox::column::type', 'iox::column_type::field::string') 81 | return new Field(name, new Utf8(), true, map) 82 | } 83 | 84 | function generateBooleanField(name: string): Field { 85 | const map = new Map() 86 | map.set('iox::column::type', 'iox::column_type::field::boolean') 87 | return new Field(name, new Bool(), true, map) 88 | } 89 | 90 | function generateIntFieldTestTypeMeta(name: string): Field { 91 | const map = new Map() 92 | map.set('iox::column::type', 'iox::column_type::field::test') 93 | return new Field(name, new Int64(), true, map) 94 | } 95 | 96 | function generateTimeStamp(name: string): Field { 97 | const map = new Map() 98 | map.set('iox::column::type', 'iox::column_type::timestamp') 99 | return new Field(name, new Timestamp(TimeUnit.NANOSECOND), true, map) 100 | } 101 | -------------------------------------------------------------------------------- /packages/client/test/unit/util/utils.test.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {throwReturn, collectAll} from '../../../src/util/common' 3 | import {replaceURLProtocolWithPort} from '../../../src/util/fixUrl' 4 | 5 | describe('utils', () => { 6 | it('throwReturn throws given message', () => { 7 | const message = 'this is error message' 8 | expect(() => throwReturn(new Error(message))).to.throw(message) 9 | }) 10 | 11 | it('fixUrl adds port if not present', () => { 12 | expect(replaceURLProtocolWithPort('http://example.com')).to.deep.equal({ 13 | safe: false, 14 | url: 'example.com:80', 15 | }) 16 | expect(replaceURLProtocolWithPort('https://example.com')).to.deep.equal({ 17 | safe: true, 18 | url: 'example.com:443', 19 | }) 20 | expect(replaceURLProtocolWithPort('http://example.com:3000')).to.deep.equal( 21 | {safe: false, url: 'example.com:3000'} 22 | ) 23 | expect( 24 | replaceURLProtocolWithPort('https://example.com:5000') 25 | ).to.deep.equal({safe: true, url: 'example.com:5000'}) 26 | }) 27 | 28 | it('collectAll correctly iterate through the generator', async () => { 29 | const generator = (async function* () { 30 | yield* [4, 5, 6] 31 | })() 32 | 33 | const result = await collectAll(generator) 34 | 35 | expect(result).to.deep.equal([4, 5, 6]) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /packages/client/test/unit/util/waitForCondition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wait for the supplied `condition` to become truethy 3 | * for at most `timeout` milliseconds. The `condition` 4 | * every `step` milliseconds. 5 | * @param condition - condition to validate 6 | * @param message 7 | * @param timeout - maximum wait time 8 | * @param step - interval to validate the condition 9 | */ 10 | export async function waitForCondition( 11 | condition: () => any, 12 | message = 'timeouted', 13 | timeout = 100, 14 | step = 5 15 | ): Promise { 16 | for (;;) { 17 | await new Promise((resolve) => setTimeout(resolve, step)) 18 | timeout -= step 19 | if (timeout <= 0) { 20 | break 21 | } 22 | if (condition()) return 23 | } 24 | return Promise.reject(`WARN:waitForCondition: ${message}`) 25 | } 26 | -------------------------------------------------------------------------------- /packages/client/test/util.ts: -------------------------------------------------------------------------------- 1 | import {expect} from 'chai' 2 | import {setLogger} from '../src/util/logger' 3 | 4 | let previous: any 5 | 6 | export interface CollectedLogs { 7 | error: Array> 8 | warn: Array> 9 | reset(): void 10 | } 11 | 12 | const createCollectedLogs = (): CollectedLogs => { 13 | const collectedLogs: CollectedLogs = { 14 | error: [], 15 | warn: [], 16 | reset() { 17 | collectedLogs.error.splice(0) 18 | collectedLogs.warn.splice(0) 19 | }, 20 | } 21 | return collectedLogs 22 | } 23 | 24 | export const collectLogging = { 25 | replace(): CollectedLogs { 26 | const collectedLogs = createCollectedLogs() 27 | previous = setLogger({ 28 | error: function (...args) { 29 | collectedLogs.error.push(args) 30 | }, 31 | warn: function (...args) { 32 | collectedLogs.warn.push(args) 33 | }, 34 | }) 35 | return collectedLogs 36 | }, 37 | after(): void { 38 | if (previous) { 39 | setLogger(previous) 40 | previous = undefined 41 | } 42 | }, 43 | } 44 | 45 | let rejections: Array = [] 46 | function addRejection(e: any) { 47 | rejections.push(e) 48 | } 49 | 50 | /** 51 | * Used by unit tests to check that no unhandled promise rejection occurs. 52 | */ 53 | export const unhandledRejections = { 54 | before(): void { 55 | rejections = [] 56 | process.on('unhandledRejection', addRejection) 57 | }, 58 | after(): void { 59 | process.off('unhandledRejection', addRejection) 60 | expect(rejections, 'Unhandled Promise rejections detected').deep.equals([]) 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /packages/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "lib": ["DOM", "es2018"] 6 | }, 7 | "include": ["src/**/*.ts", "test/**/*.ts"], 8 | "exclude": ["*.js"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/client/tsup.config.browser.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup' 2 | import {esbuildGzipOutJsPlugin} from '../../scripts/esbuild-gzip-js' 3 | import {readFile} from 'fs/promises' 4 | import pkg from './package.json' 5 | 6 | const minify = !(process.env.ESBUILD_MINIFY === '0') 7 | 8 | const outFiles = { 9 | esm: pkg.exports['.'].browser.import, 10 | cjs: pkg.exports['.'].browser.require, 11 | iife: pkg.exports['.'].browser.script, 12 | } 13 | 14 | export default defineConfig({ 15 | entry: ['src/index.ts'], 16 | sourcemap: true, 17 | format: ['cjs', 'esm', 'iife'], 18 | globalName: 'influxdb', 19 | dts: false, 20 | minify, 21 | target: ['es2018'], 22 | platform: 'browser', 23 | splitting: false, 24 | define: { 25 | 'process.env.BUILD_BROWSER': 'true', 26 | }, 27 | esbuildOptions(options, {format}) { 28 | options.outdir = undefined 29 | options.outfile = outFiles[format] 30 | if (format === 'cjs') { 31 | // esbuild does not generate UMD format OOTB, see https://github.com/evanw/esbuild/issues/507 32 | // the following code is a trick to generate UMD output in place of cjs 33 | options.format = 'iife' 34 | options.banner = { 35 | js: `(function (global, factory) { 36 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 37 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 38 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["@influxdata/influxdb-client"] = {})); 39 | })(this, (function (exports) {`, 40 | } 41 | options.footer = { 42 | js: `Object.defineProperty(exports, '__esModule', { value: true });Object.assign(exports, ${options.globalName});}));`, 43 | } 44 | } 45 | }, 46 | esbuildPlugins: [ 47 | esbuildGzipOutJsPlugin, 48 | { 49 | name: 'replaceTransportImport', 50 | setup: (build) => { 51 | build.onLoad({filter: /implSelector.ts$/}, async (args) => { 52 | const source = await readFile(args.path, 'utf8') 53 | const contents = (source as unknown as string).replace( 54 | // replace all, ./node appears in comment too 55 | /.\/node/g, 56 | './browser' 57 | ) 58 | return { 59 | contents, 60 | loader: 'ts', 61 | } 62 | }) 63 | }, 64 | }, 65 | ], 66 | }) 67 | -------------------------------------------------------------------------------- /packages/client/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'tsup' 2 | import {esbuildGzipOutJsPlugin} from '../../scripts/esbuild-gzip-js' 3 | import pkg from './package.json' 4 | 5 | const minify = !(process.env.ESBUILD_MINIFY === '0') 6 | 7 | const outFiles = { 8 | esm: pkg.exports['.'].import, 9 | cjs: pkg.exports['.'].require, 10 | } 11 | export default defineConfig({ 12 | entry: ['src/index.ts'], 13 | sourcemap: true, 14 | dts: true, 15 | format: ['cjs', 'esm'], 16 | minify, 17 | target: ['es2018'], 18 | platform: 'node', 19 | splitting: false, 20 | esbuildOptions(options, {format}) { 21 | options.outdir = undefined 22 | options.outfile = outFiles[format] 23 | }, 24 | define: { 25 | 'process.env.BUILD_BROWSER': 'false', 26 | }, 27 | esbuildPlugins: [esbuildGzipOutJsPlugin], 28 | }) 29 | -------------------------------------------------------------------------------- /packages/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | export default [{ 4 | ignores: ["dist/*.js", "**/generated/"], 5 | }, { 6 | languageOptions: { 7 | globals: { 8 | ...globals.node, 9 | }, 10 | }, 11 | 12 | rules: { 13 | "no-console": "warn", 14 | }, 15 | }]; -------------------------------------------------------------------------------- /scripts/change-version.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const {readFileSync, writeFileSync} = require('fs') 3 | const VERSION = process.env.VERSION 4 | 5 | if (!VERSION) { 6 | console.error('VERSION environment variable is not defined!') 7 | process.exit(1) 8 | } 9 | 10 | const fileName = __dirname + '/../packages/client/src/impl/version.ts' 11 | const content = readFileSync(fileName, 'utf-8') 12 | const updatedVersion = content.replace( 13 | /CLIENT_LIB_VERSION = '[^']*'/, 14 | `CLIENT_LIB_VERSION = '${VERSION}'` 15 | ) 16 | writeFileSync(fileName, updatedVersion, 'utf8') 17 | -------------------------------------------------------------------------------- /scripts/cp.js: -------------------------------------------------------------------------------- 1 | // multiplatform copy command 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | const fs = require('fs') 4 | const path = require('path') 5 | 6 | function copyFiles(source, destination) { 7 | const sourcePath = path.resolve(source) 8 | const destinationPath = path.resolve(destination) 9 | 10 | if (fs.existsSync(sourcePath)) { 11 | if (fs.lstatSync(sourcePath).isDirectory()) { 12 | fs.mkdirSync(destinationPath, {recursive: true}) 13 | const files = fs.readdirSync(sourcePath) 14 | 15 | files.forEach((file) => { 16 | const srcFile = path.join(sourcePath, file) 17 | const destFile = path.join(destinationPath, file) 18 | copyFiles(srcFile, destFile) 19 | }) 20 | } else { 21 | fs.copyFileSync(sourcePath, destinationPath) 22 | } 23 | } 24 | } 25 | 26 | const sourceDir = process.argv[2] 27 | const destinationDir = process.argv[3] 28 | 29 | copyFiles(sourceDir, destinationDir) 30 | -------------------------------------------------------------------------------- /scripts/esbuild-gzip-js.ts: -------------------------------------------------------------------------------- 1 | import {mkdir, writeFile} from 'fs/promises' 2 | import {dirname} from 'path' 3 | import {promisify} from 'util' 4 | import {gzip} from 'zlib' 5 | 6 | const compressGz = promisify(gzip) 7 | 8 | export interface BuildResult { 9 | outputFiles?: Array<{path: string; contents: Uint8Array}> 10 | } 11 | /** 12 | * ESBuild onEnd callback that additionally gzips all produced JS files. 13 | */ 14 | export async function esbuildGzipOnEnd(result: BuildResult): Promise { 15 | // gzip js files 16 | await Promise.all( 17 | (result.outputFiles || []) 18 | .filter((x) => x.path.endsWith('js')) 19 | .map(async ({path, contents}) => { 20 | await mkdir(dirname(path), {recursive: true}) 21 | await writeFile(path + '.gz', await compressGz(contents, {level: 9})) 22 | }) 23 | ) 24 | } 25 | 26 | /** ESBuild plugin that gzips output js files */ 27 | export const esbuildGzipOutJsPlugin = { 28 | name: 'gzipJsFiles', 29 | setup: (build: { 30 | onEnd: (callback: (result: BuildResult) => Promise) => void 31 | }) => { 32 | build.onEnd(esbuildGzipOnEnd) 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /scripts/generate-flight.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs') 3 | const {execSync} = require('child_process') 4 | 5 | const ARROW_REPOSITORY = 'https://github.com/apache/arrow.git' 6 | // name of proto in arrow repository 7 | const FLIGHT_PROTO_NAME = 'Flight.proto' 8 | const FLIGHT_PROTO_PATH = `./format/${FLIGHT_PROTO_NAME}` 9 | const FLIGHT_TEST = process.argv[2] == 'test' 10 | 11 | // relative to: . 12 | const FLIGHTGEN_DIR = 'flightgen' 13 | const CLIENT_GENERATED_FLIGHT_DIR = './packages/client/src/generated/flight/' 14 | const TEST_SERVER_GENERATED_FLIGHT_DIR = 15 | './packages/client/test/generated/flight/' 16 | // relative to: FLIGHTGEN_DIR 17 | const OUTPUT_DIR = 'out/flight' 18 | 19 | const packageJsonContent = { 20 | scripts: { 21 | protoc: 'protoc', 22 | 'generate-protoc': `npx protoc --experimental_allow_proto3_optional --ts_out ./${OUTPUT_DIR}/ --ts_opt optimize_code_size --ts_opt server_grpc1 --proto_path . *.proto`, 23 | }, 24 | dependencies: { 25 | '@protobuf-ts/plugin': '^2.9.0', 26 | }, 27 | license: 'MIT', 28 | } 29 | 30 | const drawProgressBar = (current, total, message) => { 31 | process.stdout.clearLine() 32 | process.stdout.cursorTo(0) 33 | process.stdout.write( 34 | `[${'▇'.repeat(current).padEnd(total)}]: ${message.padEnd(60)}` 35 | ) 36 | } 37 | 38 | process.stdout.write('Generating flight client...\n') 39 | const TOTAL_STEPS = 11 40 | let stepI = 0 41 | const step = (name) => { 42 | if (process.env.CIRCLECI) { 43 | process.stdout.write( 44 | `${stepI.toString().padStart(2)}/${TOTAL_STEPS}: ${name}\n` 45 | ) 46 | } else { 47 | drawProgressBar(stepI, TOTAL_STEPS, name) 48 | } 49 | stepI++ 50 | } 51 | 52 | step('Setting up working directory') 53 | fs.rmSync(FLIGHTGEN_DIR, {recursive: true, force: true}) 54 | fs.mkdirSync(FLIGHTGEN_DIR) 55 | process.chdir(FLIGHTGEN_DIR) 56 | // ./flightgen 57 | 58 | step('Cloning Arrow repository') 59 | execSync(`git clone -n --depth 1 ${ARROW_REPOSITORY} arrow`, {stdio: []}) 60 | process.chdir('arrow') 61 | // ./flightgen/arrow 62 | 63 | step('Checking out Arrow protocol files') 64 | execSync(`git checkout main ${FLIGHT_PROTO_PATH}`, {stdio: []}) 65 | fs.copyFileSync(FLIGHT_PROTO_PATH, `../${FLIGHT_PROTO_NAME}`) 66 | 67 | process.chdir('..') 68 | // ./flightgen 69 | 70 | step('Cleaning up cloned Arrow repository') 71 | fs.rmSync('arrow', {recursive: true, force: true}) 72 | 73 | step('Creating package.json for dependencies') 74 | fs.writeFileSync('package.json', JSON.stringify(packageJsonContent, null, 2)) 75 | 76 | step('Installing necessary Node dependencies') 77 | fs.mkdirSync(OUTPUT_DIR, {recursive: true}) 78 | execSync('yarn install', {stdio: []}) 79 | 80 | step('Generating TypeScript files from proto files') 81 | execSync('yarn generate-protoc', {stdio: []}) 82 | 83 | process.chdir('..') 84 | // ./ 85 | 86 | step('Copying generated files to implementations') 87 | if (!FLIGHT_TEST) { 88 | fs.rmSync(CLIENT_GENERATED_FLIGHT_DIR, {recursive: true, force: true}) 89 | fs.mkdirSync(CLIENT_GENERATED_FLIGHT_DIR, {recursive: true}) 90 | if (process.env.CIRCLECI) { 91 | // in CIRCLECI prepare directories for test server skeleton 92 | fs.rmSync(TEST_SERVER_GENERATED_FLIGHT_DIR, {recursive: true, force: true}) 93 | fs.mkdirSync(TEST_SERVER_GENERATED_FLIGHT_DIR, {recursive: true}) 94 | } 95 | } else { 96 | fs.rmSync(TEST_SERVER_GENERATED_FLIGHT_DIR, {recursive: true, force: true}) 97 | fs.mkdirSync(TEST_SERVER_GENERATED_FLIGHT_DIR, {recursive: true}) 98 | } 99 | 100 | function copyDirRecursively(srcDir, destDir) { 101 | fs.readdirSync(`${srcDir}`).forEach((file) => { 102 | if (fs.statSync(`${srcDir}/${file}`).isDirectory()) { 103 | if (!fs.existsSync(`${destDir}/${file}`)) { 104 | fs.mkdirSync(`${destDir}/${file}`, {recursive: true}); 105 | copyDirRecursively(`${srcDir}/${file}`, `${destDir}/${file}`) 106 | } 107 | } else { 108 | const destinationFile = `${destDir}/${file}`; 109 | fs.copyFileSync(`${srcDir}/${file}`, destinationFile) 110 | step(`Correcting BigInt initialization syntax: ${destinationFile}`) 111 | const fixed = fs 112 | .readFileSync(destinationFile, 'utf8') 113 | .replace(/ 0n/g, ' BigInt(0)') 114 | fs.writeFileSync(destinationFile, fixed, 'utf8') 115 | } 116 | }) 117 | } 118 | 119 | if (!FLIGHT_TEST) { 120 | copyDirRecursively( 121 | `${FLIGHTGEN_DIR}/${OUTPUT_DIR}`, 122 | `${CLIENT_GENERATED_FLIGHT_DIR}` 123 | ) 124 | if (process.env.CIRCLECI) { 125 | // in CircleCI go ahead and gen server skeleton 126 | copyDirRecursively( 127 | `${FLIGHTGEN_DIR}/${OUTPUT_DIR}`, 128 | `${TEST_SERVER_GENERATED_FLIGHT_DIR}` 129 | ) 130 | } 131 | } else { 132 | copyDirRecursively( 133 | `${FLIGHTGEN_DIR}/${OUTPUT_DIR}`, 134 | `${TEST_SERVER_GENERATED_FLIGHT_DIR}` 135 | ) 136 | } 137 | 138 | step('Final cleanup of temporary working directory') 139 | fs.rmSync(FLIGHTGEN_DIR, {recursive: true, force: true}) 140 | 141 | step('Remove unnecessary files from respective directories') 142 | if (!FLIGHT_TEST) { 143 | fs.readdirSync(CLIENT_GENERATED_FLIGHT_DIR) 144 | .filter((f) => /.*server\.ts/.test(f)) 145 | .map((f) => fs.unlinkSync(`${CLIENT_GENERATED_FLIGHT_DIR}${f}`)) 146 | if (process.env.CIRCLECI) { 147 | // in CIRCLECI remove files not needed by server skeleton 148 | fs.readdirSync(TEST_SERVER_GENERATED_FLIGHT_DIR) 149 | .filter((f) => /.*client\.ts/.test(f)) 150 | .map((f) => fs.unlinkSync(`${TEST_SERVER_GENERATED_FLIGHT_DIR}${f}`)) 151 | } 152 | } else { 153 | fs.readdirSync(TEST_SERVER_GENERATED_FLIGHT_DIR) 154 | .filter((f) => /.*client\.ts/.test(f)) 155 | .map((f) => fs.unlinkSync(`${TEST_SERVER_GENERATED_FLIGHT_DIR}${f}`)) 156 | } 157 | step('Done!') 158 | process.stdout.write('\n') 159 | -------------------------------------------------------------------------------- /scripts/gh-pages_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal 2 | -------------------------------------------------------------------------------- /scripts/require-yarn.js: -------------------------------------------------------------------------------- 1 | // see `yarn run env` vs `npm run env` 2 | if (!/yarn\//.test(process.env.npm_config_user_agent)) { 3 | throw new Error( 4 | `Use yarn in place of '${process.env.npm_config_user_agent}' !` 5 | ) 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["es2018"], 5 | "allowJs": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "outDir": "./dist", 9 | "removeComments": false, 10 | "strict": true, 11 | "strictPropertyInitialization": false, 12 | "noUnusedLocals": true, 13 | "moduleResolution": "node", 14 | "esModuleInterop": true, 15 | "sourceMap": true, 16 | "resolveJsonModule": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "highlightLanguages": [ 3 | "javascript", 4 | "typescript", 5 | "sh", 6 | "powershell", 7 | "console", 8 | ], 9 | "entryPoints": [ 10 | "./packages/client/src/**/*.ts" 11 | ], 12 | "cleanOutputDir": true, 13 | "validation": { 14 | "notExported": false, 15 | "invalidLink": true, 16 | "rewrittenLink": false, 17 | "notDocumented": false, 18 | "unusedMergeModuleWith": false 19 | }, 20 | "blockTags": ["@param", "@returns", "@generated", "@throws", "@defaultValue", "@example"] 21 | } --------------------------------------------------------------------------------