├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── nodeapp.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── NOTICE ├── README.md ├── THIRD-PARTY-LICENSES ├── index.ts ├── package-lock.json ├── package.json ├── src ├── Communicator.ts ├── LogUtil.ts ├── QldbDriver.ts ├── QldbHash.ts ├── QldbSession.ts ├── Result.ts ├── ResultReadable.ts ├── Transaction.ts ├── TransactionExecutor.ts ├── errors │ └── Errors.ts ├── integrationtest │ ├── .mocharc.json │ ├── SessionManagement.test.ts │ ├── StatementExecution.test.ts │ ├── TestConstants.ts │ └── TestUtils.ts ├── retry │ ├── BackoffFunction.ts │ ├── DefaultRetryConfig.ts │ └── RetryConfig.ts ├── stats │ ├── IOUsage.ts │ └── TimingInformation.ts └── test │ ├── Communicator.test.ts │ ├── Errors.test.ts │ ├── QldbDriver.test.ts │ ├── QldbSession.test.ts │ ├── Result.test.ts │ ├── ResultReadable.test.ts │ ├── Transaction.test.ts │ └── TransactionExecutor.test.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/eslint-recommended" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint" 21 | ], 22 | "rules": { 23 | "no-unused-vars": "off", 24 | "no-constant-condition": [ 25 | "error", { "checkLoops": false } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "tuesday" 8 | commit-message: 9 | prefix: "npm" 10 | open-pull-requests-limit: 10 11 | target-branch: "master" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '42 16 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.github/workflows/nodeapp.yml: -------------------------------------------------------------------------------- 1 | name: Node.js Application 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | permissions: 14 | id-token: write 15 | contents: read 16 | 17 | strategy: 18 | max-parallel: 3 19 | matrix: 20 | os: [ubuntu-latest, macos-latest, windows-latest] 21 | node-version: [16.x, 18.x] 22 | 23 | steps: 24 | - name: Configure AWS Credentials 25 | uses: aws-actions/configure-aws-credentials@v4 26 | with: 27 | aws-region: us-east-1 28 | role-to-assume: arn:aws:iam::264319671630:role/GitHubActionsOidc 29 | 30 | - uses: actions/checkout@v2 31 | 32 | - name: Use Node.js ${{ matrix.node-version }} 33 | uses: actions/setup-node@v1 34 | with: 35 | node-version: ${{ matrix.node-version }} 36 | - name: Install dependencies 37 | run: npm install 38 | - name: Build & Lint 39 | run: npm run build 40 | 41 | - name: Test 42 | run: | 43 | GITHUB_SHA_SHORT=$(git rev-parse --short $GITHUB_SHA) 44 | npm test 45 | npm run integrationTest --test-ledger-suffix=${{ strategy.job-index }}-$GITHUB_SHA_SHORT 46 | shell: bash 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .nyc_output/* 3 | **/*.js 4 | **/node_modules/* 5 | dev/stress_test/*.txt 6 | docs/* 7 | coverage/* 8 | dist/* 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .eslintrc.json 3 | .nyc_output/* 4 | buildspec.yml 5 | coverage/* 6 | dev/* 7 | docs/* 8 | index.ts 9 | tsconfig.json 10 | src/* 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 3.1.0 (2023-11-10) 2 | No new features are introduced but [570](https://github.com/awslabs/amazon-qldb-driver-nodejs/pull/570) updates the peer dependency requirement `ion-js` from `^4.3.0` to `^5.2.0`. 3 | 4 | # 3.0.1 (2022-11-04) 5 | This is a minor release to incorporate a recent PR by the community: [245](https://github.com/awslabs/amazon-qldb-driver-nodejs/pull/245) 6 | 7 | ## :bug: Bug Fixes 8 | * When the driver session is not live and a new one needs to be created and returned to the callee, the session is null resulting on an error: Cannot read properties of null (reading 'executeLambda'). The problem is that session var is not getting the new session recently created and it fails by returning null. It should re-up a connection and replace the driver object with the new session to the database. 9 | 10 | 11 | # 3.0.0 (2022-09-26) 12 | All the changes are introduced by SDK V3, please check [Migrating to the AWS SDK for JavaScript V3](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/migrating-to-v3.html) to learn how to migrate to the AWS SDK for JavaScript V3 from AWS SDK for JavaScript V2. 13 | 14 | ## :tada: Enhancements 15 | * Migrated to [AWS SDK for JavasScript V3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html). 16 | 17 | ## :bug: Bug Fixes 18 | * Fixed a order of operations bug in `defaultBackoffFunction` which would add up-to 10s of sleep over 4 retries, versus less than 300 ms total sleep between 4 retries. The defaultBackoffFunction strategy is defaulted if users do not provide their own backoff strategy function for the `RetryConfig`. 19 | 20 | ## :boom: Breaking changes 21 | * Changed driver constructor to take a new type of [qldbClientOptions](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-qldb-session/classes/qldbsessionclient.html#constructor) and added a new parameter [httpOptions](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-qldb-session/interfaces/nodehttphandleroptions.html) to configure the low level node http client separately. Application code needs to be modified for driver construction. 22 | For example, the following: 23 | ```typescript 24 | import { Agent } from 'https'; 25 | import { QldbDriver, RetryConfig } from 'amazon-qldb-driver-nodejs'; 26 | 27 | const maxConcurrentTransactions: number = 10; 28 | 29 | const agentForQldb: Agent = new Agent({ 30 | keepAlive: true, 31 | maxSockets: maxConcurrentTransactions 32 | }); 33 | 34 | const serviceConfigurationOptions = { 35 | region: "us-east-1", 36 | httpOptions: { 37 | agent: agentForQldb 38 | } 39 | }; 40 | 41 | const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, maxConcurrentTransactions); 42 | ``` 43 | Should be changed to 44 | 45 | ```typescript 46 | import { Agent } from 'https'; 47 | import { QldbDriver, RetryConfig } from 'amazon-qldb-driver-nodejs'; 48 | import { NodeHttpHandlerOptions } from "@aws-sdk/node-http-handler"; 49 | 50 | const maxConcurrentTransactions: number = 10; 51 | 52 | const lowLevelClientHttpOptions: NodeHttpHandlerOptions = { 53 | httpAgent: new Agent({ 54 | keepAlive: true, 55 | maxSockets: maxConcurrentTransactions 56 | }) 57 | }; 58 | 59 | const serviceConfigurationOptions = { 60 | region: "us-east-1" 61 | }; 62 | 63 | const qldbDriver: QldbDriver = new QldbDriver("testLedger", serviceConfigurationOptions, lowLevelClientHttpOptions, maxConcurrentTransactions); 64 | ``` 65 | * Updated driver to comply with new [service exception class](https://aws.amazon.com/blogs/developer/service-error-handling-modular-aws-sdk-js/). 66 | 67 | # 2.2.0 68 | This release is focused on improving the retry logic, optimizing it and handling more possible failures, as well as more 69 | strictly defining the API to help prevent misuse. These changes are potentially breaking if the driver is being used in 70 | a way that isn't supported by the documentation. 71 | 72 | ## :tada: Enhancements 73 | * Improved retry logic 74 | * Failures when starting a new session are now retried. 75 | * Dead sessions are immediately discarded, reducing latency when using the driver. 76 | * `ClientError`, `DriverClosedError`, `LambdaAbortedError`, and `SessionPoolEmptyError` are now exported. 77 | * Peer dependency `aws-sdk` bumped to `2.841.0` or greater, which gives visibility to `CapacityExceededException`. 78 | * Updated the exponential backoff algorithm to better align with the algorithm specified [here](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/). 79 | * Specify minimum Node.js version to 10.0 in [package.json](https://github.com/awslabs/amazon-qldb-driver-nodejs/blob/master/package.json#L13). 80 | 81 | ## :warning: API Clean-up 82 | 83 | These changes either remove unintentionally exported modules or remove the use of `any`. Updating to 2.2.0 should not break any documented usage of the driver and should not require code changes. If something is now incompatible and it's believed that it should be, please open an issue. 84 | 85 | * TypeScript: updated `executeLambda` signature 86 | 87 | * ```typescript 88 | // Old 89 | async ExecuteLambda(transactionLambda: (transactionExecutor: TransactionExecutor) => any, 90 | retryConfig?: RetryConfig): Promise; 91 | // New 92 | async ExecuteLambda(transactionLambda: (transactionExecutor: TransactionExecutor) => Promise, 93 | retryConfig?: RetryConfig): Promise; 94 | ``` 95 | 96 | * The returned value from the `transactionLambda` is what `ExecuteLambda` returns, so it's now strictly defined by the `Type`. 97 | 98 | * The `transactionLambda` must return a `Promise`, as any methods called on the `TransactionExecutor` must be awaited for the driver to properly function. 99 | 100 | * JavaScript: removed `QldbDriver.getSession()` 101 | 102 | * This is unavailable to TypeScript users and was not intended to be called directly by JavaScript users. 103 | 104 | * Module exports 105 | 106 | * Removed `Transaction` from the exports list. 107 | * Removed modules being accessible when importing from `amazon-qldb-driver-nodejs/src`. 108 | * TypeScript: Removed constructors in the API for some exported types. 109 | 110 | ## :bug: Bug Fixes 111 | 112 | * Fixed a bug where `No open transaction` or `Transaction already open` errors would occur 113 | 114 | # 2.1.1 115 | * Export `ResultReadable`. 116 | * Change the return type of `executeAndStreamResults()` from `Readable` to `ResultReadable` which extends `Readable`. 117 | * `getConsumedIOs(): IOUsage` and `getTimingInformation(): TimingInformation` functions, are accessible through `ResultReadable`. 118 | 119 | # 2.1.0 120 | Add support for obtaining basic server-side statistics on individual statement executions. 121 | 122 | ## :tada: Enhancements 123 | * Added `IOUsage` and `TimingInformation` interface to provide server-side execution statistics 124 | * IOUsage provides `getReadIOs(): number` 125 | * TimingInformation provides `getProcessingTimeMilliseconds(): number` 126 | * Added `getConsumedIOs(): IOUsage` and `getTimingInformation(): TimingInformation` to the `Result` and `ResultStream` 127 | * `getConsumedIOs(): IOUsage` and `getTimingInformation(): TimingInformation` methods are stateful, meaning the statistics returned by them reflect the state at the time of method execution 128 | 129 | #### Note: For using version 2.1.0 and above of the driver, the version of the aws-sdk should be >= 2.815 130 | 131 | # 2.0.0 (2020-08-27) 132 | 133 | The release candidate 1 (v2.0.0-rc.1) has been selected as a final release of v2.0.0. No new changes are introduced between v2.0.0-rc.1 and v2.0.0. 134 | Please check the [release notes](http://github.com/awslabs/amazon-qldb-driver-nodejs/releases/tag/v2.0.0) 135 | 136 | # 2.0.0-rc.1 (2020-08-13) 137 | 138 | ***Note: This version is a release candidate. We might introduce some additional changes before releasing v2.0.0.*** 139 | 140 | ## :tada: Enchancements 141 | 142 | * Added support for defining customer retry config and backoffs. 143 | 144 | ## :boom: Breaking changes 145 | 146 | * Renamed `QldbDriver` property `poolLimit` to `maxConcurrentTransactions`. 147 | * Removed `QldbDriver` property `poolTimeout`. 148 | * Removed `retryIndicator` from `QldbSession.executeLambda` method and replaced it with `retryConfig`. 149 | * Moved `retryLimit` from `QldbDriver` constructor to `RetryConfig` constructor. 150 | 151 | * The classes and methods marked deprecated in version v1.0.0 have now been removed. List of classes and methods: 152 | 153 | * `PooledQldbDriver` has been removed. Please use `QldbDriver` instead. 154 | * `QldbSession.getTableNames` method has been removed. Please use `QldbDriver.getTableNames` method instead. 155 | * `QldbSession.executeLambda` method has been removed. Please use `QldbDriver.executeLambda` method instead. 156 | 157 | # 1.0.0 (2020-06-05) 158 | 159 | The release candidate 2 (v1.0.0-rc.2) has been selected as a final release of v1.0.0. No new changes are introduced between v1.0.0-rc.2 and v1.0.0. 160 | Please check the [release notes](http://github.com/awslabs/amazon-qldb-driver-nodejs/releases/tag/v1.0.0) 161 | 162 | # 1.0.0-rc.2 (2020-05-29) 163 | 164 | ## :tada: Enhancements 165 | 166 | * Session pooling functionality moved to QldbDriver. More details can be found in the [release notes](http://github.com/awslabs/amazon-qldb-driver-nodejs/releases/tag/v1.0.0-rc.2) 167 | 168 | ## :bug: Fixes 169 | * Fixed the delay calculation logic when retrying the transaction due to failure. 170 | 171 | ## :warning: Deprecated 172 | 173 | * `PooledQldbDriver` has been deprecated and will be removed in future versions. Please use `QldbDriver` instead. Refer to the [release notes](https://github.com/awslabs/amazon-qldb-driver-nodejs/releases/tag/v1.0.0-rc.2) 174 | 175 | * `QldbSession.getTableNames` method has been deprecated and will be removed in future versions. Please use `QldbDriver.getTableNames` method instead. 176 | 177 | * `QldbSession.executeLambda` method has been deprecated and will be removed in future versions. Please use `QldbDriver.executeLambda` method instead. 178 | 179 | # 1.0.0-rc.1 (2020-04-03) 180 | 181 | ## :boom: Breaking changes 182 | 183 | * [(#22)](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/22) `executeInline` method renamed to `execute` and `executeStream` method renamed to `executeAndStreamResults`. 184 | * [(#23)](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/23) `execute` and `executeAndStreamResults` methods accept JavaScript built-in data types(and [Ion Value data types](https://github.com/amzn/ion-js/blob/master/src/dom/README.md#iondom-data-types)) instead of `IonWriter` type. 185 | * [(#24)](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/24) `execute` and `executeAndStreamResults` method accepts variable number of arguments instead of passing an array of arguments. 186 | * [(#25)](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/25) Query results will be returned as an [Ion Value](https://github.com/amzn/ion-js/blob/master/src/dom/Value.ts) instead of an `IonReader` when running the PartiQL query via `execute` and/or `executeAndStreamResults` 187 | * Removed `executeStatement` method from Qldb Session. 188 | * Target version changed to ES6 189 | 190 | ## :tada: Enhancements 191 | 192 | * [(#5)](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/5) The Ion Value results returned by `execute` and `executeAndStreamResults` can be converted into JSON String via `JSON.stringify(result)` 193 | 194 | * [(#26)](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/26) Introduced `executeLambda` method on Qldb Driver. 195 | 196 | 197 | 198 | # 0.1.2-preview.1 (2020-03-06) 199 | 200 | ## :bug: Fixes 201 | 202 | * "Error: stream.push() after EOF" bug [#7](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/7) 203 | * On reading from ResultStream, potential event listeners might not have received an error. Error fixed by rightly calling the destroy method and passing the error to it. 204 | * On starting a transaction, on consuming the resultstream, the last value could sometimes show up twice. 205 | 206 | # 0.1.1-preview.2 (2019-12-26) 207 | 208 | ## :bug: Fix 209 | 210 | * "Digests don't match" bug [#8](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/8) 211 | 212 | ## :nut_and_bolt: Other​ 213 | 214 | * Renamed src/logUtil.ts to src/LogUtil.ts to match PascalCase. 215 | 216 | # 0.1.0-preview.2 (2019-11-12) 217 | 218 | ## :bug: Fix 219 | 220 | * Fix a bug in the test command that caused unit tests to fail compilation. 221 | 222 | ## :tada: Enhancement 223 | 224 | * Add a valid `buildspec.yml` file for running unit tests via CodeBuild. 225 | 226 | ## :book: Documentation 227 | 228 | * Small clarifications to the README. 229 | 230 | # 0.1.0-preview.1 (2019-11-08) 231 | 232 | * Preview release of the driver. 233 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon QLDB Node.js Driver 2 | 3 | [![NPM Version](https://img.shields.io/badge/npm-v3.1.0-green)](https://www.npmjs.com/package/amazon-qldb-driver-nodejs) 4 | [![Documentation](https://img.shields.io/badge/docs-api-green.svg)](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.nodejs.html) 5 | [![license](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/awslabs/amazon-qldb-driver-nodejs/blob/master/LICENSE) 6 | [![AWS Provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900)](https://aws.amazon.com/qldb/) 7 | 8 | This is the Node.js driver for [Amazon Quantum Ledger Database (QLDB)](https://aws.amazon.com/qldb/), which allows Node.js developers to write software that makes use of Amazon QLDB. 9 | 10 | For getting started with the driver, see [Node.js and Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.nodejs.html). 11 | 12 | ## Requirements 13 | 14 | ### Basic Configuration 15 | 16 | See [Accessing Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/accessing.html) for information on connecting to AWS. 17 | 18 | The JavaScript AWS SDK needs to have `AWS_SDK_LOAD_CONFIG` environment variable set to a truthy value in order to read 19 | from the `~/.aws/config` file. 20 | 21 | See [Setting Region](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-region.html) page for more information. 22 | 23 | ### TypeScript 3.8.x 24 | 25 | Development of the driver requires TypeScript 3.8.x. It will be automatically installed as a dependency. It is also recommended to use TypeScript when using the driver. 26 | Please see the link below for more detail on TypeScript 3.8.x: 27 | 28 | * [TypeScript 3.8.x](https://www.npmjs.com/package/typescript) 29 | 30 | 31 | ## Getting Started 32 | 33 | Please see the [Quickstart guide for the Amazon QLDB Driver for Node.js](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-quickstart-nodejs.html). 34 | 35 | To use the driver, in your package that wishes to use the driver, run the following: 36 | 37 | ```npm install amazon-qldb-driver-nodejs``` 38 | 39 | The driver also has @aws-sdk/client-qldb-session, ion-js and jsbi as peer dependencies. Thus, they must also be dependencies of the package that will be using the driver as a dependency. 40 | 41 | ```npm install @aws-sdk/client-qldb-session``` 42 | 43 | ```npm install ion-js``` 44 | 45 | ```npm install jsbi``` 46 | 47 | #### Note: For using version 3.0.0 and above of the driver, the version of the aws-sdk should be >= 3.x 48 | 49 | Then from within your package, you can now use the driver by importing it. This example shows usage in TypeScript specifying the QLDB ledger name and a specific region: 50 | 51 | ```typescript 52 | import { QLDBSessionClientConfig } from "@aws-sdk/client-qldb-session"; 53 | import { QldbDriver } from "amazon-qldb-driver-nodejs"; 54 | 55 | const testServiceConfigOption: QLDBSessionClientConfig = { 56 | region: "us-east-1" 57 | }; 58 | 59 | const qldbDriver: QldbDriver = new QldbDriver("testLedger", testServiceConfigOptions); 60 | qldbDriver.getTableNames().then(function(tableNames: string[]) { 61 | console.log(tableNames); 62 | }); 63 | ``` 64 | 65 | ### See Also 66 | 67 | 1. [Getting Started with Amazon QLDB Node.js Driver](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.nodejs.html): A guide that gets you started with executing transactions with the QLDB Node.js driver. 68 | 1. [QLDB Node.js Driver Cookbook](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-cookbook-nodejs.html) The cookbook provides code samples for some simple QLDB Node.js driver use cases. 69 | 1. [Amazon QLDB Node.js Driver Tutorial](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.nodejs.tutorial.html): In this tutorial, you use the QLDB Driver for Node.js to create an Amazon QLDB ledger and populate it with tables and sample data. 70 | 1. [Amazon QLDB Node.js Driver Samples](https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs): A DMV based example application which demonstrates how to use QLDB with the QLDB Driver for Node.js. 71 | 1. QLDB Node.js driver accepts and returns [Amazon ION](http://amzn.github.io/ion-docs/) Documents. Amazon Ion is a richly-typed, self-describing, hierarchical data serialization format offering interchangeable binary and text representations. For more information read the [ION docs](http://amzn.github.io/ion-docs/docs.html). 72 | 1. Amazon QLDB supports the [PartiQL](https://partiql.org/) query language. PartiQL provides SQL-compatible query access across multiple data stores containing structured data, semistructured data, and nested data. For more information read the [PartiQL docs](https://partiql.org/docs.html). 73 | 1. Refer the section [Common Errors while using the Amazon QLDB Drivers](https://docs.aws.amazon.com/qldb/latest/developerguide/driver-errors.html) which describes runtime errors that can be thrown by the Amazon QLDB Driver when calling the qldb-session APIs. 74 | 75 | 76 | 77 | 78 | 79 | ## Development 80 | 81 | ### Setup 82 | 83 | To install the dependencies for the driver, run the following in the root directory of the project: 84 | 85 | ```npm install``` 86 | 87 | To build the driver, transpiling the TypeScript source code to JavaScript, run the following in the root directory: 88 | 89 | ```npm run build``` 90 | 91 | ### Running Tests 92 | 93 | You can run the unit tests with this command: 94 | 95 | ```npm test``` 96 | 97 | or 98 | 99 | ```npm run testWithCoverage``` 100 | 101 | ### Integration Tests 102 | 103 | You can run the integration tests with this command: 104 | 105 | ```npm run integrationTest``` 106 | 107 | This command requires that credentials are pre-configured and it has the required permissions. 108 | 109 | Additionally, a region can be specified in: `src/integrationtest/.mocharc.json`. 110 | 111 | ### Documentation 112 | 113 | TypeDoc is used for documentation. You can generate HTML locally with the following: 114 | 115 | ```npm run doc``` 116 | 117 | ## Getting Help 118 | 119 | Please use these community resources for getting help. 120 | * Ask a question on StackOverflow and tag it with the [amazon-qldb](https://stackoverflow.com/questions/tagged/amazon-qldb) tag. 121 | * Open a support ticket with [AWS Support](http://docs.aws.amazon.com/awssupport/latest/user/getting-started.html). 122 | * Make a new thread at [AWS QLDB Forum](https://forums.aws.amazon.com/forum.jspa?forumID=353&start=0). 123 | * If you think you may have found a bug, please open an [issue](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues/new). 124 | 125 | ## Opening Issues 126 | 127 | If you encounter a bug with the Amazon QLDB Node.js Driver, we would like to hear about it. Please search the [existing issues](https://github.com/awslabs/amazon-qldb-driver-nodejs/issues) and see if others are also experiencing the issue before opening a new issue. When opening a new issue, we will need the version of Amazon QLDB Node.js Driver, Node.js language version, and OS you’re using. Please also include reproduction case for the issue when appropriate. 128 | 129 | The GitHub issues are intended for bug reports and feature requests. For help and questions with using AWS QLDB Node.js Driver please make use of the resources listed in the [Getting Help](https://github.com/awslabs/amazon-qldb-driver-nodejs#getting-help) section. Keeping the list of open issues lean will help us respond in a timely manner. 130 | 131 | ## License 132 | 133 | This library is licensed under the Apache 2.0 License. 134 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ClientError, 3 | DriverClosedError, 4 | LambdaAbortedError, 5 | SessionPoolEmptyError, 6 | isBadRequestException, 7 | isInvalidParameterException, 8 | isInvalidSessionException, 9 | isOccConflictException, 10 | isResourceNotFoundException, 11 | isResourcePreconditionNotMetException, 12 | isTransactionExpiredException 13 | } from "./src/errors/Errors"; 14 | export { QldbDriver } from "./src/QldbDriver"; 15 | export { Result } from "./src/Result"; 16 | export { ResultReadable } from "./src/ResultReadable"; 17 | export { TransactionExecutor } from "./src/TransactionExecutor"; 18 | export { RetryConfig } from "./src/retry/RetryConfig"; 19 | export { IOUsage } from "./src/stats/IOUsage"; 20 | export { TimingInformation } from "./src/stats/TimingInformation"; 21 | export { BackoffFunction } from "./src/retry/BackoffFunction"; 22 | export { defaultRetryConfig } from "./src/retry/DefaultRetryConfig" 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@smithy/node-http-handler": "^2.5.0", 4 | "@smithy/smithy-client": "^2.5.0", 5 | "@types/node": "^20.11.24", 6 | "ion-hash-js": "^2.0.0", 7 | "semaphore-async-await": "^1.5.1" 8 | }, 9 | "name": "amazon-qldb-driver-nodejs", 10 | "description": "The Node.js driver for working with Amazon Quantum Ledger Database", 11 | "version": "3.1.0", 12 | "logging": false, 13 | "main": "dist/index.js", 14 | "types": "dist/index.d.ts", 15 | "engines": { 16 | "node": ">=14.0.0" 17 | }, 18 | "devDependencies": { 19 | "@aws-sdk/client-qldb": "^3.350.0", 20 | "@aws-sdk/client-qldb-session": "^3.350.0", 21 | "@types/chai": "^4.3.1", 22 | "@types/chai-as-promised": "^7.1.5", 23 | "@types/mocha": "^10.0.0", 24 | "@types/sinon": "^10.0.13", 25 | "@typescript-eslint/eslint-plugin": "^5.41.0", 26 | "@typescript-eslint/parser": "^5.41.0", 27 | "chai": "^4.2.0", 28 | "chai-as-promised": "^7.1.1", 29 | "cross-env": "^7.0.3", 30 | "eslint": "^8.26.0", 31 | "eslint-plugin-jsdoc": "^39.3.24", 32 | "grunt": "^1.5.3", 33 | "ion-js": "^5.2.0", 34 | "jsbi": "^3.1.1", 35 | "mocha": "^10.0.0", 36 | "mocha-param": "^2.0.1", 37 | "nyc": "^15.1.0", 38 | "sinon": "^14.0.0", 39 | "ts-node": "^10.8.2", 40 | "typedoc": "^0.23.18", 41 | "typescript": "^4.8.4" 42 | }, 43 | "peerDependencies": { 44 | "@aws-sdk/client-qldb": "^3.256.0", 45 | "@aws-sdk/client-qldb-session": "^3.256.0", 46 | "ion-js": "^5.2.0", 47 | "jsbi": "^3.1.1" 48 | }, 49 | "overrides": { 50 | "ion-hash-js": { 51 | "ion-js": "^5.2.0" 52 | } 53 | }, 54 | "scripts": { 55 | "clean": "rm -rf node_modules", 56 | "reinstall": "npm ci", 57 | "build": "npm run lint && tsc", 58 | "doc": "typedoc --out docs ./index.ts --exclude **/*.test.ts", 59 | "installAndPack": "npm install . && npm pack && rm $npm_package_name-$npm_package_version.tgz", 60 | "lint": "eslint src/**/*.ts", 61 | "prepublishOnly": "npm run build", 62 | "test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"strict\\\":false} mocha -r ts-node/register src/test/*.test.ts", 63 | "testWithCoverage": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"strict\\\":false} nyc -r lcov -e .ts -x \"*.test.ts\" mocha -r ts-node/register src/test/*.test.ts && nyc report", 64 | "integrationTest": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"strict\\\":false} mocha -r ts-node/register src/integrationtest/*.test.ts" 65 | }, 66 | "author": { 67 | "name": "Amazon Web Services", 68 | "url": "https://aws.amazon.com/" 69 | }, 70 | "license": "Apache-2.0", 71 | "keywords": [ 72 | "api", 73 | "amazon", 74 | "aws", 75 | "qldb", 76 | "ledger" 77 | ], 78 | "repository": { 79 | "type": "git", 80 | "url": "https://github.com/awslabs/amazon-qldb-driver-nodejs" 81 | }, 82 | "nyc": { 83 | "include": [ 84 | "src" 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Communicator.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { 15 | AbortTransactionResult, 16 | CommitTransactionRequest, 17 | CommitTransactionResult, 18 | EndSessionResult, 19 | ExecuteStatementResult, 20 | FetchPageResult, 21 | QLDBSessionClient, 22 | SendCommandCommand, 23 | SendCommandResult, 24 | StartTransactionResult, 25 | ValueHolder 26 | } from "@aws-sdk/client-qldb-session"; 27 | 28 | import { inspect } from "util"; 29 | 30 | import { debug, warn } from "./LogUtil"; 31 | 32 | /** 33 | * A class representing an independent session to a QLDB ledger that handles endpoint requests. This class is used in 34 | * {@linkcode QldbDriver} and {@linkcode QldbSessionClient}. This class is not meant to be used directly by developers. 35 | * 36 | * @internal 37 | */ 38 | export class Communicator { 39 | private _qldbClient: QLDBSessionClient; 40 | private _sessionToken: string; 41 | 42 | /** 43 | * Creates a Communicator. 44 | * @param qldbClient The low level service client. 45 | * @param sessionToken The initial session token representing the session connection. 46 | */ 47 | private constructor(qldbClient: QLDBSessionClient, sessionToken: string) { 48 | this._qldbClient = qldbClient; 49 | this._sessionToken = sessionToken; 50 | } 51 | 52 | /** 53 | * Static factory method that creates a Communicator object. 54 | * @param qldbClient The low level client that communicates with QLDB. 55 | * @param ledgerName The QLDB ledger name. 56 | * @returns Promise which fulfills with a Communicator. 57 | */ 58 | static async create(qldbClient: QLDBSessionClient, ledgerName: string): Promise { 59 | const request: SendCommandCommand = new SendCommandCommand({ 60 | StartSession: { 61 | LedgerName: ledgerName 62 | } 63 | }); 64 | const result: SendCommandResult = await qldbClient.send(request); 65 | return new Communicator(qldbClient, result.StartSession.SessionToken); 66 | } 67 | 68 | /** 69 | * Send request to abort the currently active transaction. 70 | * @returns Promise which fulfills with the abort transaction response returned from QLDB. 71 | */ 72 | async abortTransaction(): Promise { 73 | const request: SendCommandCommand = new SendCommandCommand({ 74 | SessionToken: this._sessionToken, 75 | AbortTransaction: {} 76 | }); 77 | const result: SendCommandResult = await this._sendCommand(request); 78 | return result.AbortTransaction; 79 | } 80 | 81 | /** 82 | * Send request to commit the currently active transaction. 83 | * @param txnId The ID of the transaction. 84 | * @param commitDigest The digest hash of the transaction to commit. 85 | * @returns Promise which fulfills with the commit transaction response returned from QLDB. 86 | */ 87 | async commit(commitTransaction: CommitTransactionRequest): Promise { 88 | const request: SendCommandCommand = new SendCommandCommand({ 89 | SessionToken: this._sessionToken, 90 | CommitTransaction: commitTransaction 91 | }); 92 | const result: SendCommandResult = await this._sendCommand(request); 93 | return result.CommitTransaction; 94 | } 95 | 96 | /** 97 | * Send an execute statement request with parameters to QLDB. 98 | * @param txnId The ID of the transaction. 99 | * @param statement The statement to execute. 100 | * @param parameters The parameters of the statement contained in ValueHolders. 101 | * @returns Promise which fulfills with the execute statement response returned from QLDB. 102 | */ 103 | async executeStatement( 104 | txnId: string, 105 | statement: string, 106 | parameters: ValueHolder[] 107 | ): Promise { 108 | const request: SendCommandCommand = new SendCommandCommand({ 109 | SessionToken: this._sessionToken, 110 | ExecuteStatement: { 111 | Statement: statement, 112 | TransactionId: txnId, 113 | Parameters: parameters 114 | } 115 | }); 116 | const result: SendCommandResult = await this._sendCommand(request); 117 | return result.ExecuteStatement; 118 | } 119 | 120 | /** 121 | * Send request to end the independent session represented by the instance of this class. 122 | * @returns Promise which fulfills with the end session response returned from QLDB. 123 | */ 124 | async endSession(): Promise { 125 | const request: SendCommandCommand = new SendCommandCommand({ 126 | SessionToken: this._sessionToken, 127 | EndSession: {} 128 | }); 129 | const result: SendCommandResult = await this._sendCommand(request); 130 | return result.EndSession; 131 | } 132 | 133 | /** 134 | * Send fetch result request to QLDB, retrieving the next chunk of data for the result. 135 | * @param txnId The ID of the transaction. 136 | * @param pageToken The token to fetch the next page. 137 | * @returns Promise which fulfills with the fetch page response returned from QLDB. 138 | */ 139 | async fetchPage(txnId: string, pageToken: string | undefined): Promise { 140 | const request: SendCommandCommand = new SendCommandCommand({ 141 | SessionToken: this._sessionToken, 142 | FetchPage: { 143 | TransactionId: txnId, 144 | NextPageToken: pageToken 145 | } 146 | }); 147 | const result: SendCommandResult = await this._sendCommand(request); 148 | return result.FetchPage; 149 | } 150 | 151 | /** 152 | * Get the low-level service client that communicates with QLDB. 153 | * @returns The low-level service client. 154 | */ 155 | getQldbClient(): QLDBSessionClient { 156 | return this._qldbClient; 157 | } 158 | 159 | /** 160 | * Get the session token representing the session connection. 161 | * @returns The session token. 162 | */ 163 | getSessionToken(): string { 164 | return this._sessionToken; 165 | } 166 | 167 | /** 168 | * Send a request to start a transaction. 169 | * @returns Promise which fulfills with the start transaction response returned from QLDB. 170 | */ 171 | async startTransaction(): Promise { 172 | const request: SendCommandCommand = new SendCommandCommand({ 173 | SessionToken: this._sessionToken, 174 | StartTransaction: {} 175 | }); 176 | const result: SendCommandResult = await this._sendCommand(request); 177 | return result.StartTransaction; 178 | } 179 | 180 | /** 181 | * Call the sendCommand method of the low level service client. 182 | * @param request A SendCommandRequest object containing the request information to be sent to QLDB. 183 | * @returns Promise which fulfills with a SendCommandResult object. 184 | */ 185 | private async _sendCommand(request: SendCommandCommand): Promise { 186 | try { 187 | const result = await this._qldbClient.send(request); 188 | debug(`Received response: ${inspect(result, { depth: 2 })}`); 189 | return result; 190 | } catch (e) { 191 | warn(`Error sending a command: ${e}.`); 192 | throw e; 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/LogUtil.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { version, logging } from "../package.json"; 15 | 16 | /** 17 | * Logs a debug level message. 18 | * @param line The message to be logged. 19 | * 20 | * @internal 21 | */ 22 | export function debug(line: string): void { 23 | if (isLoggerSet()) { 24 | _prepend(line, "DEBUG"); 25 | } 26 | } 27 | 28 | /** 29 | * Logs an error level message. 30 | * @param line The message to be logged. 31 | * 32 | * @internal 33 | */ 34 | export function error(line: string): void { 35 | if (isLoggerSet()) { 36 | _prepend(line, "ERROR"); 37 | } 38 | } 39 | 40 | /** 41 | * Logs an info level message. 42 | * @param line The message to be logged. 43 | * 44 | * @internal 45 | */ 46 | export function info(line: string): void { 47 | if (isLoggerSet()) { 48 | _prepend(line, "INFO"); 49 | } 50 | } 51 | 52 | /** 53 | * @returns A boolean indicating whether a logger has been set within the AWS SDK. 54 | */ 55 | function isLoggerSet(): boolean { 56 | return logging; 57 | } 58 | 59 | /** 60 | * Logs a message. 61 | * @param line The message to be logged. 62 | * 63 | * @internal 64 | */ 65 | export function log(line: string): void { 66 | if (isLoggerSet()) { 67 | _prepend(line, "LOG"); 68 | } 69 | } 70 | 71 | /** 72 | * Logs a warning level message. 73 | * @param line The message to be logged. 74 | * 75 | * @internal 76 | */ 77 | export function warn(line: string): void { 78 | if (isLoggerSet()) { 79 | _prepend(line, "WARN"); 80 | } 81 | } 82 | 83 | /** 84 | * Prepends a string identifier indicating the log level to the given log message, & writes or logs the given message 85 | * using the logger set in the AWS SDK. 86 | * @param line The message to be logged. 87 | * @param level The log level. 88 | */ 89 | function _prepend(line: any, level: string): void { 90 | console.log(`[${level}][Javascript QLDB Driver, Version: ${version}] ${line}`); 91 | } 92 | -------------------------------------------------------------------------------- /src/QldbDriver.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { 15 | QLDBSession, 16 | QLDBSessionClient, 17 | QLDBSessionClientConfig, 18 | } from "@aws-sdk/client-qldb-session"; 19 | import { globalAgent } from "http"; 20 | import { dom } from "ion-js"; 21 | import Semaphore from "semaphore-async-await"; 22 | 23 | import { version } from "../package.json"; 24 | import { Communicator } from "./Communicator"; 25 | import { 26 | DriverClosedError, 27 | ExecuteError, 28 | SessionPoolEmptyError, 29 | } from "./errors/Errors"; 30 | import { debug, info } from "./LogUtil"; 31 | import { QldbSession } from "./QldbSession"; 32 | import { Result } from "./Result"; 33 | import { BackoffFunction } from "./retry/BackoffFunction"; 34 | import { defaultRetryConfig } from "./retry/DefaultRetryConfig"; 35 | import { RetryConfig } from "./retry/RetryConfig"; 36 | import { TransactionExecutor } from "./TransactionExecutor"; 37 | import { NodeHttpHandler, NodeHttpHandlerOptions } from "@smithy/node-http-handler"; 38 | 39 | /** 40 | * This is the entry point for all interactions with Amazon QLDB. 41 | * 42 | * In order to start using the driver, you need to instantiate it with a ledger name: 43 | * 44 | * ``` 45 | * let qldbDriver: QldbDriver = new QldbDriver(your-ledger-name); 46 | * ``` 47 | * You can pass more parameters to the constructor of the driver which allow you to control certain limits 48 | * to improve the performance. Check the {@link QldbDriver.constructor} to see all the available parameters. 49 | * 50 | * A single instance of the QldbDriver is attached to only one ledger. All transactions will be executed against 51 | * the ledger specified. 52 | * 53 | * The driver exposes {@link QldbDriver.executeLambda} method which should be used to execute the transactions. 54 | * Check the {@link QldbDriver.executeLambda} method for more details on how to execute the Transaction. 55 | */ 56 | export class QldbDriver { 57 | private _maxConcurrentTransactions: number; 58 | private _sessionPool: QldbSession[]; 59 | private _semaphore: Semaphore; 60 | private _qldbClient: QLDBSessionClient; 61 | private _ledgerName: string; 62 | private _isClosed: boolean; 63 | private _retryConfig: RetryConfig; 64 | 65 | /** 66 | * Creates a QldbDriver instance that can be used to execute transactions against Amazon QLDB. A single instance of the QldbDriver 67 | * is always attached to one ledger, as specified in the ledgerName parameter. 68 | * 69 | * @param ledgerName The name of the ledger you want to connect to. This is a mandatory parameter. 70 | * @param qldbClientOptions The object containing options for configuring the low level client. 71 | * See {@link https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-qldb-session/classes/qldbsessionclient.html#constructor}. 72 | * @param httpOptions The object containing options for configuring the low level http request handler for qldb session client. 73 | * See {@link https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-qldb-session/interfaces/nodehttphandleroptions.html} 74 | * @param maxConcurrentTransactions The driver internally uses a pool of sessions to execute the transactions. 75 | * The maxConcurrentTransactions parameter specifies the number of sessions that the driver can hold in the pool. 76 | * The default is set to maximum number of sockets specified for the `httpAgent` in `httpOptions`. If `httpAgent` is not defined, 77 | * the default of `maxConcurrentTransactions` takes the `maxSockets` value from the `globalAgent`. 78 | * See {@link https://docs.aws.amazon.com/qldb/latest/developerguide/driver.best-practices.html#driver.best-practices.configuring} for more details. 79 | * @param retryConfig Config to specify max number of retries, base and custom backoff strategy for retries. Will be overridden if a different retryConfig 80 | * is passed to {@linkcode executeLambda}. 81 | * 82 | * @throws RangeError if `maxConcurrentTransactions` is less than 0 or more than `maxSockets` set by `httpAgent` or `globalAgent`. 83 | */ 84 | constructor( 85 | ledgerName: string, 86 | qldbClientOptions: QLDBSessionClientConfig = {}, 87 | httpOptions: NodeHttpHandlerOptions = {}, 88 | maxConcurrentTransactions: number = 0, 89 | retryConfig: RetryConfig = defaultRetryConfig 90 | ) { 91 | qldbClientOptions.customUserAgent = `QLDB Driver for Node.js v${version}`; 92 | qldbClientOptions.maxAttempts = 0; 93 | qldbClientOptions.requestHandler = new NodeHttpHandler(httpOptions); 94 | 95 | this._qldbClient = new QLDBSession(qldbClientOptions); 96 | this._ledgerName = ledgerName; 97 | this._isClosed = false; 98 | this._retryConfig = retryConfig; 99 | 100 | if (maxConcurrentTransactions < 0) { 101 | throw new RangeError("Value for maxConcurrentTransactions cannot be negative."); 102 | } 103 | 104 | let maxSockets: number; 105 | if (httpOptions.httpAgent) { 106 | maxSockets = httpOptions.httpAgent.maxSockets; 107 | } else if (httpOptions.httpsAgent) { 108 | maxSockets = httpOptions.httpsAgent.maxSockets; 109 | } else { 110 | maxSockets = globalAgent.maxSockets; 111 | } 112 | 113 | if (0 === maxConcurrentTransactions) { 114 | this._maxConcurrentTransactions = maxSockets; 115 | } else { 116 | this._maxConcurrentTransactions = maxConcurrentTransactions; 117 | } 118 | if (this._maxConcurrentTransactions > maxSockets) { 119 | throw new RangeError( 120 | `The session pool limit given, ${this._maxConcurrentTransactions}, exceeds the limit set by the client, 121 | ${maxSockets}. Please lower the limit and retry.` 122 | ); 123 | } 124 | 125 | this._sessionPool = []; 126 | this._semaphore = new Semaphore(this._maxConcurrentTransactions); 127 | } 128 | 129 | /** 130 | * This is a driver shutdown method which closes all the sessions and marks the driver as closed. 131 | * Once the driver is closed, no transactions can be executed on that driver instance. 132 | * 133 | * Note: There is no corresponding `open` method and the only option is to instantiate another driver. 134 | */ 135 | close(): void { 136 | this._isClosed = true; 137 | while (this._sessionPool.length > 0) { 138 | const session: QldbSession = this._sessionPool.pop(); 139 | if (session != undefined) { 140 | session.endSession(); 141 | } 142 | } 143 | } 144 | 145 | /** 146 | * This is the primary method to execute a transaction against Amazon QLDB ledger. 147 | * 148 | * When this method is invoked, the driver will acquire a `Transaction` and hand it to the `TransactionExecutor` you 149 | * passed via the `transactionFunction` parameter. Once the `transactionFunction`'s execution is done, the driver will try to 150 | * commit the transaction. 151 | * If there is a failure along the way, the driver will retry the entire transaction block. This would mean that your code inside the 152 | * `transactionFunction` function should be idempotent. 153 | * 154 | * You can also return the results from the `transactionFunction`. Here is an example code of executing a transaction 155 | * 156 | * ``` 157 | * let result = driver.executeLambda(async (txn:TransactionExecutor) => { 158 | * let a = await txn.execute("SELECT a from Table1"); 159 | * let b = await txn.execute("SELECT b from Table2"); 160 | * return {a: a, b: b}; 161 | * }); 162 | *``` 163 | * 164 | * Please keep in mind that the entire transaction will be committed once all the code inside the `transactionFunction` is executed. 165 | * So for the above example the values inside the transactionFunction, a and b, are speculative values. If the commit of the transaction fails, 166 | * the entire `transactionFunction` will be retried. 167 | * 168 | * The function passed via retryIndicator parameter is invoked whenever there is a failure and the driver is about to retry the transaction. 169 | * The retryIndicator will be called with the current attempt number. 170 | * 171 | * @param transactionLambda The function representing a transaction to be executed. Please see the method docs to understand the usage of this parameter. 172 | * @param retryConfig Config to specify max number of retries, base and custom backoff strategy for retries. This config 173 | * overrides the retry config set at driver level for a particular lambda execution. 174 | * Note that all the values of the driver level retry config will be overridden by the new config passed here. 175 | * @throws {@linkcode DriverClosedError} When a transaction is attempted on a closed driver instance. {@linkcode close} 176 | * @throws {@linkcode ClientException} When the commit digest from commit transaction result does not match. 177 | * @throws {@linkcode SessionPoolEmptyError} When maxConcurrentTransactions limit is reached and there is no session available in the pool. 178 | * @throws {@linkcode InvalidSessionException} When a session expires either due to a long-running transaction or session being idle for long time. 179 | * @throws {@linkcode BadRequestException} When Amazon QLDB is not able to execute a query or transaction. 180 | */ 181 | async executeLambda( 182 | transactionLambda: (transactionExecutor: TransactionExecutor) => Promise, 183 | retryConfig?: RetryConfig 184 | ): Promise { 185 | if (this._isClosed) { 186 | throw new DriverClosedError(); 187 | } 188 | 189 | retryConfig = (retryConfig == null) ? this._retryConfig : retryConfig; 190 | let replaceDeadSession: boolean = false; 191 | for (let retryAttempt: number = 1; true; retryAttempt++) { 192 | let session: QldbSession = null; 193 | try { 194 | if (replaceDeadSession) { 195 | session = await this.createNewSession(this); 196 | } else { 197 | session = await this.getSession(this); 198 | } 199 | return await session.executeLambda(transactionLambda); 200 | } catch (e) { 201 | if (e instanceof ExecuteError) { 202 | if (e.isRetryable) { 203 | // Always retry on the first attempt if failure was caused by a stale session in the pool 204 | if (retryAttempt == 1 && e.isInvalidSessionException) { 205 | debug("Initial session received from pool is invalid. Retrying..."); 206 | continue; 207 | } 208 | if (retryAttempt > retryConfig.getRetryLimit()) { 209 | throw e.cause; 210 | } 211 | 212 | const backoffFunction: BackoffFunction = retryConfig.getBackoffFunction(); 213 | let backoffDelay: number = backoffFunction(retryAttempt, e.cause, e.transactionId); 214 | if (backoffDelay == null || backoffDelay < 0) { 215 | backoffDelay = 0; 216 | } 217 | await new Promise(resolve => setTimeout(resolve, backoffDelay)); 218 | 219 | info(`A recoverable error has occurred. Attempting retry #${retryAttempt}.`); 220 | debug(`Error cause: ${e.cause}`); 221 | continue; 222 | } else { 223 | throw e.cause; 224 | } 225 | } else { 226 | throw e; 227 | } 228 | } finally { 229 | replaceDeadSession = !this.releaseSession(this, session); 230 | } 231 | } 232 | } 233 | 234 | // Release semaphore and if the session is alive return it to the pool and return true 235 | private releaseSession(thisDriver: QldbDriver, session: QldbSession): boolean { 236 | if (session != null && session.isAlive()) { 237 | thisDriver._sessionPool.push(session); 238 | thisDriver._semaphore.release(); 239 | debug(`Session returned to pool; pool size is now: ${thisDriver._sessionPool.length}`) 240 | return true 241 | } else if (session != null) { 242 | thisDriver._semaphore.release(); 243 | return false; 244 | } else { 245 | return false; 246 | } 247 | } 248 | 249 | 250 | // Acquire semaphore and get a session from the pool 251 | private async getSession(thisDriver: QldbDriver): Promise { 252 | debug( 253 | `Getting session. Current free session count: ${thisDriver._sessionPool.length}. ` + 254 | `Currently available permit count: ${thisDriver._semaphore.getPermits()}.` 255 | ); 256 | if (thisDriver._semaphore.tryAcquire()) { 257 | let session = thisDriver._sessionPool.pop(); 258 | if (session == undefined) { 259 | debug(`Creating a new pooled session.`); 260 | session = await this.createNewSession(thisDriver); 261 | } 262 | return session; 263 | } else { 264 | throw new SessionPoolEmptyError() 265 | } 266 | } 267 | 268 | private async createNewSession(thisDriver: QldbDriver) { 269 | try { 270 | const communicator: Communicator = 271 | await Communicator.create(thisDriver._qldbClient, thisDriver._ledgerName); 272 | return new QldbSession(communicator); 273 | } catch (e) { 274 | // An error when failing to start a new session is always retryable 275 | throw new ExecuteError(e as Error, true, true); 276 | } 277 | } 278 | 279 | /** 280 | * A helper method to get all the table names in a ledger. 281 | * @returns Promise which fulfills with an array of table names. 282 | */ 283 | async getTableNames(): Promise { 284 | const statement: string = "SELECT name FROM information_schema.user_tables WHERE status = 'ACTIVE'"; 285 | return await this.executeLambda(async (transactionExecutor: TransactionExecutor) : Promise => { 286 | const result: Result = await transactionExecutor.execute(statement); 287 | const resultStructs: dom.Value[] = result.getResultList(); 288 | 289 | return resultStructs.map(tableNameStruct => 290 | tableNameStruct.get("name").stringValue()); 291 | }); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/QldbHash.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { createHash } from "crypto"; 15 | import { cryptoHasherProvider, HashReader, makeHashReader } from "ion-hash-js"; 16 | import { makeReader, makeTextWriter, Writer } from "ion-js"; 17 | 18 | const HASH_SIZE: number = 32 19 | 20 | /** 21 | * A QLDB hash is either a 256 bit number or a special empty hash. 22 | * 23 | * @internal 24 | */ 25 | export class QldbHash { 26 | private _qldbHash: Uint8Array; 27 | 28 | /** 29 | * Creates a QldbHash. 30 | * @param qldbHash The QLDB hash. 31 | * @throws RangeError When this hash is not the correct hash size. 32 | */ 33 | constructor(qldbHash: Uint8Array) { 34 | if (qldbHash.length !== HASH_SIZE || qldbHash.length === 0) { 35 | throw new RangeError(`Hash must be either empty or ${HASH_SIZE} bytes long.`); 36 | } 37 | this._qldbHash = qldbHash; 38 | } 39 | 40 | /** 41 | * Sort the current hash value and the hash value provided by `that`, comparing by their **signed** byte values in 42 | * little-endian order. 43 | * @param that The Ion hash of the Ion value to compare. 44 | * @returns An QldbHash object that contains the concatenated hash values. 45 | */ 46 | dot(that: QldbHash): QldbHash { 47 | const concatenated: Uint8Array = QldbHash._joinHashesPairwise(this.getQldbHash(), that.getQldbHash()); 48 | const newHashLib = createHash("sha256"); 49 | newHashLib.update(concatenated); 50 | const newDigest: Uint8Array = newHashLib.digest(); 51 | return new QldbHash(newDigest); 52 | } 53 | 54 | equals(other: QldbHash): boolean { 55 | return (QldbHash._hashComparator(this.getQldbHash(), other.getQldbHash()) === 0); 56 | } 57 | 58 | getHashSize(): number { 59 | return this._qldbHash.length; 60 | } 61 | 62 | getQldbHash(): Uint8Array { 63 | return this._qldbHash; 64 | } 65 | 66 | isEmpty(): boolean { 67 | return (this._qldbHash.length === 0); 68 | } 69 | 70 | /** 71 | * The QldbHash of an IonValue is just the IonHash of that value. 72 | * @param value The string or Ion value to be converted to Ion hash. 73 | * @returns A QldbHash object that contains Ion hash. 74 | */ 75 | static toQldbHash(value: any): QldbHash { 76 | if (typeof value === "string") { 77 | const writer: Writer = makeTextWriter(); 78 | writer.writeString(value); 79 | writer.close(); 80 | value = writer.getBytes(); 81 | } 82 | const hashReader: HashReader = makeHashReader(makeReader(value), cryptoHasherProvider("sha256")); 83 | hashReader.next(); 84 | hashReader.next(); 85 | const digest: Uint8Array = hashReader.digest(); 86 | return new QldbHash(digest); 87 | } 88 | 89 | /** 90 | * Helper method that concatenates two Uint8Array. 91 | * @param arrays List of arrays to concatenate, in the order provided. 92 | * @returns The concatenated array. 93 | */ 94 | static _concatenate(...arrays: Uint8Array[]): Uint8Array { 95 | let totalLength = 0; 96 | for (const arr of arrays) { 97 | totalLength += arr.length; 98 | } 99 | const result = new Uint8Array(totalLength); 100 | let offset = 0; 101 | for (const arr of arrays) { 102 | result.set(arr, offset); 103 | offset += arr.length; 104 | } 105 | return result; 106 | } 107 | 108 | /** 109 | * Compares two hashes by their **signed** byte values in little-endian order. 110 | * @param hash1 The hash value to compare. 111 | * @param hash2 The hash value to compare. 112 | * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching 113 | * bytes. 114 | * @throws RangeError When the hash is not the correct hash size. 115 | */ 116 | static _hashComparator(hash1: Uint8Array, hash2: Uint8Array): number { 117 | if (hash1.length !== HASH_SIZE || hash2.length !== HASH_SIZE) { 118 | throw new RangeError("Invalid hash."); 119 | } 120 | for (let i = hash1.length-1; i >= 0; i--) { 121 | const difference: number = (hash1[i]<<24 >>24) - (hash2[i]<<24 >>24); 122 | if (difference !== 0) { 123 | return difference; 124 | } 125 | } 126 | return 0; 127 | } 128 | 129 | /** 130 | * Takes two hashes, sorts them, and concatenates them. 131 | * @param h1 Byte array containing one of the hashes to compare. 132 | * @param h2 Byte array containing one of the hashes to compare. 133 | * @returns The concatenated array of hashes. 134 | */ 135 | static _joinHashesPairwise(h1: Uint8Array, h2: Uint8Array): Uint8Array { 136 | if (h1.length === 0) { 137 | return h2; 138 | } 139 | if (h2.length === 0) { 140 | return h1; 141 | } 142 | let concatenated: Uint8Array; 143 | if (this._hashComparator(h1, h2) < 0) { 144 | concatenated = this._concatenate(h1, h2); 145 | } else { 146 | concatenated = this._concatenate(h2, h1); 147 | } 148 | return concatenated; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/QldbSession.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { StartTransactionResult } from "@aws-sdk/client-qldb-session"; 15 | 16 | import { Communicator } from "./Communicator"; 17 | import { 18 | ExecuteError, 19 | isInvalidSessionException, 20 | isOccConflictException, 21 | isRetryableException, 22 | isTransactionExpiredException 23 | } from "./errors/Errors"; 24 | import { warn } from "./LogUtil"; 25 | import { Transaction } from "./Transaction"; 26 | import { TransactionExecutor } from "./TransactionExecutor"; 27 | import { ServiceException } from "@smithy/smithy-client" 28 | 29 | /** 30 | * @internal 31 | */ 32 | export class QldbSession { 33 | private _communicator: Communicator; 34 | private _isAlive: boolean; 35 | 36 | constructor(communicator: Communicator) { 37 | this._communicator = communicator; 38 | this._isAlive = true; 39 | } 40 | 41 | isAlive(): boolean { 42 | return this._isAlive; 43 | } 44 | 45 | async endSession(): Promise { 46 | try { 47 | this._isAlive = false; 48 | await this._communicator.endSession(); 49 | } catch (e) { 50 | // We will only log issues ending the session, as QLDB will clean them after a timeout. 51 | warn(`Errors ending session: ${e}.`); 52 | } 53 | } 54 | 55 | async executeLambda( 56 | transactionLambda: (transactionExecutor: TransactionExecutor) => Promise 57 | ): Promise { 58 | let transaction: Transaction; 59 | let transactionId: string = null; 60 | let onCommit: boolean = false; 61 | try { 62 | transaction = await this._startTransaction(); 63 | transactionId = transaction.getTransactionId(); 64 | const executor: TransactionExecutor = new TransactionExecutor(transaction); 65 | const returnedValue: Type = await transactionLambda(executor); 66 | onCommit = true; 67 | await transaction.commit(); 68 | return returnedValue; 69 | } catch (e) { 70 | const isRetryable: boolean = isRetryableException(e as ServiceException, onCommit); 71 | const isISE: boolean = isInvalidSessionException(e as ServiceException); 72 | if (isISE && !isTransactionExpiredException(e as ServiceException)) { 73 | // Underlying session is dead on InvalidSessionException except for transaction expiry 74 | this._isAlive = false; 75 | } else if (!isOccConflictException(e as ServiceException)) { 76 | // OCC does not need session state reset as the transaction is implicitly closed 77 | await this._cleanSessionState(); 78 | } 79 | throw new ExecuteError(e as Error, isRetryable, isISE, transactionId); 80 | } 81 | } 82 | 83 | async _startTransaction(): Promise { 84 | const startTransactionResult: StartTransactionResult = await this._communicator.startTransaction(); 85 | return new Transaction(this._communicator, startTransactionResult.TransactionId); 86 | } 87 | 88 | private async _cleanSessionState(): Promise { 89 | try { 90 | await this._communicator.abortTransaction(); 91 | } catch (e) { 92 | warn(`Ignored error while aborting transaction during execution: ${e}.`); 93 | this._isAlive = false; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Result.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | import { ExecuteStatementResult, FetchPageResult, Page, ValueHolder } from "@aws-sdk/client-qldb-session"; 14 | import { dom } from "ion-js"; 15 | 16 | import { Communicator } from "./Communicator"; 17 | import { ClientError } from "./errors/Errors" 18 | import { ResultReadable } from "./ResultReadable"; 19 | import { IOUsage } from "./stats/IOUsage"; 20 | import { TimingInformation } from "./stats/TimingInformation"; 21 | 22 | interface Blob {} 23 | export type IonBinary = Buffer|Uint8Array|Blob|string; 24 | 25 | /** 26 | * A class representing a fully buffered set of results returned from QLDB. 27 | */ 28 | export class Result { 29 | private _resultList: dom.Value[]; 30 | private _ioUsage: IOUsage; 31 | private _timingInformation: TimingInformation; 32 | 33 | /** 34 | * Creates a Result. 35 | * @param resultList A list of Ion values containing the statement execution's result returned from QLDB. 36 | * @param ioUsage Contains the number of consumed IO requests for the executed statement. 37 | * @param timingInformation Holds server side processing time for the executed statement. 38 | */ 39 | private constructor(resultList: dom.Value[], ioUsage: IOUsage, timingInformation: TimingInformation) { 40 | this._resultList = resultList; 41 | this._ioUsage = ioUsage; 42 | this._timingInformation = timingInformation; 43 | } 44 | 45 | /** 46 | * Static factory method that creates a Result object, containing the results of a statement execution from QLDB. 47 | * @param txnId The ID of the transaction the statement was executed in. 48 | * @param executeResult The returned result from the statement execution. 49 | * @param communicator The Communicator used for the statement execution. 50 | * @returns Promise which fulfills with a Result. 51 | * 52 | * @internal 53 | */ 54 | static async create( 55 | txnId: string, 56 | executeResult: ExecuteStatementResult, 57 | communicator: Communicator 58 | ): Promise { 59 | const result: Result = await Result._fetchResultPages(txnId, executeResult, communicator); 60 | return result; 61 | } 62 | 63 | /** 64 | * Static method that creates a Result object by reading and buffering the contents of a ResultReadable. 65 | * @param resultReadable A ResultReadable object to convert to a Result object. 66 | * @returns Promise which fulfills with a Result. 67 | */ 68 | static async bufferResultReadable(resultReadable: ResultReadable): Promise { 69 | const resultList: dom.Value[] = await Result._readResultReadable(resultReadable); 70 | return new Result(resultList, resultReadable.getConsumedIOs(), resultReadable.getTimingInformation()); 71 | } 72 | 73 | /** 74 | * Returns the list of results of the statement execution returned from QLDB. 75 | * @returns A list of Ion values which wrap the Ion values returned from the QLDB statement execution. 76 | */ 77 | getResultList(): dom.Value[] { 78 | return this._resultList.slice(); 79 | } 80 | 81 | /** 82 | * Returns the number of read IO request for the executed statement. 83 | * @returns IOUsage, containing number of read IOs. 84 | */ 85 | getConsumedIOs(): IOUsage { 86 | return this._ioUsage; 87 | } 88 | 89 | /** 90 | * Returns server-side processing time for the executed statement. 91 | * @returns TimingInformation, containing processing time. 92 | */ 93 | getTimingInformation(): TimingInformation { 94 | return this._timingInformation; 95 | } 96 | 97 | /** 98 | * Handle the unexpected Blob return type from QLDB. 99 | * @param ionBinary The IonBinary value returned from QLDB. 100 | * @returns The IonBinary value cast explicitly to one of the types that make up the IonBinary type. This will be 101 | * either Buffer, Uint8Array, or string. 102 | * @throws {@linkcode ClientException} when the specific type of the IonBinary value is Blob. 103 | * 104 | * @internal 105 | */ 106 | static _handleBlob(ionBinary: IonBinary): Buffer|Uint8Array|string { 107 | if (ionBinary instanceof Buffer) { 108 | return ionBinary; 109 | } 110 | if (ionBinary instanceof Uint8Array) { 111 | return ionBinary; 112 | } 113 | if (typeof ionBinary === "string") { 114 | return ionBinary; 115 | } 116 | throw new ClientError("Unexpected Blob returned from QLDB."); 117 | } 118 | 119 | /** 120 | * Fetches all subsequent Pages given an initial Page, places each value of each Page in an Ion value. 121 | * @param txnId The ID of the transaction the statement was executed in. 122 | * @param executeResult The returned result from the statement execution. 123 | * @param communicator The Communicator used for the statement execution. 124 | * @returns Promise which fulfills with a Result, containing a list of Ion values, representing all the returned 125 | * values of the result set, number of IOs for the request, and the time spent processing the request. 126 | */ 127 | private static async _fetchResultPages( 128 | txnId: string, 129 | executeResult: ExecuteStatementResult, 130 | communicator: Communicator 131 | ): Promise { 132 | let currentPage: Page = executeResult.FirstPage; 133 | let readIO: number = executeResult.ConsumedIOs != null ? executeResult.ConsumedIOs.ReadIOs : null; 134 | let processingTime: number = 135 | executeResult.TimingInformation != null ? executeResult.TimingInformation.ProcessingTimeMilliseconds : null; 136 | 137 | const pageValuesArray: ValueHolder[][] = []; 138 | if (currentPage.Values && currentPage.Values.length > 0) { 139 | pageValuesArray.push(currentPage.Values); 140 | } 141 | while (currentPage.NextPageToken) { 142 | const fetchPageResult: FetchPageResult = 143 | await communicator.fetchPage(txnId, currentPage.NextPageToken); 144 | currentPage = fetchPageResult.Page; 145 | if (currentPage.Values && currentPage.Values.length > 0) { 146 | pageValuesArray.push(currentPage.Values); 147 | } 148 | 149 | if (fetchPageResult.ConsumedIOs != null) { 150 | readIO += fetchPageResult.ConsumedIOs.ReadIOs; 151 | } 152 | 153 | if (fetchPageResult.TimingInformation != null) { 154 | processingTime += fetchPageResult.TimingInformation.ProcessingTimeMilliseconds; 155 | } 156 | } 157 | const ionValues: dom.Value[] = []; 158 | pageValuesArray.forEach((valueHolders: ValueHolder[]) => { 159 | valueHolders.forEach((valueHolder: ValueHolder) => { 160 | ionValues.push(dom.load(Result._handleBlob(valueHolder.IonBinary))); 161 | }); 162 | }); 163 | const ioUsage: IOUsage = readIO != null ? new IOUsage(readIO) : null; 164 | const timingInformation = processingTime != null ? new TimingInformation(processingTime) : null; 165 | return new Result(ionValues, ioUsage, timingInformation); 166 | } 167 | 168 | /** 169 | * Helper method that reads a ResultReadable and extracts the results, placing them in an array of Ion values. 170 | * @param resultReadable The ResultReadable to read. 171 | * @returns Promise which fulfills with a list of Ion values, representing all the returned values of the result set. 172 | */ 173 | private static async _readResultReadable(resultReadable: ResultReadable): Promise { 174 | return new Promise(res => { 175 | const ionValues: dom.Value[] = []; 176 | resultReadable.on("data", function(value) { 177 | ionValues.push(value); 178 | }).on("end", function() { 179 | res(ionValues); 180 | }); 181 | }); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/ResultReadable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { ExecuteStatementResult, FetchPageResult, Page } from "@aws-sdk/client-qldb-session"; 15 | import { dom } from "ion-js"; 16 | import { Readable } from "stream"; 17 | 18 | import { Communicator } from "./Communicator"; 19 | import { Result } from "./Result"; 20 | import { IOUsage } from "./stats/IOUsage"; 21 | import { TimingInformation } from "./stats/TimingInformation"; 22 | 23 | /** 24 | * A class representing the result of a statement returned from QLDB as a stream. 25 | * Extends Readable from the Node.JS Stream API interface. 26 | * The stream will always operate in object mode. 27 | */ 28 | export class ResultReadable extends Readable { 29 | private _communicator: Communicator; 30 | private _cachedPage: Page; 31 | private _txnId: string; 32 | private _shouldPushCachedPage: boolean; 33 | private _retrieveIndex: number; 34 | private _isPushingData: boolean; 35 | private _readIOs: number; 36 | private _processingTime: number; 37 | 38 | /** 39 | * Create a ResultReadable. 40 | * @param txnId The ID of the transaction the statement was executed in. 41 | * @param executeResult The returned result from the statement execution. 42 | * @param communicator The Communicator used for the statement execution. 43 | * 44 | * @internal 45 | */ 46 | constructor(txnId: string, executeResult: ExecuteStatementResult, communicator: Communicator) { 47 | super({ objectMode: true }); 48 | this._communicator = communicator; 49 | this._cachedPage = executeResult.FirstPage; 50 | this._txnId = txnId; 51 | this._shouldPushCachedPage = true; 52 | this._retrieveIndex = 0; 53 | this._isPushingData = false; 54 | this._readIOs = executeResult.ConsumedIOs == null ? null : executeResult.ConsumedIOs.ReadIOs; 55 | this._processingTime = 56 | executeResult.TimingInformation == null ? null : executeResult.TimingInformation.ProcessingTimeMilliseconds; 57 | } 58 | 59 | /** 60 | * Returns the number of read IO request for the executed statement. The statistics are stateful. 61 | * @returns IOUsage, containing number of read IOs. 62 | */ 63 | getConsumedIOs(): IOUsage { 64 | return this._readIOs == null 65 | ? null 66 | : new IOUsage(this._readIOs); 67 | } 68 | 69 | /** 70 | * Returns server-side processing time for the executed statement. The statistics are stateful. 71 | * @returns TimingInformation, containing processing time. 72 | */ 73 | getTimingInformation(): TimingInformation { 74 | return this._processingTime == null 75 | ? null 76 | : new TimingInformation(this._processingTime); 77 | } 78 | 79 | /** 80 | * Implementation of the `readable.read` method for the Node Streams Readable Interface. 81 | * @param size The number of bytes to read asynchronously. This is currently not being used as only object mode is 82 | * supported. 83 | * 84 | * @internal 85 | */ 86 | _read(size?: number): void { 87 | if (this._isPushingData) { 88 | return; 89 | } 90 | this._isPushingData = true; 91 | this._pushPageValues(); 92 | } 93 | 94 | /** 95 | * Pushes the values for the Node Streams Readable Interface. This method fetches the next page if is required and 96 | * handles converting the values returned from QLDB into an Ion value. 97 | * @returns Promise which fulfills with void. 98 | */ 99 | private async _pushPageValues(): Promise { 100 | let canPush: boolean = true; 101 | try { 102 | if (this._shouldPushCachedPage) { 103 | this._shouldPushCachedPage = false; 104 | } else if (this._cachedPage.NextPageToken) { 105 | try { 106 | const fetchPageResult: FetchPageResult = 107 | await this._communicator.fetchPage(this._txnId, this._cachedPage.NextPageToken); 108 | this._cachedPage = fetchPageResult.Page; 109 | 110 | if (fetchPageResult.ConsumedIOs != null) { 111 | this._readIOs += fetchPageResult.ConsumedIOs.ReadIOs; 112 | } 113 | 114 | if (fetchPageResult.TimingInformation != null) { 115 | this._processingTime += fetchPageResult.TimingInformation.ProcessingTimeMilliseconds; 116 | } 117 | 118 | this._retrieveIndex = 0; 119 | } catch (e) { 120 | this.destroy(e as Error); 121 | canPush = false; 122 | return; 123 | } 124 | } 125 | 126 | while (this._retrieveIndex < this._cachedPage.Values.length) { 127 | const ionValue: dom.Value = 128 | dom.load(Result._handleBlob(this._cachedPage.Values[this._retrieveIndex++].IonBinary)); 129 | canPush = this.push(ionValue); 130 | if (!canPush) { 131 | this._shouldPushCachedPage = this._retrieveIndex < this._cachedPage.Values.length; 132 | return; 133 | } 134 | } 135 | 136 | if (!this._cachedPage.NextPageToken) { 137 | this.push(null); 138 | canPush = false; 139 | } 140 | 141 | } finally { 142 | this._isPushingData = false; 143 | 144 | if (canPush) { 145 | this._read(); 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Transaction.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { 15 | CommitTransactionResult, 16 | ExecuteStatementResult, 17 | ValueHolder 18 | } from "@aws-sdk/client-qldb-session"; 19 | import { dumpBinary, toBase64 } from "ion-js"; 20 | import { Lock } from "semaphore-async-await"; 21 | import { Communicator } from "./Communicator"; 22 | import { ClientError } from "./errors/Errors"; 23 | import { QldbHash } from "./QldbHash"; 24 | import { Result } from "./Result"; 25 | import { ResultReadable } from "./ResultReadable"; 26 | 27 | /** 28 | * A class representing a QLDB transaction. 29 | * 30 | * Every transaction is tied to a parent QldbSession, meaning that if the parent session is closed or invalidated, the 31 | * child transaction is automatically closed and cannot be used. Only one transaction can be active at any given time 32 | * per parent session. 33 | * 34 | * Any unexpected errors that occur within a transaction should not be retried using the same transaction, as the state 35 | * of the transaction is now ambiguous. 36 | * 37 | * When an OCC conflict occurs, the transaction is closed and must be handled manually by creating a new transaction 38 | * and re-executing the desired statements. 39 | * 40 | * @internal 41 | */ 42 | export class Transaction { 43 | private _communicator: Communicator; 44 | private _txnId: string; 45 | private _txnHash: QldbHash; 46 | private _hashLock: Lock; 47 | 48 | /** 49 | * Create a Transaction. 50 | * @param communicator The Communicator object representing a communication channel with QLDB. 51 | * @param txnId The ID of the transaction. 52 | */ 53 | constructor(communicator: Communicator, txnId: string) { 54 | this._communicator = communicator; 55 | this._txnId = txnId; 56 | this._txnHash = QldbHash.toQldbHash(txnId); 57 | this._hashLock = new Lock(); 58 | } 59 | 60 | /** 61 | * Commits and closes child ResultReadable objects. 62 | * @returns Promise which fulfills with void. 63 | * @throws {@linkcode ClientException} when the commit digest from commit transaction result does not match. 64 | */ 65 | async commit(): Promise { 66 | await this._hashLock.acquire(); 67 | try { 68 | const commitTxnResult: CommitTransactionResult = await this._communicator.commit({ 69 | TransactionId: this._txnId, 70 | CommitDigest: this._txnHash.getQldbHash() 71 | }); 72 | if (toBase64(this._txnHash.getQldbHash()) !== toBase64((commitTxnResult.CommitDigest))) { 73 | throw new ClientError( 74 | `Transaction's commit digest did not match returned value from QLDB. 75 | Please retry with a new transaction. Transaction ID: ${this._txnId}.` 76 | ); 77 | } 78 | } finally { 79 | this._hashLock.release(); 80 | } 81 | } 82 | 83 | /** 84 | * Execute the specified statement in the current transaction. This method returns a promise 85 | * which eventually returns all the results loaded into memory. 86 | * 87 | * @param statement A statement to execute against QLDB as a string. 88 | * @param parameters Variable number of arguments, where each argument corresponds to a 89 | * placeholder (?) in the PartiQL query. 90 | * The argument could be any native JavaScript type or an Ion DOM type. 91 | * [Details of Ion DOM type and JavaScript type](https://github.com/amzn/ion-js/blob/master/src/dom/README.md#iondom-data-types) 92 | * @returns Promise which fulfills with all results loaded into memory 93 | * @throws [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) when the passed argument value cannot be converted into Ion 94 | */ 95 | async execute(statement: string, ...parameters: any[]): Promise { 96 | const result: ExecuteStatementResult = await this._sendExecute(statement, parameters); 97 | return Result.create(this._txnId, result, this._communicator); 98 | } 99 | 100 | /** 101 | * Execute the specified statement in the current transaction. This method returns a promise 102 | * which fulfills with Readable Stream, which allows you to stream one record at time 103 | * 104 | * @param statement A statement to execute against QLDB as a string. 105 | * @param parameters Variable number of arguments, where each argument corresponds to a 106 | * placeholder (?) in the PartiQL query. 107 | * The argument could be any native JavaScript type or an Ion DOM type. 108 | * [Details of Ion DOM type and JavaScript type](https://github.com/amzn/ion-js/blob/master/src/dom/README.md#iondom-data-types) 109 | * @returns Promise which fulfills with a Readable Stream 110 | * @throws {@linkcode TransactionClosedError} when the transaction is closed. 111 | * @throws [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) when the passed argument value cannot be converted into Ion 112 | */ 113 | async executeAndStreamResults(statement: string, ...parameters: any[]): Promise { 114 | const result: ExecuteStatementResult = await this._sendExecute(statement, parameters); 115 | return new ResultReadable(this._txnId, result, this._communicator); 116 | } 117 | 118 | /** 119 | * Retrieve the transaction ID associated with this transaction. 120 | * @returns The transaction ID. 121 | */ 122 | getTransactionId(): string { 123 | return this._txnId; 124 | } 125 | 126 | /** 127 | * Helper method to execute statement against QLDB. 128 | * @param statement A statement to execute against QLDB as a string. 129 | * @param parameters An optional list of Ion values or JavaScript native types that are convertible to Ion for 130 | * filling in parameters of the statement. 131 | * @returns Promise which fulfills with a ExecuteStatementResult object. 132 | */ 133 | private async _sendExecute(statement: string, parameters: any[]): Promise { 134 | await this._hashLock.acquire(); 135 | try { 136 | let statementHash: QldbHash = QldbHash.toQldbHash(statement); 137 | 138 | const valueHolderList: ValueHolder[] = parameters.map((param: any) => { 139 | let ionBinary: Uint8Array; 140 | try { 141 | ionBinary = dumpBinary(param); 142 | } catch(e) { 143 | (e as Error).message = `Failed to convert parameter ${String(param)} to Ion Binary: ${(e as Error).message}`; 144 | throw e; 145 | } 146 | statementHash = statementHash.dot(QldbHash.toQldbHash(ionBinary)); 147 | const valueHolder: ValueHolder = { 148 | IonBinary: ionBinary 149 | }; 150 | return valueHolder; 151 | }); 152 | 153 | this._txnHash = this._txnHash.dot(statementHash); 154 | 155 | const result: ExecuteStatementResult = await this._communicator.executeStatement( 156 | this._txnId, 157 | statement, 158 | valueHolderList 159 | ); 160 | return result; 161 | } finally { 162 | this._hashLock.release(); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/TransactionExecutor.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { LambdaAbortedError } from "./errors/Errors"; 15 | import { Result } from "./Result"; 16 | import { ResultReadable } from "./ResultReadable"; 17 | import { Transaction } from "./Transaction"; 18 | 19 | /** 20 | * A class to handle lambda execution. 21 | */ 22 | export class TransactionExecutor { 23 | private _transaction: Transaction; 24 | 25 | /** 26 | * Creates a TransactionExecutor. 27 | * @param transaction The transaction that this executor is running within. 28 | * 29 | * @internal 30 | */ 31 | constructor(transaction: Transaction) { 32 | this._transaction = transaction; 33 | } 34 | 35 | /** 36 | * Abort the transaction and roll back any changes. 37 | * @throws {@linkcode LambdaAbortedError} when called. 38 | */ 39 | abort(): void { 40 | throw new LambdaAbortedError(); 41 | } 42 | 43 | /** 44 | * Execute the specified statement in the current transaction. This method returns a promise 45 | * which eventually returns all the results loaded into memory. 46 | * 47 | * The PartiQL statement executed via this transaction is not immediately committed. 48 | * The entire transaction will be committed once the all the code in `transactionFunction` 49 | * (passed as an argument to {@link QldbDriver.executeLambda}) completes. 50 | * 51 | * @param statement The statement to execute. 52 | * @param parameters Variable number of arguments, where each argument corresponds to a 53 | * placeholder (?) in the PartiQL query. 54 | * The argument could be any native JavaScript type or an Ion DOM type. 55 | * [Details of Ion DOM type and JavaScript type](https://github.com/amzn/ion-js/blob/master/src/dom/README.md#iondom-data-types) 56 | * @returns Promise which fulfills with all results loaded into memory 57 | * @throws [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) when the passed argument value cannot be converted into Ion 58 | */ 59 | async execute(statement: string, ...parameters: any[]): Promise { 60 | return await this._transaction.execute(statement, ...parameters); 61 | } 62 | 63 | /** 64 | * Execute the specified statement in the current transaction. This method returns a promise 65 | * which fulfills with Readable interface, which allows you to stream one record at time 66 | * 67 | * The PartiQL statement executed via this transaction is not immediately committed. 68 | * The entire transaction will be committed once the all the code in `transactionFunction` 69 | * (passed as an argument to {@link QldbDriver.executeLambda}) completes. 70 | * 71 | * @param statement The statement to execute. 72 | * @param parameters Variable number of arguments, where each argument corresponds to a 73 | * placeholder (?) in the PartiQL query. 74 | * The argument could be any native JavaScript type or an Ion DOM type. 75 | * [Details of Ion DOM type and JavaScript type](https://github.com/amzn/ion-js/blob/master/src/dom/README.md#iondom-data-types) 76 | * @returns Promise which fulfills with a Readable Stream 77 | * @throws {@linkcode TransactionClosedError} when the transaction is closed. 78 | * @throws [Error](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) when the passed argument value cannot be converted into Ion 79 | */ 80 | async executeAndStreamResults(statement: string, ...parameters: any[]): Promise { 81 | return await this._transaction.executeAndStreamResults(statement, ...parameters); 82 | } 83 | 84 | /** 85 | * Get the transaction ID. 86 | * @returns The transaction ID. 87 | */ 88 | getTransactionId(): string { 89 | return this._transaction.getTransactionId(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/errors/Errors.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { 15 | InvalidParameterException, 16 | ResourceNotFoundException, 17 | ResourcePreconditionNotMetException 18 | } from "@aws-sdk/client-qldb"; 19 | import { 20 | BadRequestException, 21 | InvalidSessionException, 22 | OccConflictException, 23 | QLDBSessionServiceException, 24 | } from "@aws-sdk/client-qldb-session"; 25 | 26 | import { ServiceException } from "@smithy/smithy-client" 27 | 28 | import { error } from "../LogUtil"; 29 | 30 | const transactionExpiredPattern = RegExp("Transaction .* has expired"); 31 | 32 | export class ClientError extends Error { 33 | 34 | /** 35 | * @internal 36 | */ 37 | constructor(message: string) { 38 | super(message); 39 | Object.setPrototypeOf(this, ClientError.prototype) 40 | this.message = message; 41 | this.name = "ClientError"; 42 | error(message); 43 | } 44 | } 45 | 46 | export class DriverClosedError extends Error { 47 | 48 | /** 49 | * @internal 50 | */ 51 | constructor() { 52 | const message: string = "Cannot invoke methods on a closed driver. Please create a new driver and retry."; 53 | super(message); 54 | Object.setPrototypeOf(this, DriverClosedError.prototype) 55 | this.message = message; 56 | this.name = "DriverClosedError"; 57 | error(message); 58 | } 59 | } 60 | 61 | export class LambdaAbortedError extends Error { 62 | 63 | /** 64 | * @internal 65 | */ 66 | constructor() { 67 | const message: string = "Abort called. Halting execution of lambda function."; 68 | super(message); 69 | Object.setPrototypeOf(this, LambdaAbortedError.prototype) 70 | this.message = message; 71 | this.name = "LambdaAbortedError"; 72 | error(message); 73 | } 74 | } 75 | 76 | export class SessionPoolEmptyError extends Error { 77 | 78 | /** 79 | * @internal 80 | */ 81 | constructor() { 82 | const message: string = 83 | "Session pool is empty. Please close existing sessions first before retrying."; 84 | super(message); 85 | Object.setPrototypeOf(this, SessionPoolEmptyError.prototype) 86 | this.message = message; 87 | this.name = "SessionPoolEmptyError"; 88 | error(message); 89 | } 90 | } 91 | 92 | /** 93 | * @internal 94 | */ 95 | export class ExecuteError extends Error { 96 | cause: Error; 97 | isRetryable: boolean; 98 | isInvalidSessionException: boolean; 99 | transactionId: string; 100 | 101 | constructor(cause: Error, isRetryable: boolean, isInvalidSessionException: boolean, transactionId: string = null) { 102 | const message: string = "Error containing the context of a failure during Execute."; 103 | super(message); 104 | Object.setPrototypeOf(this, ExecuteError.prototype) 105 | this.cause = cause; 106 | this.isRetryable = isRetryable; 107 | this.isInvalidSessionException = isInvalidSessionException; 108 | this.transactionId = transactionId; 109 | } 110 | } 111 | 112 | /** 113 | * Is the exception an InvalidParameterException? 114 | * @param e The client error caught. 115 | * @returns True if the exception is an InvalidParameterException. False otherwise. 116 | */ 117 | export function isInvalidParameterException(e: ServiceException): boolean { 118 | return e instanceof InvalidParameterException; 119 | } 120 | 121 | /** 122 | * Is the exception an InvalidSessionException? 123 | * @param e The client error caught. 124 | * @returns True if the exception is an InvalidSessionException. False otherwise. 125 | */ 126 | export function isInvalidSessionException(e: ServiceException): boolean { 127 | return e instanceof InvalidSessionException; 128 | } 129 | 130 | /** 131 | * Is the exception because the transaction expired? The transaction expiry is a message wrapped 132 | * inside InvalidSessionException. 133 | * @param e The client error to check to see if it is an InvalidSessionException due to transaction expiry. 134 | * @returns Whether or not the exception is is an InvalidSessionException due to transaction expiry. 135 | */ 136 | export function isTransactionExpiredException(e: ServiceException): boolean { 137 | return e instanceof InvalidSessionException && transactionExpiredPattern.test(e.message); 138 | } 139 | 140 | /** 141 | * Is the exception an OccConflictException? 142 | * @param e The client error caught. 143 | * @returns True if the exception is an OccConflictException. False otherwise. 144 | */ 145 | export function isOccConflictException(e: ServiceException): boolean { 146 | return e instanceof OccConflictException; 147 | } 148 | 149 | /** 150 | * Is the exception a ResourceNotFoundException? 151 | * @param e The client error to check to see if it is a ResourceNotFoundException. 152 | * @returns Whether or not the exception is a ResourceNotFoundException. 153 | */ 154 | export function isResourceNotFoundException(e: ServiceException): boolean { 155 | return e instanceof ResourceNotFoundException; 156 | } 157 | 158 | /** 159 | * Is the exception a ResourcePreconditionNotMetException? 160 | * @param e The client error to check to see if it is a ResourcePreconditionNotMetException. 161 | * @returns Whether or not the exception is a ResourcePreconditionNotMetException. 162 | */ 163 | export function isResourcePreconditionNotMetException(e: ServiceException): boolean { 164 | return e instanceof ResourcePreconditionNotMetException; 165 | } 166 | 167 | /** 168 | * Is the exception a BadRequestException? 169 | * @param e The client error to check to see if it is a BadRequestException. 170 | * @returns Whether or not the exception is a BadRequestException. 171 | */ 172 | export function isBadRequestException(e: ServiceException): boolean { 173 | return e instanceof BadRequestException; 174 | } 175 | 176 | /** 177 | * Is the exception a retryable exception given the state of the session's transaction? 178 | * @param e The client error caught. 179 | * @param onCommit If the error caught was on a commit command. 180 | * @returns True if the exception is a retryable exception. False otherwise. 181 | * 182 | * @internal 183 | */ 184 | export function isRetryableException(e: Error, onCommit: boolean): boolean { 185 | if (e instanceof ServiceException || e instanceof QLDBSessionServiceException) { 186 | const canSdkRetry: boolean = onCommit ? false : e.$retryable && e.$retryable.throttling; 187 | 188 | return isRetryableStatusCode(e) || isOccConflictException(e) || canSdkRetry || 189 | (isInvalidSessionException(e) && !isTransactionExpiredException(e)); 190 | } 191 | return false; 192 | } 193 | 194 | /** 195 | * Does the error have a retryable code or status code? 196 | * @param e The client error caught. 197 | * @returns True if the exception has a retryable code. 198 | */ 199 | function isRetryableStatusCode(e: Error): boolean { 200 | if (e instanceof ServiceException || e instanceof QLDBSessionServiceException) { 201 | return (e.$metadata.httpStatusCode === 500) || 202 | (e.$metadata.httpStatusCode === 503) || 203 | (e.name === "NoHttpResponseException") || 204 | (e.name === "SocketTimeoutException"); 205 | } 206 | return false; 207 | } 208 | -------------------------------------------------------------------------------- /src/integrationtest/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "region": "us-east-1" 3 | } 4 | -------------------------------------------------------------------------------- /src/integrationtest/SessionManagement.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import * as chai from "chai"; 15 | import * as chaiAsPromised from "chai-as-promised"; 16 | import { dom } from "ion-js"; 17 | 18 | import { defaultRetryConfig } from "../retry/DefaultRetryConfig"; 19 | import { isTransactionExpiredException, DriverClosedError, SessionPoolEmptyError } from "../errors/Errors"; 20 | import { QldbDriver } from "../QldbDriver"; 21 | import { Result } from "../Result"; 22 | import { RetryConfig } from "../retry/RetryConfig"; 23 | import { TransactionExecutor } from "../TransactionExecutor"; 24 | import * as constants from "./TestConstants"; 25 | import { TestUtils } from "./TestUtils"; 26 | import { QLDBSessionClientConfig } from "@aws-sdk/client-qldb-session"; 27 | import { NodeHttpHandlerOptions } from "@smithy/node-http-handler"; 28 | import { ServiceException } from "@smithy/smithy-client" 29 | 30 | chai.use(chaiAsPromised); 31 | 32 | describe("SessionManagement", function() { 33 | this.timeout(0); 34 | let testUtils: TestUtils; 35 | let config: QLDBSessionClientConfig; 36 | let httpOptions: NodeHttpHandlerOptions 37 | 38 | before(async () => { 39 | testUtils = new TestUtils(constants.LEDGER_NAME); 40 | config = testUtils.createClientConfiguration(); 41 | 42 | await testUtils.runForceDeleteLedger(); 43 | await testUtils.runCreateLedger(); 44 | 45 | // Create table 46 | const driver: QldbDriver = new QldbDriver(constants.LEDGER_NAME, config); 47 | const statement: string = `CREATE TABLE ${constants.TABLE_NAME}`; 48 | const count: number = await driver.executeLambda(async (txn: TransactionExecutor): Promise => { 49 | const result: Result = await txn.execute(statement); 50 | const resultSet: dom.Value[] = result.getResultList(); 51 | return resultSet.length; 52 | }); 53 | chai.assert.equal(count, 1); 54 | await new Promise(r => setTimeout(r, 3000)); 55 | }); 56 | 57 | after(async () => { 58 | await testUtils.runDeleteLedger(); 59 | }); 60 | 61 | it("Throws exception when connecting to a non-existent ledger", async () => { 62 | const driver: QldbDriver = new QldbDriver("NonExistentLedger", config); 63 | let error; 64 | try { 65 | error = await chai.expect(driver.executeLambda(async (txn: TransactionExecutor) => { 66 | await txn.execute(`SELECT name FROM ${constants.TABLE_NAME} WHERE name='Bob'`); 67 | })).to.be.rejected; 68 | 69 | } finally { 70 | chai.assert.equal(error.name, "BadRequestException"); 71 | driver.close(); 72 | } 73 | }); 74 | 75 | it("Can get a session when the pool has no sessions and hasn't hit the pool limit", async () => { 76 | // Start a pooled driver with default pool limit so it doesn't have sessions in the pool 77 | // and has not hit the limit 78 | const driver: QldbDriver = new QldbDriver(constants.LEDGER_NAME, config); 79 | try { 80 | // Execute a statement to implicitly create a session and return it to the pool 81 | await driver.executeLambda(async (txn: TransactionExecutor) => { 82 | await txn.execute(`SELECT name FROM ${constants.TABLE_NAME} WHERE name='Bob'`); 83 | }); 84 | } finally { 85 | driver.close(); 86 | } 87 | }); 88 | 89 | it("Throws exception when all the sessions are busy and pool limit is reached", async () => { 90 | // Set maxConcurrentTransactions to 1 91 | const driver: QldbDriver = new QldbDriver(constants.LEDGER_NAME, config, httpOptions, 1, defaultRetryConfig); 92 | try { 93 | // Execute and do not wait for the promise to resolve, exhausting the pool 94 | driver.executeLambda(async (txn: TransactionExecutor) => { 95 | await txn.execute(`SELECT name FROM ${constants.TABLE_NAME} WHERE name='Bob'`); 96 | }); 97 | // Attempt to implicitly get a session by executing 98 | await driver.executeLambda(async (txn: TransactionExecutor) => { 99 | await txn.execute(`SELECT name FROM ${constants.TABLE_NAME} WHERE name='Bob'`); 100 | }); 101 | chai.assert.fail("SessionPoolEmptyError was not thrown") 102 | } catch (e) { 103 | if (!(e instanceof SessionPoolEmptyError)) { 104 | throw e; 105 | } 106 | } finally { 107 | driver.close(); 108 | } 109 | }); 110 | 111 | it("Throws exception when the driver has been closed", async () => { 112 | const driver: QldbDriver = new QldbDriver(constants.LEDGER_NAME, config); 113 | driver.close(); 114 | try { 115 | await driver.executeLambda(async (txn: TransactionExecutor) => { 116 | await txn.execute(`SELECT name FROM ${constants.TABLE_NAME} WHERE name='Bob'`); 117 | }); 118 | } catch (e) { 119 | if (!(e instanceof DriverClosedError)) { 120 | throw e; 121 | } 122 | } 123 | }); 124 | 125 | it("Throws exception when transaction expires due to timeout", async () => { 126 | const driver: QldbDriver = new QldbDriver(constants.LEDGER_NAME, config); 127 | let error; 128 | try { 129 | error = await chai.expect(driver.executeLambda(async (txn: TransactionExecutor) => { 130 | // Wait for transaction to expire 131 | await new Promise(resolve => setTimeout(resolve, 40000)); 132 | })).to.be.rejected; 133 | } finally { 134 | chai.assert.isTrue(isTransactionExpiredException(error)); 135 | } 136 | }); 137 | 138 | it("Properly cleans the transaction state and does not abort it in the middle of a transaction", async () => { 139 | const driver: QldbDriver = new QldbDriver(constants.LEDGER_NAME, config, httpOptions, 1); 140 | 141 | const noDelayConfig: RetryConfig = new RetryConfig(Number.MAX_VALUE, () => 0); 142 | 143 | const startTime: number = Date.now(); 144 | 145 | while ((Date.now() - startTime) < 10000) { 146 | try { 147 | await driver.executeLambda(async (txn) => { 148 | await txn.execute(`SELECT * FROM ${constants.TABLE_NAME}`); 149 | if ((Date.now() - startTime) < 10000) { 150 | const err = new ServiceException({ $metadata: { httpStatusCode: 500 }, name: "mock retryable exception", $fault: "server" }); 151 | throw err; 152 | } 153 | }, noDelayConfig); 154 | } catch (e) { 155 | chai.assert.fail(e.name); 156 | } 157 | } 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /src/integrationtest/TestConstants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | export const TABLE_NAME = "NodeTestTable"; 15 | export const CREATE_TABLE_NAME = "NodeCreateTestTable"; 16 | export const INDEX_ATTRIBUTE = "Name"; 17 | export const COLUMN_NAME = "Name"; 18 | export const SINGLE_DOCUMENT_VALUE = "SingleDocumentValue"; 19 | export const MULTI_DOC_VALUE_1 = "MultipleDocumentValue1"; 20 | export const MULTI_DOC_VALUE_2 = "MultipleDocumentValue2"; 21 | export const LEDGER_NAME = process.env.npm_config_test_ledger_suffix ? 22 | `Node-TestLedger-${process.env.npm_config_test_ledger_suffix}` : 23 | "Node-TestLedger"; 24 | -------------------------------------------------------------------------------- /src/integrationtest/TestUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { dom, load } from "ion-js"; 15 | 16 | import * as mocharc from './.mocharc.json' 17 | import { isResourceNotFoundException } from "../errors/Errors"; 18 | import { Result } from "../Result"; 19 | import * as constants from "./TestConstants"; 20 | import { TransactionExecutor } from "../TransactionExecutor"; 21 | import { 22 | CreateLedgerCommand, 23 | CreateLedgerRequest, 24 | CreateLedgerResponse, 25 | DeleteLedgerCommand, 26 | DeleteLedgerRequest, 27 | DescribeLedgerCommand, 28 | DescribeLedgerRequest, 29 | DescribeLedgerResponse, 30 | QLDBClient, 31 | QLDBClientConfig, 32 | UpdateLedgerCommand, 33 | UpdateLedgerRequest 34 | } from "@aws-sdk/client-qldb"; 35 | 36 | export class TestUtils { 37 | public ledgerName: string; 38 | public regionName: string; 39 | public clientConfig: QLDBClientConfig; 40 | public qldbClient: QLDBClient; 41 | 42 | constructor(ledgerName: string) { 43 | this.ledgerName = ledgerName; 44 | this.regionName = mocharc.region; 45 | this.clientConfig = this.createClientConfiguration(); 46 | this.qldbClient = new QLDBClient(this.clientConfig); 47 | } 48 | 49 | createClientConfiguration() : QLDBClientConfig { 50 | const config: QLDBClientConfig = {}; 51 | if (this.regionName != undefined) { 52 | config.region = this.regionName; 53 | } 54 | return config; 55 | } 56 | 57 | static sleep(ms: number): Promise { 58 | return new Promise(resolve => setTimeout(resolve, ms)); 59 | } 60 | 61 | async runCreateLedger(): Promise { 62 | console.log(`Creating a ledger named: ${this.ledgerName}...`); 63 | const request: CreateLedgerRequest = { 64 | Name: this.ledgerName, 65 | PermissionsMode: "ALLOW_ALL" 66 | } 67 | const command = new CreateLedgerCommand(request); 68 | const response: CreateLedgerResponse = await this.qldbClient.send(command); 69 | console.log(`Success. Ledger state: ${response.State}.`); 70 | await this.waitForActive(); 71 | } 72 | 73 | async waitForActive(): Promise { 74 | console.log(`Waiting for ledger ${this.ledgerName} to become active...`); 75 | const request: DescribeLedgerRequest = { 76 | Name: this.ledgerName 77 | } 78 | while (true) { 79 | const command = new DescribeLedgerCommand(request); 80 | const result: DescribeLedgerResponse = await this.qldbClient.send(command); 81 | if (result.State === "ACTIVE") { 82 | console.log("Success. Ledger is active and ready to be used."); 83 | return; 84 | } 85 | console.log("The ledger is still creating. Please wait..."); 86 | await TestUtils.sleep(10000); 87 | } 88 | } 89 | 90 | async runDeleteLedger(): Promise { 91 | await this.deleteLedger(); 92 | await this.waitForDeletion(); 93 | } 94 | 95 | async runForceDeleteLedger(): Promise { 96 | try { 97 | await this.deleteLedger(); 98 | await this.waitForDeletion(); 99 | } catch (e) { 100 | if (isResourceNotFoundException(e)) { 101 | console.log("Ledger did not previously exist."); 102 | return; 103 | } else { 104 | throw e; 105 | } 106 | } 107 | } 108 | 109 | private async deleteLedger(): Promise { 110 | console.log(`Attempting to delete the ledger with name: ${this.ledgerName}`); 111 | await this.disableDeletionProtection(); 112 | const request: DeleteLedgerRequest = { 113 | Name: this.ledgerName 114 | }; 115 | const command = new DeleteLedgerCommand(request); 116 | await this.qldbClient.send(command); 117 | } 118 | 119 | private async waitForDeletion(): Promise { 120 | console.log("Waiting for the ledger to be deleted..."); 121 | const request: DescribeLedgerRequest = { 122 | Name: this.ledgerName 123 | }; 124 | while (true) { 125 | try { 126 | const command = new DescribeLedgerCommand(request); 127 | await this.qldbClient.send(command); 128 | console.log("The ledger is still being deleted. Please wait..."); 129 | await TestUtils.sleep(10000); 130 | } catch (e) { 131 | if (isResourceNotFoundException(e)) { 132 | console.log("Success. Ledger is deleted."); 133 | break; 134 | } else { 135 | throw e; 136 | } 137 | } 138 | } 139 | } 140 | 141 | private async disableDeletionProtection(): Promise { 142 | const request: UpdateLedgerRequest = { 143 | Name: this.ledgerName, 144 | DeletionProtection: false 145 | } 146 | const command = new UpdateLedgerCommand(request); 147 | await this.qldbClient.send(command); 148 | } 149 | 150 | async readIonValue(txn: TransactionExecutor, value: dom.Value): Promise { 151 | let result: Result; 152 | if (value.isNull()) { 153 | const searchQuery: string = `SELECT VALUE ${constants.COLUMN_NAME} FROM ${constants.TABLE_NAME}` + 154 | ` WHERE ${constants.COLUMN_NAME} IS NULL`; 155 | result = await txn.execute(searchQuery); 156 | } else { 157 | const searchQuery: string = `SELECT VALUE ${constants.COLUMN_NAME} FROM ${constants.TABLE_NAME}` + 158 | ` WHERE ${constants.COLUMN_NAME} = ?`; 159 | result = await txn.execute(searchQuery, value); 160 | } 161 | return result.getResultList()[0]; 162 | } 163 | 164 | static getLengthOfResultSet(result: Result): number { 165 | return result.getResultList().length; 166 | } 167 | 168 | static getIonTypes(): dom.Value[] { 169 | const values: dom.Value[] = []; 170 | 171 | const ionClob: dom.Value = load('{{"This is a CLOB of text."}}'); 172 | values.push(ionClob); 173 | const ionBlob: dom.Value = load('{{aGVsbG8=}}'); 174 | values.push(ionBlob); 175 | const ionBool: dom.Value = load('true'); 176 | values.push(ionBool); 177 | const ionDecimal: dom.Value = load('0.1'); 178 | values.push(ionDecimal); 179 | const ionFloat: dom.Value = load('0.2e0'); 180 | values.push(ionFloat); 181 | const ionInt: dom.Value = load('1'); 182 | values.push(ionInt); 183 | const ionList: dom.Value = load('[1,2]'); 184 | values.push(ionList); 185 | const ionNull: dom.Value = load('null'); 186 | values.push(ionNull); 187 | const ionSexp: dom.Value = load('(cons 1 2)'); 188 | values.push(ionSexp); 189 | const ionString: dom.Value = load('"string"'); 190 | values.push(ionString); 191 | const ionStruct: dom.Value = load('{a:1}'); 192 | values.push(ionStruct); 193 | const ionSymbol: dom.Value = load('abc'); 194 | values.push(ionSymbol); 195 | const ionTimestamp: dom.Value = load('2016-12-20T05:23:43.000000-00:00'); 196 | values.push(ionTimestamp); 197 | 198 | const ionNullClob: dom.Value = load('null.clob'); 199 | values.push(ionNullClob); 200 | const ionNullBlob: dom.Value = load('null.blob'); 201 | values.push(ionNullBlob); 202 | const ionNullBool: dom.Value = load('null.bool'); 203 | values.push(ionNullBool); 204 | const ionNullDecimal: dom.Value = load('null.decimal'); 205 | values.push(ionNullDecimal); 206 | const ionNullFloat: dom.Value = load('null.float'); 207 | values.push(ionNullFloat); 208 | const ionNullInt: dom.Value = load('null.int'); 209 | values.push(ionNullInt); 210 | const ionNullList: dom.Value = load('null.list'); 211 | values.push(ionNullList); 212 | const ionNullSexp: dom.Value = load('null.sexp'); 213 | values.push(ionNullSexp); 214 | const ionNullString: dom.Value = load('null.string'); 215 | values.push(ionNullString); 216 | const ionNullStruct: dom.Value = load('null.struct'); 217 | values.push(ionNullStruct); 218 | const ionNullSymbol: dom.Value = load('null.symbol'); 219 | values.push(ionNullSymbol); 220 | const ionNullTimestamp: dom.Value = load('null.timestamp'); 221 | values.push(ionNullTimestamp); 222 | 223 | const ionClobWithAnnotation: dom.Value = load('annotation::{{"This is a CLOB of text."}}'); 224 | values.push(ionClobWithAnnotation); 225 | const ionBlobWithAnnotation: dom.Value = load('annotation::{{aGVsbG8=}}'); 226 | values.push(ionBlobWithAnnotation); 227 | const ionBoolWithAnnotation: dom.Value = load('annotation::true'); 228 | values.push(ionBoolWithAnnotation); 229 | const ionDecimalWithAnnotation: dom.Value = load('annotation::0.1'); 230 | values.push(ionDecimalWithAnnotation); 231 | const ionFloatWithAnnotation: dom.Value = load('annotation::0.2e0'); 232 | values.push(ionFloatWithAnnotation); 233 | const ionIntWithAnnotation: dom.Value = load('annotation::1'); 234 | values.push(ionIntWithAnnotation); 235 | const ionListWithAnnotation: dom.Value = load('annotation::[1,2]'); 236 | values.push(ionListWithAnnotation); 237 | const ionNullWithAnnotation: dom.Value = load('annotation::null'); 238 | values.push(ionNullWithAnnotation); 239 | const ionSexpWithAnnotation: dom.Value = load('annotation::(cons 1 2)'); 240 | values.push(ionSexpWithAnnotation); 241 | const ionStringWithAnnotation: dom.Value = load('annotation::"string"'); 242 | values.push(ionStringWithAnnotation); 243 | const ionStructWithAnnotation: dom.Value = load('annotation::{a:1}'); 244 | values.push(ionStructWithAnnotation); 245 | const ionSymbolWithAnnotation: dom.Value = load('annotation::abc'); 246 | values.push(ionSymbolWithAnnotation); 247 | const ionTimestampWithAnnotation: dom.Value = load('annotation::2016-12-20T05:23:43.000000-00:00'); 248 | values.push(ionTimestampWithAnnotation); 249 | 250 | return values; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/retry/BackoffFunction.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | export type BackoffFunction = (retryAttempt: number, error: Error, transactionId: string) => number; 15 | -------------------------------------------------------------------------------- /src/retry/DefaultRetryConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { BackoffFunction } from "./BackoffFunction"; 15 | import { RetryConfig } from "./RetryConfig"; 16 | 17 | const SLEEP_CAP_MS: number = 5000; 18 | const SLEEP_BASE_MS: number = 10; 19 | 20 | /** 21 | * A default backoff function which returns the amount of time(in milliseconds) to delay the next retry attempt 22 | * Reference: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ 23 | * 24 | * @param retryAttempt The number of attempts done till now 25 | * @param error The error that occurred while executing the previous transaction 26 | * @param transactionId The transaction Id for which the execution was attempted 27 | * 28 | * @internal 29 | */ 30 | export const defaultBackoffFunction: BackoffFunction = (retryAttempt: number, error: Error, transactionId: string) => { 31 | const fullJitterBackoffMax: number = Math.min(SLEEP_CAP_MS, SLEEP_BASE_MS * 2 ** retryAttempt); 32 | const delayTime: number = Math.random() * fullJitterBackoffMax; 33 | return delayTime; 34 | } 35 | 36 | export const defaultRetryConfig: RetryConfig = new RetryConfig(4, defaultBackoffFunction); 37 | -------------------------------------------------------------------------------- /src/retry/RetryConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | import { BackoffFunction } from "./BackoffFunction"; 15 | import { defaultBackoffFunction } from "./DefaultRetryConfig"; 16 | 17 | export class RetryConfig { 18 | private _retryLimit: number; 19 | private _backoffFunction: BackoffFunction; 20 | 21 | /** 22 | * Retry and Backoff config for Qldb Driver. 23 | 24 | * @param retryLimit When there is a failure while executing the transaction like OCC or any other retryable failure, the driver will try running your transaction block again. 25 | * This parameter tells the driver how many times to retry when there are failures. The value must be greater than 0. The default value is 4. 26 | * See {@link https://docs.aws.amazon.com/qldb/latest/developerguide/driver.best-practices.html#driver.best-practices.configuring} for more details. 27 | * 28 | * @param backoffFunction A custom function that accepts a retry count, error, transaction id and returns the amount 29 | * of time to delay in milliseconds. If the result is a non-zero negative value the backoff will 30 | * be considered to be zero. If no backoff function is provided then {@linkcode defaultBackoffFunction} will be used. 31 | * 32 | * @throws RangeError if `retryLimit` is less than 0. 33 | */ 34 | constructor(retryLimit: number = 4, backoffFunction: BackoffFunction = defaultBackoffFunction) { 35 | if (retryLimit < 0) { 36 | throw new RangeError("Value for retryLimit cannot be negative."); 37 | } 38 | this._retryLimit = retryLimit; 39 | this._backoffFunction = backoffFunction; 40 | } 41 | 42 | getRetryLimit(): number { 43 | return this._retryLimit; 44 | } 45 | 46 | getBackoffFunction(): BackoffFunction { 47 | return this._backoffFunction; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/stats/IOUsage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Provides metrics on IO usage of requests. 16 | */ 17 | export class IOUsage { 18 | private _readIOs: number; 19 | 20 | /** 21 | * Creates an IOUsage. 22 | * @param readIOs The number of Read IOs. 23 | * 24 | * @internal 25 | */ 26 | constructor(readIOs: number) { 27 | this._readIOs = readIOs; 28 | } 29 | 30 | /** 31 | * Provides the number of Read IOs for a request. 32 | * @returns The number of Reads for a request. 33 | */ 34 | getReadIOs(): number { 35 | return this._readIOs; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/stats/TimingInformation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | /** 15 | * Provides metrics on server-side processing time for requests. 16 | */ 17 | export class TimingInformation { 18 | private _processingTimeMilliseconds: number; 19 | 20 | /** 21 | * Creates a TimingInformation. 22 | * @param processingTimeMilliseconds The server-side processing time in milliseconds. 23 | * 24 | * @internal 25 | */ 26 | constructor(processingTimeMilliseconds: number) { 27 | this._processingTimeMilliseconds = processingTimeMilliseconds; 28 | } 29 | 30 | /** 31 | * Provides the server-side time spent on a request. 32 | * @returns The server-side processing time in millisecond. 33 | */ 34 | getProcessingTimeMilliseconds(): number { 35 | return this._processingTimeMilliseconds; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/Communicator.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | // Test environment imports 15 | import "mocha"; 16 | 17 | import { 18 | QLDBSession, 19 | QLDBSessionClientConfig, 20 | CommitTransactionResult, 21 | ExecuteStatementResult, 22 | Page, 23 | SendCommandRequest, 24 | SendCommandResult, 25 | ValueHolder, 26 | QLDBSessionClient, 27 | SendCommandCommand, 28 | SendCommandCommandInput 29 | } from "@aws-sdk/client-qldb-session"; 30 | import * as chai from "chai"; 31 | import * as chaiAsPromised from "chai-as-promised"; 32 | import * as sinon from "sinon"; 33 | 34 | import { Communicator } from "../Communicator"; 35 | import { TextEncoder } from "util"; 36 | 37 | chai.use(chaiAsPromised); 38 | const sandbox = sinon.createSandbox(); 39 | 40 | const testLedgerName: string = "fakeLedgerName"; 41 | const testMessage: string = "foo"; 42 | const testPageToken: string = "pageToken"; 43 | const testSessionToken: string = "sessionToken"; 44 | const enc = new TextEncoder(); 45 | const testValueHolder: ValueHolder = { 46 | IonBinary: enc.encode("text") 47 | }; 48 | const testParameters: ValueHolder[] = [testValueHolder]; 49 | const testStatement: string = "SELECT * FROM foo"; 50 | const testTransactionId: string = "txnId"; 51 | const testHashToQldb: Uint8Array = new Uint8Array([1, 2, 3]); 52 | const testHashFromQldb: Uint8Array = new Uint8Array([4, 5, 6]); 53 | const testLowLevelClientOptions: QLDBSessionClientConfig = { 54 | region: "fakeRegion" 55 | }; 56 | 57 | const testPage: Page = {}; 58 | const testExecuteStatementResult: ExecuteStatementResult = { 59 | FirstPage: testPage 60 | }; 61 | const testCommitTransactionResult: CommitTransactionResult = { 62 | TransactionId: testTransactionId, 63 | CommitDigest: testHashFromQldb 64 | }; 65 | const testSendCommandResult: SendCommandResult = { 66 | StartSession: { 67 | SessionToken: testSessionToken 68 | }, 69 | StartTransaction: { 70 | TransactionId: testTransactionId 71 | }, 72 | FetchPage: { 73 | Page: testPage 74 | }, 75 | ExecuteStatement: testExecuteStatementResult, 76 | CommitTransaction: testCommitTransactionResult 77 | }; 78 | 79 | let sendCommandStub: sinon.SinonStub; 80 | let testQldbLowLevelClient: QLDBSession; 81 | let communicator: Communicator; 82 | 83 | describe("Communicator", () => { 84 | 85 | beforeEach(async () => { 86 | testQldbLowLevelClient = new QLDBSession(testLowLevelClientOptions); 87 | sendCommandStub = sandbox.stub(testQldbLowLevelClient, "send"); 88 | sendCommandStub.resolves(testSendCommandResult); 89 | communicator = await Communicator.create(testQldbLowLevelClient, testLedgerName); 90 | }); 91 | 92 | afterEach(() => { 93 | sandbox.restore(); 94 | }); 95 | 96 | describe("#create()", () => { 97 | it("should have all attributes equal to mock values when static factory method called", async () => { 98 | chai.assert.equal(communicator["_qldbClient"], testQldbLowLevelClient); 99 | chai.assert.equal(communicator["_sessionToken"], testSessionToken); 100 | }); 101 | 102 | it("should return a rejected promise when error is thrown", async () => { 103 | sendCommandStub.returns({ 104 | promise: () => { 105 | throw new Error(testMessage); 106 | } 107 | }); 108 | await chai.expect(Communicator.create(testQldbLowLevelClient, testLedgerName)).to.be.rejected; 109 | sinon.assert.calledTwice(sendCommandStub); 110 | }); 111 | }); 112 | 113 | describe("#abortTransaction()", () => { 114 | it("should call AWS SDK's sendCommand with abort request when called", async () => { 115 | await communicator.abortTransaction(); 116 | const testRequest: SendCommandCommandInput = { 117 | SessionToken: testSessionToken, 118 | AbortTransaction: {} 119 | }; 120 | sinon.assert.calledTwice(sendCommandStub); 121 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 122 | }); 123 | 124 | it("should return a rejected promise when error is thrown", async () => { 125 | sendCommandStub.rejects(testMessage); 126 | const testRequest: SendCommandCommandInput = { 127 | SessionToken: testSessionToken, 128 | AbortTransaction: {} 129 | }; 130 | await chai.expect(communicator.abortTransaction()).to.be.rejected; 131 | sinon.assert.calledTwice(sendCommandStub); 132 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 133 | }); 134 | }); 135 | 136 | describe("#commit()", () => { 137 | it("should call AWS SDK's sendCommand with commit request when called", async () => { 138 | const commitResult: CommitTransactionResult = await communicator.commit({ 139 | TransactionId: testTransactionId, 140 | CommitDigest: testHashToQldb 141 | }); 142 | const testRequest: SendCommandRequest = { 143 | SessionToken: testSessionToken, 144 | CommitTransaction: { 145 | TransactionId: testTransactionId, 146 | CommitDigest: testHashToQldb 147 | } 148 | }; 149 | sinon.assert.calledTwice(sendCommandStub); 150 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 151 | chai.assert.equal(commitResult, testSendCommandResult.CommitTransaction); 152 | }); 153 | 154 | it("should return a rejected promise when error is thrown", async () => { 155 | sendCommandStub.rejects(testMessage); 156 | const testRequest: SendCommandRequest = { 157 | SessionToken: testSessionToken, 158 | CommitTransaction: { 159 | TransactionId: testTransactionId, 160 | CommitDigest: testHashToQldb 161 | } 162 | }; 163 | await chai.expect( 164 | communicator.commit({ 165 | TransactionId: testTransactionId, 166 | CommitDigest: testHashToQldb 167 | }) 168 | ).to.be.rejected; 169 | sinon.assert.calledTwice(sendCommandStub); 170 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 171 | }); 172 | }); 173 | 174 | describe("#executeStatement()", () => { 175 | it("should return an ExecuteStatementResult object when provided with a statement", async () => { 176 | const result: ExecuteStatementResult = await communicator.executeStatement( 177 | testTransactionId, 178 | testStatement, 179 | [] 180 | ); 181 | const testRequest: SendCommandRequest = { 182 | SessionToken: testSessionToken, 183 | ExecuteStatement: { 184 | Statement: testStatement, 185 | TransactionId: testTransactionId, 186 | Parameters: [] 187 | } 188 | }; 189 | sinon.assert.calledTwice(sendCommandStub); 190 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 191 | chai.assert.equal(result, testExecuteStatementResult); 192 | }); 193 | 194 | it("should return an ExecuteStatementResult object when provided with a statement and parameters", async () => { 195 | const result: ExecuteStatementResult = await communicator.executeStatement( 196 | testTransactionId, 197 | testStatement, 198 | testParameters 199 | ); 200 | const testRequest: SendCommandRequest = { 201 | SessionToken: testSessionToken, 202 | ExecuteStatement: { 203 | Statement: testStatement, 204 | TransactionId: testTransactionId, 205 | Parameters: testParameters 206 | } 207 | }; 208 | sinon.assert.calledTwice(sendCommandStub); 209 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 210 | chai.assert.equal(result, testExecuteStatementResult); 211 | }); 212 | 213 | it("should return a rejected promise when error is thrown", async () => { 214 | sendCommandStub.rejects(testMessage); 215 | const testRequest: SendCommandRequest = { 216 | SessionToken: testSessionToken, 217 | ExecuteStatement: { 218 | Statement: testStatement, 219 | TransactionId: testTransactionId, 220 | Parameters: [] 221 | } 222 | }; 223 | await chai.expect(communicator.executeStatement(testTransactionId, testStatement, [])).to.be.rejected; 224 | sinon.assert.calledTwice(sendCommandStub); 225 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 226 | }); 227 | }); 228 | 229 | describe("#endSession()", () => { 230 | it("should call AWS SDK's sendCommand with end session request when called", async () => { 231 | await communicator.endSession(); 232 | const testRequest: SendCommandRequest = { 233 | EndSession: {}, 234 | SessionToken: testSessionToken 235 | }; 236 | sinon.assert.calledTwice(sendCommandStub); 237 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 238 | }); 239 | 240 | it("should return a rejected promise when error is thrown", async () => { 241 | sendCommandStub.rejects(testMessage); 242 | const testRequest: SendCommandRequest = { 243 | EndSession: {}, 244 | SessionToken: testSessionToken 245 | }; 246 | await chai.expect(communicator.endSession()).to.be.rejected; 247 | sinon.assert.calledTwice(sendCommandStub); 248 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 249 | }); 250 | }); 251 | 252 | describe("#fetchPage()", () => { 253 | it("should return a Page object when called", async () => { 254 | const page: Page = (await communicator.fetchPage(testTransactionId, testPageToken)).Page; 255 | const testRequest: SendCommandRequest = { 256 | SessionToken: testSessionToken, 257 | FetchPage: { 258 | TransactionId: testTransactionId, 259 | NextPageToken: testPageToken 260 | } 261 | }; 262 | sinon.assert.calledTwice(sendCommandStub); 263 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 264 | chai.assert.equal(page, testPage); 265 | }); 266 | 267 | it("should return a rejected promise when error is thrown", async () => { 268 | sendCommandStub.rejects(testMessage); 269 | const testRequest: SendCommandRequest = { 270 | SessionToken: testSessionToken, 271 | FetchPage: { 272 | TransactionId: testTransactionId, 273 | NextPageToken: testPageToken 274 | } 275 | }; 276 | await chai.expect(communicator.fetchPage(testTransactionId, testPageToken)).to.be.rejected; 277 | sinon.assert.calledTwice(sendCommandStub); 278 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 279 | }); 280 | }); 281 | 282 | 283 | describe("#getQldbClient()", () => { 284 | it("should return the low level client when called", () => { 285 | const lowLevelClient: QLDBSessionClient = communicator.getQldbClient(); 286 | chai.assert.equal(lowLevelClient, testQldbLowLevelClient); 287 | }); 288 | }); 289 | 290 | describe("#getSessionToken()", () => { 291 | it("should return the session token when called", () => { 292 | const sessionToken: string = communicator.getSessionToken(); 293 | chai.assert.equal(sessionToken, testSessionToken); 294 | }); 295 | }); 296 | 297 | describe("#startTransaction()", () => { 298 | it("should return the newly started transaction's transaction ID when called", async () => { 299 | const txnId: string = (await communicator.startTransaction()).TransactionId; 300 | const testRequest: SendCommandRequest = { 301 | SessionToken: testSessionToken, 302 | StartTransaction: {} 303 | }; 304 | sinon.assert.calledTwice(sendCommandStub); 305 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 306 | chai.assert.equal(txnId, testTransactionId); 307 | }); 308 | 309 | it("should return a rejected promise when error is thrown", async () => { 310 | sendCommandStub.rejects(testMessage); 311 | const testRequest: SendCommandRequest = { 312 | SessionToken: testSessionToken, 313 | StartTransaction: {} 314 | }; 315 | await chai.expect(communicator.startTransaction()).to.be.rejected; 316 | sinon.assert.calledTwice(sendCommandStub); 317 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 318 | }); 319 | }); 320 | 321 | describe("#_sendCommand()", () => { 322 | it("should return a SendCommandResult object when called", async () => { 323 | const testRequest: SendCommandRequest = { 324 | SessionToken: testSessionToken 325 | }; 326 | const mockSendCommandRequest: SendCommandCommand = new SendCommandCommand(testRequest); 327 | const result: SendCommandResult = await communicator["_sendCommand"](mockSendCommandRequest); 328 | sinon.assert.calledTwice(sendCommandStub); 329 | chai.assert.deepEqual(sendCommandStub.secondCall.args[0].input, testRequest); 330 | chai.assert.equal(result, testSendCommandResult); 331 | }); 332 | 333 | it("should return a rejected promise when error is thrown", async () => { 334 | sendCommandStub.rejects(testMessage); 335 | const mockSendCommandRequest: SendCommandCommand = new SendCommandCommand({ 336 | SessionToken: testSessionToken 337 | }) 338 | const sendCommand = communicator["_sendCommand"]; 339 | await chai.expect(sendCommand(mockSendCommandRequest)).to.be.rejected; 340 | }); 341 | }); 342 | }); 343 | -------------------------------------------------------------------------------- /src/test/Errors.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | // Test environment imports 15 | import "mocha"; 16 | 17 | import * as chai from "chai"; 18 | import * as chaiAsPromised from "chai-as-promised"; 19 | import * as sinon from "sinon"; 20 | 21 | import { 22 | ClientError, 23 | DriverClosedError, 24 | isInvalidParameterException, 25 | isInvalidSessionException, 26 | isOccConflictException, 27 | isResourceNotFoundException, 28 | isResourcePreconditionNotMetException, 29 | isRetryableException, 30 | LambdaAbortedError, 31 | SessionPoolEmptyError, 32 | isTransactionExpiredException, 33 | isBadRequestException 34 | } from "../errors/Errors"; 35 | import * as LogUtil from "../LogUtil"; 36 | import { BadRequestException, InvalidSessionException, OccConflictException } from "@aws-sdk/client-qldb-session"; 37 | import { InvalidParameterException, ResourceNotFoundException, ResourcePreconditionNotMetException } from "@aws-sdk/client-qldb"; 38 | import { ServiceException } from "@smithy/smithy-client"; 39 | 40 | chai.use(chaiAsPromised); 41 | const sandbox = sinon.createSandbox(); 42 | 43 | const testMessage: string = "foo"; 44 | 45 | describe("Errors", () => { 46 | 47 | afterEach(() => { 48 | sandbox.restore(); 49 | }); 50 | 51 | describe("#ClientError", () => { 52 | it("should be a ClientError when new ClientError created", () => { 53 | const logSpy = sandbox.spy(LogUtil, "error"); 54 | const error = new ClientError(testMessage); 55 | chai.expect(error).to.be.instanceOf(ClientError); 56 | chai.assert.equal(error.name, "ClientError"); 57 | chai.assert.equal(error.message, testMessage); 58 | sinon.assert.calledOnce(logSpy); 59 | }); 60 | }); 61 | 62 | describe("#DriverClosedError", () => { 63 | it("should be a DriverClosedError when new DriverClosedError created", () => { 64 | const logSpy = sandbox.spy(LogUtil, "error"); 65 | const error = new DriverClosedError(); 66 | chai.expect(error).to.be.instanceOf(DriverClosedError); 67 | chai.assert.equal(error.name, "DriverClosedError"); 68 | sinon.assert.calledOnce(logSpy); 69 | }); 70 | }); 71 | 72 | describe("#LambdaAbortedError", () => { 73 | it("should be a LambdaAbortedError when new LambdaAbortedError created", () => { 74 | const logSpy = sandbox.spy(LogUtil, "error"); 75 | const error = new LambdaAbortedError(); 76 | chai.expect(error).to.be.instanceOf(LambdaAbortedError); 77 | chai.assert.equal(error.name, "LambdaAbortedError"); 78 | sinon.assert.calledOnce(logSpy); 79 | }); 80 | }); 81 | 82 | describe("#SessionPoolEmptyError", () => { 83 | it("should be a SessionPoolEmptyError when new SessionPoolEmptyError created", () => { 84 | const logSpy = sandbox.spy(LogUtil, "error"); 85 | const error = new SessionPoolEmptyError(); 86 | chai.expect(error).to.be.instanceOf(SessionPoolEmptyError); 87 | chai.assert.equal(error.name, "SessionPoolEmptyError"); 88 | sinon.assert.calledOnce(logSpy); 89 | }); 90 | }); 91 | 92 | describe("#isInvalidParameterException()", () => { 93 | it("should return true when error is an InvalidParameterException", () => { 94 | const mockError = new InvalidParameterException({ message: "", $metadata: {}}); 95 | chai.assert.isTrue(isInvalidParameterException(mockError)); 96 | }); 97 | 98 | it("should return false when error is not an InvalidParameterException", () => { 99 | const mockError = new ServiceException({ $metadata: {}, name: "", $fault: "server" }); 100 | chai.assert.isFalse(isInvalidParameterException(mockError)); 101 | }); 102 | }); 103 | 104 | describe("#isInvalidSessionException()", () => { 105 | it("should return true when error is an InvalidSessionException", () => { 106 | const mockError = new InvalidSessionException({ message: "", $metadata: {}}); 107 | chai.assert.isTrue(isInvalidSessionException(mockError)); 108 | }); 109 | 110 | it("should return false when error is not an InvalidSessionException", () => { 111 | const mockError = new ServiceException({ $metadata: {}, name: "", $fault: "server" }); 112 | chai.assert.isFalse(isInvalidSessionException(mockError)); 113 | }); 114 | }); 115 | 116 | describe("#isOccConflictException()", () => { 117 | it("should return true when error is an OccConflictException", () => { 118 | const mockError = new OccConflictException({ message: "", $metadata: {}}); 119 | chai.assert.isTrue(isOccConflictException(mockError)); 120 | }); 121 | 122 | it("should return false when error is not an OccConflictException", () => { 123 | const mockError = new ServiceException({ $metadata: {}, name: "", $fault: "server" }); 124 | chai.assert.isFalse(isOccConflictException(mockError)); 125 | }); 126 | }); 127 | 128 | describe("#isResourceNotFoundException()", () => { 129 | it("should return true when error is a ResourceNotFoundException", () => { 130 | const mockError = new ResourceNotFoundException({ message: "", $metadata: {}}); 131 | chai.assert.isTrue(isResourceNotFoundException(mockError)); 132 | }); 133 | 134 | it("should return false when error is not a ResourceNotFoundException", () => { 135 | const mockError = new ServiceException({ $metadata: {}, name: "", $fault: "server" }); 136 | chai.assert.isFalse(isResourceNotFoundException(mockError)); 137 | }); 138 | }); 139 | 140 | describe("#isResourcePreconditionNotMetException()", () => { 141 | it("should return true when error is a ResourcePreconditionNotMetException", () => { 142 | const mockError = new ResourcePreconditionNotMetException({ message: "", $metadata: {}}); 143 | chai.assert.isTrue(isResourcePreconditionNotMetException(mockError)); 144 | }); 145 | 146 | it("should return false when error is not a ResourcePreconditionNotMetException", () => { 147 | const mockError = new ServiceException({ $metadata: {}, name: "", $fault: "server" }); 148 | chai.assert.isFalse(isResourcePreconditionNotMetException(mockError)); 149 | }); 150 | }); 151 | 152 | describe("#isRetryableException()", () => { 153 | it("should return true with statusCode 500", () => { 154 | const mockError = new ServiceException({ $metadata: { httpStatusCode: 500 }, name: "", $fault: "server" }); 155 | chai.assert.isTrue(isRetryableException(mockError, false)); 156 | }); 157 | 158 | it("should return true with statusCode 503", () => { 159 | const mockError = new ServiceException({ $metadata: { httpStatusCode: 503 }, name: "", $fault: "server" }); 160 | chai.assert.isTrue(isRetryableException(mockError, false)); 161 | }); 162 | 163 | it("should return true when error is NoHttpResponseException", () => { 164 | const mockError = new ServiceException({ $metadata: { }, name: "NoHttpResponseException", $fault: "client" }) 165 | chai.assert.isTrue(isRetryableException(mockError, false)); 166 | }); 167 | 168 | it("should return true when error is SocketTimeoutException", () => { 169 | const mockError = new ServiceException({ $metadata: { }, name: "SocketTimeoutException", $fault: "client" }) 170 | chai.assert.isTrue(isRetryableException(mockError, false)); 171 | }); 172 | 173 | it("should return false when not a retryable exception", () => { 174 | const mockError = new ServiceException({ $metadata: { httpStatusCode: 200 }, name: "", $fault: "server" }); 175 | chai.assert.isFalse(isRetryableException(mockError, false)); 176 | }); 177 | 178 | it("should appropriately handle retryable errors from the SDK", () => { 179 | const awsError = new ServiceException({ $metadata: { httpStatusCode: 200 }, name: "", $fault: "server", }); 180 | 181 | // Empty retryable causes false 182 | awsError.$retryable = undefined; 183 | chai.assert.isFalse(isRetryableException(awsError, false)); 184 | chai.assert.isFalse(isRetryableException(awsError, true)); 185 | 186 | // False retryable causes false 187 | awsError.$retryable = { throttling: false }; 188 | chai.assert.isFalse(isRetryableException(awsError, false)); 189 | chai.assert.isFalse(isRetryableException(awsError, true)); 190 | 191 | // True retryable causes true, but only if not on commit 192 | awsError.$retryable = { throttling: true }; 193 | chai.assert.isTrue(isRetryableException(awsError, false)); 194 | chai.assert.isFalse(isRetryableException(awsError, true)); 195 | }); 196 | }); 197 | 198 | describe("#isTransactionExpiredException", () => { 199 | it("should return true when error is an InvalidSessionException and message is Tranaction has expired", () => { 200 | const mockError = new InvalidSessionException({ message: "", $metadata: {}}); 201 | mockError.message = "Transaction ABC has expired" 202 | chai.assert.isTrue(isTransactionExpiredException(mockError)); 203 | }); 204 | 205 | it("should return false when error is an InvalidSessionException but message is different", () => { 206 | const mockError = new InvalidSessionException({ message: "", $metadata: {}}); 207 | mockError.message = "SessionNotIdentified" 208 | chai.assert.isFalse(isTransactionExpiredException(mockError)); 209 | }); 210 | 211 | it("should return false when error is not an InvalidSessionException ", () => { 212 | const mockError = new ServiceException({ $metadata: {}, name: "", $fault: "server" }); 213 | chai.assert.isFalse(isTransactionExpiredException(mockError)); 214 | }); 215 | }); 216 | 217 | describe("#isBadRequestException()", () => { 218 | it("should return true when error is a BadRequestException", () => { 219 | const mockError = new BadRequestException({ message: "", $metadata: {} }); 220 | chai.assert.isTrue(isBadRequestException(mockError)); 221 | }); 222 | 223 | it("should return false when error is not a BadRequestException", () => { 224 | const mockError = new ServiceException({ $metadata: {}, name: "", $fault: "server" }); 225 | chai.assert.isFalse(isBadRequestException(mockError)); 226 | }); 227 | }); 228 | }); 229 | -------------------------------------------------------------------------------- /src/test/QldbDriver.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | // Test environment imports 15 | import "mocha"; 16 | 17 | import * as chai from "chai"; 18 | import * as chaiAsPromised from "chai-as-promised"; 19 | import { Agent } from "https"; 20 | import Semaphore from "semaphore-async-await"; 21 | import * as sinon from "sinon"; 22 | 23 | import { DriverClosedError, ExecuteError, SessionPoolEmptyError } from "../errors/Errors"; 24 | import { QldbDriver } from "../QldbDriver"; 25 | import { QldbSession } from "../QldbSession"; 26 | import { defaultRetryConfig } from "../retry/DefaultRetryConfig"; 27 | import { Result } from "../Result"; 28 | import { TransactionExecutor } from "../TransactionExecutor"; 29 | import { RetryConfig } from "../retry/RetryConfig"; 30 | import { 31 | InvalidSessionException, 32 | OccConflictException, 33 | QLDBSession, 34 | QLDBSessionClientConfig, 35 | SendCommandResult 36 | } from "@aws-sdk/client-qldb-session"; 37 | import { NodeHttpHandler, NodeHttpHandlerOptions } from "@smithy/node-http-handler"; 38 | import { globalAgent } from "http"; 39 | 40 | chai.use(chaiAsPromised); 41 | const sandbox = sinon.createSandbox(); 42 | 43 | const testDefaultRetryLimit: number = 4; 44 | const testLedgerName: string = "LedgerName"; 45 | const testMaxRetries: number = 0; 46 | const testMaxSockets: number = 10; 47 | const testTableNames: string[] = ["Vehicle", "Person"]; 48 | const testSendCommandResult: SendCommandResult = { 49 | StartSession: { 50 | SessionToken: "sessionToken" 51 | } 52 | }; 53 | 54 | let qldbDriver: QldbDriver; 55 | let sendCommandStub; 56 | let testQldbLowLevelClient: QLDBSession; 57 | 58 | const mockAgent: Agent = sandbox.mock(Agent); 59 | mockAgent.maxSockets = testMaxSockets; 60 | const testLowLevelClientOptions: QLDBSessionClientConfig = { 61 | region: "fakeRegion" 62 | }; 63 | const testLowLevelClientHttpOptions: NodeHttpHandlerOptions = { 64 | httpAgent: mockAgent 65 | }; 66 | 67 | const mockResult: Result = sandbox.mock(Result); 68 | const mockQldbSession: QldbSession = sandbox.mock(QldbSession); 69 | mockQldbSession.executeLambda = async function(txnLambda: (txn: TransactionExecutor) => Promise): Promise { 70 | return mockResult; 71 | } 72 | mockQldbSession.endSession = async function(): Promise { 73 | return; 74 | } 75 | mockQldbSession.isAlive = () => true; 76 | 77 | describe("QldbDriver", () => { 78 | beforeEach(() => { 79 | testQldbLowLevelClient = new QLDBSession(testLowLevelClientOptions); 80 | sendCommandStub = sandbox.stub(testQldbLowLevelClient, "send"); 81 | sendCommandStub.resolves(testSendCommandResult); 82 | 83 | qldbDriver = new QldbDriver(testLedgerName, testLowLevelClientOptions); 84 | }); 85 | 86 | afterEach(() => { 87 | sandbox.restore(); 88 | }); 89 | 90 | describe("#constructor()", () => { 91 | it("should have all attributes equal to mock values when constructor called", async () => { 92 | chai.assert.equal(qldbDriver["_ledgerName"], testLedgerName); 93 | chai.assert.equal(qldbDriver["_isClosed"], false); 94 | chai.assert.instanceOf(qldbDriver["_qldbClient"], QLDBSession); 95 | chai.assert.equal(await qldbDriver["_qldbClient"].config.maxAttempts(), testMaxRetries); 96 | chai.assert.equal(qldbDriver["_maxConcurrentTransactions"], globalAgent.maxSockets); 97 | chai.assert.deepEqual(qldbDriver["_sessionPool"], []); 98 | chai.assert.instanceOf(qldbDriver["_semaphore"], Semaphore); 99 | chai.assert.equal(qldbDriver["_semaphore"]["permits"], globalAgent.maxSockets); 100 | chai.assert.equal(qldbDriver["_retryConfig"], defaultRetryConfig); 101 | chai.assert.equal(qldbDriver["_retryConfig"]["_retryLimit"], testDefaultRetryLimit); 102 | }); 103 | 104 | it("should have all attributes equal to mock values when constructor called with customized http agent", async () => { 105 | qldbDriver = new QldbDriver(testLedgerName, testLowLevelClientOptions, testLowLevelClientHttpOptions); 106 | chai.assert.equal(qldbDriver["_ledgerName"], testLedgerName); 107 | chai.assert.equal(qldbDriver["_isClosed"], false); 108 | chai.assert.instanceOf(qldbDriver["_qldbClient"], QLDBSession); 109 | chai.assert.equal(await qldbDriver["_qldbClient"].config.maxAttempts(), testMaxRetries); 110 | chai.assert.equal(qldbDriver["_maxConcurrentTransactions"], mockAgent.maxSockets); 111 | chai.assert.deepEqual(qldbDriver["_sessionPool"], []); 112 | chai.assert.instanceOf(qldbDriver["_semaphore"], Semaphore); 113 | chai.assert.equal(qldbDriver["_semaphore"]["permits"], mockAgent.maxSockets); 114 | chai.assert.equal(qldbDriver["_retryConfig"], defaultRetryConfig); 115 | chai.assert.equal(qldbDriver["_retryConfig"]["_retryLimit"], testDefaultRetryLimit); 116 | }); 117 | 118 | it("should have all attributes equal to mock values when constructor called with customized session client with http agent", async () => { 119 | testLowLevelClientOptions.requestHandler = new NodeHttpHandler({httpsAgent: new Agent({maxSockets: 30})}); 120 | testQldbLowLevelClient = new QLDBSession(testLowLevelClientOptions); 121 | qldbDriver = new QldbDriver(testLedgerName, testQldbLowLevelClient, testLowLevelClientHttpOptions); 122 | chai.assert.equal(qldbDriver["_ledgerName"], testLedgerName); 123 | chai.assert.equal(qldbDriver["_isClosed"], false); 124 | chai.assert.instanceOf(qldbDriver["_qldbClient"], QLDBSession); 125 | chai.assert.equal(await qldbDriver["_qldbClient"].config.maxAttempts(), testMaxRetries); 126 | chai.assert.equal(qldbDriver["_maxConcurrentTransactions"], mockAgent.maxSockets); 127 | chai.assert.deepEqual(qldbDriver["_sessionPool"], []); 128 | chai.assert.instanceOf(qldbDriver["_semaphore"], Semaphore); 129 | chai.assert.equal(qldbDriver["_semaphore"]["permits"], mockAgent.maxSockets); 130 | chai.assert.equal(qldbDriver["_retryConfig"], defaultRetryConfig); 131 | chai.assert.equal(qldbDriver["_retryConfig"]["_retryLimit"], testDefaultRetryLimit); 132 | }); 133 | 134 | it("should throw a RangeError when retryLimit less than zero passed in", () => { 135 | const constructorFunction: () => void = () => { 136 | new QldbDriver(testLedgerName, testLowLevelClientOptions, testLowLevelClientHttpOptions, -1); 137 | }; 138 | chai.assert.throws(constructorFunction, RangeError); 139 | }); 140 | 141 | it("should throw a RangeError when maxConcurrentTransactions greater than maxSockets", () => { 142 | const constructorFunction: () => void = () => { 143 | new QldbDriver(testLedgerName, testLowLevelClientOptions, testLowLevelClientHttpOptions, testMaxSockets + 1); 144 | }; 145 | chai.assert.throws(constructorFunction, RangeError); 146 | }); 147 | 148 | it("should throw a RangeError when maxConcurrentTransactions less than zero", () => { 149 | const constructorFunction: () => void = () => { 150 | new QldbDriver(testLedgerName, testLowLevelClientOptions, testLowLevelClientHttpOptions, -1); 151 | }; 152 | chai.assert.throws(constructorFunction, RangeError); 153 | }); 154 | }); 155 | 156 | describe("#close()", () => { 157 | it("should close qldbDriver and any session present in the pool when called", () => { 158 | const mockSession1: QldbSession = sandbox.mock(QldbSession); 159 | const mockSession2: QldbSession = sandbox.mock(QldbSession); 160 | mockSession1.endSession = async function() {}; 161 | mockSession2.endSession = async function() {}; 162 | 163 | const close1Spy = sandbox.spy(mockSession1, "endSession"); 164 | const close2Spy = sandbox.spy(mockSession2, "endSession"); 165 | 166 | qldbDriver["_sessionPool"] = [mockSession1, mockSession2]; 167 | qldbDriver.close(); 168 | 169 | sinon.assert.calledOnce(close1Spy); 170 | sinon.assert.calledOnce(close2Spy); 171 | chai.assert.equal(qldbDriver["_isClosed"], true); 172 | }); 173 | }); 174 | 175 | describe("#executeLambda()", () => { 176 | it("should start a session and return the delegated call to the session", async () => { 177 | qldbDriver["_sessionPool"] = [mockQldbSession]; 178 | const semaphoreStub = sandbox.stub(qldbDriver["_semaphore"], "tryAcquire"); 179 | semaphoreStub.returns(true); 180 | 181 | const executeStub = sandbox.stub(mockQldbSession, "executeLambda"); 182 | executeStub.returns(Promise.resolve(mockResult)); 183 | const lambda = async (transactionExecutor: TransactionExecutor) => { 184 | return true; 185 | }; 186 | 187 | const result = await qldbDriver.executeLambda(lambda, defaultRetryConfig); 188 | 189 | chai.assert.equal(result, mockResult); 190 | sinon.assert.calledOnce(executeStub); 191 | sinon.assert.calledWith(executeStub, lambda); 192 | }); 193 | 194 | it("should executeLambda correctly, when retrying and the session is recreated", async () => { 195 | const mockSession1: QldbSession = sandbox.mock(QldbSession); 196 | const mockSession2: QldbSession = sandbox.mock(QldbSession); 197 | mockSession1.isAlive = () => false; 198 | mockSession2.isAlive = () => true; 199 | sandbox.stub(qldbDriver, 'createNewSession').resolves(mockSession2); 200 | const lambda = async (transactionExecutor: TransactionExecutor) => { 201 | return true; 202 | }; 203 | const error = new InvalidSessionException({ message: "", $metadata: {}}); 204 | error.message = "Some error"; 205 | 206 | mockSession1.executeLambda = async () => { 207 | throw new ExecuteError(error, true, true); 208 | }; 209 | 210 | mockSession2.executeLambda = async function(txnLambda: (txn: TransactionExecutor) => Promise): Promise { 211 | return; 212 | }; 213 | 214 | qldbDriver["_sessionPool"] = [mockSession1]; 215 | const executeLambdaSpy1 = sandbox.spy(mockSession1, "executeLambda"); 216 | const executeLambdaSpy2 = sandbox.spy(mockSession2, "executeLambda"); 217 | await qldbDriver.executeLambda(lambda, defaultRetryConfig); 218 | 219 | sinon.assert.calledOnce(executeLambdaSpy1); 220 | sinon.assert.calledWith(executeLambdaSpy1, lambda); 221 | sinon.assert.calledOnce(executeLambdaSpy2); 222 | sinon.assert.calledWith(executeLambdaSpy2, lambda); 223 | }); 224 | 225 | it("should throw Error, without retrying, when Transaction expires", async () => { 226 | const mockSession1: QldbSession = sandbox.mock(QldbSession); 227 | const mockSession2: QldbSession = sandbox.mock(QldbSession); 228 | mockSession1.isAlive = () => true; 229 | mockSession2.isAlive = () => true; 230 | const lambda = async (transactionExecutor: TransactionExecutor) => { 231 | return true; 232 | }; 233 | const error = new InvalidSessionException({ message: "", $metadata: {}}); 234 | error.message = "Transaction ABC has expired"; 235 | 236 | mockSession1.executeLambda = async () => { 237 | throw new ExecuteError(error, false, true); 238 | }; 239 | 240 | mockSession2.executeLambda = async function(txnLambda: (txn: TransactionExecutor) => Promise): Promise { 241 | // This should never be called 242 | return; 243 | }; 244 | 245 | qldbDriver["_sessionPool"] = [mockSession2, mockSession1]; 246 | const executeLambdaSpy1 = sandbox.spy(mockSession1, "executeLambda"); 247 | const executeLambdaSpy2 = sandbox.spy(mockSession2, "executeLambda"); 248 | const result = await chai.expect(qldbDriver.executeLambda(lambda, defaultRetryConfig)).to.be.rejected; 249 | chai.assert.equal(result.name, error.name); 250 | 251 | sinon.assert.calledOnce(executeLambdaSpy1); 252 | sinon.assert.calledWith(executeLambdaSpy1, lambda); 253 | 254 | sinon.assert.notCalled(executeLambdaSpy2); 255 | }); 256 | 257 | it("should retry only up to retry limit times when there is retryable error", async () => { 258 | const mockSession: QldbSession = sandbox.mock(QldbSession); 259 | const errorCode: string = "OccConflictException"; 260 | mockSession.executeLambda = async () => { 261 | const error = new OccConflictException({ message: "", $metadata: {}}); 262 | throw new ExecuteError(error, true, false); 263 | }; 264 | const executeLambdaSpy = sandbox.spy(mockSession, "executeLambda"); 265 | const lambda = async (transactionExecutor: TransactionExecutor) => { 266 | return true; 267 | }; 268 | mockSession.isAlive = () => true; 269 | 270 | const retryConfig: RetryConfig = new RetryConfig(2) 271 | qldbDriver["_sessionPool"] = [mockSession]; 272 | const result = await chai.expect(qldbDriver.executeLambda(lambda, retryConfig)).to.be.rejected; 273 | chai.assert.equal(result.name, errorCode); 274 | sinon.assert.callCount(executeLambdaSpy, retryConfig.getRetryLimit() + 1); 275 | }); 276 | 277 | it("should throw DriverClosedError wrapped in a rejected promise when closed", async () => { 278 | const lambda = async (transactionExecutor: TransactionExecutor) => { 279 | return true; 280 | }; 281 | 282 | qldbDriver["_isClosed"] = true; 283 | chai.expect(qldbDriver.executeLambda(lambda, defaultRetryConfig)).to.be.rejectedWith(DriverClosedError); 284 | }); 285 | 286 | it("should return a SessionPoolEmptyError wrapped in a rejected promise when session pool empty", async () => { 287 | const semaphoreStub = sandbox.stub(qldbDriver["_semaphore"], "tryAcquire"); 288 | semaphoreStub.returns(false); 289 | 290 | const lambda = async (transactionExecutor: TransactionExecutor) => { 291 | return true; 292 | }; 293 | 294 | const result = await chai.expect(qldbDriver.executeLambda(lambda)).to.be.rejected; 295 | chai.assert.equal(result.name, SessionPoolEmptyError.name); 296 | }); 297 | 298 | it("should not increment semaphore permit count if called when pool empty", async () => { 299 | const onePermitDriver = new QldbDriver(testLedgerName, testLowLevelClientOptions, testLowLevelClientHttpOptions, 1); 300 | onePermitDriver["_sessionPool"] = [mockQldbSession]; 301 | const executeStub = sandbox.stub(mockQldbSession, "executeLambda"); 302 | executeStub.returns(new Promise(resolve => setTimeout(resolve, 10))); 303 | 304 | let promise1 = onePermitDriver.executeLambda(async (txn) => { 305 | return true; 306 | }); 307 | let promise2 = onePermitDriver.executeLambda(async (txn) => { 308 | return true; 309 | }); 310 | 311 | // Two concurrent transactions will fail due to session pool being empty 312 | promise1.catch((e) => { 313 | chai.assert.fail(e); 314 | }); 315 | promise2.catch((e) => { 316 | }); 317 | await promise1; 318 | let result = await chai.expect(promise2).to.be.rejected; 319 | chai.assert.equal(result.name, SessionPoolEmptyError.name); 320 | 321 | promise1 = onePermitDriver.executeLambda(async (txn) => { 322 | return true; 323 | }); 324 | promise2 = onePermitDriver.executeLambda(async (txn) => { 325 | return true; 326 | }); 327 | // If permit leaked, this will succeed since now there's two permits 328 | promise1.catch((e) => { 329 | chai.assert.fail(e); 330 | }); 331 | promise2.catch((e) => { 332 | }); 333 | await promise1; 334 | result = await chai.expect(promise2).to.be.rejected; 335 | chai.assert.equal(result.name, SessionPoolEmptyError.name); 336 | }); 337 | }); 338 | 339 | describe("#getTableNames()", () => { 340 | it("should return a list of table names when called", async () => { 341 | const executeStub = sandbox.stub(qldbDriver, "executeLambda"); 342 | executeStub.returns(Promise.resolve(testTableNames)); 343 | const listOfTableNames: string[] = await qldbDriver.getTableNames(); 344 | chai.assert.equal(listOfTableNames.length, testTableNames.length); 345 | chai.assert.equal(listOfTableNames, testTableNames); 346 | }); 347 | 348 | it("should return a DriverClosedError wrapped in a rejected promise when closed", async () => { 349 | qldbDriver["_isClosed"] = true; 350 | chai.expect(qldbDriver.getTableNames()).to.be.rejectedWith(DriverClosedError); 351 | }); 352 | }); 353 | }); 354 | -------------------------------------------------------------------------------- /src/test/QldbSession.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | // Test environment imports 15 | import "mocha"; 16 | 17 | import * as chai from "chai"; 18 | import * as chaiAsPromised from "chai-as-promised"; 19 | import * as sinon from "sinon"; 20 | 21 | import { Communicator } from "../Communicator"; 22 | import * as Errors from "../errors/Errors"; 23 | import * as LogUtil from "../LogUtil"; 24 | import { QldbSession } from "../QldbSession"; 25 | import { Result } from "../Result"; 26 | import { ResultReadable } from "../ResultReadable"; 27 | import { Transaction } from "../Transaction"; 28 | import { AbortTransactionResult, ExecuteStatementResult, InvalidSessionException, OccConflictException, QLDBSession, QLDBSessionClientConfig, StartTransactionResult, ValueHolder } from "@aws-sdk/client-qldb-session"; 29 | import { TextEncoder } from "util"; 30 | 31 | chai.use(chaiAsPromised); 32 | const sandbox = sinon.createSandbox(); 33 | 34 | const testSessionToken: string = "sessionToken"; 35 | const testTransactionId: string = "txnId"; 36 | const testStartTransactionResult: StartTransactionResult = { 37 | TransactionId: testTransactionId 38 | }; 39 | const testMessage: string = "foo"; 40 | const testStatement: string = "SELECT * FROM foo"; 41 | const testAbortTransactionResult: AbortTransactionResult = {}; 42 | 43 | const enc = new TextEncoder(); 44 | const testValueHolder: ValueHolder[] = [{IonBinary: enc.encode("{ hello:\"world\" }")}]; 45 | const testPageToken: string = "foo"; 46 | const testExecuteStatementResult: ExecuteStatementResult = { 47 | FirstPage: { 48 | NextPageToken: testPageToken, 49 | Values: testValueHolder 50 | } 51 | }; 52 | const mockLowLevelClientOptions: QLDBSessionClientConfig = { 53 | region: "fakeRegion" 54 | }; 55 | const testQldbLowLevelClient: QLDBSession = new QLDBSession(mockLowLevelClientOptions); 56 | 57 | const mockCommunicator: Communicator = sandbox.mock(Communicator); 58 | const mockResult: Result = sandbox.mock(Result); 59 | const mockTransaction: Transaction = sandbox.mock(Transaction); 60 | mockTransaction.getTransactionId = () => { 61 | return "mockTransactionId"; 62 | }; 63 | 64 | const resultReadableObject: ResultReadable = new ResultReadable(testTransactionId, testExecuteStatementResult, mockCommunicator); 65 | let qldbSession: QldbSession; 66 | 67 | describe("QldbSession", () => { 68 | 69 | beforeEach(() => { 70 | qldbSession = new QldbSession(mockCommunicator); 71 | mockCommunicator.endSession = async () => { 72 | return null; 73 | }; 74 | mockCommunicator.getSessionToken = () => { 75 | return testSessionToken; 76 | }; 77 | mockCommunicator.startTransaction = async () => { 78 | return testStartTransactionResult; 79 | }; 80 | mockCommunicator.abortTransaction = async () => { 81 | return testAbortTransactionResult; 82 | }; 83 | mockCommunicator.executeStatement = async () => { 84 | return testExecuteStatementResult; 85 | }; 86 | mockCommunicator.getQldbClient = () => { 87 | return testQldbLowLevelClient; 88 | }; 89 | }); 90 | 91 | afterEach(() => { 92 | sandbox.restore(); 93 | }); 94 | 95 | describe("#constructor()", () => { 96 | it("should have all attributes equal to mock values when constructor called", () => { 97 | chai.assert.equal(qldbSession["_communicator"], mockCommunicator); 98 | chai.assert.equal(qldbSession["_isAlive"], true); 99 | }); 100 | }); 101 | 102 | describe("#endSession()", () => { 103 | it("should end qldbSession when called", async () => { 104 | const communicatorEndSpy = sandbox.spy(mockCommunicator, "endSession"); 105 | await qldbSession.endSession(); 106 | chai.assert.equal(qldbSession["_isAlive"], false); 107 | sinon.assert.calledOnce(communicatorEndSpy); 108 | }); 109 | }); 110 | 111 | describe("#executeLambda()", () => { 112 | it("should return a Result object when called with execute as the lambda", async () => { 113 | qldbSession._startTransaction = async () => { 114 | return mockTransaction; 115 | }; 116 | mockTransaction.execute = async () => { 117 | return mockResult; 118 | }; 119 | mockTransaction.commit = async () => {}; 120 | 121 | const executeSpy = sandbox.spy(mockTransaction, "execute"); 122 | const startTransactionSpy = sandbox.spy(qldbSession, "_startTransaction"); 123 | const commitSpy = sandbox.spy(mockTransaction, "commit"); 124 | 125 | const result = await qldbSession.executeLambda(async (txn) => { 126 | return await txn.execute(testStatement); 127 | }); 128 | sinon.assert.calledOnce(executeSpy); 129 | sinon.assert.calledWith(executeSpy, testStatement); 130 | sinon.assert.calledOnce(startTransactionSpy); 131 | sinon.assert.calledOnce(commitSpy); 132 | chai.assert.equal(result, mockResult); 133 | }); 134 | 135 | it("should return a Result object when called with executeAndStreamResults as the lambda", async () => { 136 | const resultStub = sandbox.stub(Result, "bufferResultReadable"); 137 | resultStub.returns(Promise.resolve(mockResult)); 138 | 139 | qldbSession._startTransaction = async () => { 140 | return mockTransaction; 141 | }; 142 | mockTransaction.executeAndStreamResults = async () => { 143 | return resultReadableObject; 144 | }; 145 | mockTransaction.commit = async () => {}; 146 | 147 | const executeAndStreamResultsSpy = sandbox.spy(mockTransaction, "executeAndStreamResults"); 148 | const startTransactionSpy = sandbox.spy(qldbSession, "_startTransaction"); 149 | const commitSpy = sandbox.spy(mockTransaction, "commit"); 150 | 151 | const result = await qldbSession.executeLambda(async (txn) => { 152 | const resultReadable: ResultReadable = await txn.executeAndStreamResults(testStatement); 153 | return Result.bufferResultReadable(resultReadable); 154 | }); 155 | sinon.assert.calledOnce(executeAndStreamResultsSpy); 156 | sinon.assert.calledWith(executeAndStreamResultsSpy, testStatement); 157 | sinon.assert.calledOnce(startTransactionSpy); 158 | sinon.assert.calledOnce(resultStub); 159 | sinon.assert.calledOnce(commitSpy); 160 | chai.assert.equal(result, mockResult); 161 | }); 162 | 163 | it("should return a rejected promise when non-retryable error is thrown", async () => { 164 | qldbSession._startTransaction = async () => { 165 | throw new Error(testMessage); 166 | }; 167 | 168 | const startTransactionSpy = sandbox.spy(qldbSession, "_startTransaction"); 169 | 170 | await chai.expect(qldbSession.executeLambda(async (txn) => { 171 | return await txn.execute(testStatement); 172 | })).to.be.rejected; 173 | sinon.assert.calledOnce(startTransactionSpy); 174 | }); 175 | 176 | it("should throw a wrapped exception when fails containing the original exception", async () => { 177 | const testError = new Error(testMessage); 178 | mockCommunicator.startTransaction = async () => { 179 | throw testError; 180 | }; 181 | 182 | const result = await chai.expect(qldbSession.executeLambda(async (txn) => { 183 | return await txn.execute(testStatement); 184 | })).to.be.rejectedWith(Errors.ExecuteError); 185 | chai.assert.equal(result.cause, testError); 186 | }); 187 | 188 | it("should wrap when fails with InvalidSessionException and close the session", async () => { 189 | const testError = new InvalidSessionException({ message: "", $metadata: {}}); 190 | testError.$retryable = { throttling: true }; 191 | testError.message = testMessage; 192 | 193 | mockCommunicator.startTransaction = async () => { 194 | throw testError; 195 | }; 196 | 197 | const startTransactionSpy = sandbox.spy(qldbSession, "_startTransaction"); 198 | const result = await chai.expect(qldbSession.executeLambda(async (txn) => { 199 | return await txn.execute(testStatement); 200 | })).to.be.rejected; 201 | sinon.assert.callCount(startTransactionSpy, 1); 202 | chai.assert.equal(result.cause, testError); 203 | chai.assert.isFalse(qldbSession.isAlive()); 204 | }); 205 | 206 | it("should wrap when fails with OccConflictException and session is still alive", async () => { 207 | const testError = new OccConflictException({ message: "", $metadata: {}}); 208 | testError.$retryable = { throttling: true }; 209 | testError.message = testMessage; 210 | const tryAbortSpy = sandbox.spy(mockCommunicator, "abortTransaction"); 211 | 212 | mockCommunicator.startTransaction = async () => { 213 | throw testError; 214 | }; 215 | 216 | const startTransactionSpy = sandbox.spy(qldbSession, "_startTransaction"); 217 | const result = await chai.expect(qldbSession.executeLambda(async (txn) => { 218 | return await txn.execute(testStatement); 219 | })).to.be.rejected; 220 | sinon.assert.callCount(startTransactionSpy, 1); 221 | chai.assert.equal(result.cause, testError); 222 | chai.assert.isTrue(qldbSession.isAlive()); 223 | sinon.assert.notCalled(tryAbortSpy); 224 | }); 225 | 226 | it("should return a rejected promise with wrapped retryable error when retryable exception occurs", async () => { 227 | const isRetryableStub = sandbox.stub(Errors, "isRetryableException"); 228 | isRetryableStub.returns(true); 229 | 230 | const startTransactionSpy = sandbox.spy(qldbSession, "_startTransaction"); 231 | 232 | const result = await chai.expect(qldbSession.executeLambda(async (txn) => { 233 | throw new Error(testMessage); 234 | })).to.be.rejected; 235 | 236 | sinon.assert.calledOnce(startTransactionSpy); 237 | chai.assert.isTrue(result.isRetryable); 238 | }); 239 | 240 | it("should return a rejected promise with a wrapped error when Transaction expires", async () => { 241 | const error = new InvalidSessionException({ message: "", $metadata: {}}); 242 | error.$retryable = { throttling: true }; 243 | error.message = "Transaction ABC has expired"; 244 | const communicatorTransactionSpy = sandbox.spy(mockCommunicator, "startTransaction"); 245 | const communicatorAbortTransactionSpy = sandbox.spy(mockCommunicator, "abortTransaction"); 246 | 247 | const result = await chai.expect(qldbSession.executeLambda(async (txn) => { 248 | throw error; 249 | })).to.be.rejected; 250 | sinon.assert.calledOnce(communicatorTransactionSpy); 251 | chai.assert.equal(result.cause.message, error.message); 252 | sinon.assert.calledOnce(communicatorAbortTransactionSpy); 253 | chai.assert.isTrue(qldbSession.isAlive()); 254 | }); 255 | 256 | it("should return a rejected promise when a LambdaAbortedError occurs", async () => { 257 | const lambdaAbortedError: Errors.LambdaAbortedError = new Errors.LambdaAbortedError(); 258 | await chai.expect(qldbSession.executeLambda(async (txn) => { 259 | throw lambdaAbortedError; 260 | })).to.be.rejected; 261 | }); 262 | }); 263 | 264 | describe("#_startTransaction()", () => { 265 | it("should return a Transaction object when called", async () => { 266 | const communicatorTransactionSpy = sandbox.spy(mockCommunicator, "startTransaction"); 267 | const transaction = await qldbSession._startTransaction(); 268 | chai.expect(transaction).to.be.an.instanceOf(Transaction); 269 | chai.assert.equal(transaction["_txnId"], testTransactionId); 270 | sinon.assert.calledOnce(communicatorTransactionSpy); 271 | }); 272 | }); 273 | 274 | describe("#_cleanSessionState()", () => { 275 | it("should call abortTransaction()", async () => { 276 | const communicatorAbortSpy = sandbox.spy(mockCommunicator, "abortTransaction"); 277 | await qldbSession["_cleanSessionState"](); 278 | sinon.assert.calledOnce(communicatorAbortSpy); 279 | }); 280 | 281 | it("should log warning message when error is thrown and set alive state to false", async () => { 282 | const communicatorAbortStub = sandbox.stub(mockCommunicator, "abortTransaction"); 283 | communicatorAbortStub.throws(new Error("testError")); 284 | const logSpy = sandbox.spy(LogUtil, "warn"); 285 | await qldbSession["_cleanSessionState"](); 286 | sinon.assert.calledOnce(communicatorAbortStub); 287 | sinon.assert.calledOnce(logSpy); 288 | chai.assert.isFalse(qldbSession.isAlive()); 289 | }); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /src/test/Transaction.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | // Test environment imports 15 | import "mocha"; 16 | 17 | import { 18 | CommitTransactionResult, 19 | ExecuteStatementResult, 20 | ValueHolder 21 | } from "@aws-sdk/client-qldb-session"; 22 | import * as chai from "chai"; 23 | import * as chaiAsPromised from "chai-as-promised"; 24 | import * as ionJs from "ion-js"; 25 | import { Lock } from "semaphore-async-await"; 26 | import * as sinon from "sinon"; 27 | import { Readable } from "stream"; 28 | 29 | import { Communicator } from "../Communicator"; 30 | import * as Errors from "../errors/Errors"; 31 | import { QldbHash } from "../QldbHash"; 32 | import { Result } from "../Result"; 33 | import { ResultReadable } from "../ResultReadable"; 34 | import { Transaction } from "../Transaction"; 35 | import { expect } from "chai"; 36 | 37 | chai.use(chaiAsPromised); 38 | const sandbox = sinon.createSandbox(); 39 | 40 | const testMessage: string = "foo"; 41 | const testStatement: string = "SELECT * FROM foo"; 42 | const testStatementWithQuotes: string = `SELECT * FROM "foo"`; 43 | const testPageToken: string = "foo"; 44 | const testTransactionId: string = "txnId"; 45 | const testHash: Uint8Array = new Uint8Array([1, 2, 3]); 46 | const testCommitTransactionResult: CommitTransactionResult = { 47 | TransactionId: testTransactionId, 48 | CommitDigest: QldbHash.toQldbHash(testTransactionId).getQldbHash() 49 | }; 50 | const testExecuteStatementResult: ExecuteStatementResult = { 51 | FirstPage: { 52 | NextPageToken: testPageToken 53 | } 54 | }; 55 | 56 | const mockCommunicator: Communicator = sandbox.mock(Communicator); 57 | const mockResult: Result = sandbox.mock(Result); 58 | 59 | let transaction: Transaction; 60 | 61 | describe("Transaction", () => { 62 | 63 | beforeEach(() => { 64 | transaction = new Transaction(mockCommunicator, testTransactionId); 65 | mockCommunicator.executeStatement = async () => { 66 | return testExecuteStatementResult; 67 | }; 68 | mockCommunicator.commit = async () => { 69 | return testCommitTransactionResult; 70 | }; 71 | mockCommunicator.abortTransaction = async () => { 72 | return {}; 73 | }; 74 | }); 75 | 76 | afterEach(() => { 77 | sandbox.restore(); 78 | }); 79 | 80 | describe("#constructor()", () => { 81 | it("should have all attributes equal to mock values when constructor called", () => { 82 | chai.assert.equal(transaction["_communicator"], mockCommunicator); 83 | chai.assert.equal(transaction["_txnId"], testTransactionId); 84 | chai.assert.deepEqual(transaction["_txnHash"], QldbHash.toQldbHash(testTransactionId)); 85 | chai.expect(transaction["_hashLock"]).to.be.an.instanceOf(Lock); 86 | }); 87 | }); 88 | 89 | describe("#commit()", () => { 90 | it("should call Communicator's commit() when called", async () => { 91 | const commitSpy = sandbox.spy(mockCommunicator, "commit"); 92 | await transaction.commit(); 93 | sinon.assert.calledOnce(commitSpy); 94 | }); 95 | 96 | it("should return a rejected promise when hashes don't match", async () => { 97 | const invalidHashCommitTransactionResult: CommitTransactionResult = { 98 | TransactionId: testTransactionId, 99 | CommitDigest: testHash 100 | }; 101 | mockCommunicator.commit = async () => { 102 | return invalidHashCommitTransactionResult; 103 | }; 104 | const commitSpy = sandbox.spy(mockCommunicator, "commit"); 105 | const error = await chai.expect(transaction.commit()).to.be.rejected; 106 | chai.assert.equal(error.name, "ClientError"); 107 | sinon.assert.calledOnce(commitSpy); 108 | }); 109 | 110 | it("should return a rejected promise when error is thrown", async () => { 111 | const testError: Error = new Error(testMessage); 112 | mockCommunicator.commit = async () => { 113 | throw testError; 114 | }; 115 | const commitSpy = sandbox.spy(mockCommunicator, "commit"); 116 | const result = await chai.expect(transaction.commit()).to.be.rejected; 117 | chai.assert.equal(result, testError); 118 | sinon.assert.calledOnce(commitSpy); 119 | }); 120 | }); 121 | 122 | describe("#execute()", () => { 123 | it("should return a Result object when provided with a statement", async () => { 124 | Result.create = async () => { 125 | return mockResult 126 | }; 127 | const executeSpy = sandbox.spy(mockCommunicator, "executeStatement"); 128 | const result: Result = await transaction.execute(testStatement); 129 | sinon.assert.calledOnce(executeSpy); 130 | sinon.assert.calledWith(executeSpy, testTransactionId, testStatement, []); 131 | chai.assert.equal(result, mockResult); 132 | }); 133 | 134 | it("should return a Result object when provided with a statement and parameters", async () => { 135 | Result.create = async () => { 136 | return mockResult 137 | }; 138 | const sendExecuteSpy = sandbox.spy(transaction as any, "_sendExecute"); 139 | const param1: number = 5; 140 | const param2: string = "a"; 141 | 142 | const result: Result = await transaction.execute(testStatement, param1, param2); 143 | sinon.assert.calledOnce(sendExecuteSpy); 144 | sinon.assert.calledWith(sendExecuteSpy, testStatement, [param1, param2]); 145 | chai.assert.equal(result, mockResult); 146 | }); 147 | 148 | it("should properly map a list as a single parameter", async () => { 149 | Result.create = async () => { 150 | return mockResult 151 | }; 152 | const sendExecuteSpy = sandbox.spy(transaction as any, "_sendExecute"); 153 | const param1: number = 5; 154 | const param2: string = "a"; 155 | 156 | const result: Result = await transaction.execute(testStatement, [param1, param2]); 157 | sinon.assert.calledOnce(sendExecuteSpy); 158 | sinon.assert.calledWith(sendExecuteSpy, testStatement, [[param1, param2]]); 159 | chai.assert.equal(result, mockResult); 160 | }); 161 | 162 | it("should return a rejected promise when error is thrown", async () => { 163 | mockCommunicator.executeStatement = async () => { 164 | throw new Error(testMessage); 165 | }; 166 | const isOccStub = sandbox.stub(Errors, "isOccConflictException"); 167 | isOccStub.returns(false); 168 | const executeSpy = sandbox.spy(mockCommunicator, "executeStatement"); 169 | await chai.expect(transaction.execute(testStatement)).to.be.rejected; 170 | sinon.assert.calledOnce(executeSpy); 171 | sinon.assert.calledWith(executeSpy, testTransactionId, testStatement, []); 172 | }); 173 | 174 | it("should call Communicator's executeStatement() twice when called twice", async () => { 175 | const executeSpy = sandbox.spy(mockCommunicator, "executeStatement"); 176 | await transaction.execute(testStatement); 177 | await transaction.execute(testStatement); 178 | sinon.assert.calledTwice(executeSpy); 179 | }); 180 | }); 181 | 182 | describe("#executeAndStreamResults()", () => { 183 | it("should return a Stream object when provided with a statement", async () => { 184 | const sampleResultReadableObject: ResultReadable = new ResultReadable( 185 | testTransactionId, 186 | testExecuteStatementResult, 187 | mockCommunicator 188 | ); 189 | const executeSpy = sandbox.spy(mockCommunicator, "executeStatement"); 190 | const result: Readable = await transaction.executeAndStreamResults(testStatement); 191 | sinon.assert.calledOnce(executeSpy); 192 | sinon.assert.calledWith(executeSpy, testTransactionId, testStatement, []); 193 | chai.assert.equal(JSON.stringify(result), JSON.stringify(sampleResultReadableObject)); 194 | }); 195 | 196 | it("should return a Stream object when provided with a statement and parameters", async () => { 197 | const sampleResultReadableObject: ResultReadable = new ResultReadable( 198 | testTransactionId, 199 | testExecuteStatementResult, 200 | mockCommunicator 201 | ); 202 | const sendExecuteSpy = sandbox.spy(transaction as any, "_sendExecute"); 203 | const param1: number = 5; 204 | const param2: string = "a"; 205 | const result: Readable = await transaction.executeAndStreamResults(testStatement, param1, param2); 206 | sinon.assert.calledOnce(sendExecuteSpy); 207 | sinon.assert.calledWith(sendExecuteSpy, testStatement, [param1, param2]); 208 | chai.assert.equal(JSON.stringify(result), JSON.stringify(sampleResultReadableObject)); 209 | }); 210 | 211 | it("should return a rejected promise when error is thrown", async () => { 212 | mockCommunicator.executeStatement = async () => { 213 | throw new Error(testMessage); 214 | }; 215 | const isOccStub = sandbox.stub(Errors, "isOccConflictException"); 216 | isOccStub.returns(false); 217 | const executeSpy = sandbox.spy(mockCommunicator, "executeStatement"); 218 | await chai.expect(transaction.executeAndStreamResults(testStatement)).to.be.rejected; 219 | sinon.assert.calledOnce(executeSpy); 220 | sinon.assert.calledWith(executeSpy, testTransactionId, testStatement, []); 221 | }); 222 | 223 | it("should call Communicator's executeStatement() twice when called twice", async () => { 224 | const executeSpy = sandbox.spy(mockCommunicator, "executeStatement"); 225 | await transaction.executeAndStreamResults(testStatement); 226 | await transaction.executeAndStreamResults(testStatement); 227 | sinon.assert.calledTwice(executeSpy); 228 | }); 229 | }); 230 | 231 | describe("#getTransactionId()", () => { 232 | it("should return the transaction ID when called", () => { 233 | const transactionIdSpy = sandbox.spy(transaction, "getTransactionId"); 234 | const transactionId: string = transaction.getTransactionId(); 235 | chai.assert.equal(transactionId, testTransactionId); 236 | sinon.assert.calledOnce(transactionIdSpy); 237 | }); 238 | }); 239 | 240 | describe("#_sendExecute()", () => { 241 | it("should compute hashes correctly when called", async () => { 242 | let testStatementHash: QldbHash = QldbHash.toQldbHash(testStatement); 243 | 244 | const parameters: any[] = [5, "a"]; 245 | const ionBinaryValues: Uint8Array[] = parameters.map((value: any): Uint8Array => { 246 | const valueIonBinary:Uint8Array = ionJs.dumpBinary(value); 247 | testStatementHash = testStatementHash.dot(QldbHash.toQldbHash(valueIonBinary)); 248 | return valueIonBinary; 249 | }); 250 | 251 | const updatedHash: Uint8Array = transaction["_txnHash"].dot(testStatementHash).getQldbHash(); 252 | 253 | const toQldbHashSpy = sandbox.spy(QldbHash, "toQldbHash"); 254 | 255 | const result: ExecuteStatementResult = await transaction["_sendExecute"](testStatement, parameters); 256 | 257 | sinon.assert.calledThrice(toQldbHashSpy); 258 | sinon.assert.calledWith(toQldbHashSpy, testStatement); 259 | sinon.assert.calledWith(toQldbHashSpy, ionBinaryValues[0]); 260 | sinon.assert.calledWith(toQldbHashSpy, ionBinaryValues[1]); 261 | 262 | chai.assert.equal(ionJs.toBase64(transaction["_txnHash"].getQldbHash()), ionJs.toBase64(updatedHash)); 263 | chai.assert.equal(testExecuteStatementResult, result); 264 | 265 | }); 266 | 267 | it("should compute hashes correctly when called from a statement that contain quotes", async () => { 268 | let testStatementHash: QldbHash = QldbHash.toQldbHash(testStatementWithQuotes); 269 | 270 | const parameters: any[] = [5, "a"]; 271 | const ionBinaryValues: Uint8Array[] = parameters.map((value: any): Uint8Array => { 272 | const valueIonBinary:Uint8Array = ionJs.dumpBinary(value); 273 | testStatementHash = testStatementHash.dot(QldbHash.toQldbHash(valueIonBinary)); 274 | return valueIonBinary; 275 | }); 276 | const updatedHash: Uint8Array = transaction["_txnHash"].dot(testStatementHash).getQldbHash(); 277 | 278 | const toQldbHashSpy = sandbox.spy(QldbHash, "toQldbHash"); 279 | 280 | const result: ExecuteStatementResult = await transaction["_sendExecute"]( 281 | testStatementWithQuotes, 282 | parameters 283 | ); 284 | 285 | sinon.assert.calledThrice(toQldbHashSpy); 286 | sinon.assert.calledWith(toQldbHashSpy, testStatementWithQuotes); 287 | sinon.assert.calledWith(toQldbHashSpy, ionBinaryValues[0]); 288 | sinon.assert.calledWith(toQldbHashSpy, ionBinaryValues[1]); 289 | 290 | chai.assert.equal(ionJs.toBase64(transaction["_txnHash"].getQldbHash()), ionJs.toBase64(updatedHash)); 291 | chai.assert.equal(testExecuteStatementResult, result); 292 | 293 | }); 294 | 295 | it("should compute different hashes when called from different statements that contain quotes", async () => { 296 | const firstStatement: string = `INSERT INTO "first_table" VALUE {'test': 'hello world'}`; 297 | const secondStatement: string = `INSERT INTO "second_table" VALUE {'test': 'hello world'}`; 298 | 299 | const firstStatementHash: QldbHash = QldbHash.toQldbHash(firstStatement); 300 | const secondStatementHash: QldbHash = QldbHash.toQldbHash(secondStatement); 301 | 302 | // If the different statements that contain quotes are hashed incorrectly, then the hash of 303 | // 92Hs4IGd3Gnq4O9sVQX/S0AanTKWolpiAXzv+9GLzP0= would be produced every time. 304 | // It's asserted here that the hashes are different and computed correctly. 305 | chai.assert.notEqual( 306 | ionJs.toBase64(firstStatementHash.getQldbHash()), 307 | ionJs.toBase64(secondStatementHash.getQldbHash()) 308 | ); 309 | }); 310 | 311 | it("should have different hashes when called from same statements, one with quotes one without", async () => { 312 | const firstStatement: string = `INSERT INTO "first_table" VALUE {'test': 'hello world'}`; 313 | const secondStatement: string = `INSERT INTO first_table VALUE {'test': 'hello world'}`; 314 | 315 | const firstStatementHash: QldbHash = QldbHash.toQldbHash(firstStatement); 316 | const secondStatementHash: QldbHash = QldbHash.toQldbHash(secondStatement); 317 | 318 | chai.assert.notEqual( 319 | ionJs.toBase64(firstStatementHash.getQldbHash()), 320 | ionJs.toBase64(secondStatementHash.getQldbHash()) 321 | ); 322 | }); 323 | 324 | it("should convert native types to ValueHolders correctly when called", async () => { 325 | const parameters: any[] = [ 326 | true, 327 | Date.now(), 328 | 3e2, 329 | 5, 330 | 2.2, 331 | "a", 332 | new ionJs.Timestamp(0, 2000), 333 | new Uint8Array(3) 334 | ]; 335 | 336 | const executeStatementSpy = sandbox.spy(transaction["_communicator"], "executeStatement"); 337 | const result: ExecuteStatementResult = await transaction["_sendExecute"](testStatement, parameters); 338 | 339 | const expectedValueHolders: ValueHolder[] = []; 340 | parameters.forEach((value: any) => { 341 | const valueHolder: ValueHolder = { 342 | IonBinary: ionJs.dumpBinary(value) 343 | }; 344 | expectedValueHolders.push(valueHolder); 345 | }); 346 | 347 | sinon.assert.calledWith( 348 | executeStatementSpy, 349 | transaction["_txnId"], 350 | testStatement, 351 | sinon.match.array.deepEquals(expectedValueHolders) 352 | ); 353 | chai.assert.equal(testExecuteStatementResult, result); 354 | }); 355 | 356 | it("should throw Error when called with parameters which cannot be converted to Ion", async () => { 357 | const validParameter1 = 5; 358 | const invalidParameter = Symbol('foo'); 359 | const validParameter2 = 3; 360 | 361 | 362 | const toQldbHashSpy = sandbox.spy(QldbHash, "toQldbHash"); 363 | const executeStatementSpy = sandbox.spy(transaction["_communicator"], "executeStatement"); 364 | await expect( 365 | transaction["_sendExecute"](testStatement, [validParameter1, invalidParameter, validParameter2]) 366 | ).to.be.rejected; 367 | 368 | sinon.assert.notCalled(executeStatementSpy); 369 | 370 | sinon.assert.calledTwice(toQldbHashSpy); 371 | sinon.assert.calledWith(toQldbHashSpy, testStatement); 372 | //Ensure that the first valid parameter was added to qldbHash 373 | sinon.assert.calledWith(toQldbHashSpy, ionJs.dumpBinary(validParameter1)); 374 | //Ensure that the second valid parameter was not called as the invalid parameter throws an error before it 375 | sinon.assert.neverCalledWith(toQldbHashSpy, ionJs.dumpBinary(validParameter2)); 376 | 377 | }); 378 | }); 379 | }); 380 | -------------------------------------------------------------------------------- /src/test/TransactionExecutor.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with 5 | * the License. A copy of the License is located at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 10 | * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions 11 | * and limitations under the License. 12 | */ 13 | 14 | // Test environment imports 15 | import "mocha"; 16 | 17 | import * as chai from "chai"; 18 | import * as chaiAsPromised from "chai-as-promised"; 19 | import * as sinon from "sinon"; 20 | 21 | import { LambdaAbortedError } from "../errors/Errors"; 22 | import { Result } from "../Result"; 23 | import { ResultReadable } from "../ResultReadable"; 24 | import { Transaction } from "../Transaction"; 25 | import { TransactionExecutor } from "../TransactionExecutor"; 26 | 27 | chai.use(chaiAsPromised); 28 | const sandbox = sinon.createSandbox(); 29 | 30 | const testStatement: string = "SELECT * FROM foo"; 31 | const testMessage: string = "foo"; 32 | const testTransactionId: string = "txnId"; 33 | 34 | const mockResult: Result = sandbox.mock(Result); 35 | const mockResultReadable: ResultReadable = sandbox.mock(ResultReadable); 36 | const mockTransaction: Transaction = sandbox.mock(Transaction); 37 | 38 | let transactionExecutor: TransactionExecutor; 39 | 40 | describe("TransactionExecutor", () => { 41 | 42 | beforeEach(() => { 43 | transactionExecutor = new TransactionExecutor(mockTransaction); 44 | }); 45 | 46 | afterEach(() => { 47 | sandbox.restore(); 48 | }); 49 | 50 | describe("#constructor()", () => { 51 | it("should have all attributes equal to mock values when constructor called", () => { 52 | chai.assert.equal(transactionExecutor["_transaction"], mockTransaction); 53 | }); 54 | }); 55 | 56 | describe("#abort()", () => { 57 | it("should throw LambdaAbortedError when called", () => { 58 | chai.expect(() => { 59 | transactionExecutor.abort(); 60 | }).to.throw(LambdaAbortedError); 61 | }); 62 | }); 63 | 64 | describe("#execute()", () => { 65 | it("should return a Result object when provided with a statement", async () => { 66 | mockTransaction.execute = async () => { 67 | return mockResult; 68 | }; 69 | const transactionExecuteSpy = sandbox.spy(mockTransaction, "execute"); 70 | const result = await transactionExecutor.execute(testStatement); 71 | chai.assert.equal(mockResult, result); 72 | sinon.assert.calledOnce(transactionExecuteSpy); 73 | sinon.assert.calledWith(transactionExecuteSpy, testStatement); 74 | }); 75 | 76 | it("should return a Result object when provided with a statement and parameters", async () => { 77 | mockTransaction.execute = async () => { 78 | return mockResult; 79 | }; 80 | 81 | const transactionExecuteSpy = sandbox.spy(mockTransaction, "execute"); 82 | const result = await transactionExecutor.execute(testStatement, ["a"]); 83 | chai.assert.equal(mockResult, result); 84 | sinon.assert.calledOnce(transactionExecuteSpy); 85 | sinon.assert.calledWith(transactionExecuteSpy, testStatement, ["a"]); 86 | }); 87 | 88 | it("should return a rejected promise when error is thrown", async () => { 89 | mockTransaction.execute = async () => { 90 | throw new Error(testMessage); 91 | }; 92 | const transactionExecuteSpy = sandbox.spy(mockTransaction, "execute"); 93 | const errorMessage = await chai.expect(transactionExecutor.execute(testStatement)).to.be.rejected; 94 | chai.assert.equal(errorMessage.name, "Error"); 95 | sinon.assert.calledOnce(transactionExecuteSpy); 96 | sinon.assert.calledWith(transactionExecuteSpy, testStatement); 97 | }); 98 | }); 99 | 100 | describe("#executeAndStreamResults()", () => { 101 | it("should return a Result object when provided with a statement", async () => { 102 | mockTransaction.executeAndStreamResults = async () => { 103 | return mockResultReadable; 104 | }; 105 | const transactionExecuteSpy = sandbox.spy(mockTransaction, "executeAndStreamResults"); 106 | const resultReadable = await transactionExecutor.executeAndStreamResults(testStatement); 107 | chai.assert.equal(mockResultReadable, resultReadable); 108 | sinon.assert.calledOnce(transactionExecuteSpy); 109 | sinon.assert.calledWith(transactionExecuteSpy, testStatement); 110 | }); 111 | 112 | it("should return a Result object when provided with a statement and parameters", async () => { 113 | mockTransaction.executeAndStreamResults = async () => { 114 | return mockResultReadable; 115 | }; 116 | 117 | const transactionExecuteSpy = sandbox.spy(mockTransaction, "executeAndStreamResults"); 118 | const resultReadable = await transactionExecutor.executeAndStreamResults(testStatement, [5]); 119 | chai.assert.equal(mockResultReadable, resultReadable); 120 | sinon.assert.calledOnce(transactionExecuteSpy); 121 | sinon.assert.calledWith(transactionExecuteSpy, testStatement, [5]); 122 | }); 123 | 124 | it("should return a rejected promise when error is thrown", async () => { 125 | mockTransaction.executeAndStreamResults = async () => { 126 | throw new Error(testMessage); 127 | }; 128 | const transactionExecuteSpy = sandbox.spy(mockTransaction, "executeAndStreamResults"); 129 | const errorMessage = await chai.expect(transactionExecutor.executeAndStreamResults(testStatement)).to.be.rejected; 130 | chai.assert.equal(errorMessage.name, "Error"); 131 | sinon.assert.calledOnce(transactionExecuteSpy); 132 | sinon.assert.calledWith(transactionExecuteSpy, testStatement); 133 | }); 134 | }); 135 | 136 | describe("#getTransactionId()", () => { 137 | it("should return the transaction ID when called", async () => { 138 | mockTransaction.getTransactionId = () => { 139 | return testTransactionId; 140 | }; 141 | const transactionIdSpy = sandbox.spy(mockTransaction, "getTransactionId"); 142 | const transactionId = transactionExecutor.getTransactionId(); 143 | chai.assert.equal(transactionId, testTransactionId); 144 | sinon.assert.calledOnce(transactionIdSpy); 145 | }); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "lib": [ "ES2015", "DOM" ], 6 | "strict": true, 7 | "strictNullChecks": false, 8 | "resolveJsonModule": true, 9 | "declaration": true, 10 | "outDir": "dist", 11 | "stripInternal": true 12 | }, 13 | "include": [ 14 | "src/**/*.ts", 15 | "index.ts" 16 | ], 17 | "exclude": [ 18 | "src/integrationtest/*", 19 | "src/test/*" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------