├── .eslintrc.json ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── NOTICE ├── README.md ├── THIRD-PARTY-LICENSES ├── package-lock.json ├── package.json ├── src ├── AddSecondaryOwner.ts ├── ConnectToLedger.ts ├── CreateIndex.ts ├── CreateLedger.ts ├── CreateTable.ts ├── DeleteLedger.ts ├── DeletionProtection.ts ├── DeregisterDriversLicense.ts ├── DescribeJournalExport.ts ├── DescribeLedger.ts ├── ExportJournal.ts ├── FindVehicles.ts ├── GetBlock.ts ├── GetDigest.ts ├── GetRevision.ts ├── InsertDocument.ts ├── InsertIonTypes.ts ├── ListJournalExports.ts ├── ListLedgers.ts ├── QueryHistory.ts ├── RedactRevision.ts ├── RegisterDriversLicense.ts ├── RenewDriversLicense.ts ├── ScanTable.ts ├── TagResources.ts ├── TransferVehicleOwnership.ts ├── ValidateQldbHashchain.ts ├── index.ts ├── model │ └── SampleData.ts ├── qldb │ ├── BlockAddress.ts │ ├── Constants.ts │ ├── JournalBlock.ts │ ├── JournalS3ExportReader.ts │ ├── LogUtil.ts │ ├── Util.ts │ └── Verifier.ts └── test │ └── Sequence.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 | "require-atomic-updates": "off", 28 | "object-curly-spacing": [ 2, "always" ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | target-branch: "master" 6 | schedule: 7 | interval: "weekly" 8 | day: "tuesday" 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | id-token: write 11 | contents: read 12 | 13 | jobs: 14 | build: 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | node-version: [14.x, 16.x] 20 | 21 | steps: 22 | - name: Configure AWS Credentials 23 | uses: aws-actions/configure-aws-credentials@v1 24 | with: 25 | aws-region: us-east-1 26 | role-to-assume: arn:aws:iam::264319671630:role/GitHubSamplesActionsOidc 27 | 28 | - uses: actions/checkout@v2 29 | 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v1 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - name: Install dependencies 35 | run: npm install 36 | - name: Build & Lint 37 | run: npm run build 38 | - name: Test 39 | shell: bash 40 | run: | 41 | GITHUB_SHA_SHORT=$(git rev-parse --short $GITHUB_SHA) 42 | CI_ID=${{ strategy.job-index }}-$GITHUB_SHA_SHORT npm test 43 | -------------------------------------------------------------------------------- /.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: '23 23 * * 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/* 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | *.swp 12 | 13 | pids 14 | logs 15 | results 16 | tmp 17 | 18 | # Build 19 | public 20 | 21 | # Coverage reports 22 | coverage 23 | 24 | # API keys and secrets 25 | .env 26 | 27 | # Dependency directory 28 | node_modules 29 | bower_components 30 | 31 | # Editors 32 | .idea 33 | *.iml 34 | 35 | # OS metadata 36 | .DS_Store 37 | Thumbs.db 38 | 39 | # Ignore built ts files 40 | dist/**/* 41 | 42 | # ignore yarn.lock 43 | yarn.lock 44 | -------------------------------------------------------------------------------- /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 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | SPDX-License-Identifier: MIT-0 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 5 | software and associated documentation files (the "Software"), to deal in the Software 6 | without restriction, including without limitation the rights to use, copy, modify, 7 | erge, publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 11 | NCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 12 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 13 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 14 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 15 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amazon Qldb Dmv Sample Nodejs 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon QLDB Node.js DMV Sample App 2 | [![license](https://img.shields.io/badge/license-MIT-green)](https://github.com/aws-samples/amazon-qldb-dmv-sample-nodejs/blob/master/LICENSE) 3 | [![AWS Provider](https://img.shields.io/badge/provider-AWS-orange?logo=amazon-aws&color=ff9900)](https://aws.amazon.com/qldb/) 4 | 5 | The samples in this project demonstrate several uses of Amazon Quantum Ledger Database (QLDB). 6 | 7 | For our tutorial, see [Node.js and Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.nodejs.html). 8 | 9 | ## Requirements 10 | 11 | ### Basic Configuration 12 | 13 | See [Accessing Amazon QLDB](https://docs.aws.amazon.com/qldb/latest/developerguide/accessing.html) for information on connecting to AWS. 14 | 15 | See [Setting Region](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html) page for more information on using the AWS SDK for JavaScript. You will need to set a region before running the sample code. 16 | 17 | See [Setting credentials](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials.html) for credential configuration options using the AWS Nodejs SDK v3. 18 | 19 | ### TypeScript 3.5.x 20 | 21 | The sample code is written in, and requires, TypeScript 3.5.x. It will be automatically installed as a dependency. 22 | Please see the link below for more detail on TypeScript 3.5.x: 23 | 24 | * [TypeScript 3.5.x](https://www.npmjs.com/package/typescript) 25 | 26 | ## Installing the Dependencies 27 | 28 | Install Node.js QLDB driver, and other dependencies using the npm utility: 29 | 30 | ``` 31 | npm install 32 | ``` 33 | 34 | ## Running the Sample Code 35 | 36 | Since the sample code is written in TypeScript, it must first be transpiled in order to be run: 37 | 38 | ``` 39 | npm run build 40 | ``` 41 | 42 | The transpiled JavaScript files can be now found in the `./dist` directory. 43 | 44 | The sample code creates a ledger with tables and indexes, and inserts some documents into those tables, 45 | among other things. Each of the examples in this project can be run in the following way: 46 | 47 | ``` 48 | node dist/CreateLedger.js 49 | ``` 50 | 51 | The above example will create a ledger named: `vehicle-registration`. 52 | You may run other examples after creating a ledger. 53 | 54 | See [Getting Started](https://docs.aws.amazon.com/qldb/latest/developerguide/getting-started.nodejs.html) to learn more about the sample app. 55 | 56 | ## Documentation 57 | 58 | TypeDoc is used for documentation. You can generate HTML locally with the following: 59 | 60 | ``` 61 | npm run doc 62 | ``` 63 | 64 | ## Release Notes 65 | ### Release 1.0.0 66 | 67 | * Modify the sample app to use Qldb Node.js Driver v1.0.0 68 | 69 | ### Release 1.0.0-rc.2 70 | 71 | * Modify the sample app to use Qldb Node.js Driver v1.0.0-rc.2 72 | 73 | ### Release 1.0.0-rc.1 74 | 75 | * Modify the sample app to use Qldb Node.js Driver v1.0.0-rc.1 76 | 77 | ### Release 0.1.0-preview 78 | 79 | * Initial preview release of the QLDB Node.js Sample Application. 80 | 81 | ## License 82 | 83 | This library is licensed under [the MIT-0 License](https://github.com/aws/mit-0). 84 | -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES: -------------------------------------------------------------------------------- 1 | ** ion-js; version 3.1.2 -- https://github.com/amzn/ion-js/ 2 | Amazon Ion JavaScript 3 | Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | ** typescript; version 3.5.3 -- http://www.typescriptlang.org/ 5 | Copyright (c) Microsoft Corporation. All rights reserved. 6 | ** typescript; version 3.5.3 -- http://www.typescriptlang.org/ 7 | 8 | Apache License 9 | 10 | Version 2.0, January 2004 11 | 12 | http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND 13 | DISTRIBUTION 14 | 15 | 1. Definitions. 16 | 17 | "License" shall mean the terms and conditions for use, reproduction, and 18 | distribution as defined by Sections 1 through 9 of this document. 19 | 20 | "Licensor" shall mean the copyright owner or entity authorized by the 21 | copyright owner that is granting the License. 22 | 23 | "Legal Entity" shall mean the union of the acting entity and all other 24 | entities that control, are controlled by, or are under common control 25 | with that entity. For the purposes of this definition, "control" means 26 | (i) the power, direct or indirect, to cause the direction or management 27 | of such entity, whether by contract or otherwise, or (ii) ownership of 28 | fifty percent (50%) or more of the outstanding shares, or (iii) 29 | beneficial ownership of such entity. 30 | 31 | "You" (or "Your") shall mean an individual or Legal Entity exercising 32 | permissions granted by this License. 33 | 34 | "Source" form shall mean the preferred form for making modifications, 35 | including but not limited to software source code, documentation source, 36 | and configuration files. 37 | 38 | "Object" form shall mean any form resulting from mechanical 39 | transformation or translation of a Source form, including but not limited 40 | to compiled object code, generated documentation, and conversions to 41 | other media types. 42 | 43 | "Work" shall mean the work of authorship, whether in Source or Object 44 | form, made available under the License, as indicated by a copyright 45 | notice that is included in or attached to the work (an example is 46 | provided in the Appendix below). 47 | 48 | "Derivative Works" shall mean any work, whether in Source or Object form, 49 | that is based on (or derived from) the Work and for which the editorial 50 | revisions, annotations, elaborations, or other modifications represent, 51 | as a whole, an original work of authorship. For the purposes of this 52 | License, Derivative Works shall not include works that remain separable 53 | from, or merely link (or bind by name) to the interfaces of, the Work and 54 | Derivative Works thereof. 55 | 56 | "Contribution" shall mean any work of authorship, including the original 57 | version of the Work and any modifications or additions to that Work or 58 | Derivative Works thereof, that is intentionally submitted to Licensor for 59 | inclusion in the Work by the copyright owner or by an individual or Legal 60 | Entity authorized to submit on behalf of the copyright owner. For the 61 | purposes of this definition, "submitted" means any form of electronic, 62 | verbal, or written communication sent to the Licensor or its 63 | representatives, including but not limited to communication on electronic 64 | mailing lists, source code control systems, and issue tracking systems 65 | that are managed by, or on behalf of, the Licensor for the purpose of 66 | discussing and improving the Work, but excluding communication that is 67 | conspicuously marked or otherwise designated in writing by the copyright 68 | owner as "Not a Contribution." 69 | 70 | "Contributor" shall mean Licensor and any individual or Legal Entity on 71 | behalf of whom a Contribution has been received by Licensor and 72 | subsequently incorporated within the Work. 73 | 74 | 2. Grant of Copyright License. Subject to the terms and conditions of this 75 | License, each Contributor hereby grants to You a perpetual, worldwide, 76 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 77 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 78 | sublicense, and distribute the Work and such Derivative Works in Source or 79 | Object form. 80 | 81 | 3. Grant of Patent License. Subject to the terms and conditions of this 82 | License, each Contributor hereby grants to You a perpetual, worldwide, 83 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 84 | this section) patent license to make, have made, use, offer to sell, sell, 85 | import, and otherwise transfer the Work, where such license applies only to 86 | those patent claims licensable by such Contributor that are necessarily 87 | infringed by their Contribution(s) alone or by combination of their 88 | Contribution(s) with the Work to which such Contribution(s) was submitted. 89 | If You institute patent litigation against any entity (including a 90 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 91 | Contribution incorporated within the Work constitutes direct or contributory 92 | patent infringement, then any patent licenses granted to You under this 93 | License for that Work shall terminate as of the date such litigation is 94 | filed. 95 | 96 | 4. Redistribution. You may reproduce and distribute copies of the Work or 97 | Derivative Works thereof in any medium, with or without modifications, and 98 | in Source or Object form, provided that You meet the following conditions: 99 | 100 | (a) You must give any other recipients of the Work or Derivative Works a 101 | copy of this License; and 102 | 103 | (b) You must cause any modified files to carry prominent notices stating 104 | that You changed the files; and 105 | 106 | (c) You must retain, in the Source form of any Derivative Works that You 107 | distribute, all copyright, patent, trademark, and attribution notices 108 | from the Source form of the Work, excluding those notices that do not 109 | pertain to any part of the Derivative Works; and 110 | 111 | (d) If the Work includes a "NOTICE" text file as part of its 112 | distribution, then any Derivative Works that You distribute must include 113 | a readable copy of the attribution notices contained within such NOTICE 114 | file, excluding those notices that do not pertain to any part of the 115 | Derivative Works, in at least one of the following places: within a 116 | NOTICE text file distributed as part of the Derivative Works; within the 117 | Source form or documentation, if provided along with the Derivative 118 | Works; or, within a display generated by the Derivative Works, if and 119 | wherever such third-party notices normally appear. The contents of the 120 | NOTICE file are for informational purposes only and do not modify the 121 | License. You may add Your own attribution notices within Derivative Works 122 | that You distribute, alongside or as an addendum to the NOTICE text from 123 | the Work, provided that such additional attribution notices cannot be 124 | construed as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and may 127 | provide additional or different license terms and conditions for use, 128 | reproduction, or distribution of Your modifications, or for any such 129 | Derivative Works as a whole, provided Your use, reproduction, and 130 | distribution of the Work otherwise complies with the conditions stated in 131 | this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 134 | Contribution intentionally submitted for inclusion in the Work by You to the 135 | Licensor shall be under the terms and conditions of this License, without 136 | any additional terms or conditions. Notwithstanding the above, nothing 137 | herein shall supersede or modify the terms of any separate license agreement 138 | you may have executed with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, except 142 | as required for reasonable and customary use in describing the origin of the 143 | Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 146 | writing, Licensor provides the Work (and each Contributor provides its 147 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 148 | KIND, either express or implied, including, without limitation, any 149 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 150 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 151 | the appropriateness of using or redistributing the Work and assume any risks 152 | associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, whether 155 | in tort (including negligence), contract, or otherwise, unless required by 156 | applicable law (such as deliberate and grossly negligent acts) or agreed to 157 | in writing, shall any Contributor be liable to You for damages, including 158 | any direct, indirect, special, incidental, or consequential damages of any 159 | character arising as a result of this License or out of the use or inability 160 | to use the Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all other 162 | commercial damages or losses), even if such Contributor has been advised of 163 | the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing the Work 166 | or Derivative Works thereof, You may choose to offer, and charge a fee for, 167 | acceptance of support, warranty, indemnity, or other liability obligations 168 | and/or rights consistent with this License. However, in accepting such 169 | obligations, You may act only on Your own behalf and on Your sole 170 | responsibility, not on behalf of any other Contributor, and only if You 171 | agree to indemnify, defend, and hold each Contributor harmless for any 172 | liability incurred by, or claims asserted against, such Contributor by 173 | reason of your accepting any such warranty or additional liability. END OF 174 | TERMS AND CONDITIONS 175 | 176 | APPENDIX: How to apply the Apache License to your work. 177 | 178 | To apply the Apache License to your work, attach the following boilerplate 179 | notice, with the fields enclosed by brackets "[]" replaced with your own 180 | identifying information. (Don't include the brackets!) The text should be 181 | enclosed in the appropriate comment syntax for the file format. We also 182 | recommend that a file or class name and description of purpose be included on 183 | the same "printed page" as the copyright notice for easier identification 184 | within third-party archives. 185 | 186 | Copyright [yyyy] [name of copyright owner] 187 | 188 | Licensed under the Apache License, Version 2.0 (the "License"); 189 | 190 | you may not use this file except in compliance with the License. 191 | 192 | You may obtain a copy of the License at 193 | 194 | http://www.apache.org/licenses/LICENSE-2.0 195 | 196 | Unless required by applicable law or agreed to in writing, software 197 | 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | 202 | See the License for the specific language governing permissions and 203 | 204 | limitations under the License. 205 | 206 | * For ion-js see also this required NOTICE: 207 | Amazon Ion JavaScript 208 | Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights 209 | Reserved. 210 | * For typescript see also this required NOTICE: 211 | Copyright (c) Microsoft Corporation. All rights reserved. 212 | * For typescript see also this required NOTICE: 213 | Copyright (c) Microsoft Corporation. All rights reserved. 214 | * For typescript see also this required NOTICE: 215 | Copyright (c) Microsoft Corporation. All rights reserved. 216 | 217 | ------ 218 | 219 | ** DefinitelyTyped; version 12.0.2 -- http://definitelytyped.org/ 220 | Copyright (c) Microsoft Corporation. All rights reserved 221 | 222 | MIT License 223 | 224 | Copyright (c) Microsoft Corporation. All rights reserved. 225 | 226 | Permission is hereby granted, free of charge, to any person obtaining a 227 | copy 228 | of this software and associated documentation files (the "Software"), to 229 | deal 230 | in the Software without restriction, including without limitation the 231 | rights 232 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 233 | copies of the Software, and to permit persons to whom the Software is 234 | furnished to do so, subject to the following conditions: 235 | 236 | The above copyright notice and this permission notice shall be included in 237 | all 238 | copies or substantial portions of the Software. 239 | 240 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 241 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 242 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 243 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 244 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 245 | FROM, 246 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 247 | THE 248 | SOFTWARE 249 | 250 | ------ 251 | 252 | ** ts-node; version 8.3.0 -- https://github.com/TypeStrong/ts-node 253 | Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com) 254 | 255 | MIT License 256 | 257 | Copyright (c) 258 | 259 | Permission is hereby granted, free of charge, to any person obtaining a copy of 260 | this software and associated documentation files (the "Software"), to deal in 261 | the Software without restriction, including without limitation the rights to 262 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 263 | of the Software, and to permit persons to whom the Software is furnished to do 264 | so, subject to the following conditions: 265 | 266 | The above copyright notice and this permission notice shall be included in all 267 | copies or substantial portions of the Software. 268 | 269 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 270 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 271 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 272 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 273 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 274 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 275 | SOFTWARE. 276 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@aws-sdk/client-config-service": "^3.451.0", 4 | "@aws-sdk/client-iam": "^3.451.0", 5 | "@aws-sdk/client-qldb": "^3.451.0", 6 | "@aws-sdk/client-qldb-session": "^3.451.0", 7 | "@aws-sdk/client-resource-groups-tagging-api": "^3.451.0", 8 | "@aws-sdk/client-s3": "^3.451.0", 9 | "@types/node": "^18.18.9", 10 | "amazon-qldb-driver-nodejs": "^3.0.1", 11 | "aws-sdk": "^2.1499.0", 12 | "ion-js": "^5.2.0", 13 | "jsbi": "^3.2.5" 14 | }, 15 | "name": "amazon-qldb-dmv-sample-nodejs", 16 | "description": "Amazon Quantum Ledger Database Node.js sample application", 17 | "version": "2.0.0", 18 | "main": "dist/index.js", 19 | "types": "dist/index.d.ts", 20 | "devDependencies": { 21 | "@types/chai": "^4.3.10", 22 | "@types/mocha": "^10.0.4", 23 | "@typescript-eslint/eslint-plugin": "^5.62.0", 24 | "@typescript-eslint/parser": "^5.62.0", 25 | "chai": "^4.3.10", 26 | "cross-env": "^7.0.3", 27 | "eslint": "^8.53.0", 28 | "mocha": "^10.2.0", 29 | "ts-node": "^10.9.1", 30 | "typedoc": "^0.23.28", 31 | "typescript": "^4.9.5" 32 | }, 33 | "scripts": { 34 | "build": "npm run lint && tsc", 35 | "doc": "typedoc --out docs ./src", 36 | "lint": "eslint src", 37 | "test": "cross-env TS_NODE_COMPILER_OPTIONS={\\\"strict\\\":false} mocha -r ts-node/register src/test/*.test.ts" 38 | }, 39 | "author": { 40 | "name": "Amazon Web Services", 41 | "url": "https://aws.amazon.com/" 42 | }, 43 | "license": "Apache-2.0", 44 | "keywords": [ 45 | "amazon", 46 | "aws", 47 | "qldb", 48 | "ledger" 49 | ], 50 | "repository": { 51 | "type": "git", 52 | "url": "https://github.com/awslabs/amazon-qldb-dmv-sample-nodejs" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/AddSecondaryOwner.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { PERSON, VEHICLE_REGISTRATION } from "./model/SampleData"; 24 | import { PERSON_TABLE_NAME } from "./qldb/Constants"; 25 | import { error, log } from "./qldb/LogUtil"; 26 | import { getDocumentId } from "./qldb/Util"; 27 | import { prettyPrintResultList } from "./ScanTable"; 28 | 29 | /** 30 | * Add a secondary owner into 'VehicleRegistration' table for a particular VIN. 31 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 32 | * @param vin VIN of the vehicle to query. 33 | * @param secondaryOwnerId The secondary owner's person ID. 34 | * @returns Promise which fulfills with void. 35 | */ 36 | export async function addSecondaryOwner( 37 | txn: TransactionExecutor, 38 | vin: string, 39 | secondaryOwnerId: string 40 | ): Promise { 41 | log(`Inserting secondary owner for vehicle with VIN: ${vin}`); 42 | const query: string = 43 | `FROM VehicleRegistration AS v WHERE v.VIN = ? INSERT INTO v.Owners.SecondaryOwners VALUE ?`; 44 | 45 | const personToInsert = { PersonId: secondaryOwnerId }; 46 | return await txn.execute(query, vin, personToInsert).then(async (result: Result) => { 47 | const resultList: dom.Value[] = result.getResultList(); 48 | log("VehicleRegistration Document IDs which had secondary owners added: "); 49 | prettyPrintResultList(resultList); 50 | return resultList; 51 | }); 52 | } 53 | 54 | /** 55 | * Query for a document ID with a government ID. 56 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 57 | * @param governmentId The government ID to query with. 58 | * @returns Promise which fulfills with the document ID as a string. 59 | */ 60 | export async function getDocumentIdByGovId(txn: TransactionExecutor, governmentId: string): Promise { 61 | const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", governmentId); 62 | return documentId; 63 | } 64 | 65 | /** 66 | * Check whether a driver has already been registered for the given VIN. 67 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 68 | * @param vin VIN of the vehicle to query. 69 | * @param secondaryOwnerId The secondary owner's person ID. 70 | * @returns Promise which fulfills with a boolean. 71 | */ 72 | export async function isSecondaryOwnerForVehicle( 73 | txn: TransactionExecutor, 74 | vin: string, 75 | secondaryOwnerId: string 76 | ): Promise { 77 | log(`Finding secondary owners for vehicle with VIN: ${vin}`); 78 | const query: string = "SELECT Owners.SecondaryOwners FROM VehicleRegistration AS v WHERE v.VIN = ?"; 79 | 80 | let doesExist: boolean = false; 81 | 82 | await txn.execute(query, vin).then((result: Result) => { 83 | const resultList: dom.Value[] = result.getResultList(); 84 | 85 | resultList.forEach((value: dom.Value) => { 86 | const secondaryOwnersList: dom.Value[] = value.get("SecondaryOwners").elements(); 87 | 88 | secondaryOwnersList.forEach((secondaryOwner) => { 89 | const personId: dom.Value = secondaryOwner.get("PersonId"); 90 | if (personId !== null && personId.stringValue() === secondaryOwnerId) { 91 | doesExist = true; 92 | } 93 | }); 94 | }); 95 | }); 96 | return doesExist; 97 | } 98 | 99 | /** 100 | * Finds and adds secondary owners for a vehicle. 101 | * @returns Promise which fulfills with void. 102 | */ 103 | export const main = async function(): Promise { 104 | try { 105 | const qldbDriver: QldbDriver = getQldbDriver(); 106 | const vin: string = VEHICLE_REGISTRATION[1].VIN; 107 | const govId: string = PERSON[0].GovId; 108 | 109 | const registration = await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 110 | const documentId: string = await getDocumentIdByGovId(txn, govId); 111 | 112 | if (await isSecondaryOwnerForVehicle(txn, vin, documentId)) { 113 | log(`Person with ID ${documentId} has already been added as a secondary owner of this vehicle.`); 114 | } else { 115 | return await addSecondaryOwner(txn, vin, documentId); 116 | } 117 | }); 118 | 119 | log("Secondary owners successfully updated."); 120 | 121 | return registration; 122 | } catch (e) { 123 | error(`Unable to add secondary owner: ${e}`); 124 | } 125 | } 126 | 127 | if (require.main === module) { 128 | main(); 129 | } 130 | -------------------------------------------------------------------------------- /src/ConnectToLedger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { Agent } from 'https'; 20 | import { QldbDriver, RetryConfig } from "amazon-qldb-driver-nodejs"; 21 | import { QLDBSessionClientConfig } from "@aws-sdk/client-qldb-session"; 22 | import { NodeHttpHandlerOptions } from "@smithy/node-http-handler"; 23 | 24 | import { LEDGER_NAME } from "./qldb/Constants"; 25 | import { error, log } from "./qldb/LogUtil"; 26 | 27 | const qldbDriver: QldbDriver = createQldbDriver(); 28 | 29 | /** 30 | * Create a driver for creating sessions. 31 | * @param ledgerName The name of the ledger to create the driver on. 32 | * @param serviceConfigurationOptions The configurations for the AWS SDK client that the driver uses. 33 | * @returns The driver for creating sessions. 34 | */ 35 | export function createQldbDriver( 36 | ledgerName: string = LEDGER_NAME, 37 | serviceConfigurationOptions: QLDBSessionClientConfig = {} 38 | ): QldbDriver { 39 | const retryLimit = 4; 40 | const maxConcurrentTransactions = 10; 41 | const lowLevelClientHttpOptions: NodeHttpHandlerOptions = { 42 | httpAgent: new Agent({ 43 | maxSockets: maxConcurrentTransactions 44 | }) 45 | }; 46 | //Use driver's default backoff function (and hence, no second parameter provided to RetryConfig) 47 | const retryConfig: RetryConfig = new RetryConfig(retryLimit); 48 | const qldbDriver: QldbDriver = new QldbDriver(ledgerName,serviceConfigurationOptions, lowLevelClientHttpOptions, maxConcurrentTransactions, retryConfig); 49 | return qldbDriver; 50 | } 51 | 52 | 53 | export function getQldbDriver(): QldbDriver { 54 | return qldbDriver; 55 | } 56 | 57 | /** 58 | * Connect to a session for a given ledger using default settings. 59 | * @returns Promise which fulfills with void. 60 | */ 61 | export const main = async function(): Promise { 62 | try { 63 | log("Listing table names..."); 64 | const tableNames: string[] = await qldbDriver.getTableNames(); 65 | tableNames.forEach((tableName: string): void => { 66 | log(tableName); 67 | }); 68 | } catch (e) { 69 | error(`Unable to create session: ${e}`); 70 | } 71 | } 72 | 73 | if (require.main === module) { 74 | main(); 75 | } 76 | -------------------------------------------------------------------------------- /src/CreateIndex.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | 21 | import { getQldbDriver } from "./ConnectToLedger"; 22 | import { 23 | DRIVERS_LICENSE_TABLE_NAME, 24 | GOV_ID_INDEX_NAME, 25 | LICENSE_NUMBER_INDEX_NAME, 26 | LICENSE_PLATE_NUMBER_INDEX_NAME, 27 | PERSON_ID_INDEX_NAME, 28 | PERSON_TABLE_NAME, 29 | VEHICLE_REGISTRATION_TABLE_NAME, 30 | VEHICLE_TABLE_NAME, 31 | VIN_INDEX_NAME 32 | } from "./qldb/Constants"; 33 | import { error, log } from "./qldb/LogUtil"; 34 | 35 | /** 36 | * Create an index for a particular table. 37 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 38 | * @param tableName Name of the table to add indexes for. 39 | * @param indexAttribute Index to create on a single attribute. 40 | * @returns Promise which fulfills with the number of changes to the database. 41 | */ 42 | export async function createIndex( 43 | txn: TransactionExecutor, 44 | tableName: string, 45 | indexAttribute: string 46 | ): Promise { 47 | const statement: string = `CREATE INDEX on ${tableName} (${indexAttribute})`; 48 | return await txn.execute(statement).then((result) => { 49 | log(`Successfully created index ${indexAttribute} on table ${tableName}.`); 50 | return result.getResultList().length; 51 | }); 52 | } 53 | 54 | /** 55 | * Create indexes on tables in a particular ledger. 56 | * @returns Promise which fulfills with void. 57 | */ 58 | export const main = async function(): Promise { 59 | try { 60 | const qldbDriver: QldbDriver = getQldbDriver(); 61 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 62 | Promise.all([ 63 | createIndex(txn, PERSON_TABLE_NAME, GOV_ID_INDEX_NAME), 64 | createIndex(txn, VEHICLE_TABLE_NAME, VIN_INDEX_NAME), 65 | createIndex(txn, VEHICLE_REGISTRATION_TABLE_NAME, VIN_INDEX_NAME), 66 | createIndex(txn, VEHICLE_REGISTRATION_TABLE_NAME, LICENSE_PLATE_NUMBER_INDEX_NAME), 67 | createIndex(txn, DRIVERS_LICENSE_TABLE_NAME, PERSON_ID_INDEX_NAME), 68 | createIndex(txn, DRIVERS_LICENSE_TABLE_NAME, LICENSE_NUMBER_INDEX_NAME) 69 | ]); 70 | }); 71 | } catch (e) { 72 | error(`Unable to create indexes: ${e}`); 73 | } 74 | } 75 | 76 | if (require.main === module) { 77 | main(); 78 | } 79 | -------------------------------------------------------------------------------- /src/CreateLedger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QLDB, 20 | CreateLedgerRequest, 21 | CreateLedgerResponse, 22 | DescribeLedgerRequest, 23 | DescribeLedgerResponse 24 | } from "@aws-sdk/client-qldb"; 25 | 26 | import { LEDGER_NAME } from "./qldb/Constants"; 27 | import { error, log } from "./qldb/LogUtil"; 28 | import { sleep } from "./qldb/Util"; 29 | 30 | const LEDGER_CREATION_POLL_PERIOD_MS = 10000; 31 | const ACTIVE_STATE = "ACTIVE"; 32 | 33 | /** 34 | * Create a new ledger with the specified name. 35 | * @param ledgerName Name of the ledger to be created. 36 | * @param qldbClient The QLDB control plane client to use. 37 | * @returns Promise which fulfills with a CreateLedgerResponse. 38 | */ 39 | export async function createLedger(ledgerName: string, qldbClient: QLDB): Promise { 40 | log(`Creating a ledger named: ${ledgerName}...`); 41 | const request: CreateLedgerRequest = { 42 | Name: ledgerName, 43 | PermissionsMode: "ALLOW_ALL" 44 | } 45 | const result: CreateLedgerResponse = await qldbClient.createLedger(request); 46 | log(`Success. Ledger state: ${result.State}.`); 47 | return result; 48 | } 49 | 50 | /** 51 | * Wait for the newly created ledger to become active. 52 | * @param ledgerName Name of the ledger to be checked on. 53 | * @param qldbClient The QLDB control plane client to use. 54 | * @returns Promise which fulfills with a DescribeLedgerResponse. 55 | */ 56 | export async function waitForActive(ledgerName: string, qldbClient: QLDB): Promise { 57 | log(`Waiting for ledger ${ledgerName} to become active...`); 58 | const request: DescribeLedgerRequest = { 59 | Name: ledgerName 60 | } 61 | while (true) { 62 | const result: DescribeLedgerResponse = await qldbClient.describeLedger(request); 63 | if (result.State === ACTIVE_STATE) { 64 | log("Success. Ledger is active and ready to be used."); 65 | return result; 66 | } 67 | log("The ledger is still creating. Please wait..."); 68 | await sleep(LEDGER_CREATION_POLL_PERIOD_MS); 69 | } 70 | } 71 | 72 | /** 73 | * Create a ledger and wait for it to be active. 74 | * @returns Promise which fulfills with void. 75 | */ 76 | export const main = async function(): Promise { 77 | try { 78 | const qldbClient: QLDB = new QLDB({ }); 79 | await createLedger(LEDGER_NAME, qldbClient); 80 | await waitForActive(LEDGER_NAME, qldbClient); 81 | } catch (e) { 82 | error(`Unable to create the ledger: ${e}`); 83 | } 84 | } 85 | 86 | if (require.main === module) { 87 | main(); 88 | } 89 | -------------------------------------------------------------------------------- /src/CreateTable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | 21 | import { getQldbDriver } from "./ConnectToLedger"; 22 | import { 23 | DRIVERS_LICENSE_TABLE_NAME, 24 | PERSON_TABLE_NAME, 25 | VEHICLE_REGISTRATION_TABLE_NAME, 26 | VEHICLE_TABLE_NAME 27 | } from "./qldb/Constants"; 28 | import { error, log } from "./qldb/LogUtil"; 29 | 30 | /** 31 | * Create multiple tables in a single transaction. 32 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 33 | * @param tableName Name of the table to create. 34 | * @returns Promise which fulfills with the number of changes to the database. 35 | */ 36 | export async function createTable(txn: TransactionExecutor, tableName: string): Promise { 37 | const statement: string = `CREATE TABLE ${tableName}`; 38 | return await txn.execute(statement).then((result: Result) => { 39 | log(`Successfully created table ${tableName}.`); 40 | return result.getResultList().length; 41 | }); 42 | } 43 | 44 | /** 45 | * Create tables in a QLDB ledger. 46 | * @returns Promise which fulfills with void. 47 | */ 48 | export const main = async function(): Promise { 49 | try { 50 | const qldbDriver: QldbDriver = getQldbDriver(); 51 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 52 | Promise.all([ 53 | createTable(txn, VEHICLE_REGISTRATION_TABLE_NAME), 54 | createTable(txn, VEHICLE_TABLE_NAME), 55 | createTable(txn, PERSON_TABLE_NAME), 56 | createTable(txn, DRIVERS_LICENSE_TABLE_NAME) 57 | ]); 58 | }); 59 | } catch (e) { 60 | error(`Unable to create tables: ${e}`); 61 | } 62 | } 63 | 64 | if (require.main === module) { 65 | main(); 66 | } 67 | -------------------------------------------------------------------------------- /src/DeleteLedger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { isResourceNotFoundException } from "amazon-qldb-driver-nodejs"; 20 | import { QLDB, 21 | DeleteLedgerRequest, 22 | DescribeLedgerRequest 23 | } from "@aws-sdk/client-qldb"; 24 | import { ServiceException } from "@smithy/smithy-client"; 25 | 26 | import { setDeletionProtection } from "./DeletionProtection"; 27 | import { 28 | LEDGER_NAME, 29 | } from "./qldb/Constants"; 30 | import { error, log } from "./qldb/LogUtil"; 31 | import { sleep } from "./qldb/Util"; 32 | 33 | const LEDGER_DELETION_POLL_PERIOD_MS = 20000; 34 | 35 | /** 36 | * Send a request to QLDB to delete the specified ledger. 37 | * @param ledgerName Name of the ledger to be deleted. 38 | * @param qldbClient The QLDB control plane client to use. 39 | * @returns Promise which fulfills with void. 40 | */ 41 | export async function deleteLedger(ledgerName: string, qldbClient: QLDB): Promise { 42 | log(`Attempting to delete the ledger with name: ${ledgerName}`); 43 | const request: DeleteLedgerRequest = { 44 | Name: ledgerName 45 | }; 46 | await qldbClient.deleteLedger(request); 47 | log("Success."); 48 | } 49 | 50 | /** 51 | * Wait for the ledger to be deleted. 52 | * @param ledgerName Name of the ledger to be deleted. 53 | * @param qldbClient The QLDB control plane client to use. 54 | * @returns Promise which fulfills with void. 55 | */ 56 | export async function waitForDeleted(ledgerName: string, qldbClient: QLDB): Promise { 57 | log("Waiting for the ledger to be deleted..."); 58 | const request: DescribeLedgerRequest = { 59 | Name: ledgerName 60 | }; 61 | let isDeleted: boolean = false; 62 | while (true) { 63 | await qldbClient.describeLedger(request).catch((error: ServiceException) => { 64 | if (isResourceNotFoundException(error)) { 65 | isDeleted = true; 66 | log("Success. Ledger is deleted."); 67 | } 68 | }); 69 | if (isDeleted) { 70 | break; 71 | } 72 | log("The ledger is still being deleted. Please wait..."); 73 | await sleep(LEDGER_DELETION_POLL_PERIOD_MS); 74 | } 75 | } 76 | 77 | /** 78 | * Delete a ledger. 79 | * @returns Promise which fulfills with void. 80 | */ 81 | export const main = async function(): Promise { 82 | try { 83 | const qldbClient: QLDB = new QLDB({ }); 84 | await setDeletionProtection(LEDGER_NAME, qldbClient, false); 85 | await deleteLedger(LEDGER_NAME, qldbClient); 86 | await waitForDeleted(LEDGER_NAME, qldbClient); 87 | } catch (e) { 88 | error(`Unable to delete the ledger: ${e}`); 89 | } 90 | } 91 | 92 | if (require.main === module) { 93 | main(); 94 | } 95 | -------------------------------------------------------------------------------- /src/DeletionProtection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { isResourcePreconditionNotMetException } from "amazon-qldb-driver-nodejs"; 20 | import { QLDB, 21 | CreateLedgerRequest, 22 | CreateLedgerResponse, 23 | UpdateLedgerRequest, 24 | UpdateLedgerResponse, 25 | } from "@aws-sdk/client-qldb"; 26 | import { ServiceException } from "@smithy/smithy-client"; 27 | 28 | import { waitForActive } from "./CreateLedger" 29 | import { deleteLedger } from "./DeleteLedger" 30 | import { error, log } from "./qldb/LogUtil"; 31 | 32 | const LEDGER_NAME = "deletion-protection-demo"; 33 | 34 | /** 35 | * Create a new ledger with the specified name and with deletion protection enabled. 36 | * @param ledgerName Name of the ledger to be created. 37 | * @param qldbClient The QLDB control plane client to use. 38 | * @returns Promise which fulfills with a CreateLedgerResponse. 39 | */ 40 | async function createWithDeletionProtection(ledgerName: string, qldbClient: QLDB): Promise { 41 | log(`Creating a ledger named: ${ledgerName}...`); 42 | const request: CreateLedgerRequest = { 43 | Name: ledgerName, 44 | PermissionsMode: "ALLOW_ALL" 45 | }; 46 | const result: CreateLedgerResponse = await qldbClient.createLedger(request); 47 | log(`Success. Ledger state: ${result.State}.`); 48 | return result; 49 | } 50 | 51 | /** 52 | * Update an existing ledger's deletion protection. 53 | * @param ledgerName Name of the ledger to update. 54 | * @param qldbClient The QLDB control plane client to use. 55 | * @param deletionProtection Enables or disables the deletion protection. 56 | * @returns Promise which fulfills with void. 57 | */ 58 | export async function setDeletionProtection( 59 | ledgerName: string, 60 | qldbClient: QLDB, 61 | deletionProtection: boolean 62 | ): Promise { 63 | log(`Let's set deletion protection to ${deletionProtection} for the ledger with name ${ledgerName}.`); 64 | const request: UpdateLedgerRequest = { 65 | Name: ledgerName, 66 | DeletionProtection: deletionProtection 67 | }; 68 | const result: UpdateLedgerResponse = await qldbClient.updateLedger(request); 69 | log(`Success. Ledger updated: ${JSON.stringify(result)}."`); 70 | return result; 71 | } 72 | 73 | /** 74 | * Demonstrate the protection of QLDB ledgers against deletion. 75 | * @returns Promise which fulfills with void. 76 | */ 77 | export const main = async function(ledgerName: string = LEDGER_NAME): Promise { 78 | try { 79 | const qldbClient: QLDB = new QLDB({ }); 80 | await createWithDeletionProtection(ledgerName, qldbClient); 81 | await waitForActive(ledgerName, qldbClient); 82 | await deleteLedger(ledgerName, qldbClient).catch((error: ServiceException) => { 83 | if (isResourcePreconditionNotMetException(error)) { 84 | log("Ledger protected against deletions!"); 85 | } 86 | }); 87 | const updateDeletionProtectionResult = await setDeletionProtection(ledgerName, qldbClient, false); 88 | await deleteLedger(ledgerName, qldbClient); 89 | return updateDeletionProtectionResult; 90 | } catch (e) { 91 | error(`Unable to update or delete the ledger: ${e}`); 92 | } 93 | } 94 | 95 | if (require.main === module) { 96 | main(); 97 | } 98 | -------------------------------------------------------------------------------- /src/DeregisterDriversLicense.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { DRIVERS_LICENSE } from "./model/SampleData"; 24 | import { error, log } from "./qldb/LogUtil"; 25 | 26 | /** 27 | * Delete a driver's license given a license number. 28 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 29 | * @param licenseNumber The license number of the driver's license to de-register. 30 | * @returns Promise which fulfills with void. 31 | */ 32 | export async function deregisterDriversLicense(txn: TransactionExecutor, licenseNumber: string): Promise { 33 | const statement: string = "DELETE FROM DriversLicense AS d WHERE d.LicenseNumber = ?"; 34 | 35 | return await txn.execute(statement, licenseNumber).then((result: Result) => { 36 | const resultList: dom.Value[] = result.getResultList(); 37 | if (resultList.length !== 0) { 38 | log(`Successfully de-registered license: ${licenseNumber}.`); 39 | } else { 40 | log(`Error de-registering license, license ${licenseNumber} not found.`); 41 | } 42 | return resultList; 43 | }); 44 | } 45 | 46 | /** 47 | * De-register a driver's license. 48 | * @returns Promise which fulfills with void. 49 | */ 50 | export const main = async function(): Promise { 51 | try { 52 | const qldbDriver: QldbDriver = getQldbDriver(); 53 | return await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 54 | return await deregisterDriversLicense(txn, DRIVERS_LICENSE[1].LicenseNumber); 55 | }); 56 | } catch (e) { 57 | error(`Error de-registering driver's license: ${e}`); 58 | } 59 | } 60 | 61 | if (require.main === module) { 62 | main(); 63 | } 64 | -------------------------------------------------------------------------------- /src/DescribeJournalExport.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QLDB, 20 | DescribeJournalS3ExportRequest, 21 | DescribeJournalS3ExportResponse, 22 | } from "@aws-sdk/client-qldb"; 23 | 24 | import { LEDGER_NAME } from './qldb/Constants'; 25 | import { error, log } from "./qldb/LogUtil"; 26 | 27 | /** 28 | * Describe a journal export. 29 | * @param ledgerName The ledger from which the journal is being exported. 30 | * @param exportId The ExportId of the journal. 31 | * @param qldbClient The QLDB control plane client to use. 32 | * @returns Promise which fulfills with a DescribeJournalS3ExportResponse. 33 | */ 34 | export async function describeJournalExport( 35 | ledgerName: string, 36 | exportId: string, 37 | qldbClient: QLDB 38 | ): Promise { 39 | log(`Describing a journal export for ledger with name: ${ledgerName}, ExportId: ${exportId}.`); 40 | const request: DescribeJournalS3ExportRequest = { 41 | Name: LEDGER_NAME, 42 | ExportId: exportId 43 | }; 44 | const exportResult: DescribeJournalS3ExportResponse = await qldbClient.describeJournalS3Export(request); 45 | log(`Export described. Result = ${JSON.stringify(exportResult)}`); 46 | return exportResult; 47 | } 48 | 49 | /** 50 | * Describe a specific journal export with the given ExportId. 51 | * @returns Promise which fulfills with void. 52 | */ 53 | export const main = async function(exportId: string = undefined): Promise { 54 | const qldbClient: QLDB = new QLDB({ }); 55 | try { 56 | if (exportId == undefined) { 57 | if (process.argv.length !== 3) { 58 | throw new ReferenceError("Missing ExportId argument in DescribeJournalExport."); 59 | } 60 | exportId = process.argv[2].toString(); 61 | } 62 | log(`Running describe export journal tutorial with ExportId: ${exportId}.`); 63 | return await describeJournalExport(LEDGER_NAME, exportId, qldbClient); 64 | } catch (e) { 65 | error(`Unable to describe an export: ${e}`); 66 | } 67 | } 68 | 69 | if (require.main === module) { 70 | main(); 71 | } 72 | -------------------------------------------------------------------------------- /src/DescribeLedger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { 20 | QLDB, 21 | DescribeLedgerRequest, 22 | DescribeLedgerResponse, 23 | } from "@aws-sdk/client-qldb"; 24 | 25 | import { LEDGER_NAME } from "./qldb/Constants"; 26 | import { error, log } from "./qldb/LogUtil"; 27 | 28 | /** 29 | * Describe a ledger. 30 | * @param ledgerName Name of the ledger to describe. 31 | * @param qldbClient The QLDB control plane client to use. 32 | * @returns Promise which fulfills with a DescribeLedgerResponse. 33 | */ 34 | export async function describeLedger(ledgerName: string, qldbClient: QLDB): Promise { 35 | const request: DescribeLedgerRequest = { 36 | Name: ledgerName 37 | }; 38 | const result: DescribeLedgerResponse = await qldbClient.describeLedger(request); 39 | log(`Success. Ledger description: ${JSON.stringify(result)}`); 40 | return result; 41 | } 42 | 43 | /** 44 | * Describe a QLDB ledger. 45 | * @returns Promise which fulfills with void. 46 | */ 47 | export const main = async function(): Promise { 48 | try { 49 | const qldbClient: QLDB = new QLDB({ }); 50 | return await describeLedger(LEDGER_NAME, qldbClient); 51 | } catch (e) { 52 | error(`Unable to describe a ledger: ${e}`); 53 | } 54 | } 55 | 56 | if (require.main === module) { 57 | main(); 58 | } 59 | -------------------------------------------------------------------------------- /src/ExportJournal.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { isInvalidParameterException } from "amazon-qldb-driver-nodejs"; 20 | import { 21 | STS, 22 | GetCallerIdentityRequest, 23 | GetCallerIdentityResponse, 24 | } from '@aws-sdk/client-sts'; 25 | import { 26 | S3, 27 | CreateBucketCommand, 28 | S3Client, 29 | HeadBucketCommand, 30 | CreateBucketRequest, 31 | HeadBucketRequest, 32 | HeadBucketCommandOutput, 33 | } from '@aws-sdk/client-s3'; 34 | import { 35 | IAM, 36 | AttachRolePolicyRequest, 37 | CreatePolicyRequest, 38 | CreatePolicyResponse, 39 | CreateRoleRequest, 40 | CreateRoleResponse, 41 | GetRoleRequest, 42 | } from "@aws-sdk/client-iam"; 43 | import { 44 | QLDB, 45 | ExportJournalToS3Request, 46 | ExportJournalToS3Response, 47 | JournalS3ExportDescription, 48 | S3EncryptionConfiguration, 49 | ExportJournalToS3Command, 50 | } from "@aws-sdk/client-qldb"; 51 | import { ServiceException } from "@smithy/smithy-client"; 52 | 53 | import { describeJournalExport } from './DescribeJournalExport'; 54 | import { 55 | JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX, 56 | LEDGER_NAME, 57 | } from './qldb/Constants'; 58 | import { error, log } from "./qldb/LogUtil"; 59 | import { sleep } from "./qldb/Util"; 60 | 61 | const EXPORT_ROLE_NAME = "QLDBTutorialJournalExportRole"; 62 | const ROLE_POLICY_NAME = "QLDBTutorialJournalExportRolePolicy"; 63 | const MAX_RETRY_COUNT = 40; 64 | const EXPORT_COMPLETION_POLL_PERIOD_MS = 10000; 65 | const EMPTY_ARRAY: any[] = [] 66 | const POLICY_TEMPLATE = { 67 | Version: "2012-10-17", 68 | Statement: EMPTY_ARRAY 69 | }; 70 | const ASSUME_ROLE_POLICY_TEMPLATE = { 71 | Effect: "Allow", 72 | Principal: { 73 | Service: ["qldb.amazonaws.com"] 74 | }, 75 | Action: ["sts:AssumeRole"] 76 | }; 77 | const EXPORT_ROLE_S3_STATEMENT_TEMPLATE = { 78 | Sid: "QLDBJournalExportPermission", 79 | Effect: "Allow", 80 | Action: ["s3:PutObjectAcl", "s3:PutObject"], 81 | Resource: "arn:aws:s3:::{bucket_name}/*" 82 | }; 83 | const EXPORT_ROLE_KMS_STATEMENT_TEMPLATE = { 84 | Sid: "QLDBJournalExportPermission", 85 | Effect: "Allow", 86 | Action: ["kms:GenerateDataKey"], 87 | Resource: "{kms_arn}" 88 | }; 89 | 90 | /** 91 | * Request QLDB to export the contents of the journal for the given time period and S3 configuration. Before calling 92 | * this function the S3 bucket should be created. 93 | * @param ledgerName Name of the ledger. 94 | * @param startTime Time from when the journal contents should be exported. 95 | * @param endTime Time until which the journal contents should be exported. 96 | * @param s3BucketName S3 bucket to write the data to. 97 | * @param s3Prefix S3 prefix to be prefixed to the files written. 98 | * @param encryptionConfig Encryption configuration for S3. 99 | * @param roleArn The IAM role ARN to be used when exporting the journal. 100 | * @param qldbClient The QLDB control plane client to use. 101 | * @returns Promise which fulfills with a ExportJournalToS3Response. 102 | */ 103 | async function createExport( 104 | ledgerName: string, 105 | startTime: Date, 106 | endTime: Date, 107 | s3BucketName: string, 108 | s3Prefix: string, 109 | encryptionConfig: S3EncryptionConfiguration, 110 | roleArn: string, 111 | qldbClient: QLDB 112 | ): Promise { 113 | log(`Let's create a journal export for ledger with name: ${ledgerName}`); 114 | const request: ExportJournalToS3Request = { 115 | Name: ledgerName, 116 | InclusiveStartTime: startTime, 117 | ExclusiveEndTime: endTime, 118 | S3ExportConfiguration: { 119 | Bucket: s3BucketName, 120 | Prefix: s3Prefix, 121 | EncryptionConfiguration: encryptionConfig 122 | }, 123 | RoleArn: roleArn 124 | }; 125 | const ExportJournalRequeset = new ExportJournalToS3Command(request); 126 | const result: ExportJournalToS3Response = await qldbClient.send(ExportJournalRequeset).catch((err: ServiceException ) => { 127 | if (isInvalidParameterException(err)) { 128 | error( 129 | "The eventually consistent behavior of the IAM service may cause this export to fail its first " + 130 | "attempts, please retry." 131 | ); 132 | } 133 | throw err; 134 | }); 135 | log("Requested QLDB to export contents of the journal."); 136 | return result; 137 | } 138 | 139 | /** 140 | * Send a request to the QLDB database to export a journal to the specified S3 bucket. 141 | * @param ledgerName Name of the ledger to create a journal export for. 142 | * @param bucketName S3 bucket to write the data to. 143 | * @param prefix S3 prefix to be suffixed to the files being written. 144 | * @param encryptionConfig Encryption for S3 files. 145 | * @param roleArn The IAM role ARN to be used when exporting the journal. 146 | * @param qldbClient The QLDB control plane client to use. 147 | * @returns Promise which fulfills with a ExportJournalToS3Response. 148 | */ 149 | export async function createExportAndWaitForCompletion( 150 | ledgerName: string, 151 | bucketName: string, 152 | prefix: string, 153 | encryptionConfig: S3EncryptionConfiguration, 154 | roleArn: string, 155 | qldbClient: QLDB 156 | ): Promise { 157 | if (roleArn === null) { 158 | roleArn = await createExportRole(EXPORT_ROLE_NAME, encryptionConfig.KmsKeyArn, ROLE_POLICY_NAME, bucketName); 159 | } 160 | try { 161 | const exclusiveEndTime: Date = new Date(); 162 | const inclusiveStartTime: Date = new Date(exclusiveEndTime); 163 | inclusiveStartTime.setMinutes(exclusiveEndTime.getMinutes() - 10); 164 | 165 | const result: ExportJournalToS3Response = await createExport( 166 | ledgerName, 167 | inclusiveStartTime, 168 | exclusiveEndTime, 169 | bucketName, 170 | prefix, 171 | encryptionConfig, 172 | roleArn, 173 | qldbClient 174 | ); 175 | await waitForExportToComplete(ledgerName, result.ExportId, qldbClient); 176 | log(`JournalS3Export for exportId ${result.ExportId} is completed.`); 177 | return result; 178 | } catch (e) { 179 | error("Unable to create an export!"); 180 | error(JSON.stringify(e)); 181 | throw e; 182 | } 183 | } 184 | 185 | /** 186 | * Create a new export rule and a new managed policy for the current AWS account. 187 | * @param roleName The name of the role to be created. 188 | * @param bucketName If key_arn is None, create a new ARN using the given bucket name. 189 | * @param keyArn The optional KMS Key ARN used to configure the role policy statement. 190 | * @param region The current AWS region, should be the same as the current ledger's region. 191 | * @param rolePolicyName Name of the role policy to be created. 192 | * @returns Promise which fulfills with the newly created role ARN as a string. 193 | */ 194 | async function createExportRole( 195 | roleName: string, 196 | keyArn: string, 197 | rolePolicyName: string, 198 | s3BucketName: string 199 | ): Promise { 200 | const iAmClient: IAM = new IAM({ }); 201 | log(`Trying to retrieve role with name: ${roleName}`); 202 | let newRoleArn: string = ""; 203 | try { 204 | const getRoleRequest: GetRoleRequest = { 205 | RoleName: roleName 206 | }; 207 | newRoleArn = (await iAmClient.getRole(getRoleRequest)).Role.Arn; 208 | log(`The role called ${roleName} already exists.`); 209 | } 210 | catch { 211 | log(`The role called ${roleName} does not exist. Creating it now.`); 212 | POLICY_TEMPLATE.Statement[0] = ASSUME_ROLE_POLICY_TEMPLATE; 213 | const createRoleRequest: CreateRoleRequest = { 214 | RoleName: roleName, 215 | AssumeRolePolicyDocument: JSON.stringify(POLICY_TEMPLATE) 216 | }; 217 | const role: CreateRoleResponse = await iAmClient.createRole(createRoleRequest); 218 | log(`Created a role called ${roleName}.`); 219 | 220 | newRoleArn = role.Role.Arn; 221 | POLICY_TEMPLATE.Statement[0] = EXPORT_ROLE_S3_STATEMENT_TEMPLATE; 222 | if (keyArn) { 223 | POLICY_TEMPLATE.Statement[1] = EXPORT_ROLE_KMS_STATEMENT_TEMPLATE; 224 | } 225 | let rolePolicy: string = JSON.stringify(POLICY_TEMPLATE).replace("{kms_arn}", keyArn); 226 | rolePolicy = rolePolicy.replace("{bucket_name}", s3BucketName); 227 | const createPolicyRequest: CreatePolicyRequest = { 228 | PolicyName: rolePolicyName, 229 | PolicyDocument: rolePolicy 230 | }; 231 | const createPolicyResult: CreatePolicyResponse = await iAmClient.createPolicy(createPolicyRequest); 232 | const attachRolePolicyRequest: AttachRolePolicyRequest = { 233 | RoleName: roleName, 234 | PolicyArn: createPolicyResult.Policy.Arn 235 | }; 236 | await iAmClient.attachRolePolicy(attachRolePolicyRequest); 237 | log(`Role ${roleName} created with ARN: ${newRoleArn} and policy: ${rolePolicy}.`); 238 | } 239 | return newRoleArn; 240 | } 241 | 242 | /** 243 | * Create a S3 bucket if one with the given bucket_name does not exists. 244 | * @param bucketName The name of the bucket to check. 245 | * @param s3Client The low-level S3 client. 246 | * @returns Promise which fulfills with void. 247 | */ 248 | export async function createS3BucketIfNotExists(bucketName: string, s3Client: S3Client): Promise { 249 | log(`Creating bucket: ${bucketName} if it doesnt exist`); 250 | const bucketExists = await doesBucketExist(bucketName, s3Client); 251 | if (!(bucketExists)) { 252 | log(`Bucket ${bucketName} does not exist. Creating it now.`); 253 | try { 254 | const request: CreateBucketRequest = { 255 | Bucket: bucketName 256 | }; 257 | await s3Client.send(new CreateBucketCommand(request)); 258 | log(`Bucket with name ${bucketName} created.`); 259 | } catch (e) { 260 | log(`Unable to create S3 bucket named ${bucketName}: ${e}`); 261 | throw e; 262 | } 263 | } 264 | } 265 | 266 | /** 267 | * Check whether a bucket exists in S3. 268 | * @param bucketName The name of the bucket to check. 269 | * @param s3Client The low-level S3 client. 270 | * @returns Promise which fulfills with whether the bucket exists or not. 271 | */ 272 | async function doesBucketExist(bucketName: string, s3Client: S3Client): Promise { 273 | const request: HeadBucketRequest = { 274 | Bucket: bucketName, 275 | }; 276 | let doesBucketExist: boolean = true; 277 | log(`Probing if ${bucketName} head exists`); 278 | const response: void | HeadBucketCommandOutput = await s3Client.send( new HeadBucketCommand(request)) 279 | .then((response: HeadBucketCommandOutput) => { 280 | log('S3 bucket head probe success'); 281 | log(JSON.stringify(response)); 282 | },(response: HeadBucketCommandOutput) => { 283 | log('S3 bucket head probe partially failed'); 284 | log(JSON.stringify(response)); 285 | doesBucketExist = false; 286 | }) 287 | .catch((err: ServiceException) => { 288 | log('S3 bucket head probe failed'); 289 | if (err.message === 'NotFound') { 290 | doesBucketExist = false; 291 | } 292 | }); 293 | log('returning doesBucketExist'); 294 | // doesBucketExist = response !== undefined ? false : true; 295 | return doesBucketExist; 296 | } 297 | 298 | /** 299 | * Use the default SSE S3 configuration for the journal export if a KMS Key ARN was not given. 300 | * @param kmsArn The Amazon Resource Name to encrypt. 301 | * @returns The encryption configuration for JournalS3Export. 302 | */ 303 | export function setUpS3EncryptionConfiguration(kmsArn: string): S3EncryptionConfiguration { 304 | if (kmsArn === null) { 305 | return { ObjectEncryptionType: 'SSE_S3' }; 306 | } else { 307 | return { ObjectEncryptionType: 'SSE_KMS', KmsKeyArn: kmsArn }; 308 | } 309 | } 310 | 311 | /** 312 | * Wait for the journal export to complete. 313 | * @param ledgerName Name of the ledger to wait on. 314 | * @param exportId The unique export ID of the journal export. 315 | * @param qldbClient The QLDB control plane client to use. 316 | * @returns Promise which fulfills with a JournalS3ExportDescription. 317 | * @throws Error: When the export fails to complete within a constant number of retries. 318 | */ 319 | async function waitForExportToComplete( 320 | ledgerName: string, 321 | exportId: string, 322 | qldbClient: QLDB 323 | ): Promise { 324 | log(`Waiting for JournalS3Export for ${exportId} to complete...`); 325 | let count: number = 0; 326 | while (count < MAX_RETRY_COUNT) { 327 | const exportDescription: JournalS3ExportDescription = 328 | (await describeJournalExport(ledgerName, exportId, qldbClient)).ExportDescription; 329 | if (exportDescription.Status === 'COMPLETED') { 330 | log("JournalS3Export completed."); 331 | return exportDescription; 332 | } 333 | log("JournalS3Export is still in progress. Please wait."); 334 | await sleep(EXPORT_COMPLETION_POLL_PERIOD_MS); 335 | count += 1; 336 | } 337 | throw new Error(`Journal Export did not complete for ${exportId}.`); 338 | } 339 | 340 | /** 341 | * Export a journal to S3. 342 | * 343 | * This code requires an S3 bucket. You can provide the name of an S3 bucket that 344 | * you wish to use via the arguments (args[0]). The code will check if the bucket 345 | * exists and create it if not. If you don't provide a bucket name, the code will 346 | * create a unique bucket for the purposes of this tutorial. 347 | * 348 | * Optionally, you can provide an IAM role ARN to use for the journal export via 349 | * the arguments (args[1]). Otherwise, the code will create and use a role named 350 | * "QLDBTutorialJournalExportRole". 351 | * 352 | * S3 Export Encryption: 353 | * Optionally, you can provide a KMS key ARN to use for S3-KMS encryption, via 354 | * the arguments (args[2]). The tutorial code will fail if you provide a KMS key 355 | * ARN that doesn't exist. 356 | * 357 | * If KMS Key ARN is not provided, the Tutorial Code will use 358 | * SSE-S3 for the S3 Export. 359 | * 360 | * If provided, the target KMS Key is expected to have at least the following 361 | * KeyPolicy: 362 | * ------------- 363 | * CustomCmkForQLDBExportEncryption: 364 | * Type: AWS::KMS::Key 365 | * Properties: 366 | * KeyUsage: ENCRYPT_DECRYPT 367 | * KeyPolicy: 368 | * Version: "2012-10-17" 369 | * Id: key-default-1 370 | * Statement: 371 | * - Sid: Grant Permissions for QLDB to use the key 372 | * Effect: Allow 373 | * Principal: 374 | * Service: qldb.qldb.amazonaws.com 375 | * Action: 376 | * - kms:Encrypt 377 | * - kms:GenerateDataKey 378 | * # In a key policy, you use "*" for the resource, which means "this CMK." 379 | * # A key policy applies only to the CMK it is attached to. 380 | * Resource: '*' 381 | * ------------- 382 | * Please see the KMS key policy developer guide here: 383 | * https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html 384 | * @returns Promise which fulfills with void. 385 | */ 386 | export const main = async function(bypassArgv: boolean = false): Promise { 387 | try { 388 | const s3Client: S3Client = new S3({ }); 389 | const sts: STS = new STS({ }); 390 | const qldbClient: QLDB = new QLDB({ }); 391 | 392 | let s3BucketName: string = null; 393 | let kmsArn: string = null; 394 | let roleArn: string = null; 395 | 396 | if (!bypassArgv && process.argv.length >= 3) { 397 | s3BucketName = process.argv[2].toString(); 398 | if (process.argv.length >= 4) { 399 | roleArn = process.argv[3].toString(); 400 | } 401 | if (process.argv.length === 5) { 402 | kmsArn = process.argv[4].toString(); 403 | } 404 | } else { 405 | const request: GetCallerIdentityRequest = {}; 406 | const identity: GetCallerIdentityResponse = await sts.getCallerIdentity(request); 407 | s3BucketName = `${JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX}-${identity.Account}`; 408 | } 409 | await createS3BucketIfNotExists(s3BucketName, s3Client); 410 | const s3EncryptionConfig: S3EncryptionConfiguration = setUpS3EncryptionConfiguration(kmsArn); 411 | const exportResult: ExportJournalToS3Response = await createExportAndWaitForCompletion( 412 | LEDGER_NAME, 413 | s3BucketName, 414 | LEDGER_NAME + "/", 415 | s3EncryptionConfig, 416 | roleArn, 417 | qldbClient 418 | ); 419 | return exportResult; 420 | } catch (e) { 421 | error(`Unable to create an export: ${e}`); 422 | } 423 | } 424 | 425 | if (require.main === module) { 426 | main(); 427 | } 428 | -------------------------------------------------------------------------------- /src/FindVehicles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { PERSON } from "./model/SampleData"; 24 | import { PERSON_TABLE_NAME } from "./qldb/Constants"; 25 | import { error, log } from "./qldb/LogUtil"; 26 | import { getDocumentId } from "./qldb/Util"; 27 | import { prettyPrintResultList } from "./ScanTable"; 28 | 29 | /** 30 | * Query 'Vehicle' and 'VehicleRegistration' tables using a unique document ID in one transaction. 31 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 32 | * @param govId The owner's government ID. 33 | * @returns Promise which fulfills with void. 34 | */ 35 | async function findVehiclesForOwner(txn: TransactionExecutor, govId: string): Promise { 36 | const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", govId); 37 | const query: string = "SELECT Vehicle FROM Vehicle INNER JOIN VehicleRegistration AS r " + 38 | "ON Vehicle.VIN = r.VIN WHERE r.Owners.PrimaryOwner.PersonId = ?"; 39 | 40 | return await txn.execute(query, documentId).then((result: Result) => { 41 | const resultList: dom.Value[] = result.getResultList(); 42 | log(`List of vehicles for owner with GovId: ${govId}`); 43 | prettyPrintResultList(resultList); 44 | return resultList; 45 | }); 46 | } 47 | 48 | /** 49 | * Find all vehicles registered under a person. 50 | * @returns Promise which fulfills with void. 51 | */ 52 | export const main = async function(): Promise { 53 | try { 54 | const qldbDriver: QldbDriver = getQldbDriver(); 55 | return await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 56 | return await findVehiclesForOwner(txn, PERSON[0].GovId); 57 | }); 58 | } catch (e) { 59 | error(`Error getting vehicles for owner: ${e}`); 60 | } 61 | } 62 | 63 | if (require.main === module) { 64 | main(); 65 | } 66 | -------------------------------------------------------------------------------- /src/GetBlock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { QLDB, GetBlockRequest, GetBlockResponse, ValueHolder, GetDigestCommand, GetDigestResponse, GetDigestCommandInput } from "@aws-sdk/client-qldb"; 21 | import { dom, toBase64 } from "ion-js"; 22 | 23 | import { getQldbDriver } from "./ConnectToLedger"; 24 | import { lookupRegistrationForVin } from "./GetRevision"; 25 | import { VEHICLE_REGISTRATION } from "./model/SampleData"; 26 | import { blockAddressToValueHolder } from './qldb/BlockAddress'; 27 | import { LEDGER_NAME } from './qldb/Constants'; 28 | import { error, log } from "./qldb/LogUtil"; 29 | import { blockResponseToString, valueHolderToString } from "./qldb/Util"; 30 | import { flipRandomBit, parseBlock, verifyDocument } from "./qldb/Verifier"; 31 | 32 | /** 33 | * Get the block of a ledger's journal. 34 | * @param ledgerName Name of the ledger to operate on. 35 | * @param blockAddress The location of the block to request. 36 | * @param qldbClient The QLDB control plane client to use. 37 | * @returns Promise which fulfills with a GetBlockResponse. 38 | */ 39 | async function getBlock(ledgerName: string, blockAddress: ValueHolder, qldbClient: QLDB): Promise { 40 | log( 41 | `Let's get the block for block address \n${valueHolderToString(blockAddress)} \nof the ledger ` + 42 | `named ${ledgerName}.` 43 | ); 44 | const request: GetBlockRequest = { 45 | Name: ledgerName, 46 | BlockAddress: blockAddress 47 | }; 48 | const result: GetBlockResponse = await qldbClient.getBlock(request); 49 | log(`Success. GetBlock: \n${blockResponseToString(result)}.`); 50 | return result; 51 | } 52 | 53 | /** 54 | * Get the block of a ledger's journal. Also returns a proof of the block for verification. 55 | * @param ledgerName Name of the ledger to operate on. 56 | * @param blockAddress The location of the block to request. 57 | * @param digestTipAddress The location of the digest tip. 58 | * @param qldbClient The QLDB control plane client to use. 59 | * @returns Promise which fulfills with a GetBlockResponse. 60 | */ 61 | async function getBlockWithProof( 62 | ledgerName: string, 63 | blockAddress: ValueHolder, 64 | digestTipAddress: ValueHolder, 65 | qldbClient: QLDB 66 | ): Promise { 67 | log( 68 | `Let's get the block for block address \n${valueHolderToString(blockAddress)}, \ndigest tip address: 69 | ${valueHolderToString(digestTipAddress)} \nof the ledger named ${ledgerName}.` 70 | ); 71 | const request: GetBlockRequest = { 72 | Name: ledgerName, 73 | BlockAddress: blockAddress, 74 | DigestTipAddress: digestTipAddress 75 | }; 76 | const result: GetBlockResponse = await qldbClient.getBlock(request); 77 | log(`Success. GetBlock: \n${blockResponseToString(result)}.`); 78 | return result; 79 | } 80 | 81 | /** 82 | * Verify block by validating the proof returned in the getBlock response. 83 | * @param ledgerName The ledger to get the digest from. 84 | * @param blockAddress The address of the block to verify. 85 | * @param qldbClient The QLDB control plane client to use. 86 | * @returns Promise which fulfills with void. 87 | * @throws Error: When verification fails. 88 | */ 89 | export async function verifyBlock(ledgerName: string, blockAddress: ValueHolder, qldbClient: QLDB): Promise { 90 | log(`Let's verify blocks for ledger with name = ${ledgerName}.`); 91 | try { 92 | log("First, let's get a digest."); 93 | const digestCommandInput: GetDigestCommandInput = { Name: ledgerName }; 94 | const digestResult: GetDigestResponse = await qldbClient.send( new GetDigestCommand(digestCommandInput)); 95 | const digestBytes: GetDigestResponse["Digest"] = digestResult.Digest; 96 | const digestTipAddress: ValueHolder = digestResult.DigestTipAddress; 97 | log( 98 | `Got a ledger digest. Digest end address = \n${valueHolderToString(digestTipAddress)}, ` + 99 | `\ndigest = ${toBase64( digestBytes)}.` 100 | ); 101 | 102 | const getBlockResult: GetBlockResponse = await getBlockWithProof( 103 | ledgerName, 104 | blockAddress, 105 | digestTipAddress, 106 | qldbClient 107 | ); 108 | const block: ValueHolder = getBlockResult.Block; 109 | const blockHash: Uint8Array = parseBlock(block); 110 | 111 | let verified: boolean = verifyDocument(blockHash, digestBytes, getBlockResult.Proof); 112 | if (!verified) { 113 | throw new Error("Block is not verified!"); 114 | } else { 115 | log("Success! The block is verified!"); 116 | } 117 | 118 | const alteredDigest: Uint8Array = flipRandomBit(digestBytes); 119 | log( 120 | `Let's try flipping one bit in the digest and assert that the block is NOT verified. 121 | The altered digest is: ${toBase64(alteredDigest)}.` 122 | ); 123 | verified = verifyDocument(blockHash, alteredDigest, getBlockResult.Proof); 124 | if (verified) { 125 | throw new Error("Expected block to not be verified against altered digest."); 126 | } else { 127 | log("Success! As expected flipping a bit in the digest causes verification to fail."); 128 | } 129 | 130 | const alteredBlockHash: Uint8Array = flipRandomBit(blockHash); 131 | log( 132 | `Let's try flipping one bit in the block's hash and assert that the block is NOT verified. 133 | The altered block hash is: ${toBase64(alteredBlockHash)}.` 134 | ); 135 | verified = verifyDocument(alteredBlockHash, digestBytes, getBlockResult.Proof); 136 | if (verified) { 137 | throw new Error("Expected altered block hash to not be verified against digest."); 138 | } else { 139 | log("Success! As expected flipping a bit in the block hash causes verification to fail."); 140 | } 141 | } catch (e) { 142 | log(`Failed to verify blocks in the ledger with name = ${ledgerName}.`); 143 | throw e; 144 | } 145 | } 146 | 147 | /** 148 | * Get a journal block from a QLDB ledger. 149 | * After getting the block, we get the digest of the ledger and validate the proof returned in the getBlock response. 150 | * @returns Promise which fulfills with void. 151 | */ 152 | export const main = async function(): Promise { 153 | try { 154 | const qldbClient: QLDB = new QLDB({ }); 155 | const qldbDriver: QldbDriver = getQldbDriver(); 156 | 157 | const registration = VEHICLE_REGISTRATION[1]; 158 | const vin: string = registration.VIN; 159 | 160 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 161 | const registrations : dom.Value[] = await lookupRegistrationForVin(txn, vin); 162 | for (const registration of registrations) { 163 | const blockAddress: ValueHolder = blockAddressToValueHolder(registration); 164 | await verifyBlock(LEDGER_NAME, blockAddress, qldbClient); 165 | } 166 | }); 167 | } catch (e) { 168 | error(`Unable to query vehicle registration by Vin: ${e}`); 169 | } 170 | } 171 | 172 | if (require.main === module) { 173 | main(); 174 | } 175 | -------------------------------------------------------------------------------- /src/GetDigest.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { 20 | QLDB, 21 | GetDigestRequest, 22 | GetDigestResponse, 23 | } from "@aws-sdk/client-qldb"; 24 | 25 | import { LEDGER_NAME } from "./qldb/Constants"; 26 | import { error, log } from "./qldb/LogUtil"; 27 | import { digestResponseToString } from "./qldb/Util"; 28 | 29 | /** 30 | * Get the digest of a ledger's journal. 31 | * @param ledgerName Name of the ledger to operate on. 32 | * @param qldbClient The QLDB control plane client to use. 33 | * @returns Promise which fulfills with a GetDigestResponse. 34 | */ 35 | export async function getDigestResult(ledgerName: string, qldbClient: QLDB): Promise { 36 | const request: GetDigestRequest = { 37 | Name: ledgerName 38 | }; 39 | const result: GetDigestResponse = await qldbClient.getDigest(request); 40 | return result; 41 | } 42 | 43 | /** 44 | * This is an example for retrieving the digest of a particular ledger. 45 | * @returns Promise which fulfills with void. 46 | */ 47 | export const main = async function(): Promise { 48 | try { 49 | const qldbClient: QLDB = new QLDB({ }); 50 | log(`Retrieving the current digest for ledger: ${LEDGER_NAME}.`); 51 | const digest: GetDigestResponse = await getDigestResult(LEDGER_NAME, qldbClient); 52 | log(`Success. Ledger digest: \n${digestResponseToString(digest)}.`); 53 | return digest; 54 | } catch (e) { 55 | error(`Unable to get a ledger digest: ${e}`); 56 | } 57 | } 58 | 59 | if (require.main === module) { 60 | main(); 61 | } 62 | -------------------------------------------------------------------------------- /src/GetRevision.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { 21 | QLDB, 22 | GetDigestResponse, 23 | GetRevisionRequest, 24 | GetRevisionResponse, 25 | ValueHolder, 26 | } from "@aws-sdk/client-qldb"; 27 | import { dom, toBase64 } from "ion-js"; 28 | 29 | import { getQldbDriver } from "./ConnectToLedger"; 30 | import { getDigestResult } from './GetDigest'; 31 | import { VEHICLE_REGISTRATION } from "./model/SampleData" 32 | import { blockAddressToValueHolder, getMetadataId } from './qldb/BlockAddress'; 33 | import { LEDGER_NAME } from './qldb/Constants'; 34 | import { error, log } from "./qldb/LogUtil"; 35 | import { getBlobValue, valueHolderToString } from "./qldb/Util"; 36 | import { flipRandomBit, verifyDocument } from "./qldb/Verifier"; 37 | 38 | /** 39 | * Get the revision data object for a specified document ID and block address. 40 | * Also returns a proof of the specified revision for verification. 41 | * @param ledgerName Name of the ledger containing the document to query. 42 | * @param documentId Unique ID for the document to be verified, contained in the committed view of the document. 43 | * @param blockAddress The location of the block to request. 44 | * @param digestTipAddress The latest block location covered by the digest. 45 | * @param qldbClient The QLDB control plane client to use. 46 | * @returns Promise which fulfills with a GetRevisionResponse. 47 | */ 48 | async function getRevision( 49 | ledgerName: string, 50 | documentId: string, 51 | blockAddress: ValueHolder, 52 | digestTipAddress: ValueHolder, 53 | qldbClient: QLDB 54 | ): Promise { 55 | const request: GetRevisionRequest = { 56 | Name: ledgerName, 57 | BlockAddress: blockAddress, 58 | DocumentId: documentId, 59 | DigestTipAddress: digestTipAddress 60 | }; 61 | const result: GetRevisionResponse = await qldbClient.getRevision(request); 62 | return result; 63 | } 64 | 65 | /** 66 | * Query the table metadata for a particular vehicle for verification. 67 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 68 | * @param vin VIN to query the table metadata of a specific registration with. 69 | * @returns Promise which fulfills with a list of Ion values that contains the results of the query. 70 | */ 71 | export async function lookupRegistrationForVin(txn: TransactionExecutor, vin: string): Promise { 72 | log(`Querying the 'VehicleRegistration' table for VIN: ${vin}...`); 73 | let resultList: dom.Value[]; 74 | const query: string = "SELECT blockAddress, metadata.id FROM _ql_committed_VehicleRegistration WHERE data.VIN = ?"; 75 | 76 | await txn.execute(query, vin).then(function(result) { 77 | resultList = result.getResultList(); 78 | }); 79 | return resultList; 80 | } 81 | 82 | /** 83 | * Verify each version of the registration for the given VIN. 84 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 85 | * @param ledgerName The ledger to get the digest from. 86 | * @param vin VIN to query the revision history of a specific registration with. 87 | * @param qldbClient The QLDB control plane client to use. 88 | * @returns Promise which fulfills with void. 89 | * @throws Error: When verification fails. 90 | */ 91 | export async function verifyRegistration( 92 | txn: TransactionExecutor, 93 | ledgerName: string, 94 | vin: string, 95 | qldbClient: QLDB 96 | ): Promise { 97 | log(`Let's verify the registration with VIN = ${vin}, in ledger = ${ledgerName}.`); 98 | const digest: GetDigestResponse = await getDigestResult(ledgerName, qldbClient); 99 | const digestBytes: GetDigestResponse["Digest"] = digest.Digest; 100 | const digestTipAddress: ValueHolder = digest.DigestTipAddress; 101 | 102 | log( 103 | `Got a ledger digest: digest tip address = \n${valueHolderToString(digestTipAddress)}, 104 | digest = \n${toBase64( digestBytes)}.` 105 | ); 106 | log(`Querying the registration with VIN = ${vin} to verify each version of the registration...`); 107 | const resultList: dom.Value[] = await lookupRegistrationForVin(txn, vin); 108 | log("Getting a proof for the document."); 109 | 110 | for (const result of resultList) { 111 | const blockAddress: ValueHolder = blockAddressToValueHolder(result); 112 | const documentId: string = getMetadataId(result); 113 | 114 | const revisionResponse: GetRevisionResponse = await getRevision( 115 | ledgerName, 116 | documentId, 117 | blockAddress, 118 | digestTipAddress, 119 | qldbClient 120 | ); 121 | 122 | const revision: dom.Value = dom.load(revisionResponse.Revision.IonText); 123 | const documentHash: Uint8Array = getBlobValue(revision, "hash"); 124 | const proof: ValueHolder = revisionResponse.Proof; 125 | log(`Got back a proof: ${valueHolderToString(proof)}.`); 126 | 127 | let verified: boolean = verifyDocument(documentHash, digestBytes, proof); 128 | if (!verified) { 129 | throw new Error("Document revision is not verified."); 130 | } else { 131 | log("Success! The document is verified."); 132 | } 133 | const alteredDocumentHash: Uint8Array = flipRandomBit(documentHash); 134 | 135 | log( 136 | `Flipping one bit in the document's hash and assert that the document is NOT verified. 137 | The altered document hash is: ${toBase64(alteredDocumentHash)}` 138 | ); 139 | verified = verifyDocument(alteredDocumentHash, digestBytes, proof); 140 | 141 | if (verified) { 142 | throw new Error("Expected altered document hash to not be verified against digest."); 143 | } else { 144 | log("Success! As expected flipping a bit in the document hash causes verification to fail."); 145 | } 146 | log(`Finished verifying the registration with VIN = ${vin} in ledger = ${ledgerName}.`); 147 | } 148 | } 149 | 150 | /** 151 | * Verify the integrity of a document revision in a QLDB ledger. 152 | * @returns Promise which fulfills with void. 153 | */ 154 | export const main = async function(): Promise { 155 | try { 156 | const qldbClient: QLDB = new QLDB({ }); 157 | const qldbDriver: QldbDriver = getQldbDriver(); 158 | 159 | const registration = VEHICLE_REGISTRATION[0]; 160 | const vin: string = registration.VIN; 161 | 162 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 163 | await verifyRegistration(txn, LEDGER_NAME, vin, qldbClient); 164 | }); 165 | } catch (e) { 166 | error(`Unable to verify revision: ${e}`); 167 | } 168 | } 169 | 170 | if (require.main === module) { 171 | main(); 172 | } 173 | -------------------------------------------------------------------------------- /src/InsertDocument.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { DRIVERS_LICENSE, PERSON, VEHICLE, VEHICLE_REGISTRATION } from "./model/SampleData"; 24 | import { 25 | DRIVERS_LICENSE_TABLE_NAME, 26 | PERSON_TABLE_NAME, 27 | VEHICLE_REGISTRATION_TABLE_NAME, 28 | VEHICLE_TABLE_NAME 29 | } from "./qldb/Constants"; 30 | import { error, log } from "./qldb/LogUtil"; 31 | 32 | /** 33 | * Insert the given list of documents into a table in a single transaction. 34 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 35 | * @param tableName Name of the table to insert documents into. 36 | * @param documents List of documents to insert. 37 | * @returns Promise which fulfills with a {@linkcode Result} object. 38 | */ 39 | export async function insertDocument( 40 | txn: TransactionExecutor, 41 | tableName: string, 42 | documents: object[] 43 | ): Promise { 44 | const statement: string = `INSERT INTO ${tableName} ?`; 45 | const result: Result = await txn.execute(statement, documents); 46 | return result; 47 | } 48 | 49 | /** 50 | * Handle the insertion of documents and updating PersonIds all in a single transaction. 51 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 52 | * @returns Promise which fulfills with void. 53 | */ 54 | async function updateAndInsertDocuments(txn: TransactionExecutor): Promise { 55 | log("Inserting multiple documents into the 'Person' table..."); 56 | const documentIds: Result = await insertDocument(txn, PERSON_TABLE_NAME, PERSON); 57 | 58 | const listOfDocumentIds: dom.Value[] = documentIds.getResultList(); 59 | log("Updating PersonIds for 'DriversLicense' and PrimaryOwner for 'VehicleRegistration'..."); 60 | updatePersonId(listOfDocumentIds); 61 | 62 | log("Inserting multiple documents into the remaining tables..."); 63 | await Promise.all([ 64 | insertDocument(txn, DRIVERS_LICENSE_TABLE_NAME, DRIVERS_LICENSE), 65 | insertDocument(txn, VEHICLE_REGISTRATION_TABLE_NAME, VEHICLE_REGISTRATION), 66 | insertDocument(txn, VEHICLE_TABLE_NAME, VEHICLE) 67 | ]); 68 | } 69 | 70 | /** 71 | * Update the PersonId value for DriversLicense records and the PrimaryOwner value for VehicleRegistration records. 72 | * @param documentIds List of document IDs. 73 | */ 74 | export function updatePersonId(documentIds: dom.Value[]): void { 75 | documentIds.forEach((value: dom.Value, i: number) => { 76 | const documentId: string = value.get("documentId").stringValue(); 77 | DRIVERS_LICENSE[i].PersonId = documentId; 78 | VEHICLE_REGISTRATION[i].Owners.PrimaryOwner.PersonId = documentId; 79 | }); 80 | } 81 | 82 | /** 83 | * Insert documents into a table in a QLDB ledger. 84 | * @returns Promise which fulfills with void. 85 | */ 86 | export const main = async function(): Promise { 87 | try { 88 | const qldbDriver: QldbDriver = getQldbDriver(); 89 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 90 | await updateAndInsertDocuments(txn); 91 | }); 92 | } catch (e) { 93 | error(`Unable to insert documents: ${e}`); 94 | } 95 | } 96 | 97 | if (require.main === module) { 98 | main(); 99 | } 100 | -------------------------------------------------------------------------------- /src/InsertIonTypes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { AssertionError } from "assert"; 21 | import { dom, IonType, IonTypes } from "ion-js"; 22 | 23 | import { insertDocument } from "./InsertDocument"; 24 | import { getQldbDriver } from "./ConnectToLedger"; 25 | import { createTable } from "./CreateTable"; 26 | import { error, log } from "./qldb/LogUtil"; 27 | 28 | const TABLE_NAME: string = "IonTypes"; 29 | 30 | /** 31 | * Delete a table. 32 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 33 | * @param tableName Name of the table to delete. 34 | * @returns Promise which fulfills with void. 35 | */ 36 | export async function deleteTable(txn: TransactionExecutor, tableName: string): Promise { 37 | log(`Deleting ${tableName} table...`); 38 | const statement: string = `DROP TABLE ${tableName}`; 39 | await txn.execute(statement); 40 | log(`${tableName} table successfully deleted.`); 41 | } 42 | 43 | /** 44 | * Update a document's Name value in QLDB. Then, query the value of the Name key and verify the expected Ion type was 45 | * saved. 46 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 47 | * @param parameter The IonValue to set the document's Name value to. 48 | * @param ionType The Ion type that the Name value should be. 49 | * @returns Promise which fulfills with void. 50 | */ 51 | async function updateRecordAndVerifyType( 52 | txn: TransactionExecutor, 53 | parameter: any, 54 | ionType: IonType 55 | ): Promise { 56 | const updateStatement: string = `UPDATE ${TABLE_NAME} SET Name = ?`; 57 | await txn.execute(updateStatement, parameter); 58 | log("Updated record."); 59 | 60 | const searchStatement: string = `SELECT VALUE Name FROM ${TABLE_NAME}`; 61 | const result: Result = await txn.execute(searchStatement); 62 | 63 | const results: dom.Value[] = result.getResultList(); 64 | 65 | if (0 === results.length) { 66 | throw new AssertionError({ 67 | message: "Did not find any values for the Name key." 68 | }); 69 | } 70 | 71 | results.forEach((value: dom.Value) => { 72 | if (value.getType().binaryTypeId !== ionType.binaryTypeId) { 73 | throw new AssertionError({ 74 | message: `The queried value type, ${value.getType().name}, does not match expected type, ${ionType.name}.` 75 | }); 76 | } 77 | }); 78 | 79 | log(`Successfully verified value is of type ${ionType.name}.`); 80 | } 81 | 82 | /** 83 | * Insert all the supported Ion types into a table and verify that they are stored and can be retrieved properly, 84 | * retaining their original properties. 85 | * @returns Promise which fulfills with void. 86 | */ 87 | export const main = async function(): Promise { 88 | try { 89 | const qldbDriver: QldbDriver = getQldbDriver(); 90 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 91 | await createTable(txn, TABLE_NAME); 92 | await insertDocument(txn, TABLE_NAME, [{ "Name": "val" }]); 93 | await updateRecordAndVerifyType(txn, dom.load("null"), IonTypes.NULL); 94 | await updateRecordAndVerifyType(txn, true, IonTypes.BOOL); 95 | await updateRecordAndVerifyType(txn, 1, IonTypes.INT); 96 | await updateRecordAndVerifyType(txn, 3.2, IonTypes.FLOAT); 97 | await updateRecordAndVerifyType(txn, dom.load("5.5"), IonTypes.DECIMAL); 98 | await updateRecordAndVerifyType(txn, dom.load("2020-02-02"), IonTypes.TIMESTAMP); 99 | await updateRecordAndVerifyType(txn, dom.load("abc123"), IonTypes.SYMBOL); 100 | await updateRecordAndVerifyType(txn, dom.load("\"string\""), IonTypes.STRING); 101 | await updateRecordAndVerifyType(txn, dom.load("{{ \"clob\" }}"), IonTypes.CLOB); 102 | await updateRecordAndVerifyType(txn, dom.load("{{ blob }}"), IonTypes.BLOB); 103 | await updateRecordAndVerifyType(txn, dom.load("(1 2 3)"), IonTypes.SEXP); 104 | await updateRecordAndVerifyType(txn, dom.load("[1, 2, 3]"), IonTypes.LIST); 105 | await updateRecordAndVerifyType(txn, dom.load("{brand: ford}"), IonTypes.STRUCT); 106 | await deleteTable(txn, TABLE_NAME); 107 | }); 108 | } catch (e) { 109 | error(`Error updating and validating Ion types: ${e}`); 110 | } 111 | } 112 | 113 | if (require.main === module) { 114 | main(); 115 | } 116 | -------------------------------------------------------------------------------- /src/ListJournalExports.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { 20 | QLDB, 21 | JournalS3ExportDescription, 22 | ListJournalS3ExportsForLedgerRequest, 23 | ListJournalS3ExportsForLedgerResponse, 24 | ListJournalS3ExportsRequest, 25 | ListJournalS3ExportsResponse 26 | } from "@aws-sdk/client-qldb"; 27 | 28 | import { LEDGER_NAME } from './qldb/Constants'; 29 | import { error, log } from "./qldb/LogUtil"; 30 | 31 | const MAX_RESULTS = 2; 32 | 33 | /** 34 | * List all journal exports. 35 | * @param qldbClient The QLDB control plane client to use. 36 | */ 37 | async function listAllJournalExports(qldbClient: QLDB): Promise { 38 | const exportList: JournalS3ExportDescription[] = []; 39 | let nextToken: string = null; 40 | do { 41 | const request: ListJournalS3ExportsRequest = { 42 | MaxResults: MAX_RESULTS, 43 | NextToken: nextToken 44 | }; 45 | const result: ListJournalS3ExportsResponse = await qldbClient.listJournalS3Exports(request); 46 | exportList.push(...result.JournalS3Exports); 47 | nextToken = result.NextToken; 48 | } while (nextToken != null); 49 | log(`Success. List of journal exports: ${JSON.stringify(exportList)}`); 50 | } 51 | 52 | /** 53 | * List all journal exports for the given ledger. 54 | * @param ledgerName List all journal exports for the given ledger. 55 | */ 56 | async function listJournalExports(ledgerName: string): Promise { 57 | log(`Listing journal exports for ledger: ${ledgerName}.`); 58 | 59 | const qldbClient: QLDB = new QLDB({ }); 60 | const exportDescriptions: JournalS3ExportDescription[] = []; 61 | let nextToken: string = null; 62 | do { 63 | const request: ListJournalS3ExportsForLedgerRequest = { 64 | Name: ledgerName, 65 | MaxResults: MAX_RESULTS, 66 | NextToken: nextToken 67 | }; 68 | const result: ListJournalS3ExportsForLedgerResponse = 69 | await qldbClient.listJournalS3ExportsForLedger(request); 70 | exportDescriptions.push(...result.JournalS3Exports); 71 | nextToken = result.NextToken; 72 | } while (nextToken != null); 73 | 74 | log(`Success. List of journal exports: ${JSON.stringify(exportDescriptions)}`); 75 | return exportDescriptions; 76 | } 77 | 78 | /** 79 | * List the journal exports of a given QLDB ledger. 80 | * @returns Promise which fulfills with void. 81 | */ 82 | export const main = async function(): Promise { 83 | try { 84 | return await listJournalExports(LEDGER_NAME); 85 | } catch (e) { 86 | error(`Unable to list exports: ${e}`); 87 | } 88 | } 89 | 90 | if (require.main === module) { 91 | main(); 92 | } 93 | -------------------------------------------------------------------------------- /src/ListLedgers.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { 20 | QLDB, 21 | LedgerSummary, 22 | ListLedgersRequest, 23 | ListLedgersResponse, 24 | } from "@aws-sdk/client-qldb"; 25 | 26 | import { error, log } from "./qldb/LogUtil"; 27 | 28 | /** 29 | * List all ledgers. 30 | * @param qldbClient The QLDB control plane client to use. 31 | * @returns Promise which fulfills with a LedgerSummary array. 32 | */ 33 | export async function listLedgers(qldbClient: QLDB): Promise { 34 | const ledgerSummaries: LedgerSummary[] = []; 35 | let nextToken: string = null; 36 | do { 37 | const request: ListLedgersRequest = { 38 | NextToken: nextToken 39 | }; 40 | const result: ListLedgersResponse = await qldbClient.listLedgers(request); 41 | ledgerSummaries.push(...result.Ledgers); 42 | nextToken = result.NextToken; 43 | } while (nextToken != null); 44 | return ledgerSummaries; 45 | } 46 | 47 | /** 48 | * List all QLDB ledgers in a given account. 49 | * @returns Promise which fulfills with void. 50 | */ 51 | export const main = async function(): Promise { 52 | try { 53 | const qldbClient: QLDB = new QLDB({ }); 54 | log("Retrieving all the ledgers..."); 55 | const result: LedgerSummary[] = await listLedgers(qldbClient); 56 | log(`Success. List of ledgers: ${JSON.stringify(result)}`); 57 | } catch (e) { 58 | error(`Unable to list ledgers: ${e}`); 59 | } 60 | } 61 | 62 | if (require.main === module) { 63 | main(); 64 | } 65 | -------------------------------------------------------------------------------- /src/QueryHistory.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { VEHICLE_REGISTRATION } from "./model/SampleData"; 24 | import { VEHICLE_REGISTRATION_TABLE_NAME } from "./qldb/Constants"; 25 | import { prettyPrintResultList } from "./ScanTable"; 26 | import { error, log } from "./qldb/LogUtil"; 27 | import { getDocumentId } from "./qldb/Util"; 28 | 29 | /** 30 | * Find previous primary owners for the given VIN in a single transaction. 31 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 32 | * @param vin The VIN to find previous primary owners for. 33 | * @returns Promise which fulfills with void. 34 | */ 35 | async function previousPrimaryOwners(txn: TransactionExecutor, vin: string): Promise { 36 | const documentId: string = await getDocumentId(txn, VEHICLE_REGISTRATION_TABLE_NAME, "VIN", vin); 37 | const todaysDate: Date = new Date(); 38 | // set todaysDate back one minute to ensure end time is in the past 39 | // by the time the request reaches our backend 40 | todaysDate.setMinutes(todaysDate.getMinutes() - 1); 41 | const threeMonthsAgo: Date = new Date(todaysDate); 42 | threeMonthsAgo.setMonth(todaysDate.getMonth() - 3); 43 | 44 | const query: string = 45 | `SELECT data.Owners.PrimaryOwner, metadata.version FROM history ` + 46 | `(${VEHICLE_REGISTRATION_TABLE_NAME}, \`${threeMonthsAgo.toISOString()}\`, \`${todaysDate.toISOString()}\`) ` + 47 | `AS h WHERE h.metadata.id = ?`; 48 | 49 | return await txn.execute(query, documentId).then((result: Result) => { 50 | log(`Querying the 'VehicleRegistration' table's history using VIN: ${vin}.`); 51 | const resultList: dom.Value[] = result.getResultList(); 52 | prettyPrintResultList(resultList); 53 | return resultList; 54 | }); 55 | } 56 | 57 | /** 58 | * Query a table's history for a particular set of documents. 59 | * @returns Promise which fulfills with void. 60 | */ 61 | export const main = async function(): Promise { 62 | try { 63 | const qldbDriver: QldbDriver = getQldbDriver(); 64 | const vin: string = VEHICLE_REGISTRATION[0].VIN; 65 | return await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 66 | return await previousPrimaryOwners(txn, vin); 67 | }); 68 | } catch (e) { 69 | error(`Unable to query history to find previous owners: ${e}`); 70 | } 71 | } 72 | 73 | if (require.main === module) { 74 | main(); 75 | } 76 | -------------------------------------------------------------------------------- /src/RedactRevision.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { PERSON, VEHICLE } from "./model/SampleData"; 24 | import { PERSON_TABLE_NAME, VEHICLE_REGISTRATION_TABLE_NAME } from "./qldb/Constants"; 25 | import { error, log } from "./qldb/LogUtil"; 26 | import { getDocumentId, sleep } from "./qldb/Util"; 27 | import { validateAndUpdateRegistration } from "./TransferVehicleOwnership"; 28 | import { prettyPrintResultList } from "./ScanTable"; 29 | 30 | /** 31 | * Query the information schema to get the tableId for a table with the provided table name. 32 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 33 | * @param tableName The table name to find a tableId for. 34 | * @return Promise which fulfills with the tableId string. 35 | */ 36 | export async function getTableId(txn: TransactionExecutor, tableName: string): Promise { 37 | log(`Getting the tableId for table with name: ${tableName}`); 38 | const query: string = "SELECT VALUE tableId FROM information_schema.user_tables WHERE name = ?"; 39 | 40 | let tableId: string; 41 | await txn.execute(query, tableName).then((result: Result) => { 42 | const resultList: dom.Value[] = result.getResultList(); 43 | if (resultList.length == 0) { 44 | throw new Error(`Unable to find table with name: ${tableName}.`); 45 | } 46 | tableId = resultList[0].stringValue(); 47 | }); 48 | return tableId; 49 | } 50 | 51 | /** 52 | * Find the previous vehicle registration with the provided Primary Owner. 53 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 54 | * @param registrationDocumentId The unique ID of the vehicle registration document in the VehicleRegistration table. 55 | * @param ownerDocumentId The unique ID of the primary owner for this vehicle registration. 56 | * @return Promise which fulfills with an Ion value containing the historic registration. 57 | */ 58 | export async function getHistoricRegistrationByOwner( 59 | txn: TransactionExecutor, 60 | registrationDocumentId: string, 61 | ownerDocumentId: string 62 | ): Promise { 63 | log(`Querying the 'VehicleRegistration' table's history for a registration with documentId: ${registrationDocumentId} and owner: ${ownerDocumentId}`); 64 | const query: string = 65 | `SELECT * FROM history(${VEHICLE_REGISTRATION_TABLE_NAME}) AS h ` + 66 | `WHERE h.metadata.id = '${registrationDocumentId}' ` + 67 | `AND h.data.Owners.PrimaryOwner.PersonId = '${ownerDocumentId}'`; 68 | 69 | let revision: dom.Value; 70 | await txn.execute(query).then((result: Result) => { 71 | const resultList: dom.Value[] = result.getResultList(); 72 | if (resultList.length == 0) { 73 | throw new Error(`Unable to find a historic registration with documentId: ${registrationDocumentId} and owner: ${ownerDocumentId}.`); 74 | } else if (resultList.length > 1) { 75 | throw new Error(`Found more than 1 historic registrations with documentId: ${registrationDocumentId} and owner: ${ownerDocumentId}.`) 76 | } 77 | prettyPrintResultList(resultList); 78 | revision = resultList[0]; 79 | }); 80 | return revision; 81 | } 82 | 83 | /** 84 | * Find the previous vehicle registration with the provided document version. 85 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 86 | * @param registrationDocumentId The unique ID of the vehicle registration document in the VehicleRegistration table. 87 | * @param version The document version of the vehicle registration document. 88 | * @return Promise which fulfills with an Ion value containing the historic registration. 89 | */ 90 | export async function getHistoricRegistrationByVersion( 91 | txn: TransactionExecutor, 92 | registrationDocumentId: string, 93 | version: number 94 | ): Promise { 95 | log(`Querying the 'VehicleRegistration' table's history for a registration with documentId: ${registrationDocumentId} and version: ${version}`); 96 | const query: string = 97 | `SELECT * FROM history(${VEHICLE_REGISTRATION_TABLE_NAME}) AS h ` + 98 | `WHERE h.metadata.id = '${registrationDocumentId}' ` + 99 | `AND h.metadata.version = ${version}`; 100 | 101 | let revision: dom.Value; 102 | await txn.execute(query).then((result: Result) => { 103 | const resultList: dom.Value[] = result.getResultList(); 104 | if (resultList.length == 0) { 105 | throw new Error(`Unable to find a historic registration with documentId: ${registrationDocumentId} and version: ${version}.`); 106 | } 107 | prettyPrintResultList(resultList); 108 | revision = resultList[0]; 109 | }); 110 | return revision; 111 | } 112 | 113 | /** 114 | * Redact the historic version of a vehicle registration with the provided VIN and Owner. 115 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 116 | * @param vin The VIN for which a historic revision will be redacted. 117 | * @param previousOwnerGovId The previous owner of the vehicle registration. 118 | * @return Promise which fulfills with an Ion value containing details about the redaction. 119 | */ 120 | export async function redactPreviousRegistration( 121 | txn: TransactionExecutor, 122 | vin: string, 123 | previousOwnerGovId: string 124 | ): Promise { 125 | let redactRequest: dom.Value; 126 | 127 | const tableId: string = await getTableId(txn, VEHICLE_REGISTRATION_TABLE_NAME); 128 | const registrationDocumentId: string = await getDocumentId(txn, VEHICLE_REGISTRATION_TABLE_NAME, 'VIN', vin); 129 | const previousOwnerDocumentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, 'GovId', previousOwnerGovId); 130 | const historicRegistration = await getHistoricRegistrationByOwner(txn, registrationDocumentId, previousOwnerDocumentId); 131 | const blockAddress = historicRegistration.get("blockAddress"); 132 | 133 | log(`Redacting the revision at blockAddress: ${blockAddress} with tableId: ${tableId} and documentId: ${registrationDocumentId}`); 134 | const query: string = `EXEC redact_revision ?, '${tableId}', '${registrationDocumentId}'`; 135 | await txn.execute(query, blockAddress).then((result: Result) => { 136 | const resultList: dom.Value[] = result.getResultList(); 137 | if (resultList.length == 0) { 138 | throw new Error(`Unable to redact the historic registration.`); 139 | } 140 | prettyPrintResultList(resultList); 141 | redactRequest = resultList[0]; 142 | }); 143 | 144 | return redactRequest; 145 | } 146 | 147 | /** 148 | * Wait until the redaction is complete by querying history. 149 | * @param driver The driver for creating sessions. 150 | * @param redactRequest The Ion value containing details about the redaction. 151 | * @return Promise which fulfills with void. 152 | */ 153 | export async function waitUntilRevisionRedacted(driver: QldbDriver, redactRequest: dom.Value): Promise { 154 | let isRedacted: boolean = false; 155 | 156 | while (!isRedacted) { 157 | await driver.executeLambda(async (txn: TransactionExecutor) => { 158 | const revision = await getHistoricRegistrationByVersion( 159 | txn, 160 | redactRequest.get("documentId").stringValue(), 161 | redactRequest.get("version").numberValue() 162 | ); 163 | 164 | if (revision.get("dataHash") != null && revision.get("data") == null) { 165 | log(`Revision was successfully redacted!`); 166 | isRedacted = true; 167 | } 168 | }); 169 | if (!isRedacted) { 170 | log(`Revision is not yet redacted. Waiting for some time.`); 171 | await sleep(10000); 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * Transfer a vehicle registration to another owner and then redact 178 | * the previous revision of the vehicle registration. 179 | * @returns Promise which fulfills with void. 180 | */ 181 | export const main = async function(): Promise { 182 | try { 183 | const qldbDriver: QldbDriver = getQldbDriver(); 184 | 185 | const vin: string = VEHICLE[2].VIN; 186 | const previousOwnerGovId: string = PERSON[2].GovId; 187 | const newPrimaryOwnerGovId: string = PERSON[3].GovId; 188 | 189 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 190 | await validateAndUpdateRegistration(txn, vin, previousOwnerGovId, newPrimaryOwnerGovId); 191 | }); 192 | 193 | const redactRequest: dom.Value = await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 194 | return await redactPreviousRegistration(txn, vin, previousOwnerGovId); 195 | }); 196 | 197 | await waitUntilRevisionRedacted(qldbDriver, redactRequest); 198 | } catch (e) { 199 | error(`Unable to connect and run queries: ${e}`); 200 | } 201 | } 202 | 203 | if (require.main === module) { 204 | main(); 205 | } 206 | -------------------------------------------------------------------------------- /src/RegisterDriversLicense.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { insertDocument } from "./InsertDocument"; 24 | import { PERSON_TABLE_NAME } from "./qldb/Constants"; 25 | import { error, log } from "./qldb/LogUtil"; 26 | import { getDocumentId } from "./qldb/Util"; 27 | 28 | /** 29 | * Verify whether a driver already exists in the database. 30 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 31 | * @param govId The government ID of the new owner. 32 | * @returns Promise which fulfills with a boolean. 33 | */ 34 | async function personAlreadyExists(txn: TransactionExecutor, govId: string): Promise { 35 | const query: string = "SELECT * FROM Person AS p WHERE p.GovId = ?"; 36 | 37 | let personAlreadyExists: boolean = true; 38 | await txn.execute(query, govId).then((result: Result) => { 39 | const resultList: dom.Value[] = result.getResultList(); 40 | if (resultList.length === 0) { 41 | personAlreadyExists = false; 42 | } 43 | }); 44 | return personAlreadyExists; 45 | } 46 | 47 | /** 48 | * Query drivers license table by person ID. 49 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 50 | * @param personId The person ID to check. 51 | * @returns Promise which fulfills with a {@linkcode Result} object. 52 | */ 53 | export async function lookUpDriversLicenseForPerson(txn: TransactionExecutor, personId: string): Promise { 54 | const query: string = "SELECT * FROM DriversLicense AS d WHERE d.PersonId = ?"; 55 | const result: Result = await txn.execute(query, personId); 56 | return result; 57 | } 58 | 59 | /** 60 | * Verify whether a driver has a driver's license in the database. 61 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 62 | * @param personId The unique personId of the new owner. 63 | * @returns Promise which fulfills with a boolean. 64 | */ 65 | async function personHasDriversLicense(txn: TransactionExecutor, personId: string): Promise { 66 | const result: Result = await lookUpDriversLicenseForPerson(txn, personId); 67 | return result.getResultList().length !== 0; 68 | } 69 | 70 | /** 71 | * Register a new driver in the QLDB database. 72 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 73 | * @param driver The new driver to register. 74 | * @returns Promise which fulfills with a {@linkcode Result} object. 75 | */ 76 | async function registerNewDriver(txn: TransactionExecutor, driver: any): Promise { 77 | const result: Result = await insertDocument(txn, PERSON_TABLE_NAME, driver); 78 | return result; 79 | } 80 | 81 | /** 82 | * Register a new driver and a new driver's license in a single transaction. 83 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 84 | * @param license The driver's license to register. 85 | * @param personId The unique personId of the new owner. 86 | * @returns Promise which fulfills with void. 87 | */ 88 | async function registerNewDriversLicense(txn: TransactionExecutor, license: any, personId: string): Promise { 89 | if (await personHasDriversLicense(txn, personId)) { 90 | log("Person already has a license! No new license added."); 91 | return; 92 | } 93 | const statement: string = "INSERT INTO DriversLicense ?"; 94 | return await txn.execute(statement, license).then((result: Result) => { 95 | log("Successfully registered new license."); 96 | return result.getResultList(); 97 | }); 98 | } 99 | 100 | /** 101 | * Register a new driver's license. 102 | * @returns Promise which fulfills with void. 103 | */ 104 | export const main = async function(): Promise { 105 | try { 106 | const qldbDriver: QldbDriver = getQldbDriver(); 107 | let documentId: string; 108 | 109 | const newPerson = { 110 | FirstName: "Kate", 111 | LastName: "Mulberry", 112 | Address: "22 Commercial Drive, Blaine, WA, 97722", 113 | DOB: new Date("1995-02-09"), 114 | GovId: "AQQ17B2342", 115 | GovIdType: "Passport" 116 | }; 117 | const newLicense = { 118 | PersonId: "", 119 | LicenseNumber: "112 360 PXJ", 120 | LicenseType: "Full", 121 | ValidFromDate: new Date("2018-06-30"), 122 | ValidToDate: new Date("2022-10-30") 123 | }; 124 | return await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 125 | if (await personAlreadyExists(txn, newPerson.GovId)) { 126 | log("Person with this GovId already exists."); 127 | documentId = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", newPerson.GovId); 128 | } else { 129 | const documentIdResult: Result = await registerNewDriver(txn, newPerson); 130 | documentId = documentIdResult.getResultList()[0].get("documentId").stringValue(); 131 | } 132 | newLicense.PersonId = documentId; 133 | return await registerNewDriversLicense(txn, newLicense, documentId); 134 | }); 135 | } catch (e) { 136 | error(`Unable to register drivers license: ${e}`); 137 | } 138 | } 139 | 140 | if (require.main === module) { 141 | main(); 142 | } 143 | -------------------------------------------------------------------------------- /src/RenewDriversLicense.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { DRIVERS_LICENSE } from "./model/SampleData"; 24 | import { error, log } from "./qldb/LogUtil"; 25 | import { prettyPrintResultList } from "./ScanTable"; 26 | 27 | /** 28 | * Get the PersonId of a driver's license using the given license number. 29 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 30 | * @param licenseNumber License number of the driver's license to query. 31 | * @returns Promise which fulfills with a PersonId as a string. 32 | */ 33 | async function getPersonIdFromLicenseNumber(txn: TransactionExecutor, licenseNumber: string): Promise { 34 | const query: string = "SELECT PersonId FROM DriversLicense WHERE LicenseNumber = ?"; 35 | let personId: string; 36 | 37 | await txn.execute(query, licenseNumber).then((result: Result) => { 38 | const resultList: dom.Value[] = result.getResultList(); 39 | if (resultList.length === 0) { 40 | throw new Error(`Unable to find person with ID: ${licenseNumber}.`); 41 | } 42 | 43 | const PersonIdValue: dom.Value = resultList[0].get("PersonId"); 44 | if (PersonIdValue === null) { 45 | throw new Error(`Expected field name PersonId not found.`); 46 | } 47 | personId = PersonIdValue.stringValue(); 48 | }); 49 | return personId; 50 | } 51 | 52 | /** 53 | * Renew the ValidToDate and ValidFromDate of a driver's license. 54 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 55 | * @param validFromDate The new ValidFromDate. 56 | * @param validToDate The new ValidToDate. 57 | * @param licenseNumber License number of the driver's license to update. 58 | * @returns Promise which fulfills with {@linkcode Result} object. 59 | */ 60 | export async function renewDriversLicense( 61 | txn: TransactionExecutor, 62 | validFromDate: Date, 63 | validToDate: Date, 64 | licenseNumber: string 65 | ): Promise { 66 | const statement: string = 67 | "UPDATE DriversLicense AS d SET d.ValidFromDate = ?, d.ValidToDate = ? WHERE d.LicenseNumber = ?"; 68 | 69 | return await txn.execute(statement, validFromDate, validToDate, licenseNumber).then((result: Result) => { 70 | log("DriversLicense Document IDs which had licenses renewed: "); 71 | prettyPrintResultList(result.getResultList()); 72 | return result; 73 | }); 74 | } 75 | 76 | /** 77 | * Verify whether a driver exists in the system using the given license number. 78 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 79 | * @param personId The unique personId of a driver. 80 | * @returns Promise which fulfills with a boolean. 81 | */ 82 | async function verifyDriverFromLicenseNumber(txn: TransactionExecutor, personId: string): Promise { 83 | log(`Finding person with person ID: ${personId}`); 84 | const query: string = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?"; 85 | 86 | return await txn.execute(query, personId).then((result: Result) => { 87 | const resultList: dom.Value[] = result.getResultList(); 88 | if (resultList.length === 0) { 89 | log(`Unable to find person with ID: ${personId}`); 90 | return false; 91 | } 92 | return true; 93 | }); 94 | } 95 | 96 | /** 97 | * Find the person associated with a license number. 98 | * Renew a driver's license. 99 | * @returns Promise which fulfills with void. 100 | */ 101 | export const main = async function(): Promise { 102 | try { 103 | const qldbDriver: QldbDriver = getQldbDriver(); 104 | const fromDate: Date = new Date("2019-04-19"); 105 | const toDate: Date = new Date("2023-04-19"); 106 | const licenseNumber: string = DRIVERS_LICENSE[0].LicenseNumber; 107 | 108 | return await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 109 | const personId: string = await getPersonIdFromLicenseNumber(txn, licenseNumber); 110 | if (await verifyDriverFromLicenseNumber(txn, personId)) { 111 | const license = await renewDriversLicense(txn, fromDate, toDate, licenseNumber); 112 | return license.getResultList(); 113 | } 114 | }); 115 | } catch (e) { 116 | error(`Unable to renew drivers license: ${e}`); 117 | } 118 | } 119 | 120 | if (require.main === module) { 121 | main(); 122 | } 123 | -------------------------------------------------------------------------------- /src/ScanTable.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { log } from "./qldb/LogUtil"; 24 | 25 | /** 26 | * Pretty print Ion values in the provided result list. 27 | * @param resultList The result list containing Ion values to pretty print. 28 | */ 29 | export function prettyPrintResultList(resultList: dom.Value[]): void { 30 | log(JSON.stringify(resultList, null, 2)); 31 | } 32 | 33 | /** 34 | * Scan for all the documents in a table. 35 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 36 | * @param tableName The name of the table to operate on. 37 | * @returns Promise which fulfills with a {@linkcode Result} object. 38 | */ 39 | export async function scanTableForDocuments(txn: TransactionExecutor, tableName: string): Promise { 40 | log(`Scanning ${tableName}...`); 41 | const query: string = `SELECT * FROM ${tableName}`; 42 | return await txn.execute(query).then((result: Result) => { 43 | return result; 44 | }); 45 | } 46 | 47 | /** 48 | * Scan for all the documents in a table. 49 | * @returns Promise which fulfills with void. 50 | */ 51 | export const main = async function(): Promise { 52 | try { 53 | const qldbDriver: QldbDriver = getQldbDriver(); 54 | await qldbDriver.getTableNames().then(async (listOfTables: string[]) => { 55 | for (const tableName of listOfTables) { 56 | await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 57 | const result: Result = await scanTableForDocuments(txn, tableName); 58 | prettyPrintResultList(result.getResultList()); 59 | }); 60 | } 61 | }); 62 | } catch (e) { 63 | log(`Error displaying documents: ${e}`); 64 | } 65 | } 66 | 67 | if (require.main === module) { 68 | main(); 69 | } 70 | -------------------------------------------------------------------------------- /src/TagResources.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { Tag } from "@aws-sdk/client-config-service"; 20 | import { QLDB, 21 | CreateLedgerRequest, 22 | CreateLedgerResponse, 23 | ListTagsForResourceRequest, 24 | ListTagsForResourceResponse, 25 | TagResourceRequest, 26 | UntagResourceRequest, 27 | } from "@aws-sdk/client-qldb"; 28 | import { waitForActive } from "./CreateLedger"; 29 | import { deleteLedger, waitForDeleted } from "./DeleteLedger"; 30 | import { setDeletionProtection } from "./DeletionProtection"; 31 | 32 | import { LEDGER_NAME_WITH_TAGS } from "./qldb/Constants"; 33 | import { error, log } from "./qldb/LogUtil"; 34 | 35 | const ADD_TAGS = { 36 | Domain: 'Prod' 37 | }; 38 | const CREATE_TAGS = { 39 | IsTest: 'true', 40 | Domain: 'Test' 41 | }; 42 | const REMOVE_TAGS = ['IsTest']; 43 | 44 | /** 45 | * Create a ledger with the specified name and the given tags. 46 | * @param ledgerName Name of the ledger to be created. 47 | * @param tags The map of key-value pairs to create the ledger with. 48 | * @param qldbClient The QLDB control plane client to use. 49 | * @returns Promise which fulfills with a CreateLedgerResponse. 50 | */ 51 | async function createWithTags(ledgerName: string, tags: Record, qldbClient: QLDB): Promise { 52 | log(`Creating ledger with name: ${ledgerName}.`); 53 | const request: CreateLedgerRequest = { 54 | Name: ledgerName, 55 | Tags: tags, 56 | PermissionsMode: "ALLOW_ALL" 57 | }; 58 | const result: CreateLedgerResponse = await qldbClient.createLedger(request); 59 | log(`Success. Ledger state: ${result.State}.`); 60 | return result; 61 | } 62 | 63 | /** 64 | * Return all tags for a specified Amazon QLDB resource. 65 | * @param resourceArn The Amazon Resource Name (ARN) for which to list tags off. 66 | * @param qldbClient The QLDB control plane client to use. 67 | * @returns Promise which fulfills with a ListTagsForResourceResponse. 68 | */ 69 | export async function listTags(resourceArn: string, qldbClient: QLDB): Promise { 70 | log(`Listing the tags for resource with arn: ${resourceArn}.`); 71 | const request: ListTagsForResourceRequest = { 72 | ResourceArn: resourceArn 73 | }; 74 | const result: ListTagsForResourceResponse = await qldbClient.listTagsForResource(request); 75 | log(`Success. Tags: ${JSON.stringify(result.Tags)}`); 76 | return result; 77 | } 78 | 79 | /** 80 | * Add one or more tags to the specified QLDB resource. 81 | * @param resourceArn The Amazon Resource Name (ARN) of the ledger to which to add tags. 82 | * @param tags The map of key-value pairs to add to a ledger. 83 | * @param qldbClient The QLDB control plane client to use. 84 | * @returns Promise which fulfills with void. 85 | */ 86 | export async function tagResource(resourceArn: string, tags: Record, qldbClient: QLDB): Promise { 87 | log(`Adding tags ${JSON.stringify(tags)} for resource with arn: ${resourceArn}.`); 88 | const request: TagResourceRequest = { 89 | ResourceArn: resourceArn, 90 | Tags: tags 91 | }; 92 | await qldbClient.tagResource(request); 93 | log("Successfully added tags."); 94 | } 95 | 96 | /** 97 | * Remove one or more tags from the specified QLDB resource. 98 | * @param resourceArn The Amazon Resource Name (ARN) from which to remove tags. 99 | * @param tagsKeys The list of tag keys to remove. 100 | * @param qldbClient The QLDB control plane client to use. 101 | * @returns Promise which fulfills with void. 102 | */ 103 | export async function untagResource(resourceArn: string, tagsKeys: string[], qldbClient: QLDB): Promise { 104 | log(`Removing tags ${JSON.stringify(tagsKeys)} for resource with arn: ${resourceArn}.`); 105 | const request: UntagResourceRequest = { 106 | ResourceArn: resourceArn, 107 | TagKeys: tagsKeys 108 | }; 109 | await qldbClient.untagResource(request); 110 | log("Successfully removed tags."); 111 | } 112 | 113 | /** 114 | * Tagging and un-tagging resources, including tag on create. 115 | * @returns Promise which fulfills with void. 116 | */ 117 | export const main = async function(): Promise[]> { 118 | const qldbClient: QLDB = new QLDB({ }); 119 | try { 120 | const tags: Record[] = []; 121 | const result: CreateLedgerResponse = await createWithTags(LEDGER_NAME_WITH_TAGS, CREATE_TAGS, qldbClient); 122 | const arn: string = result.Arn; 123 | 124 | tags.push((await listTags(arn, qldbClient)).Tags); 125 | 126 | await tagResource(arn, ADD_TAGS, qldbClient); 127 | tags.push((await listTags(arn, qldbClient)).Tags); 128 | 129 | await untagResource(arn, REMOVE_TAGS, qldbClient); 130 | tags.push((await listTags(arn, qldbClient)).Tags); 131 | 132 | return tags; 133 | } catch (e) { 134 | error(`Unable to tag resources: ${e}`); 135 | } finally { 136 | await waitForActive(LEDGER_NAME_WITH_TAGS, qldbClient); 137 | await setDeletionProtection(LEDGER_NAME_WITH_TAGS, qldbClient, false); 138 | await deleteLedger(LEDGER_NAME_WITH_TAGS, qldbClient); 139 | await waitForDeleted(LEDGER_NAME_WITH_TAGS, qldbClient); 140 | } 141 | } 142 | 143 | if (require.main === module) { 144 | main(); 145 | } 146 | -------------------------------------------------------------------------------- /src/TransferVehicleOwnership.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QldbDriver, Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { dom } from "ion-js"; 21 | 22 | import { getQldbDriver } from "./ConnectToLedger"; 23 | import { PERSON, VEHICLE } from "./model/SampleData"; 24 | import { PERSON_TABLE_NAME } from "./qldb/Constants"; 25 | import { error, log } from "./qldb/LogUtil"; 26 | import { getDocumentId } from "./qldb/Util"; 27 | 28 | /** 29 | * Query a driver's information using the given ID. 30 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 31 | * @param documentId The unique ID of a document in the Person table. 32 | * @returns Promise which fulfills with an Ion value containing the person. 33 | */ 34 | export async function findPersonFromDocumentId(txn: TransactionExecutor, documentId: string): Promise { 35 | const query: string = "SELECT p.* FROM Person AS p BY pid WHERE pid = ?"; 36 | 37 | let personId: dom.Value; 38 | await txn.execute(query, documentId).then((result: Result) => { 39 | const resultList: dom.Value[] = result.getResultList(); 40 | if (resultList.length === 0) { 41 | throw new Error(`Unable to find person with ID: ${documentId}.`); 42 | } 43 | personId = resultList[0]; 44 | }); 45 | return personId; 46 | } 47 | 48 | /** 49 | * Find the primary owner for the given VIN. 50 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 51 | * @param vin The VIN to find primary owner for. 52 | * @returns Promise which fulfills with an Ion value containing the primary owner. 53 | */ 54 | export async function findPrimaryOwnerForVehicle(txn: TransactionExecutor, vin: string): Promise { 55 | log(`Finding primary owner for vehicle with VIN: ${vin}`); 56 | const query: string = "SELECT Owners.PrimaryOwner.PersonId FROM VehicleRegistration AS v WHERE v.VIN = ?"; 57 | 58 | let documentId: string = undefined; 59 | await txn.execute(query, vin).then((result: Result) => { 60 | const resultList: dom.Value[] = result.getResultList(); 61 | if (resultList.length === 0) { 62 | throw new Error(`Unable to retrieve document ID using ${vin}.`); 63 | } 64 | const PersonIdValue: dom.Value = resultList[0].get("PersonId"); 65 | if (PersonIdValue === null) { 66 | throw new Error(`Expected field name PersonId not found.`); 67 | } 68 | documentId = PersonIdValue.stringValue(); 69 | }); 70 | return findPersonFromDocumentId(txn, documentId); 71 | } 72 | 73 | /** 74 | * Update the primary owner for a vehicle using the given VIN. 75 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 76 | * @param vin The VIN for the vehicle to operate on. 77 | * @param documentId New PersonId for the primary owner. 78 | * @returns Promise which fulfills with void. 79 | */ 80 | async function updateVehicleRegistration(txn: TransactionExecutor, vin: string, documentId: string): Promise { 81 | const statement: string = "UPDATE VehicleRegistration AS r SET r.Owners.PrimaryOwner.PersonId = ? WHERE r.VIN = ?"; 82 | 83 | log(`Updating the primary owner for vehicle with VIN: ${vin}...`); 84 | return await txn.execute(statement, documentId, vin).then((result: Result) => { 85 | const resultList: dom.Value[] = result.getResultList(); 86 | if (resultList.length === 0) { 87 | throw new Error("Unable to transfer vehicle, could not find registration."); 88 | } 89 | log(`Successfully transferred vehicle with VIN ${vin} to new owner.`); 90 | return resultList; 91 | }); 92 | } 93 | 94 | /** 95 | * Validate the current owner of the given vehicle and transfer its ownership to a new owner in a single transaction. 96 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 97 | * @param vin The VIN of the vehicle to transfer ownership of. 98 | * @param currentOwner The GovId of the current owner of the vehicle. 99 | * @param newOwner The GovId of the new owner of the vehicle. 100 | */ 101 | export async function validateAndUpdateRegistration( 102 | txn: TransactionExecutor, 103 | vin: string, 104 | currentOwner: string, 105 | newOwner: string 106 | ): Promise { 107 | const primaryOwner: dom.Value = await findPrimaryOwnerForVehicle(txn, vin); 108 | const govIdValue: dom.Value = primaryOwner.get("GovId"); 109 | if (govIdValue !== null && govIdValue.stringValue() !== currentOwner) { 110 | log("Incorrect primary owner identified for vehicle, unable to transfer."); 111 | } 112 | else { 113 | const documentId: string = await getDocumentId(txn, PERSON_TABLE_NAME, "GovId", newOwner); 114 | const registration = await updateVehicleRegistration(txn, vin, documentId); 115 | log("Successfully transferred vehicle ownership!"); 116 | return registration; 117 | } 118 | } 119 | 120 | /** 121 | * Find primary owner for a particular vehicle's VIN. 122 | * Transfer to another primary owner for a particular vehicle's VIN. 123 | * @returns Promise which fulfills with void. 124 | */ 125 | export const main = async function(): Promise { 126 | try { 127 | const qldbDriver: QldbDriver = getQldbDriver(); 128 | 129 | const vin: string = VEHICLE[0].VIN; 130 | const previousOwnerGovId: string = PERSON[0].GovId; 131 | const newPrimaryOwnerGovId: string = PERSON[1].GovId; 132 | 133 | return await qldbDriver.executeLambda(async (txn: TransactionExecutor) => { 134 | return await validateAndUpdateRegistration(txn, vin, previousOwnerGovId, newPrimaryOwnerGovId); 135 | }); 136 | } catch (e) { 137 | error(`Unable to connect and run queries: ${e}`); 138 | } 139 | } 140 | 141 | if (require.main === module) { 142 | main(); 143 | } 144 | -------------------------------------------------------------------------------- /src/ValidateQldbHashchain.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { QLDB, 20 | JournalS3ExportDescription, 21 | } from "@aws-sdk/client-qldb"; 22 | import { 23 | STS, 24 | GetCallerIdentityRequest, 25 | GetCallerIdentityResponse, 26 | } from '@aws-sdk/client-sts'; 27 | import { S3Client, S3 } from '@aws-sdk/client-s3'; 28 | import { toBase64 } from "ion-js"; 29 | import { format } from "util"; 30 | 31 | import { describeJournalExport } from './DescribeJournalExport'; 32 | import { 33 | createExportAndWaitForCompletion, 34 | createS3BucketIfNotExists, 35 | setUpS3EncryptionConfiguration 36 | } from "./ExportJournal"; 37 | import { 38 | JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX, 39 | LEDGER_NAME, 40 | } from './qldb/Constants'; 41 | import { JournalBlock } from "./qldb/JournalBlock"; 42 | import { readExport } from "./qldb/JournalS3ExportReader"; 43 | import { error, log } from "./qldb/LogUtil"; 44 | import { joinHashesPairwise } from "./qldb/Verifier"; 45 | 46 | const s3Client: S3Client = new S3Client({ }); 47 | 48 | /** 49 | * Compare the hash values on the given journal blocks. 50 | * @param previousJournalBlock Previous journal block in the chain. 51 | * @param journalBlock Current journal block in the chain. 52 | * @returns The current journal block in the chain. 53 | */ 54 | function compareJournalBlocks(previousJournalBlock: JournalBlock, journalBlock: JournalBlock): JournalBlock { 55 | if (previousJournalBlock === undefined) { 56 | return journalBlock; 57 | } 58 | if (toBase64(previousJournalBlock._blockHash) !== toBase64(journalBlock._previousBlockHash)) { 59 | throw new Error("Previous block hash does not match!"); 60 | } 61 | const blockHash: Uint8Array = joinHashesPairwise(journalBlock._entriesHash, previousJournalBlock._blockHash); 62 | if (toBase64(blockHash) !== toBase64(journalBlock._blockHash)) { 63 | throw new Error("Block hash doesn't match expected block hash. Verification failed."); 64 | } 65 | return journalBlock; 66 | } 67 | 68 | /** 69 | * Export journal contents to a S3 bucket. 70 | * @param qldbClient The QLDB control plane client to use. 71 | * @returns The ExportId for the journal export. 72 | */ 73 | async function createJournalExport(qldbClient: QLDB): Promise { 74 | const sts: STS = new STS({ }); 75 | const request: GetCallerIdentityRequest = {}; 76 | const identity: GetCallerIdentityResponse = await sts.getCallerIdentity(request); 77 | 78 | const bucketName: string = format('%s-%s', JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX, identity.Account); 79 | const prefix: string = format('%s-%s', LEDGER_NAME, Date.now().toString()); 80 | 81 | await createS3BucketIfNotExists(bucketName, s3Client); 82 | 83 | const exportJournalToS3Result = await createExportAndWaitForCompletion( 84 | LEDGER_NAME, 85 | bucketName, 86 | prefix, 87 | setUpS3EncryptionConfiguration(null), 88 | null, 89 | qldbClient 90 | ); 91 | return exportJournalToS3Result.ExportId; 92 | } 93 | 94 | /** 95 | * Validate that the chain hash on the journal block is valid. 96 | * @param journalBlocks A list of journal blocks. 97 | * @returns None if the given list of journal blocks is empty. 98 | */ 99 | function verify(journalBlocks: JournalBlock[]): void { 100 | if (journalBlocks.length === 0) { 101 | return; 102 | } 103 | journalBlocks.reduce(compareJournalBlocks); 104 | } 105 | 106 | /** 107 | * Validate the hash chain of a QLDB ledger by stepping through its S3 export. 108 | * This code accepts an exportID as an argument, if exportID is passed the code 109 | * will use that or request QLDB to generate a new export to perform QLDB hash 110 | * chain validation. 111 | * @returns Promise which fulfills with void. 112 | */ 113 | export const main = async function(exportId: string = undefined): Promise { 114 | try { 115 | const qldbClient = new QLDB({ }); 116 | if (exportId === undefined) { 117 | if (process.argv.length === 3) { 118 | exportId = process.argv[2].toString(); 119 | log(`Validating QLDB hash chain for ExportId: ${exportId}.`); 120 | } else { 121 | log("Requesting QLDB to create an export."); 122 | exportId = await createJournalExport(qldbClient); 123 | } 124 | } 125 | const journalExport: JournalS3ExportDescription = 126 | (await describeJournalExport(LEDGER_NAME, exportId, qldbClient)).ExportDescription; 127 | const journalBlocks: JournalBlock[] = await readExport(journalExport, s3Client); 128 | verify(journalBlocks); 129 | log(`QLDB hash chain validation for ExportId: ${exportId} is successful`); 130 | } catch (e) { 131 | error(`Unable to perform hash chain verification: ${e}`); 132 | } 133 | } 134 | 135 | if (require.main === module) { 136 | main(); 137 | } 138 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { addSecondaryOwner, getDocumentIdByGovId, isSecondaryOwnerForVehicle } from "./AddSecondaryOwner"; 2 | export { createQldbDriver, getQldbDriver } from "./ConnectToLedger"; 3 | export { 4 | DRIVERS_LICENSE_TABLE_NAME, 5 | GOV_ID_INDEX_NAME, 6 | JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX, 7 | LEDGER_NAME, 8 | LEDGER_NAME_WITH_TAGS, 9 | LICENSE_NUMBER_INDEX_NAME, 10 | LICENSE_PLATE_NUMBER_INDEX_NAME, 11 | PERSON_ID_INDEX_NAME, 12 | PERSON_TABLE_NAME, 13 | RETRY_LIMIT, 14 | USER_TABLES, 15 | VEHICLE_REGISTRATION_TABLE_NAME, 16 | VEHICLE_TABLE_NAME, 17 | VIN_INDEX_NAME, 18 | } from "./qldb/Constants"; 19 | export { createIndex } from "./CreateIndex"; 20 | export { createLedger, waitForActive } from "./CreateLedger"; 21 | export { createTable } from "./CreateTable"; 22 | export { deleteLedger, waitForDeleted } from "./DeleteLedger"; 23 | export { setDeletionProtection } from "./DeletionProtection"; 24 | export { deregisterDriversLicense } from "./DeregisterDriversLicense"; 25 | export { describeJournalExport } from "./DescribeJournalExport" 26 | export { describeLedger } from "./DescribeLedger"; 27 | export { 28 | createExportAndWaitForCompletion, 29 | createS3BucketIfNotExists, 30 | setUpS3EncryptionConfiguration 31 | } from "./ExportJournal"; 32 | export { verifyBlock } from "./GetBlock"; 33 | export { getDigestResult } from "./GetDigest"; 34 | export { lookupRegistrationForVin, verifyRegistration } from "./GetRevision"; 35 | export { insertDocument, updatePersonId } from "./InsertDocument"; 36 | export { listLedgers } from "./ListLedgers"; 37 | export { readExport } from "./qldb/JournalS3ExportReader"; 38 | export { getBlobValue, getDocumentId, sleep } from "./qldb/Util"; 39 | export { flipRandomBit, joinHashesPairwise, parseBlock, verifyDocument } from "./qldb/Verifier"; 40 | export { lookUpDriversLicenseForPerson } from "./RegisterDriversLicense"; 41 | export { renewDriversLicense } from "./RenewDriversLicense"; 42 | export { DRIVERS_LICENSE, PERSON, VEHICLE, VEHICLE_REGISTRATION } from "./model/SampleData"; 43 | export { prettyPrintResultList, scanTableForDocuments } from "./ScanTable"; 44 | export { listTags, tagResource, untagResource } from "./TagResources"; 45 | export { 46 | findPersonFromDocumentId, 47 | findPrimaryOwnerForVehicle, 48 | validateAndUpdateRegistration 49 | } from "./TransferVehicleOwnership"; 50 | -------------------------------------------------------------------------------- /src/model/SampleData.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { Decimal } from "ion-js"; 20 | 21 | const EMPTY_SECONDARY_OWNERS: object[] = []; 22 | export const DRIVERS_LICENSE = [ 23 | { 24 | PersonId: "", 25 | LicenseNumber: "LEWISR261LL", 26 | LicenseType: "Learner", 27 | ValidFromDate: new Date("2016-12-20"), 28 | ValidToDate: new Date("2020-11-15") 29 | }, 30 | { 31 | PersonId: "", 32 | LicenseNumber : "LOGANB486CG", 33 | LicenseType: "Probationary", 34 | ValidFromDate : new Date("2016-04-06"), 35 | ValidToDate : new Date("2020-11-15") 36 | }, 37 | { 38 | PersonId: "", 39 | LicenseNumber : "744 849 301", 40 | LicenseType: "Full", 41 | ValidFromDate : new Date("2017-12-06"), 42 | ValidToDate : new Date("2022-10-15") 43 | }, 44 | { 45 | PersonId: "", 46 | LicenseNumber : "P626-168-229-765", 47 | LicenseType: "Learner", 48 | ValidFromDate : new Date("2017-08-16"), 49 | ValidToDate : new Date("2021-11-15") 50 | }, 51 | { 52 | PersonId: "", 53 | LicenseNumber : "S152-780-97-415-0", 54 | LicenseType: "Probationary", 55 | ValidFromDate : new Date("2015-08-15"), 56 | ValidToDate : new Date("2021-08-21") 57 | } 58 | ]; 59 | export const PERSON = [ 60 | { 61 | FirstName : "Raul", 62 | LastName : "Lewis", 63 | DOB : new Date("1963-08-19"), 64 | Address : "1719 University Street, Seattle, WA, 98109", 65 | GovId : "LEWISR261LL", 66 | GovIdType : "Driver License" 67 | }, 68 | { 69 | FirstName : "Brent", 70 | LastName : "Logan", 71 | DOB : new Date("1967-07-03"), 72 | Address : "43 Stockert Hollow Road, Everett, WA, 98203", 73 | GovId : "LOGANB486CG", 74 | GovIdType : "Driver License" 75 | }, 76 | { 77 | FirstName : "Alexis", 78 | LastName : "Pena", 79 | DOB : new Date("1974-02-10"), 80 | Address : "4058 Melrose Street, Spokane Valley, WA, 99206", 81 | GovId : "744 849 301", 82 | GovIdType : "SSN" 83 | }, 84 | { 85 | FirstName : "Melvin", 86 | LastName : "Parker", 87 | DOB : new Date("1976-05-22"), 88 | Address : "4362 Ryder Avenue, Seattle, WA, 98101", 89 | GovId : "P626-168-229-765", 90 | GovIdType : "Passport" 91 | }, 92 | { 93 | FirstName : "Salvatore", 94 | LastName : "Spencer", 95 | DOB : new Date("1997-11-15"), 96 | Address : "4450 Honeysuckle Lane, Seattle, WA, 98101", 97 | GovId : "S152-780-97-415-0", 98 | GovIdType : "Passport" 99 | } 100 | ]; 101 | export const VEHICLE = [ 102 | { 103 | VIN : "1N4AL11D75C109151", 104 | Type : "Sedan", 105 | Year : 2011, 106 | Make : "Audi", 107 | Model : "A5", 108 | Color : "Silver" 109 | }, 110 | { 111 | VIN : "KM8SRDHF6EU074761", 112 | Type : "Sedan", 113 | Year : 2015, 114 | Make : "Tesla", 115 | Model : "Model S", 116 | Color : "Blue" 117 | }, 118 | { 119 | VIN : "3HGGK5G53FM761765", 120 | Type : "Motorcycle", 121 | Year : 2011, 122 | Make : "Ducati", 123 | Model : "Monster 1200", 124 | Color : "Yellow" 125 | }, 126 | { 127 | VIN : "1HVBBAANXWH544237", 128 | Type : "Semi", 129 | Year : 2009, 130 | Make : "Ford", 131 | Model : "F 150", 132 | Color : "Black" 133 | }, 134 | { 135 | VIN : "1C4RJFAG0FC625797", 136 | Type : "Sedan", 137 | Year : 2019, 138 | Make : "Mercedes", 139 | Model : "CLK 350", 140 | Color : "White" 141 | } 142 | ]; 143 | export const VEHICLE_REGISTRATION = [ 144 | { 145 | VIN : "1N4AL11D75C109151", 146 | LicensePlateNumber : "LEWISR261LL", 147 | State : "WA", 148 | City : "Seattle", 149 | ValidFromDate : new Date("2017-08-21"), 150 | ValidToDate : new Date("2020-05-11"), 151 | PendingPenaltyTicketAmount : new Decimal(9025, -2), 152 | Owners : { 153 | PrimaryOwner : { PersonId : "" }, 154 | SecondaryOwners : EMPTY_SECONDARY_OWNERS 155 | } 156 | }, 157 | { 158 | VIN : "KM8SRDHF6EU074761", 159 | LicensePlateNumber : "CA762X", 160 | State : "WA", 161 | City : "Kent", 162 | PendingPenaltyTicketAmount : new Decimal(13075, -2), 163 | ValidFromDate : new Date("2017-09-14"), 164 | ValidToDate : new Date("2020-06-25"), 165 | Owners : { 166 | PrimaryOwner : { PersonId : "" }, 167 | SecondaryOwners : EMPTY_SECONDARY_OWNERS 168 | } 169 | }, 170 | { 171 | VIN : "3HGGK5G53FM761765", 172 | LicensePlateNumber : "CD820Z", 173 | State : "WA", 174 | City : "Everett", 175 | PendingPenaltyTicketAmount : new Decimal(44230, -2), 176 | ValidFromDate : new Date("2011-03-17"), 177 | ValidToDate : new Date("2021-03-24"), 178 | Owners : { 179 | PrimaryOwner : { PersonId : "" }, 180 | SecondaryOwners : EMPTY_SECONDARY_OWNERS 181 | } 182 | }, 183 | { 184 | VIN : "1HVBBAANXWH544237", 185 | LicensePlateNumber : "LS477D", 186 | State : "WA", 187 | City : "Tacoma", 188 | PendingPenaltyTicketAmount : new Decimal(4220, -2), 189 | ValidFromDate : new Date("2011-10-26"), 190 | ValidToDate : new Date("2023-09-25"), 191 | Owners : { 192 | PrimaryOwner : { PersonId : "" }, 193 | SecondaryOwners : EMPTY_SECONDARY_OWNERS 194 | } 195 | }, 196 | { 197 | VIN : "1C4RJFAG0FC625797", 198 | LicensePlateNumber : "TH393F", 199 | State : "WA", 200 | City : "Olympia", 201 | PendingPenaltyTicketAmount : new Decimal(3045, -2), 202 | ValidFromDate : new Date("2013-09-02"), 203 | ValidToDate : new Date("2024-03-19"), 204 | Owners : { 205 | PrimaryOwner : { PersonId : "" }, 206 | SecondaryOwners : EMPTY_SECONDARY_OWNERS 207 | } 208 | } 209 | ]; 210 | -------------------------------------------------------------------------------- /src/qldb/BlockAddress.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { ValueHolder } from "@aws-sdk/client-qldb"; 20 | import { dom, IonTypes } from "ion-js"; 21 | 22 | export class BlockAddress { 23 | _strandId: string; 24 | _sequenceNo: number; 25 | 26 | constructor(strandId: string, sequenceNo: number) { 27 | this._strandId = strandId; 28 | this._sequenceNo = sequenceNo; 29 | } 30 | } 31 | 32 | /** 33 | * Convert a block address from an Ion value into a ValueHolder. 34 | * Shape of the ValueHolder must be: {'IonText': "{strandId: <"strandId">, sequenceNo: }"} 35 | * @param value The Ion value that contains the block address values to convert. 36 | * @returns The ValueHolder that contains the strandId and sequenceNo. 37 | */ 38 | export function blockAddressToValueHolder(value: dom.Value): ValueHolder { 39 | const blockAddressValue : dom.Value = getBlockAddressValue(value); 40 | const strandId: string = getStrandId(blockAddressValue); 41 | const sequenceNo: number = getSequenceNo(blockAddressValue); 42 | const valueHolder: string = `{strandId: "${strandId}", sequenceNo: ${sequenceNo}}`; 43 | const blockAddress: ValueHolder = { IonText: valueHolder }; 44 | return blockAddress; 45 | } 46 | 47 | /** 48 | * Helper method that to get the Metadata ID. 49 | * @param value The Ion value. 50 | * @returns The Metadata ID. 51 | */ 52 | export function getMetadataId(value: dom.Value): string { 53 | const metaDataId: dom.Value = value.get("id"); 54 | if (metaDataId === null) { 55 | throw new Error(`Expected field name id, but not found.`); 56 | } 57 | return metaDataId.stringValue(); 58 | } 59 | 60 | /** 61 | * Helper method to get the Sequence No. 62 | * @param value The Ion value. 63 | * @returns The Sequence No. 64 | */ 65 | export function getSequenceNo(value : dom.Value): number { 66 | const sequenceNo: dom.Value = value.get("sequenceNo"); 67 | if (sequenceNo === null) { 68 | throw new Error(`Expected field name sequenceNo, but not found.`); 69 | } 70 | return sequenceNo.numberValue(); 71 | } 72 | 73 | /** 74 | * Helper method to get the Strand ID. 75 | * @param value The Ion value. 76 | * @returns The Strand ID. 77 | */ 78 | export function getStrandId(value: dom.Value): string { 79 | const strandId: dom.Value = value.get("strandId"); 80 | if (strandId === null) { 81 | throw new Error(`Expected field name strandId, but not found.`); 82 | } 83 | return strandId.stringValue(); 84 | } 85 | 86 | export function getBlockAddressValue(value: dom.Value) : dom.Value { 87 | const type = value.getType(); 88 | if (type !== IonTypes.STRUCT) { 89 | throw new Error(`Unexpected format: expected struct, but got IonType: ${type.name}`); 90 | } 91 | const blockAddress: dom.Value = value.get("blockAddress"); 92 | if (blockAddress == null) { 93 | throw new Error(`Expected field name blockAddress, but not found.`); 94 | } 95 | return blockAddress; 96 | } 97 | -------------------------------------------------------------------------------- /src/qldb/Constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | /** 20 | * Constant values used throughout this tutorial. 21 | */ 22 | 23 | const ciIdentifier = process.env["CI_ID"] ?? ""; 24 | 25 | export const LEDGER_NAME = `vehicle-registration${ciIdentifier}`; 26 | export const LEDGER_NAME_WITH_TAGS = `tags${ciIdentifier}`; 27 | export const LEDGER_NAME_FOR_DELETIONS = `deletions${ciIdentifier}`; 28 | 29 | export const DRIVERS_LICENSE_TABLE_NAME = "DriversLicense"; 30 | export const PERSON_TABLE_NAME = "Person"; 31 | export const VEHICLE_REGISTRATION_TABLE_NAME = "VehicleRegistration"; 32 | export const VEHICLE_TABLE_NAME = "Vehicle"; 33 | 34 | export const GOV_ID_INDEX_NAME = "GovId"; 35 | export const LICENSE_NUMBER_INDEX_NAME = "LicenseNumber"; 36 | export const LICENSE_PLATE_NUMBER_INDEX_NAME = "LicensePlateNumber"; 37 | export const PERSON_ID_INDEX_NAME = "PersonId"; 38 | export const VIN_INDEX_NAME = "VIN"; 39 | 40 | export const RETRY_LIMIT = 4; 41 | 42 | export const JOURNAL_EXPORT_S3_BUCKET_NAME_PREFIX = "qldb-tutorial-journal-export"; 43 | export const USER_TABLES = "information_schema.user_tables"; 44 | -------------------------------------------------------------------------------- /src/qldb/JournalBlock.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { dom } from "ion-js"; 20 | 21 | import { BlockAddress, getBlockAddressValue, getSequenceNo, getStrandId } from "./BlockAddress"; 22 | import { getBlobValue } from "./Util"; 23 | 24 | /** 25 | * Represents a JournalBlock that was recorded after executing a transaction in the ledger. 26 | */ 27 | export class JournalBlock { 28 | _blockAddress: BlockAddress; 29 | _blockHash: Uint8Array; 30 | _entriesHash: Uint8Array; 31 | _previousBlockHash: Uint8Array; 32 | 33 | constructor( 34 | blockAddress: BlockAddress, 35 | blockHash: Uint8Array, 36 | entriesHash: Uint8Array, 37 | previousBlockHash: Uint8Array) 38 | { 39 | this._blockAddress = blockAddress; 40 | this._blockHash = blockHash; 41 | this._entriesHash = entriesHash; 42 | this._previousBlockHash = previousBlockHash; 43 | } 44 | } 45 | 46 | /** 47 | * Construct a new JournalBlock object from an IonStruct. 48 | * @param value The Ion value that contains the journal block attributes. 49 | * For this to work, the value is expected to have the structure 50 | * { 51 | * blockAddress:{ 52 | * strandId:"string", 53 | * sequenceNo:number 54 | * }, 55 | * transactionId:"string", 56 | * blockTimestamp:Date, 57 | * blockHash:{ 58 | * { 59 | * blob 60 | * } 61 | * }, 62 | * entriesHash:{ 63 | * { 64 | * blob 65 | * } 66 | * }, 67 | * previousBlockHash:{ 68 | * { 69 | * blob 70 | * } 71 | * } 72 | * ......... 73 | * } 74 | * @returns The constructed JournalBlock object. 75 | */ 76 | export function fromIon(journalValue: dom.Value): JournalBlock { 77 | const blockAddressValue = getBlockAddressValue(journalValue); 78 | const strandId: string = getStrandId(blockAddressValue); 79 | const sequenceNo: number = getSequenceNo(blockAddressValue); 80 | const blockAddress: BlockAddress = new BlockAddress(strandId, sequenceNo); 81 | 82 | const blockHash: Uint8Array = getBlobValue(journalValue, "blockHash"); 83 | const entriesHash: Uint8Array = getBlobValue(journalValue, "entriesHash"); 84 | const previousBlockHash: Uint8Array = getBlobValue(journalValue, "previousBlockHash"); 85 | 86 | if (!blockAddress || !blockHash || !entriesHash || !previousBlockHash) { 87 | throw new Error( 88 | "BlockAddress, blockHash, entriesHash, or previousHash field(s) not found. Please check the data in the journal value." 89 | ); 90 | } 91 | const journalBlock: JournalBlock = new JournalBlock(blockAddress, blockHash, entriesHash, previousBlockHash); 92 | return journalBlock; 93 | } 94 | -------------------------------------------------------------------------------- /src/qldb/JournalS3ExportReader.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { 20 | S3Client, 21 | ListObjectsV2Command, 22 | ListObjectsV2Output, 23 | ListObjectsV2Request, 24 | GetObjectRequest, 25 | GetObjectCommand, 26 | _Object, 27 | } from "@aws-sdk/client-s3"; 28 | import { 29 | JournalS3ExportDescription, 30 | S3ExportConfiguration 31 | } from "@aws-sdk/client-qldb"; 32 | import { dom } from "ion-js"; 33 | 34 | import { fromIon, JournalBlock } from "./JournalBlock"; 35 | import { log } from "./LogUtil"; 36 | 37 | /** 38 | * Compare the expected block range, derived from File Key, with the actual object content. 39 | * @param fileKey The key of data file containing the chunk of journal block. 40 | * The fileKey pattern is {[strandId].[firstSequenceNo]-[lastSequenceNo].ion}. 41 | * @param firstBlock The first block in the block chain for a particular journal strand. 42 | * @param lastBlock The last block in the block chain for a particular journal strand. 43 | * @throws Error: If the SequenceNo on the blockAddress does not match the expected SequenceNo. 44 | */ 45 | function compareKeyWithContentRange(fileKey: string, firstBlock: JournalBlock, lastBlock: JournalBlock): void { 46 | const sequenceNoRange: string = fileKey.split(".")[1]; 47 | const keyTokens: string[] = sequenceNoRange.split("-"); 48 | const startSequenceNo: string = keyTokens[0]; 49 | const lastsequenceNo: string = keyTokens[1]; 50 | 51 | if (firstBlock._blockAddress._sequenceNo.toString() !== startSequenceNo) { 52 | throw new Error(`Expected first block SequenceNo to be ${startSequenceNo}`); 53 | } 54 | if (lastBlock._blockAddress._sequenceNo.toString() !== lastsequenceNo) { 55 | throw new Error(`Expected last block SequenceNo to be ${lastsequenceNo}`); 56 | } 57 | } 58 | 59 | /** 60 | * Find the final manifest objects created after the completion of an export job. 61 | * @param objects List of objects in a particular bucket. 62 | * @returns The identifier for the final manifest object. 63 | * @throws Error: If the final manifest is not found. 64 | */ 65 | function filterForCompletedManifest(objects: _Object[]): string { 66 | const object: _Object = objects.find(({ Key }) => Key.endsWith("completed.manifest")); 67 | if (object) { 68 | return object.Key; 69 | } 70 | throw new Error("Completed manifest not found."); 71 | } 72 | 73 | /** 74 | * Find the initial manifest created at the beginning of an export request. 75 | * @param objects List of objects in a particular bucket. 76 | * @param manifest The expected identifier for the initial manifest. 77 | * @returns The identifier for the initial manifest object. 78 | * @throws Error: If the initial manifest is not found. 79 | */ 80 | function filterForInitialManifest(objects: _Object[], manifest: string): string { 81 | const object: _Object = objects.find(({ Key }) => Key === manifest); 82 | if (object) { 83 | return object.Key; 84 | } 85 | throw new Error("Initial manifest not found."); 86 | } 87 | 88 | /** 89 | * Retrieve the ordered list of data object keys within the given final manifest. 90 | * @param manifestObject The content of the final manifest. 91 | * For this to work, the value is expected to have the structure: 92 | * { 93 | * keys:[ 94 | * "2019/04/15/22/JdxjkR9bSYB5jMHWcI464T.1-4.ion", 95 | * "2019/04/15/22/JdxjkR9bSYB5jMHWcI464T.5-10.ion", 96 | * "2019/04/15/22/JdxjkR9bSYB5jMHWcI464T.11-12.ion" 97 | * ] 98 | * } 99 | * 100 | * @returns List of data object keys. 101 | */ 102 | function getDataFileKeysFromManifest(manifestObject: string): string[] { 103 | const listOfKeys: string[] = []; 104 | const manifestValue: dom.Value = dom.load(manifestObject); 105 | const keys: dom.Value[] = manifestValue.get("keys").elements(); 106 | keys.forEach((key: dom.Value) => { 107 | listOfKeys.push(key.stringValue()); 108 | }); 109 | return listOfKeys; 110 | } 111 | 112 | /** 113 | * Parse a S3 object's content for the journal data objects in Ion format. 114 | * @param s3Object The content within a S3 object. 115 | * @returns List of journal blocks. 116 | * @throws Error: If there is an error loading the journal. 117 | */ 118 | function getJournalBlocks(s3Object: string): JournalBlock[] { 119 | const journals: dom.Value[] = dom.loadAll(s3Object); 120 | const journalBlocks: JournalBlock[] = journals.map(journal => fromIon(journal)); 121 | return journalBlocks; 122 | } 123 | 124 | /** 125 | * Read the S3 export within a journal block. 126 | * @param describeJournalExportResult The result from the QLDB database describing a journal export. 127 | * @param s3Client The low-level S3 client. 128 | * @returns Promise which fulfills with a list of journal blocks. 129 | */ 130 | export async function readExport( 131 | describeJournalExportResult: JournalS3ExportDescription, 132 | s3Client: S3Client 133 | ): Promise { 134 | const exportConfiguration: S3ExportConfiguration = describeJournalExportResult.S3ExportConfiguration; 135 | const prefix: string = exportConfiguration.Prefix; 136 | const bucketName: string = exportConfiguration.Bucket; 137 | const request: ListObjectsV2Request = { 138 | Bucket: bucketName, 139 | Prefix: prefix 140 | }; 141 | const response: ListObjectsV2Output = await s3Client.send(new ListObjectsV2Command(request)); 142 | const objects: _Object[] = response.Contents; 143 | log("Found the following objects for list from S3:"); 144 | objects.forEach(function(object) { 145 | log(object.Key); 146 | }); 147 | 148 | // Validate initial manifest file was written. 149 | const expectedManifestKey: string = (`${prefix}${describeJournalExportResult.ExportId}.started.manifest`); 150 | const initialManifest: string = filterForInitialManifest(objects, expectedManifestKey); 151 | log(`Found the initial manifest with key: ${initialManifest}.`); 152 | 153 | // Find the final manifest file, it should contain the exportId in it. 154 | const completedManifestFileKey: string = filterForCompletedManifest(objects); 155 | let getObjectRequest: GetObjectRequest = { 156 | Bucket: bucketName, 157 | Key: completedManifestFileKey 158 | }; 159 | const completedManifestObject: string = (await s3Client.send( new GetObjectCommand(getObjectRequest))).Body.toString(); 160 | const dataFileKeys: string[] = getDataFileKeysFromManifest(completedManifestObject); 161 | 162 | log(`Found the following keys in the manifest files: ${JSON.stringify(dataFileKeys)}`); 163 | const journalBlocks: JournalBlock[] = []; 164 | 165 | for (const dataFileKey of dataFileKeys) { 166 | log(`Reading file with S3 key ${dataFileKey} from bucket: ${bucketName}`); 167 | getObjectRequest = { 168 | Bucket: bucketName, 169 | Key: dataFileKey 170 | }; 171 | const s3Object: string = (await s3Client.send( new GetObjectCommand(getObjectRequest))).Body.toString(); 172 | const blocks: JournalBlock[] = getJournalBlocks(s3Object); 173 | 174 | compareKeyWithContentRange(dataFileKey, blocks[0], blocks[blocks.length-1]); 175 | blocks.forEach(function(block) { 176 | journalBlocks.push(block); 177 | }); 178 | } 179 | return journalBlocks; 180 | } 181 | -------------------------------------------------------------------------------- /src/qldb/LogUtil.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 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 { config } from "aws-sdk"; 15 | 16 | config.logger = console; 17 | 18 | /** 19 | * Logs an error level message. 20 | * @param line The message to be logged. 21 | */ 22 | export function error(line: string): void { 23 | if (isLoggerSet()) { 24 | _prepend(line, "ERROR"); 25 | } 26 | } 27 | 28 | /** 29 | * Logs a message. 30 | * @param line The message to be logged. 31 | */ 32 | export function log(line: string): void { 33 | if (isLoggerSet()) { 34 | _prepend(line, "LOG"); 35 | } 36 | } 37 | 38 | /** 39 | * @returns A boolean indicating whether a logger has been set within the AWS SDK. 40 | */ 41 | export function isLoggerSet(): boolean { 42 | return config.logger !== null; 43 | } 44 | 45 | /** 46 | * Prepends a string identifier indicating the log level to the given log message, & writes or logs the given message 47 | * using the logger set in the AWS SDK. 48 | * @param line The message to be logged. 49 | * @param level The log level. 50 | */ 51 | function _prepend(line: string, level: string): void { 52 | if (config.logger) { 53 | if (typeof config.logger.log === "function") { 54 | config.logger.log(`[${level}][Node.js QLDB Sample Code] ${line}`); 55 | } else if (typeof config.logger.write === "function") { 56 | config.logger.write(`[${level}][Node.js QLDB Sample Code] ${line}\n`); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/qldb/Util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { Result, TransactionExecutor } from "amazon-qldb-driver-nodejs"; 20 | import { 21 | GetBlockResponse, 22 | GetDigestResponse, 23 | ValueHolder, 24 | } from "@aws-sdk/client-qldb"; 25 | import { 26 | decodeUtf8, 27 | dom, 28 | IonTypes, 29 | makePrettyWriter, 30 | makeReader, 31 | Reader, 32 | toBase64, 33 | Writer 34 | } from "ion-js"; 35 | 36 | import { error } from "./LogUtil"; 37 | 38 | 39 | 40 | /** 41 | * Returns the string representation of a given BlockResponse. 42 | * @param blockResponse The BlockResponse to convert to string. 43 | * @returns The string representation of the supplied BlockResponse. 44 | */ 45 | export function blockResponseToString(blockResponse: GetBlockResponse): string { 46 | let stringBuilder: string = ""; 47 | if (blockResponse.Block.IonText) { 48 | stringBuilder = stringBuilder + "Block: " + blockResponse.Block.IonText + ", "; 49 | } 50 | if (blockResponse.Proof.IonText) { 51 | stringBuilder = stringBuilder + "Proof: " + blockResponse.Proof.IonText; 52 | } 53 | stringBuilder = "{" + stringBuilder + "}"; 54 | const writer: Writer = makePrettyWriter(); 55 | const reader: Reader = makeReader(stringBuilder); 56 | writer.writeValues(reader); 57 | return decodeUtf8(writer.getBytes()); 58 | } 59 | 60 | /** 61 | * Returns the string representation of a given GetDigestResponse. 62 | * @param digestResponse The GetDigestResponse to convert to string. 63 | * @returns The string representation of the supplied GetDigestResponse. 64 | */ 65 | export function digestResponseToString(digestResponse: GetDigestResponse): string { 66 | let stringBuilder: string = ""; 67 | if (digestResponse.Digest) { 68 | stringBuilder += "Digest: " + JSON.stringify(toBase64( digestResponse.Digest)) + ", "; 69 | } 70 | if (digestResponse.DigestTipAddress.IonText) { 71 | stringBuilder += "DigestTipAddress: " + digestResponse.DigestTipAddress.IonText; 72 | } 73 | stringBuilder = "{" + stringBuilder + "}"; 74 | const writer: Writer = makePrettyWriter(); 75 | const reader: Reader = makeReader(stringBuilder); 76 | writer.writeValues(reader); 77 | return decodeUtf8(writer.getBytes()); 78 | } 79 | 80 | /** 81 | * Get the document IDs from the given table. 82 | * @param txn The {@linkcode TransactionExecutor} for lambda execute. 83 | * @param tableName The table name to query. 84 | * @param field A field to query. 85 | * @param value The key of the given field. 86 | * @returns Promise which fulfills with the document ID as a string. 87 | */ 88 | export async function getDocumentId( 89 | txn: TransactionExecutor, 90 | tableName: string, 91 | field: string, 92 | value: string 93 | ): Promise { 94 | const query: string = `SELECT id FROM ${tableName} AS t BY id WHERE t.${field} = ?`; 95 | let documentId: string = undefined; 96 | await txn.execute(query, value).then((result: Result) => { 97 | const resultList: dom.Value[] = result.getResultList(); 98 | if (resultList.length === 0) { 99 | throw new Error(`Unable to retrieve document ID using ${value}.`); 100 | } 101 | documentId = resultList[0].get("id").stringValue(); 102 | 103 | }).catch((err: any) => { 104 | error(`Error getting documentId: ${err}`); 105 | }); 106 | return documentId; 107 | } 108 | 109 | /** 110 | * Sleep for the specified amount of time. 111 | * @param ms The amount of time to sleep in milliseconds. 112 | * @returns Promise which fulfills with void. 113 | */ 114 | export function sleep(ms: number): Promise { 115 | return new Promise(resolve => setTimeout(resolve, ms)); 116 | } 117 | 118 | /** 119 | * Find the value of a given path in an Ion value. The path should contain a blob value. 120 | * @param value The Ion value that contains the journal block attributes. 121 | * @param path The path to a certain attribute. 122 | * @returns Uint8Array value of the blob, or null if the attribute cannot be found in the Ion value 123 | * or is not of type Blob 124 | */ 125 | export function getBlobValue(value: dom.Value, path: string): Uint8Array | null { 126 | const attribute: dom.Value = value.get(path); 127 | if (attribute !== null && attribute.getType() === IonTypes.BLOB) { 128 | return attribute.uInt8ArrayValue(); 129 | } 130 | return null; 131 | } 132 | 133 | /** 134 | * Returns the string representation of a given ValueHolder. 135 | * @param valueHolder The ValueHolder to convert to string. 136 | * @returns The string representation of the supplied ValueHolder. 137 | */ 138 | export function valueHolderToString(valueHolder: ValueHolder): string { 139 | const stringBuilder: string = `{ IonText: ${valueHolder.IonText}}`; 140 | const writer: Writer = makePrettyWriter(); 141 | const reader: Reader = makeReader(stringBuilder); 142 | writer.writeValues(reader); 143 | return decodeUtf8(writer.getBytes()); 144 | } 145 | -------------------------------------------------------------------------------- /src/qldb/Verifier.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { 20 | GetDigestCommandOutput, 21 | ValueHolder, 22 | } from "@aws-sdk/client-qldb"; 23 | import { createHash } from "crypto"; 24 | import { dom, toBase64 } from "ion-js"; 25 | 26 | import { getBlobValue } from "./Util"; 27 | 28 | const HASH_LENGTH: number = 32; 29 | const UPPER_BOUND: number = 8; 30 | 31 | /** 32 | * Build the candidate digest representing the entire ledger from the Proof hashes. 33 | * @param proof The Proof object. 34 | * @param leafHash The revision hash to pair with the first hash in the Proof hashes list. 35 | * @returns The calculated root hash. 36 | */ 37 | function buildCandidateDigest(proof: ValueHolder, leafHash: Uint8Array): Uint8Array { 38 | const parsedProof: Uint8Array[] = parseProof(proof); 39 | const rootHash: Uint8Array = calculateRootHashFromInternalHash(parsedProof, leafHash); 40 | return rootHash; 41 | } 42 | 43 | /** 44 | * Combine the internal hashes and the leaf hash until only one root hash remains. 45 | * @param internalHashes An array of hash values. 46 | * @param leafHash The revision hash to pair with the first hash in the Proof hashes list. 47 | * @returns The root hash constructed by combining internal hashes. 48 | */ 49 | function calculateRootHashFromInternalHash(internalHashes: Uint8Array[], leafHash: Uint8Array): Uint8Array { 50 | const rootHash: Uint8Array = internalHashes.reduce(joinHashesPairwise, leafHash); 51 | return rootHash; 52 | } 53 | 54 | /** 55 | * Compare two hash values by converting each Uint8Array byte, which is unsigned by default, 56 | * into a signed byte, assuming they are little endian. 57 | * @param hash1 The hash value to compare. 58 | * @param hash2 The hash value to compare. 59 | * @returns Zero if the hash values are equal, otherwise return the difference of the first pair of non-matching bytes. 60 | */ 61 | function compareHashValues(hash1: Uint8Array, hash2: Uint8Array): number { 62 | if (hash1.length !== HASH_LENGTH || hash2.length !== HASH_LENGTH) { 63 | throw new Error("Invalid hash."); 64 | } 65 | for (let i = hash1.length-1; i >= 0; i--) { 66 | const difference: number = (hash1[i]<<24 >>24) - (hash2[i]<<24 >>24); 67 | if (difference !== 0) { 68 | return difference; 69 | } 70 | } 71 | return 0; 72 | } 73 | 74 | /** 75 | * Helper method that concatenates two Uint8Array. 76 | * @param arrays List of array to concatenate, in the order provided. 77 | * @returns The concatenated array. 78 | */ 79 | function concatenate(...arrays: Uint8Array[]): Uint8Array { 80 | let totalLength = 0; 81 | for (const arr of arrays) { 82 | totalLength += arr.length; 83 | } 84 | const result = new Uint8Array(totalLength); 85 | let offset = 0; 86 | for (const arr of arrays) { 87 | result.set(arr, offset); 88 | offset += arr.length; 89 | } 90 | return result; 91 | } 92 | 93 | /** 94 | * Flip a single random bit in the given hash value. 95 | * This method is intended to be used for purpose of demonstrating the QLDB verification features only. 96 | * @param original The hash value to alter. 97 | * @returns The altered hash with a single random bit changed. 98 | */ 99 | export function flipRandomBit(original: any): Uint8Array { 100 | if (original.length === 0) { 101 | throw new Error("Array cannot be empty!"); 102 | } 103 | const bytePos: number = Math.floor(Math.random() * original.length); 104 | const bitShift: number = Math.floor(Math.random() * UPPER_BOUND); 105 | const alteredHash: Uint8Array = original; 106 | 107 | alteredHash[bytePos] = alteredHash[bytePos] ^ (1 << bitShift); 108 | return alteredHash; 109 | } 110 | 111 | /** 112 | * Take two hash values, sort them, concatenate them, and generate a new hash value from the concatenated values. 113 | * @param h1 Byte array containing one of the hashes to compare. 114 | * @param h2 Byte array containing one of the hashes to compare. 115 | * @returns The concatenated array of hashes. 116 | */ 117 | export function joinHashesPairwise(h1: Uint8Array, h2: Uint8Array): Uint8Array { 118 | if (h1.length === 0) { 119 | return h2; 120 | } 121 | if (h2.length === 0) { 122 | return h1; 123 | } 124 | let concat: Uint8Array; 125 | if (compareHashValues(h1, h2) < 0) { 126 | concat = concatenate(h1, h2); 127 | } else { 128 | concat = concatenate(h2, h1); 129 | } 130 | const hash = createHash('sha256'); 131 | hash.update(concat); 132 | const newDigest: Uint8Array = hash.digest(); 133 | return newDigest; 134 | } 135 | 136 | /** 137 | * Parse the Block object returned by QLDB and retrieve block hash. 138 | * @param valueHolder A structure containing an Ion string value. 139 | * @returns The block hash. 140 | */ 141 | export function parseBlock(valueHolder: ValueHolder): Uint8Array { 142 | const block: dom.Value = dom.load(valueHolder.IonText); 143 | const blockHash: Uint8Array = getBlobValue(block, "blockHash"); 144 | return blockHash; 145 | } 146 | 147 | /** 148 | * Parse the Proof object returned by QLDB into an iterator. 149 | * The Proof object returned by QLDB is a dictionary like the following: 150 | * {'IonText': '[{{}},{{}}]'} 151 | * @param valueHolder A structure containing an Ion string value. 152 | * @returns A list of hash values. 153 | */ 154 | function parseProof(valueHolder: ValueHolder): Uint8Array[] { 155 | const proofs : dom.Value = dom.load(valueHolder.IonText); 156 | return proofs.elements().map(proof => proof.uInt8ArrayValue()); 157 | } 158 | 159 | /** 160 | * Verify document revision against the provided digest. 161 | * @param documentHash The SHA-256 value representing the document revision to be verified. 162 | * @param digest The SHA-256 hash value representing the ledger digest. 163 | * @param proof The Proof object retrieved from GetRevision.getRevision. 164 | * @returns If the document revision verifies against the ledger digest. 165 | */ 166 | export function verifyDocument(documentHash: Uint8Array, digest: GetDigestCommandOutput["Digest"], proof: ValueHolder): boolean { 167 | const candidateDigest = buildCandidateDigest(proof, documentHash); 168 | return (toBase64( digest) === toBase64(candidateDigest)); 169 | } 170 | -------------------------------------------------------------------------------- /src/test/Sequence.test.ts: -------------------------------------------------------------------------------- 1 | import * as ListLedgers from "../ListLedgers"; 2 | import * as CreateLedger from "../CreateLedger"; 3 | import * as DescribeLedger from "../DescribeLedger"; 4 | import * as CreateTable from "../CreateTable"; 5 | import * as ConnectToLedger from "../ConnectToLedger"; 6 | import * as CreateIndex from "../CreateIndex"; 7 | import * as InsertDocument from "../InsertDocument"; 8 | import * as ScanTable from "../ScanTable"; 9 | import * as FindVehicles from "../FindVehicles"; 10 | import * as RegisterDriversLicense from "../RegisterDriversLicense"; 11 | import * as RenewDriversLicense from "../RenewDriversLicense"; 12 | import * as DeregisterDriversLicense from "../DeregisterDriversLicense"; 13 | import * as TransferVehicleOwnership from "../TransferVehicleOwnership"; 14 | import * as AddSecondaryOwner from "../AddSecondaryOwner"; 15 | import * as QueryHistory from "../QueryHistory"; 16 | import * as TagResources from "../TagResources"; 17 | import * as DeletionProtection from "../DeletionProtection"; 18 | import * as InsertIonTypes from "../InsertIonTypes"; 19 | import * as ExportJournal from "../ExportJournal"; 20 | import * as DescribeJournalExport from "../DescribeJournalExport"; 21 | import * as ListJournalExports from "../ListJournalExports"; 22 | import * as ValidateQldbHashchain from "../ValidateQldbHashchain"; 23 | import * as GetDigest from "../GetDigest"; 24 | import * as GetRevision from "../GetRevision"; 25 | import * as GetBlock from "../GetBlock"; 26 | import * as RedactRevision from "../RedactRevision"; 27 | import * as DeleteLedger from "../DeleteLedger"; 28 | 29 | import { LEDGER_NAME, LEDGER_NAME_FOR_DELETIONS } from "../qldb/Constants"; 30 | 31 | import * as chai from "chai"; 32 | 33 | describe("Run Sample App", function () { 34 | 35 | this.timeout(0); 36 | 37 | it("Can list ledgers", async () => { 38 | await ListLedgers.main(); 39 | }) 40 | 41 | it("Can create ledger", async () => { 42 | await CreateLedger.main(); 43 | }) 44 | 45 | it("Can describe ledger", async () => { 46 | const actual = await DescribeLedger.main(); 47 | chai.assert.equal(actual.Name, LEDGER_NAME); 48 | chai.assert.equal(actual.State, "ACTIVE"); 49 | }) 50 | 51 | it("Can tag resources", async () => { 52 | const actual = await TagResources.main(); 53 | const expected = [ 54 | { 55 | "Domain": "Test", 56 | "IsTest": "true" 57 | }, 58 | { 59 | "Domain": "Prod", 60 | "IsTest": "true" 61 | }, 62 | { 63 | "Domain": "Prod" 64 | } 65 | ]; 66 | chai.assert.equal(actual.length, 3); 67 | chai.assert.deepEqual(actual[0], expected[0]); 68 | chai.assert.deepEqual(actual[1], expected[1]); 69 | chai.assert.deepEqual(actual[2], expected[2]); 70 | }) 71 | 72 | it("Can set deletion protection and delete ledger", async () => { 73 | const updateDeletionProtectionResult = await DeletionProtection.main(LEDGER_NAME_FOR_DELETIONS); 74 | chai.assert.equal(updateDeletionProtectionResult.Name, LEDGER_NAME_FOR_DELETIONS); 75 | chai.assert.isFalse(updateDeletionProtectionResult.DeletionProtection); 76 | }) 77 | 78 | it("Can create tables", async () => { 79 | await CreateTable.main(); 80 | }) 81 | 82 | it("Can connect to ledger", async () => { 83 | await ConnectToLedger.main(); 84 | }) 85 | 86 | it("Can create indexes", async () => { 87 | await CreateIndex.main(); 88 | }) 89 | 90 | it("Can insert documents", async () => { 91 | await InsertDocument.main(); 92 | }) 93 | 94 | it("Can scan tables", async () => { 95 | await ScanTable.main(); 96 | }) 97 | 98 | it("Can find vehicles", async () => { 99 | const actual = await FindVehicles.main(); 100 | const expected = { 101 | "Vehicle": { 102 | "VIN": "1N4AL11D75C109151", 103 | "Type": "Sedan", 104 | "Year": 2011, 105 | "Make": "Audi", 106 | "Model": "A5", 107 | "Color": "Silver" 108 | } 109 | }; 110 | chai.assert.equal(actual.length, 1); 111 | chai.assert.isTrue(actual[0].equals(expected)); 112 | }) 113 | 114 | it("Can register drivers license", async () => { 115 | const actual = await RegisterDriversLicense.main(); 116 | chai.assert.equal(actual.length, 1); 117 | chai.assert.equal(actual[0].allFields().length, 1); 118 | chai.assert.equal(actual[0].allFields()[0][0], "documentId"); 119 | }) 120 | 121 | it("Can renew drivers license", async () => { 122 | const actual = await RenewDriversLicense.main(); 123 | chai.assert.equal(actual.length, 2); 124 | chai.assert.isTrue(actual[0].ionEquals(actual[1])); 125 | chai.assert.equal(actual[0].allFields().length, 1); 126 | chai.assert.equal(actual[0].allFields()[0][0], "documentId"); 127 | }) 128 | 129 | it("Can deregister drivers license", async () => { 130 | const actual = await DeregisterDriversLicense.main(); 131 | chai.assert.equal(actual.length, 1); 132 | chai.assert.equal(actual[0].allFields().length, 1); 133 | chai.assert.equal(actual[0].allFields()[0][0], "documentId"); 134 | }) 135 | 136 | it("Can transfer vehicle ownership", async () => { 137 | const actual = await TransferVehicleOwnership.main(); 138 | chai.assert.equal(actual.length, 1); 139 | chai.assert.equal(actual[0].allFields().length, 1); 140 | chai.assert.equal(actual[0].allFields()[0][0], "documentId"); 141 | }) 142 | 143 | it("Can add secondary owner", async () => { 144 | const actual = await AddSecondaryOwner.main(); 145 | chai.assert.equal(actual.length, 1); 146 | chai.assert.equal(actual[0].allFields().length, 1); 147 | chai.assert.equal(actual[0].allFields()[0][0], "documentId"); 148 | }) 149 | 150 | it("Can query history", async () => { 151 | const actual = await QueryHistory.main(); 152 | chai.assert.equal(actual.length, 0); 153 | }) 154 | 155 | it("Can insert ion types", async () => { 156 | await InsertIonTypes.main(); 157 | }) 158 | 159 | it("Can perform journal export functionalities", async () => { 160 | const exportResult = await ExportJournal.main(true); 161 | chai.assert.isNotNull(exportResult.ExportId) 162 | 163 | const describeExportResult = await DescribeJournalExport.main(exportResult.ExportId); 164 | chai.assert.equal(describeExportResult.ExportDescription.LedgerName, LEDGER_NAME); 165 | chai.assert.equal(describeExportResult.ExportDescription.Status, "COMPLETED"); 166 | 167 | const listExportResults = await ListJournalExports.main(); 168 | chai.assert.equal(listExportResults.length, 1); 169 | chai.assert.equal(listExportResults[0].LedgerName, LEDGER_NAME); 170 | chai.assert.equal(listExportResults[0].Status, "COMPLETED"); 171 | 172 | await ValidateQldbHashchain.main(exportResult.ExportId); 173 | }) 174 | 175 | it("Can perform journal verification functionalities", async () => { 176 | const digest = await GetDigest.main(); 177 | chai.assert.isNotNull(digest); 178 | await GetRevision.main(); 179 | await GetBlock.main(); 180 | }) 181 | 182 | it("Can perform redaction", async() => { 183 | await RedactRevision.main(); 184 | }) 185 | 186 | it("Can delete ledger", async () => { 187 | await DeleteLedger.main(); 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": [ "es6" ], 6 | "strict": true, 7 | "strictNullChecks": false, 8 | "declaration": true, 9 | "outDir": "dist" 10 | }, 11 | "include": [ 12 | "src/**/*.ts", 13 | "index.ts" 14 | ] 15 | } 16 | --------------------------------------------------------------------------------