├── .eslintrc.json ├── .github └── workflows │ ├── build_napi.yml │ └── package.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── deps └── README.txt ├── package-lock.json ├── package.json ├── resources ├── icon │ ├── dark │ │ ├── bucket.svg │ │ ├── collection.svg │ │ ├── documents.svg │ │ └── scope.svg │ ├── icon_128x128.png │ ├── light │ │ ├── bucket.svg │ │ ├── collection.svg │ │ ├── documents.svg │ │ └── scope.svg │ └── logo.svg └── readme │ ├── browse_documents.gif │ ├── cblite_context.png │ ├── createdoc.gif │ ├── get_document.gif │ ├── open_pallete.gif │ ├── open_rightclick.gif │ ├── run_query.gif │ └── update_document.gif ├── scripts ├── build_native.ps1 └── build_native.sh ├── src ├── cbliteworkspace │ ├── databaseStatusBarItem.ts │ ├── documentDatabaseBindings.ts │ └── index.ts ├── common │ └── index.ts ├── configuration │ └── index.ts ├── constants │ └── constants.ts ├── explorer │ ├── explorerTreeProvider.ts │ ├── index.ts │ └── treeItem.ts ├── extension.ts ├── logging │ └── logger.ts ├── native │ ├── Blob.cc │ ├── Blob.hh │ ├── Collection.cc │ ├── Collection.hh │ ├── CouchbaseWrapper.hh │ ├── Database.cc │ ├── Database.hh │ ├── DatabaseConfiguration.cc │ ├── DatabaseConfiguration.hh │ ├── Document.cc │ ├── Document.hh │ ├── Query.cc │ ├── Query.hh │ ├── binding.ts │ ├── cblite.cc │ └── cblite.hh ├── providers │ └── sqlpp.provider.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts ├── utils │ ├── clipboard.ts │ └── files.ts └── vscodewrapper │ ├── errorMessage.ts │ ├── index.ts │ ├── quickPick.ts │ └── workspace.ts ├── standalone_test └── test.js ├── syntaxes └── sqlpp.tmLanguage.json ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention":[ 13 | "warn", 14 | { 15 | "selector": "accessor", 16 | "modifiers": ["private"], 17 | "format": ["camelCase"], 18 | "leadingUnderscore": "require" 19 | }, 20 | { 21 | "selector": "variable", 22 | "format": ["camelCase", "PascalCase"] 23 | }, 24 | { 25 | "selector": "enumMember", 26 | "format": ["UPPER_CASE"] 27 | } 28 | ], 29 | "@typescript-eslint/semi": "warn", 30 | "curly": "warn", 31 | "eqeqeq": "warn", 32 | "no-throw-literal": "warn", 33 | "semi": "off" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/build_napi.yml: -------------------------------------------------------------------------------- 1 | name: Build NAPI Addons 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | build_napi: 13 | name: Build 14 | strategy: 15 | matrix: 16 | kind: ['linux', 'macos', 'windows'] 17 | include: 18 | - kind: linux 19 | os: ubuntu-latest 20 | script: build_native.sh 21 | - kind: macos 22 | os: macOS-latest 23 | script: build_native.sh 24 | - kind: windows 25 | os: windows-latest 26 | script: build_native.ps1 27 | runs-on: ${{ matrix.os }} 28 | 29 | steps: 30 | - uses: actions/checkout@v2 31 | 32 | - name: Compile 33 | working-directory: ${{ github.workspace }}/scripts 34 | run: ./${{ matrix.script }} 35 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Package VSCode Plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | prepare: 10 | name: Build Native Dependencies 11 | strategy: 12 | matrix: 13 | kind: ['linux', 'macos', 'windows'] 14 | include: 15 | - kind: linux 16 | os: ubuntu-latest 17 | script: build_native.sh 18 | folder_name: Linux 19 | - kind: macos 20 | os: macOS-latest 21 | script: build_native.sh 22 | folder_name: Darwin 23 | - kind: windows 24 | os: windows-latest 25 | script: build_native.ps1 26 | folder_name: Windows 27 | 28 | runs-on: ${{ matrix.os }} 29 | 30 | steps: 31 | - uses: actions/checkout@v2 32 | 33 | - name: Compile 34 | working-directory: ${{ github.workspace }}/scripts 35 | run: ./${{ matrix.script }} 36 | 37 | - name: Save Artifact 38 | uses: actions/upload-artifact@v2 39 | with: 40 | name: ${{ matrix.folder_name }} 41 | path: ${{ github.workspace }}/out/${{ matrix.folder_name }} 42 | retention-days: 1 43 | 44 | publish: 45 | name: Publish 46 | runs-on: ubuntu-latest 47 | needs: prepare 48 | steps: 49 | - uses: actions/checkout@v2 50 | 51 | - name: Retrieve Linux Artifact 52 | uses: actions/download-artifact@v2 53 | with: 54 | name: Linux 55 | path: ${{ github.workspace }}/out/Linux 56 | 57 | - name: Retrieve macOS Artifact 58 | uses: actions/download-artifact@v2 59 | with: 60 | name: Darwin 61 | path: ${{ github.workspace }}/out/Darwin 62 | 63 | - name: Retrieve Windows Artifact 64 | uses: actions/download-artifact@v2 65 | with: 66 | name: Windows 67 | path: ${{ github.workspace }}/out/Windows 68 | 69 | - name: Package 70 | run: | 71 | mkdir -p ${{ github.workspace }}/dist 72 | short_filename=$(ls ${{ github.workspace }}/out/Darwin | grep -P "libcblite\.\d\.dylib") 73 | cp ${{ github.workspace }}/out/Darwin/$short_filename ${{ github.workspace }}/dist 74 | cp ${{ github.workspace }}/out/Windows/cblite.dll ${{ github.workspace }}/dist 75 | npm install 76 | npm run package-only 77 | 78 | 79 | - name: Publish 80 | run: vsce publish -p {{ secrets.VSCODE_PAT }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | build/ 3 | dist/ 4 | node_modules/ 5 | deps/* 6 | !deps/README.txt 7 | 8 | .vscode-test/ 9 | *.vsix 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "eamodio.tsl-problem-matcher" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "extensionHost", 9 | "request": "launch", 10 | "name": "Launch Extension", 11 | "runtimeExecutable": "${execPath}", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js*" 17 | ], 18 | "preLaunchTask": "npm: compile" 19 | }, 20 | { 21 | "type": "pwa-node", 22 | "request": "launch", 23 | "name": "Launch Node Test", 24 | "args": [ 25 | "--napi-modules", 26 | "${workspaceFolder}/standalone_test/test.js" 27 | ], 28 | "preLaunchTask": "npm: compile", 29 | "skipFiles": [ 30 | "/**" 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "dist": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "dist": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off", 11 | "files.associations": { 12 | "system_error": "cpp", 13 | "algorithm": "cpp", 14 | "array": "cpp", 15 | "atomic": "cpp", 16 | "bit": "cpp", 17 | "cctype": "cpp", 18 | "charconv": "cpp", 19 | "chrono": "cpp", 20 | "clocale": "cpp", 21 | "cmath": "cpp", 22 | "compare": "cpp", 23 | "concepts": "cpp", 24 | "cstddef": "cpp", 25 | "cstdint": "cpp", 26 | "cstdio": "cpp", 27 | "cstdlib": "cpp", 28 | "cstring": "cpp", 29 | "ctime": "cpp", 30 | "cwchar": "cpp", 31 | "exception": "cpp", 32 | "format": "cpp", 33 | "forward_list": "cpp", 34 | "functional": "cpp", 35 | "initializer_list": "cpp", 36 | "iomanip": "cpp", 37 | "ios": "cpp", 38 | "iosfwd": "cpp", 39 | "iostream": "cpp", 40 | "istream": "cpp", 41 | "iterator": "cpp", 42 | "limits": "cpp", 43 | "list": "cpp", 44 | "locale": "cpp", 45 | "map": "cpp", 46 | "memory": "cpp", 47 | "mutex": "cpp", 48 | "new": "cpp", 49 | "optional": "cpp", 50 | "ostream": "cpp", 51 | "ratio": "cpp", 52 | "sstream": "cpp", 53 | "stdexcept": "cpp", 54 | "stop_token": "cpp", 55 | "streambuf": "cpp", 56 | "string": "cpp", 57 | "thread": "cpp", 58 | "tuple": "cpp", 59 | "type_traits": "cpp", 60 | "typeinfo": "cpp", 61 | "unordered_map": "cpp", 62 | "utility": "cpp", 63 | "vector": "cpp", 64 | "xfacet": "cpp", 65 | "xhash": "cpp", 66 | "xiosbase": "cpp", 67 | "xlocale": "cpp", 68 | "xlocbuf": "cpp", 69 | "xlocinfo": "cpp", 70 | "xlocmes": "cpp", 71 | "xlocmon": "cpp", 72 | "xlocnum": "cpp", 73 | "xloctime": "cpp", 74 | "xmemory": "cpp", 75 | "xstddef": "cpp", 76 | "xstring": "cpp", 77 | "xtr1common": "cpp", 78 | "xtree": "cpp", 79 | "xutility": "cpp" 80 | } 81 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": ["$ts-webpack-watch"], 10 | "isBackground": true, 11 | "presentation": { 12 | "echo": true, 13 | "reveal": "never", 14 | "focus": false, 15 | "panel": "shared", 16 | "showReuseMessage": true, 17 | "clear": false 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .eslintrc.json 2 | .gitignore 3 | .github/ 4 | .vscodeignore 5 | CMakeLists.txt 6 | README.md 7 | tsconfig.json 8 | *.vsix 9 | webpack.config.js 10 | 11 | .vscode/ 12 | .vscode-test/ 13 | build/ 14 | deps/ 15 | node_modules/ 16 | out/ 17 | src/ 18 | standalone_test/ 19 | syntaxes/ 20 | resources/readme -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.3 - 3 Mar 2022 2 | 3 | * Add support for opening encrypted databases created by non-C versions of the SDK 4 | 5 | ## 0.1.2 - 2 Mar 2022 6 | 7 | * Add macOS M1 Support 8 | 9 | ## 0.1.1 - 23 Feb 2022 10 | 11 | * Add icons to explorer tree view 12 | * Fix issues with updated underlying library and doc IDs, creating documents, etc 13 | * Migrate N1QL references to SQL++ 14 | 15 | ## 0.1.0 - 18 Jan 2022 16 | 17 | * Initial Release (GH only) -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(cblite-js) 2 | cmake_minimum_required(VERSION 3.10) 3 | set (CMAKE_CXX_STANDARD 17) 4 | set (CMAKE_C_STANDARD 11) 5 | 6 | if(APPLE) 7 | set (CMAKE_INSTALL_RPATH "@loader_path") 8 | set (CMAKE_OSX_ARCHITECTURES "arm64;x86_64") 9 | else() 10 | set (CMAKE_INSTALL_RPATH "\$ORIGIN") 11 | endif() 12 | 13 | set( 14 | SRC_FILES 15 | src/native/DatabaseConfiguration.cc 16 | src/native/Database.cc 17 | src/native/Document.cc 18 | src/native/Query.cc 19 | src/native/Blob.cc 20 | src/native/Collection.cc 21 | src/native/cblite.cc 22 | ) 23 | 24 | include_directories(${CMAKE_JS_INC}) 25 | add_library(${PROJECT_NAME} SHARED ${SRC_FILES} ${CMAKE_JS_SRC}) 26 | set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") 27 | set_property(DIRECTORY APPEND PROPERTY COMPILE_DEFINITIONS 28 | $<$:DEBUG> 29 | ) 30 | target_include_directories( 31 | ${PROJECT_NAME} 32 | PRIVATE 33 | ${CMAKE_CURRENT_SOURCE_DIR}/deps/include 34 | ${CMAKE_CURRENT_SOURCE_DIR}/node_modules/node-addon-api 35 | ) 36 | 37 | target_compile_definitions( 38 | ${PROJECT_NAME} 39 | PUBLIC 40 | NAPI_VERSION=6 41 | ) 42 | 43 | find_package( 44 | CouchbaseLite 3.1.0 REQUIRED 45 | HINTS ${CMAKE_CURRENT_SOURCE_DIR}/deps 46 | ) 47 | 48 | target_link_libraries( 49 | ${PROJECT_NAME} 50 | PRIVATE 51 | ${CMAKE_JS_LIB} 52 | cblite 53 | ) 54 | 55 | install( 56 | TARGETS ${PROJECT_NAME} 57 | DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/out/${CMAKE_SYSTEM_NAME} 58 | ) 59 | 60 | 61 | 62 | # Install the runtime runtime files into both location (node.js and vscode extension) 63 | 64 | # Note: Later when we get a portable libcblite.so, reinstate this line! 65 | # file(GLOB_RECURSE LINUX_LIBS ${CouchbaseLite_DIR}/../../../libcblite.so*) 66 | file(GLOB_RECURSE MAC_LIBS ${CouchbaseLite_DIR}/../../../lib/libcblite*.dylib) 67 | install( 68 | FILES 69 | ${CouchbaseLite_DIR}/../../../bin/cblite.dll 70 | ${MAC_LIBS} 71 | #${LINUX_LIBS} 72 | DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/out/${CMAKE_SYSTEM_NAME} 73 | OPTIONAL 74 | ) 75 | 76 | install( 77 | FILES 78 | ${CouchbaseLite_DIR}/../../../bin/cblite.dll 79 | ${MAC_LIBS} 80 | #${LINUX_LIBS} 81 | DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/dist 82 | OPTIONAL 83 | ) 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Couchbase Lite for VSCode 2 | 3 | This is a Visual Studio Code extension for interacting with a Couchbase Lite database. [Couchbase Lite](https://docs.couchbase.com/couchbase-lite/3.0/index.html) is full featured NoSQL JSON document database for mobile, desktop and embedded platforms. This extension includes support for enumerating documents and running ad-hoc queries. This plugin is heavily inspired by the [SQLite extension](https://github.com/AlexCovizzi/vscode-sqlite/) that provides similar functionality. Some icons used in the product were downloaded from [IconScout](https://iconscout.com/) via the free license. 4 | 5 | ## Quick Tour 6 | 7 | ### Open a Database 8 | 9 | This can be accomplished in a few different ways. 10 | - The fastest way is to have Visual Studio Code open with a workspace that contains some `cblite2` databases inside. Then you can right click and open as in the following: 11 | 12 | Open Database With Right Click 13 | 14 | - If you have opened a folder as described above you can also use the *Open Database* command from the [command pallete](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette), and any cblite2 databases discovered will be listed, with a final entry (*Choose database from file*) to open using a standard file open dialog. 15 | 16 | Open Database With Command Pallete 17 | 18 | 19 | :warning: Opening a database using this plugin will automatically upgrade it to 3.0 version of Couchbase Lite silently. If this is undesirable, make a copy of the database folder before opening it with the plugin. 20 | 21 | ### Interact With Documents 22 | 23 | Once you have opened a database, you can browse its contents in the *CBLite Explorer* pane that appears on the left hand side of Visual Studio Code: 24 | 25 | Browse through Database Documents 26 | 27 | If you want to retrieve the contents of a given document, you can right click it and choose *Get Document* 28 | 29 | Retrieve Document From Database 30 | 31 | Now notice the status bar shows that you are currently working in the context of the document you opened: 32 | 33 | cblite Status Bar Context 34 | 35 | You can now make changes and save back to the database by using the *Update Document* command: 36 | 37 | Save Document To Database 38 | 39 | ### Querying 40 | 41 | By default only [SQL++ (aka N1QL)](https://docs.couchbase.com/couchbase-lite/3.0/c/query-n1ql-mobile.html#query-format) commands are enabled, and so only those will be covered here. Beware of dragons if you enable JSON queries. 42 | 43 | To run a query, right click on an opened database and select *New SQL++ Query*. That will open a new document editor in which you can run your query and get the results: 44 | 45 | Run a Query 46 | 47 | ### Create Document 48 | 49 | To create a document, create a file of type "json" and add the JSON document contents. Then select *Create New Document* from the command palette to add the document to the database. 50 | 51 | Create a document 52 | 53 | ## Building 54 | 55 | - First, clone this repo 56 | 57 | ```bash 58 | git clone https://github.com/couchbaselabs/vscode-cblite 59 | 60 | ``` 61 | 62 | - Then, since this plugin relies on the Couchbase Lite for C API, [download](https://www.couchbase.com/downloads?family=couchbase-lite) Couchbase Lite C SDK and copy the contents from the root folder of the package into the *deps* folder as indicated in [deps/README.txt](deps/README.txt). 63 | 64 | - Next, run `npm install` to install all the needed dependency node modules into the project folder. 65 | 66 | - Lastly, webpack requires all references native shared libraries to exist and will fail to build without products present for all three supported platforms (Windows, macOS, Linux). The current platform's library will get built when performing a build, but the remaining two will not. If you are developing, it is enough to just shim them in as empty files (i.e. `touch` / `New-Item`). The error message that happens when you build should be indicative, but the location the libraries are expected are `out/Windows/cblite-js.node`, `out/Darwin/cblite-js.node` and `out/Linux/cblite-js.node`. 67 | 68 | 69 | ### Debug, for local testing 70 | 71 | - Opening the root directory of the project with Visual Studio Code and pressing `F5` will build and open the extension for debugging. 72 | 73 | - However, if you want to build from the command line then running `npm run compile` will get you the final product, unpackaged. 74 | 75 | - The notable parts are `dist/extension.js`, containing the final Javascript, and various flavors of `cblite-js-.node` which are the result of the native compilation. Also any of the downloaded cblite C shared libraries will also be present (i.e. cblite.dll, libcblite.dylib, etc) on Windows or macOS. 76 | 77 | ### Release, for packaging 78 | 79 | - `npm run package` will perform similar steps but make a production build instead. 80 | 81 | Note: if you've built a debug build you will need to erase the `build/` directory first. 82 | 83 | - The final product of this is `vscode-cblite-.vsix` and can be installed into a Visual Studio Code instance either through the extension manager, or by running `code --install-extension ` on the command line. 84 | 85 | ### Use as node.js Binding 86 | 87 | The [repo](https://github.com/couchbaselabs/vscode-cblite) can also be used as a standalone node.js binding for Couchbase Lite for C. 88 | 89 | For this, 90 | 91 | - Follow the normal process of building as described in "Building" section above. 92 | 93 | - The file `out/native/binding.js` can be imported into a node.js application. 94 | 95 | - You must also copy the cblite shared binary (i.e. libcblite.so.3 libcblite.dylib or cblite.dll) into `build/Release`(or `build/Debug`) folder (which will contain `cblite-js.node*`file). 96 | -------------------------------------------------------------------------------- /deps/README.txt: -------------------------------------------------------------------------------- 1 | Extract the downloadable package into this folder and move everything out of the top level folder (e.g. mv libcblite-3.0.0/* .). In other words you should see the following inside of the deps folder: 2 | 3 | include/ 4 | lib/ 5 | bin/ (Windows only, contains .dll) 6 | 7 | The CMake project will take care of downloading the C++ headers that are needed. If this structure is not obeyed you will get a scary looking error about being unable to find Couchbase Lite libraries. 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-cblite", 3 | "icon": "resources/icon/icon_128x128.png", 4 | "displayName": "Couchbase Lite", 5 | "description": "Explore and query Couchbase Lite databases", 6 | "version": "0.2.0", 7 | "publisher": "Couchbase", 8 | "homepage": "https://www.couchbase.com/products/lite", 9 | "repository": { 10 | "url": "https://github.com/couchbaselabs/vscode-cblite", 11 | "type": "git" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/couchbaselabs/vscode-cblite/issues" 15 | }, 16 | "license": "Apache-2.0", 17 | "engines": { 18 | "vscode": "^1.30.0" 19 | }, 20 | "categories": [ 21 | "Other" 22 | ], 23 | "activationEvents": [ 24 | "onLanguage:db", 25 | "onLanguage:sqlpp", 26 | "onLanguage:json", 27 | "onCommand:cblite.showOutputChannel", 28 | "onCommand:cblite.explorer.add", 29 | "onCommand:cblite.explorer.remove", 30 | "onCommand:cblite.newQuerySqlpp", 31 | "onCommand:cblite.newQueryJson" 32 | ], 33 | "cmake-js": { 34 | "runtime": "node", 35 | "runtimeVersion": "14.15.0" 36 | }, 37 | "main": "./dist/extension", 38 | "contributes": { 39 | "viewsContainers": { 40 | "activitybar": [ 41 | { 42 | "id": "cblite_container", 43 | "title": "Couchbase", 44 | "icon": "resources/icon/logo.svg" 45 | } 46 | ] 47 | }, 48 | "commands": [ 49 | { 50 | "command": "cblite.showOutputChannel", 51 | "title": "Show output", 52 | "category": "cblite" 53 | }, 54 | { 55 | "command": "cblite.explorer.add", 56 | "title": "Open Database", 57 | "category": "cblite" 58 | }, 59 | { 60 | "command": "cblite.explorer.remove", 61 | "title": "Close Database", 62 | "category": "cblite" 63 | }, 64 | { 65 | "command": "cblite.explorer.copyKey", 66 | "title": "Copy Keypath", 67 | "category": "cblite" 68 | }, 69 | { 70 | "command": "cblite.explorer.copyValue", 71 | "title": "Copy Value", 72 | "category": "cblite" 73 | }, 74 | { 75 | "command": "cblite.explorer.copyPath", 76 | "title": "Copy Path", 77 | "category": "cblite" 78 | }, 79 | { 80 | "command": "cblite.explorer.copyRelativePath", 81 | "title": "Copy Relative Path", 82 | "category": "cblite" 83 | }, 84 | { 85 | "command": "cblite.newCollection", 86 | "title": "New Collection", 87 | "category": "cblite" 88 | }, 89 | { 90 | "command": "cblite.explorer.lookupDocument", 91 | "title": "Get Document", 92 | "category": "cblite" 93 | }, 94 | { 95 | "command": "cblite.deleteCollection", 96 | "title": "Delete Collection", 97 | "category": "cblite" 98 | }, 99 | { 100 | "command": "cblite.runDocumentQuery", 101 | "title": "Run Query", 102 | "category": "cblite" 103 | }, 104 | { 105 | "command": "cblite.useDatabase", 106 | "title": "Use Database", 107 | "category": "cblite" 108 | }, 109 | { 110 | "command": "cblite.newQuerySqlpp", 111 | "title": "New SQL++ Query", 112 | "category": "cblite" 113 | }, 114 | { 115 | "command": "cblite.newQueryJson", 116 | "title": "New JSON Query", 117 | "category": "cblite", 118 | "enablement": "config.cblite.enableJSON" 119 | }, 120 | { 121 | "command": "cblite.createDocument", 122 | "title": "Create New Document", 123 | "category": "cblite" 124 | }, 125 | { 126 | "command": "cblite.updateDocument", 127 | "title": "Update Document", 128 | "category": "cblite" 129 | }, 130 | { 131 | "command": "cblite.explorer.getDocument", 132 | "title": "Get Document", 133 | "category": "cblite" 134 | } 135 | ], 136 | "keybindings": [ 137 | { 138 | "command": "cblite.runDocumentQuery", 139 | "key": "ctrl+shift+q", 140 | "mac": "cmd+shift+q", 141 | "when": "editorLangId =~ /sqlpp/ || (config.cblite.enableJSON && editorLangId =~ /json/)" 142 | } 143 | ], 144 | "views": { 145 | "cblite_container": [ 146 | { 147 | "id": "cblite.explorer", 148 | "name": "CBLite explorer", 149 | "when": "cblite.explorer.show" 150 | } 151 | ] 152 | }, 153 | "menus": { 154 | "commandPalette": [ 155 | { 156 | "command": "cblite.showOutputChannel", 157 | "group": "cblite" 158 | }, 159 | { 160 | "command": "cblite.explorer.add", 161 | "group": "cblite" 162 | }, 163 | { 164 | "command": "cblite.explorer.remove", 165 | "group": "cblite", 166 | "when": "cblite.explorer.show" 167 | }, 168 | { 169 | "command": "cblite.explorer.copyKey", 170 | "group": "cblite", 171 | "when": "false" 172 | }, 173 | { 174 | "command": "cblite.explorer.copyValue", 175 | "group": "cblite", 176 | "when": "false" 177 | }, 178 | { 179 | "command": "cblite.explorer.copyPath", 180 | "group": "cblite", 181 | "when": "false" 182 | }, 183 | { 184 | "command": "cblite.explorer.copyRelativePath", 185 | "group": "cblite", 186 | "when": "false" 187 | }, 188 | { 189 | "command": "cblite.newQuerySqlpp", 190 | "group": "cblite" 191 | }, 192 | { 193 | "command": "cblite.newQueryJson", 194 | "group": "cblite", 195 | "when": "config.cblite.enableJSON" 196 | }, 197 | { 198 | "command": "cblite.runDocumentQuery", 199 | "when": "editorLangId =~ /sqlpp/ || (config.cblite.enableJSON && editorLangId =~ /json/)", 200 | "group": "cblite" 201 | }, 202 | { 203 | "command": "cblite.useDatabase", 204 | "when": "editorLangId =~ /sqlpp/ || editorLangId =~ /json/", 205 | "group": "cblite" 206 | }, 207 | { 208 | "command": "cblite.createDocument", 209 | "when": "editorLangId =~ /json/", 210 | "group": "cblite" 211 | }, 212 | { 213 | "command": "cblite.updateDocument", 214 | "when": "editorLangId =~ /json/", 215 | "group": "cblite" 216 | } 217 | ], 218 | "explorer/context": [ 219 | { 220 | "command": "cblite.explorer.add", 221 | "when": "resourceLangId == cblite && explorerResourceIsFolder", 222 | "group": "navigation@1" 223 | } 224 | ], 225 | "editor/context": [ 226 | { 227 | "when": "editorTextFocus && editorLangId =~ /(sqlpp|json)/", 228 | "command": "cblite.useDatabase", 229 | "group": "9.1_cblite@3" 230 | } 231 | ], 232 | "view/item/context": [ 233 | { 234 | "command": "cblite.newQuerySqlpp", 235 | "when": "view == cblite.explorer && viewItem == cblite.databaseItem", 236 | "group": "2_query@1" 237 | }, 238 | { 239 | "command": "cblite.newQueryJson", 240 | "when": "view == cblite.explorer && viewItem == cblite.databaseItem", 241 | "group": "2_query@2" 242 | }, 243 | { 244 | "command": "cblite.newCollection", 245 | "when": "view == cblite.explorer && viewItem == cblite.databaseItem", 246 | "group": "3_collection@1" 247 | }, 248 | { 249 | "command": "cblite.explorer.copyPath", 250 | "when": "view == cblite.explorer && viewItem == cblite.databaseItem", 251 | "group": "4_copy@1" 252 | }, 253 | { 254 | "command": "cblite.explorer.copyRelativePath", 255 | "when": "view == cblite.explorer && viewItem == cblite.databaseItem", 256 | "group": "4_copy@2" 257 | }, 258 | { 259 | "command": "cblite.explorer.remove", 260 | "when": "view == cblite.explorer && viewItem == cblite.databaseItem", 261 | "group": "5_utils@1" 262 | }, 263 | { 264 | "command": "cblite.explorer.lookupDocument", 265 | "when": "view == cblite.explorer && viewItem == cblite.DBCollectionItem" 266 | }, 267 | { 268 | "command": "cblite.deleteCollection", 269 | "when": "view == cblite.explorer && viewItem == cblite.DBCollectionItem" 270 | }, 271 | { 272 | "command": "cblite.explorer.copyKey", 273 | "when": "view == cblite.explorer && viewItem == cblite.documentItem", 274 | "group": "3_copy@1" 275 | }, 276 | { 277 | "command": "cblite.explorer.getDocument", 278 | "when": "view == cblite.explorer && viewItem == cblite.documentItem", 279 | "group": "4_utils@1" 280 | }, 281 | { 282 | "command": "cblite.explorer.copyKey", 283 | "when": "view == cblite.explorer && viewItem == cblite.collectionItem", 284 | "group": "3_copy@1" 285 | }, 286 | { 287 | "command": "cblite.explorer.copyKey", 288 | "when": "view == cblite.explorer && viewItem == cblite.keyItem", 289 | "group": "3_copy@1" 290 | }, 291 | { 292 | "command": "cblite.explorer.copyKey", 293 | "when": "view == cblite.explorer && viewItem == cblite.valueItem", 294 | "group": "3_copy@1" 295 | }, 296 | { 297 | "command": "cblite.explorer.copyValue", 298 | "when": "view == cblite.explorer && viewItem == cblite.valueItem", 299 | "group": "3_copy@2" 300 | } 301 | ] 302 | }, 303 | "languages": [ 304 | { 305 | "id": "sqlpp", 306 | "aliases": [ 307 | "SQL++" 308 | ], 309 | "extensions": [ 310 | ".sqlpp" 311 | ] 312 | }, 313 | { 314 | "id": "cblite", 315 | "aliases": [ 316 | "Database" 317 | ], 318 | "extensions": [ 319 | ".cblite2" 320 | ] 321 | } 322 | ], 323 | "grammars": [ 324 | { 325 | "language": "sqlpp", 326 | "scopeName": "source.sql.sqlpp", 327 | "path": "./syntaxes/sqlpp.tmLanguage.json" 328 | } 329 | ], 330 | "configuration": { 331 | "type": "object", 332 | "title": "CBLite Configuration", 333 | "properties": { 334 | "cblite.logLevel": { 335 | "type": "string", 336 | "enum": [ 337 | "DEBUG", 338 | "INFO", 339 | "WARN", 340 | "ERROR" 341 | ], 342 | "default": "INFO", 343 | "description": "Logging level in the output channel (DEBUG, INFO, WARN, ERROR)" 344 | }, 345 | "cblite.enableJSON": { 346 | "type": "boolean", 347 | "default": false, 348 | "description": "[ADVANCED] If enabled, adds the ability to run JSON queries in addition to SQL++" 349 | } 350 | } 351 | } 352 | }, 353 | "scripts": { 354 | "build-native": "cmake-js compile -T install", 355 | "compile": "cmake-js compile --debug -T install && tsc && webpack --mode development", 356 | "lint": "eslint src --ext ts", 357 | "pretest": "npm run compile && npm run lint", 358 | "test": "node --napi-modules standalone_test/test.js", 359 | "package-only": "webpack --mode production && vsce package", 360 | "package": "npm run build-native && webpack --mode production && vsce package", 361 | "tsc": "tsc" 362 | }, 363 | "devDependencies": { 364 | "@eslint/eslintrc": "^0.1.3", 365 | "@types/better-sqlite3": "^7.4.1", 366 | "@types/node": "^12.11.7", 367 | "@types/vscode": "^1.23.0", 368 | "@typescript-eslint/eslint-plugin": "^5.5.0", 369 | "@typescript-eslint/parser": "^5.5.0", 370 | "cmake-js": "^6.3.0", 371 | "eslint": "^7.11.0", 372 | "glob": "^7.1.6", 373 | "node-loader": "^2.0.0", 374 | "typescript": "^4.0.2", 375 | "vsce": "^2.5.1", 376 | "vscode-test": "^1.4.0", 377 | "webpack-cli": "^4.9.1" 378 | }, 379 | "dependencies": { 380 | "bindings": "^1.5.0", 381 | "json-bigint": "^1.0.0", 382 | "node-addon-api": "^4.0.0", 383 | "ts-loader": "^9.2.6" 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /resources/icon/dark/bucket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/dark/collection.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/dark/documents.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/dark/scope.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/icon/icon_128x128.png -------------------------------------------------------------------------------- /resources/icon/light/bucket.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/light/collection.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/light/documents.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/light/scope.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/icon/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/readme/browse_documents.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/browse_documents.gif -------------------------------------------------------------------------------- /resources/readme/cblite_context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/cblite_context.png -------------------------------------------------------------------------------- /resources/readme/createdoc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/createdoc.gif -------------------------------------------------------------------------------- /resources/readme/get_document.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/get_document.gif -------------------------------------------------------------------------------- /resources/readme/open_pallete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/open_pallete.gif -------------------------------------------------------------------------------- /resources/readme/open_rightclick.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/open_rightclick.gif -------------------------------------------------------------------------------- /resources/readme/run_query.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/run_query.gif -------------------------------------------------------------------------------- /resources/readme/update_document.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/couchbaselabs/vscode-cblite/2cedd3f4c63185671d50149e281975a8d8c6627a/resources/readme/update_document.gif -------------------------------------------------------------------------------- /scripts/build_native.ps1: -------------------------------------------------------------------------------- 1 | Push-Location $PSScriptRoot/.. 2 | 3 | $CBL_C_VERSION = "3.1.0" 4 | $CBL_C_VERSION_SHORT = "3.1.0" 5 | $DOWNLOAD_URL = "https://packages.couchbase.com/releases/couchbase-lite-c/$CBL_C_VERSION/couchbase-lite-c-enterprise-$CBL_C_VERSION-windows-x86_64.zip" 6 | 7 | Push-Location deps 8 | Invoke-WebRequest $DOWNLOAD_URL -OutFile cbl.zip 9 | Expand-Archive cbl.zip -DestinationPath $pwd 10 | Copy-Item -Recurse libcblite-$CBL_C_VERSION_SHORT/* . 11 | Remove-Item -Recurse libcblite-$CBL_C_VERSION_SHORT/* 12 | Remove-Item cbl.zip 13 | Pop-Location 14 | 15 | npm install 16 | npm run build-native 17 | 18 | Pop-Location -------------------------------------------------------------------------------- /scripts/build_native.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | ROOT="$( cd "$(dirname "$0")" ; pwd -P )/.." 4 | pushd $ROOT 5 | 6 | CBL_C_VERSION="3.1.0" 7 | CBL_C_VERSION_SHORT="3.1.0" 8 | 9 | case "$OSTYPE" in 10 | linux*) 11 | DOWNLOAD_URL="https://packages.couchbase.com/releases/couchbase-lite-c/$CBL_C_VERSION/couchbase-lite-c-enterprise-$CBL_C_VERSION-linux-x86_64.tar.gz" 12 | DOWNLOAD_FILENAME="cbl.tar.gz" 13 | EXPAND="tar xf" 14 | ;; 15 | darwin*) 16 | DOWNLOAD_URL="https://packages.couchbase.com/releases/couchbase-lite-c/$CBL_C_VERSION/couchbase-lite-c-enterprise-$CBL_C_VERSION-macos.zip" 17 | DOWNLOAD_FILENAME="cbl.zip" 18 | EXPAND="unzip" 19 | ;; 20 | esac 21 | 22 | pushd deps 23 | wget $DOWNLOAD_URL -O $DOWNLOAD_FILENAME 24 | $EXPAND $DOWNLOAD_FILENAME 25 | mv libcblite-$CBL_C_VERSION_SHORT/* . 26 | rmdir libcblite-$CBL_C_VERSION_SHORT 27 | rm $DOWNLOAD_FILENAME 28 | popd 29 | 30 | npm install 31 | npm run build-native -------------------------------------------------------------------------------- /src/cbliteworkspace/databaseStatusBarItem.ts: -------------------------------------------------------------------------------- 1 | import { DocumentDatabaseBindings } from "./documentDatabaseBindings"; 2 | import { StatusBarItem, window, StatusBarAlignment, Disposable, workspace } from "vscode"; 3 | 4 | export class DatabaseStatusBarItem implements Disposable { 5 | private _disposable: Disposable; 6 | private _statusBarItem: StatusBarItem; 7 | 8 | constructor(private documentDatabase: DocumentDatabaseBindings) { 9 | let subscriptions: Disposable[] = []; 10 | 11 | this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Right, 100); 12 | this._statusBarItem.command = "cblite.useDatabase"; 13 | subscriptions.push(this._statusBarItem); 14 | 15 | subscriptions.push(window.onDidChangeActiveTextEditor(e => this.update())); 16 | subscriptions.push(window.onDidChangeTextEditorViewColumn(e => this.update())); 17 | subscriptions.push(workspace.onDidOpenTextDocument(e => this.update())); 18 | subscriptions.push(workspace.onDidCloseTextDocument(e => this.update())); 19 | 20 | this._disposable = Disposable.from(...subscriptions); 21 | } 22 | 23 | update() { 24 | let doc = window.activeTextEditor && 25 | (window.activeTextEditor.document.languageId === 'sqlpp' || window.activeTextEditor.document.languageId === 'json') ? 26 | window.activeTextEditor.document : undefined; 27 | 28 | if(doc) { 29 | let db = this.documentDatabase.getDatabase(doc); 30 | let dbPath: string; 31 | let dbName: string; 32 | if(db) { 33 | dbPath = db.path; 34 | dbName = db.name; 35 | } else { 36 | dbPath = "No database"; 37 | dbName = dbPath; 38 | } 39 | 40 | this._statusBarItem.tooltip = `cblite: ${dbPath}`; 41 | this._statusBarItem.text = `cblite: ${dbName}`; 42 | 43 | let cblDoc = this.documentDatabase.getDocument(doc); 44 | if(cblDoc) { 45 | this._statusBarItem.tooltip += ` | ${cblDoc.collection.scopeName}.${cblDoc.collection.name}.${cblDoc.id}`; 46 | this._statusBarItem.text += ` | ${cblDoc.id}`; 47 | } 48 | 49 | this._statusBarItem.show(); 50 | } else { 51 | this._statusBarItem.hide(); 52 | } 53 | } 54 | 55 | dispose() { 56 | this._disposable.dispose(); 57 | } 58 | } -------------------------------------------------------------------------------- /src/cbliteworkspace/documentDatabaseBindings.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument } from "vscode"; 2 | import { Database, MutableDocument } from "../native/binding"; 3 | 4 | class DocumentCBLiteInfo { 5 | db?: Database; 6 | doc?: MutableDocument; 7 | } 8 | 9 | interface Bindings { 10 | [documentId: string]: DocumentCBLiteInfo 11 | } 12 | 13 | export class DocumentDatabaseBindings { 14 | private bindings: Bindings; 15 | 16 | constructor() { 17 | this.bindings = {}; 18 | } 19 | 20 | bindDatabase(document: TextDocument | undefined, dbObj: Database): boolean { 21 | if(document) { 22 | let key = document.uri.toString(); 23 | if(!(key in this.bindings)) { 24 | this.bindings[key] = new DocumentCBLiteInfo(); 25 | } 26 | 27 | this.bindings[document.uri.toString()].db = dbObj; 28 | return true; 29 | } 30 | 31 | return false; 32 | } 33 | 34 | bindDocument(document: TextDocument | undefined, cblDocument: MutableDocument): boolean { 35 | if(document) { 36 | let key = document.uri.toString(); 37 | if(!(key in this.bindings)) { 38 | this.bindings[key] = new DocumentCBLiteInfo(); 39 | } 40 | 41 | this.bindings[document.uri.toString()].doc = cblDocument; 42 | return true; 43 | } 44 | 45 | return false; 46 | } 47 | 48 | getDatabase(document: TextDocument | undefined): Database | undefined { 49 | if(document) { 50 | return this.bindings[document.uri.toString()]?.db; 51 | } 52 | 53 | return undefined; 54 | } 55 | 56 | getDocument(document: TextDocument | undefined): MutableDocument | undefined { 57 | if(document) { 58 | return this.bindings[document.uri.toString()]?.doc; 59 | } 60 | 61 | return undefined; 62 | } 63 | 64 | unbindDatabase(dbObj: Database) { 65 | Object.keys(this.bindings).forEach((docId) => { 66 | if(this.bindings[docId].db === dbObj) { 67 | this.bindings[docId].db = undefined; 68 | if(!this.bindings[docId].doc) { 69 | delete this.bindings[docId]; 70 | } 71 | } 72 | }); 73 | } 74 | 75 | unbindDocument(doc: MutableDocument) { 76 | Object.keys(this.bindings).forEach((docId) => { 77 | if(this.bindings[docId].doc === doc) { 78 | this.bindings[docId].doc = undefined; 79 | if(!this.bindings[docId].db) { 80 | delete this.bindings[docId]; 81 | } 82 | } 83 | }); 84 | } 85 | } -------------------------------------------------------------------------------- /src/cbliteworkspace/index.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument, Disposable } from "vscode"; 2 | import { DocumentDatabaseBindings } from "./documentDatabaseBindings"; 3 | import { DatabaseStatusBarItem } from "./databaseStatusBarItem"; 4 | import { Database, MutableDocument } from "../native/binding"; 5 | 6 | class CbliteWorkspace implements Disposable { 7 | private disposable: Disposable; 8 | 9 | private documentDatabaseBindings: DocumentDatabaseBindings; 10 | private databaseStatusBarItem: DatabaseStatusBarItem; 11 | 12 | constructor() { 13 | let subscriptions = []; 14 | 15 | this.documentDatabaseBindings = new DocumentDatabaseBindings(); 16 | this.databaseStatusBarItem = new DatabaseStatusBarItem(this.documentDatabaseBindings); 17 | 18 | subscriptions.push(this.databaseStatusBarItem); 19 | 20 | this.disposable = Disposable.from(...subscriptions); 21 | } 22 | 23 | bindDatabaseToDocument(dbObj: Database, cbliteDocument: TextDocument): boolean { 24 | let success = this.documentDatabaseBindings.bindDatabase(cbliteDocument, dbObj); 25 | if(success) { 26 | this.databaseStatusBarItem.update(); 27 | } 28 | 29 | return success; 30 | } 31 | 32 | bindDocToDocument(doc: MutableDocument, cbliteDocument: TextDocument): boolean { 33 | let success = this.documentDatabaseBindings.bindDocument(cbliteDocument, doc); 34 | if(success) { 35 | this.databaseStatusBarItem.update(); 36 | } 37 | 38 | return success; 39 | } 40 | 41 | getDocumentDatabase(cbliteDocument: TextDocument): Database | undefined { 42 | return this.documentDatabaseBindings.getDatabase(cbliteDocument); 43 | } 44 | 45 | getDocumentDoc(cbliteDocument: TextDocument): MutableDocument | undefined { 46 | return this.documentDatabaseBindings.getDocument(cbliteDocument); 47 | } 48 | 49 | dispose() { 50 | this.disposable.dispose(); 51 | } 52 | } 53 | 54 | export default CbliteWorkspace; -------------------------------------------------------------------------------- /src/common/index.ts: -------------------------------------------------------------------------------- 1 | import { ShowMoreItem } from "../explorer/treeItem"; 2 | import { openDbAtPath } from "../utils/files"; 3 | import { Collection, Database, MutableDocument, QueryLanguage } from "../native/binding"; 4 | 5 | export type SchemaItem = SchemaDatabase | SchemaScope | SchemaCollection | SchemaDocument | SchemaKey | SchemaValue | ShowMore; 6 | 7 | export interface ShowMore { 8 | parent: SchemaCollection 9 | } 10 | 11 | export interface SchemaDatabase { 12 | obj: Database, 13 | scopes: SchemaScope[] 14 | } 15 | 16 | export interface SchemaScope { 17 | name: string, 18 | collections: SchemaCollection[], 19 | parent: SchemaDatabase 20 | } 21 | 22 | export interface SchemaCollection { 23 | obj: Collection, 24 | documents: SchemaDocument[], 25 | limit: number, 26 | parent: SchemaScope 27 | } 28 | 29 | export interface SchemaDocument { 30 | id: string, 31 | keys: SchemaKey[], 32 | parent: SchemaCollection 33 | } 34 | 35 | export interface SchemaKey { 36 | name: string, 37 | parent: SchemaKey|SchemaDocument, 38 | scalar?: SchemaValue, 39 | array?: SchemaValue[], 40 | dict?: SchemaKey[] 41 | } 42 | 43 | export interface SchemaValue { 44 | parent: SchemaKey, 45 | value: any 46 | } 47 | 48 | export type Schema = SchemaDatabase; 49 | 50 | export async function buildSchema(dbPath: string): Promise { 51 | let db = await openDbAtPath(dbPath); 52 | if(!db) { 53 | return undefined; 54 | } 55 | 56 | let schema = { 57 | obj: db, 58 | scopes: [] 59 | } as SchemaDatabase; 60 | 61 | let scopes = db.getScopeNames(); 62 | scopes.forEach(scope => { 63 | let scopeSchema = { 64 | name: scope, 65 | collections: [], 66 | parent: schema 67 | } as SchemaScope; 68 | 69 | schema.scopes.push(scopeSchema); 70 | let collections = db!.getCollectionNames(scope); 71 | collections.forEach(collection => { 72 | let collectionSchema = { 73 | obj: db!.getCollection(collection, scope), 74 | documents: [], 75 | limit: ShowMoreItem.batchSize, 76 | parent: scopeSchema 77 | } as SchemaCollection; 78 | 79 | scopeSchema.collections.push(collectionSchema); 80 | let query = db!.createQuery(QueryLanguage.SQLPP, "SELECT * AS body, meta().id FROM " + scope + "." + collection); 81 | let results = query.execute(); 82 | 83 | results.forEach(raw => { 84 | let r = raw["body"]; 85 | let doc: SchemaDocument = { 86 | id: raw["id"], 87 | keys: [], 88 | parent: collectionSchema 89 | }; 90 | 91 | for(let key in r) { 92 | recursiveBuild(doc, doc.keys, r[key], key); 93 | } 94 | 95 | collectionSchema.documents.push(doc); 96 | }); 97 | }); 98 | }); 99 | 100 | return schema; 101 | } 102 | 103 | export function buildDocumentSchema(c: SchemaCollection, doc: MutableDocument) : SchemaDocument | undefined { 104 | let schemaDoc: SchemaDocument = { 105 | parent: c, 106 | id: doc.id, 107 | keys: [] 108 | }; 109 | 110 | let content = JSON.parse(doc.propertiesAsJSON()); 111 | for(let key in content) { 112 | recursiveBuild(schemaDoc, schemaDoc.keys, content[key], key); 113 | } 114 | 115 | return schemaDoc; 116 | } 117 | 118 | function recursiveBuild(owner: SchemaDocument|SchemaKey, collection: any[], item: any, key?: string): void { 119 | if(key === "_id") { 120 | return; 121 | } 122 | 123 | if(Array.isArray(item)) { 124 | let c: SchemaKey = { 125 | name: key!, 126 | array: [], 127 | parent: owner 128 | }; 129 | 130 | item.forEach((val) => { 131 | recursiveBuild(c, c.array!, val); 132 | }); 133 | 134 | collection.push(c); 135 | } else if(item?.constructor === Object) { 136 | let c: SchemaKey = { 137 | name: key!, 138 | dict: [], 139 | parent: owner 140 | }; 141 | 142 | for(let k in item) { 143 | recursiveBuild(c, c.dict!, item[k], k); 144 | } 145 | 146 | collection.push(c); 147 | } else { 148 | if(key) { 149 | let toInsert = {name: key, parent: owner} as SchemaKey; 150 | toInsert.scalar = {value: item, parent: toInsert}; 151 | collection.push(toInsert); 152 | } else { 153 | collection.push({value: item, parent: owner} as SchemaValue); 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /src/configuration/index.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | import { Level } from "../logging/logger"; 3 | 4 | const properties = require('../../package.json').contributes.configuration.properties; 5 | 6 | export interface Configuration { 7 | cblite: string; 8 | logLevel: string; 9 | } 10 | 11 | export function getConfiguration() { 12 | return { 13 | cblite: _cblite(), 14 | logLevel: _logLevel() 15 | } as Configuration; 16 | } 17 | 18 | function _cblite() : string { 19 | let cbliteConf = workspace.getConfiguration().get('cblite.cblite'); 20 | let cblite = cbliteConf ? cbliteConf.toString() : ''; 21 | return cblite; 22 | } 23 | 24 | function _logLevel() : string { 25 | let logLevelConf = workspace.getConfiguration().get('cblite.logLevel'); 26 | let logLevel : string = properties["cblite.logLevel"]["default"]; 27 | if(logLevelConf && (Level)[`${logLevelConf}`] !== null) { 28 | logLevel = logLevelConf.toString(); 29 | } 30 | 31 | return logLevel; 32 | } -------------------------------------------------------------------------------- /src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | const pkg = require('../../package.json'); 2 | 3 | export namespace Constants { 4 | /* extension */ 5 | export const extensionName = pkg.name; 6 | export const extentionDisplayName = pkg.displayName; 7 | export const extensionVersion = pkg.version; 8 | 9 | /* output channel */ 10 | export const outputChannelName = `${extentionDisplayName}`; 11 | 12 | /* explorer */ 13 | export const cbliteExplorerViewId = pkg.contributes.views.cblite_container[0].id; 14 | } -------------------------------------------------------------------------------- /src/explorer/explorerTreeProvider.ts: -------------------------------------------------------------------------------- 1 | import { SchemaDatabase, SchemaItem, ShowMore } from "../common"; 2 | import { Event, EventEmitter, ProviderResult, TreeDataProvider, TreeItem } from "vscode"; 3 | import { DBCollectionItem, DBItem, DocumentItem, KeyItem, ScopeItem, ShowMoreItem, ValueItem } from "./treeItem"; 4 | import { Database } from "../native/binding"; 5 | 6 | export class ExplorerTreeProvider implements TreeDataProvider { 7 | private _onDidChangeTreeData: EventEmitter = new EventEmitter(); 8 | readonly onDidChangeTreeData: Event = this._onDidChangeTreeData.event; 9 | 10 | private databaseList: SchemaDatabase[]; 11 | 12 | constructor() { 13 | this.databaseList = []; 14 | } 15 | 16 | refresh(): void { 17 | this._onDidChangeTreeData.fire(); 18 | } 19 | 20 | addToTree(database: SchemaDatabase): Number { 21 | let index = this.databaseList.findIndex(db => db.obj === database.obj); 22 | if(index < 0) { 23 | this.databaseList.push(database); 24 | } else { 25 | this.databaseList[index] = database; 26 | } 27 | 28 | this.refresh(); 29 | return this.databaseList.length; 30 | } 31 | 32 | removeFromTree(dbObj: Database): Number { 33 | let index = this.databaseList.findIndex(db => db.obj === dbObj); 34 | if(index > -1) { 35 | this.databaseList.splice(index, 1); 36 | } 37 | 38 | this.refresh(); 39 | return this.databaseList.length; 40 | } 41 | 42 | getTreeItem(item: SchemaItem): TreeItem { 43 | if('scopes' in item) { 44 | return new DBItem(item.obj.path); 45 | } else if('collections' in item) { 46 | return new ScopeItem(item.name); 47 | } else if('documents' in item) { 48 | return new DBCollectionItem(item.obj.name); 49 | } else if('keys' in item) { 50 | return new DocumentItem(item.id); 51 | } else if('name' in item) { 52 | return new KeyItem(item.name); 53 | } else if(!('value' in item)) { 54 | return new ShowMoreItem(item.parent, this); 55 | } 56 | 57 | return new ValueItem(item.value); 58 | } 59 | 60 | getDatabaseList() { 61 | return this.databaseList; 62 | } 63 | 64 | getChildren(item?: SchemaItem): ProviderResult { 65 | if(item) { 66 | if('scopes' in item) { 67 | return item.scopes; 68 | } else if('collections' in item) { 69 | return item.collections; 70 | } else if('documents' in item) { 71 | if(item.limit < item.documents.length) { 72 | var l = []; 73 | l.push(... item.documents.slice(0, item.limit)); 74 | l.push({parent: item} as ShowMore); 75 | return l; 76 | } else { 77 | return item.documents; 78 | } 79 | } else if('keys' in item) { 80 | return item.keys; 81 | } else if('array' in item) { 82 | return item.array!; 83 | } else if('dict' in item) { 84 | return item.dict!; 85 | } else if('scalar' in item) { 86 | return [item.scalar!]; 87 | } 88 | 89 | return undefined; 90 | } else { 91 | return this.databaseList; 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/explorer/index.ts: -------------------------------------------------------------------------------- 1 | import { commands, Disposable, ExtensionContext, window } from "vscode"; 2 | import { SchemaCollection, SchemaDatabase, SchemaDocument } from "../common"; 3 | import { Constants } from "../constants/constants"; 4 | import { Collection, Database } from "../native/binding"; 5 | import { ExplorerTreeProvider } from "./explorerTreeProvider"; 6 | import * as treeItem from "./treeItem"; 7 | import { ShowMoreItem } from "./treeItem"; 8 | 9 | type NewType = SchemaDatabase; 10 | 11 | class Explorer implements Disposable { 12 | private disposable: Disposable; 13 | private explorerTreeProvider: ExplorerTreeProvider; 14 | 15 | constructor(context: ExtensionContext) { 16 | let subscriptions = []; 17 | 18 | this.explorerTreeProvider = new ExplorerTreeProvider(); 19 | subscriptions.push(window.createTreeView(Constants.cbliteExplorerViewId, { treeDataProvider: this.explorerTreeProvider })); 20 | 21 | this.disposable = Disposable.from(...subscriptions); 22 | } 23 | 24 | add(database: NewType) { 25 | let length = this.explorerTreeProvider.addToTree(database); 26 | if(length > 0) { 27 | commands.executeCommand('setContext', 'cblite.explorer.show', true); 28 | commands.executeCommand('cblite.explorer.focus'); 29 | } 30 | 31 | } 32 | 33 | addCollection(schemaDb: SchemaDatabase, c: Collection): void { 34 | let scopeIndex = schemaDb.scopes.findIndex(s => s.name == c.scopeName); 35 | if(scopeIndex == -1) { 36 | scopeIndex = schemaDb.scopes.length; 37 | schemaDb.scopes.push({ 38 | name: c.scopeName, 39 | collections: [], 40 | parent: schemaDb 41 | }); 42 | } 43 | 44 | let schemaScope = schemaDb.scopes[scopeIndex]; 45 | if(schemaDb.scopes.findIndex(col => col.name == c.name) != -1) { 46 | return; 47 | } 48 | 49 | schemaScope.collections.push({ 50 | obj: c, 51 | documents: [], 52 | limit: ShowMoreItem.batchSize, 53 | parent: schemaScope 54 | }); 55 | 56 | this.refresh(); 57 | } 58 | 59 | removeCollection(c: SchemaCollection): void { 60 | let schemaScope = c.parent; 61 | let collectionIndex = schemaScope.collections.indexOf(c); 62 | if(collectionIndex == -1) { 63 | return; 64 | } 65 | 66 | schemaScope.collections.splice(collectionIndex, 1); 67 | if(schemaScope.collections.length === 0) { 68 | let scopeIndex = schemaScope.parent.scopes.indexOf(schemaScope); 69 | if(scopeIndex == -1) { 70 | return; 71 | } 72 | 73 | schemaScope.parent.scopes.splice(scopeIndex, 1); 74 | } 75 | 76 | this.refresh(); 77 | } 78 | 79 | update(collection: SchemaCollection, document: SchemaDocument): void { 80 | let index = collection.documents.findIndex(d => d.id === document.id); 81 | if(index > -1) { 82 | collection.documents[index] = document; 83 | } else { 84 | collection.documents.push(document); 85 | } 86 | 87 | this.refresh(); 88 | } 89 | 90 | get(dbObj: Database) : SchemaDatabase | undefined { 91 | return this.explorerTreeProvider.getDatabaseList().find(x => x.obj.path === dbObj.path); 92 | } 93 | 94 | getCollection(dbObj: Database, c: Collection) : SchemaCollection | undefined { 95 | let db = this.get(dbObj); 96 | if(!db) { 97 | return undefined; 98 | } 99 | 100 | let sIndex = db.scopes.findIndex(s => s.name == c.scopeName); 101 | if(sIndex == -1) { 102 | return undefined; 103 | } 104 | 105 | let schemaScope = db.scopes[sIndex]; 106 | let cIndex = schemaScope.collections.findIndex(col => col.obj.name == c.name); 107 | if(cIndex == -1) { 108 | return undefined; 109 | } 110 | 111 | return schemaScope.collections[cIndex]; 112 | } 113 | 114 | remove(dbObj: Database) { 115 | let length = this.explorerTreeProvider.removeFromTree(dbObj); 116 | if(length === 0) { 117 | setTimeout(() => { 118 | commands.executeCommand("setContext", "cblite.explorer.show", false); 119 | }, 100); 120 | } 121 | } 122 | 123 | list() { 124 | return this.explorerTreeProvider.getDatabaseList(); 125 | } 126 | 127 | refresh() { 128 | this.explorerTreeProvider.refresh(); 129 | } 130 | 131 | dispose() { 132 | this.disposable.dispose(); 133 | } 134 | } 135 | 136 | export type SqlppItem = treeItem.SqlppItem; 137 | export type DBItem = treeItem.DBItem; 138 | export type DocumentItem = treeItem.DocumentItem; 139 | 140 | export default Explorer; -------------------------------------------------------------------------------- /src/explorer/treeItem.ts: -------------------------------------------------------------------------------- 1 | //import { Schema } from "inspector"; 2 | import { basename, join } from "path"; 3 | import { Command, TreeItem, TreeItemCollapsibleState } from "vscode"; 4 | import { SchemaCollection } from "../common"; 5 | import { ExplorerTreeProvider } from "./explorerTreeProvider"; 6 | 7 | export interface SqlppTree { 8 | [dbPath: string]: DBItem; 9 | } 10 | 11 | export class SqlppItem extends TreeItem { 12 | constructor(public readonly name: string, public readonly label: string, 13 | public readonly collapsibleState: TreeItemCollapsibleState, 14 | public readonly command?: Command) { 15 | super(label, collapsibleState); 16 | } 17 | } 18 | 19 | export class DBItem extends SqlppItem { 20 | constructor(public dbPath: string, command?: Command) { 21 | super(dbPath, basename(dbPath), TreeItemCollapsibleState.Collapsed, command); 22 | 23 | this.contextValue = "cblite.databaseItem"; 24 | this.iconPath = { 25 | light: join(__dirname, "..", "resources", "icon", "light", "bucket.svg"), 26 | dark: join(__dirname, "..", "resources", "icon", "dark", "bucket.svg") 27 | }; 28 | } 29 | 30 | // @ts-ignore 31 | get tooltip(): string { 32 | return this.dbPath; 33 | } 34 | } 35 | 36 | export class ScopeItem extends SqlppItem { 37 | constructor(readonly name: string, command?: Command) { 38 | super(name, name, TreeItemCollapsibleState.Collapsed, command); 39 | 40 | this.contextValue = "cblite.scopeItem"; 41 | this.iconPath = { 42 | light: join(__dirname, "..", "resources", "icon", "light", "scope.svg"), 43 | dark: join(__dirname, "..", "resources", "icon", "dark", "scope.svg") 44 | }; 45 | } 46 | 47 | // @ts-ignore 48 | get tooltip(): string { 49 | return this.name; 50 | } 51 | } 52 | 53 | export class DBCollectionItem extends SqlppItem { 54 | constructor(readonly name: string, command?: Command) { 55 | super(name, name, TreeItemCollapsibleState.Collapsed, command); 56 | 57 | this.contextValue = "cblite.DBCollectionItem"; 58 | this.iconPath = { 59 | light: join(__dirname, "..", "resources", "icon", "light", "collection.svg"), 60 | dark: join(__dirname, "..", "resources", "icon", "dark", "collection.svg") 61 | }; 62 | } 63 | 64 | // @ts-ignore 65 | get tooltip(): string { 66 | return this.name; 67 | } 68 | } 69 | 70 | export class DocumentItem extends SqlppItem { 71 | constructor(readonly id: string, command?: Command) { 72 | super(id, id, TreeItemCollapsibleState.Collapsed, command); 73 | 74 | this.contextValue = "cblite.documentItem"; 75 | this.iconPath = { 76 | light: join(__dirname, "..", "resources", "icon", "light", "documents.svg"), 77 | dark: join(__dirname, "..", "resources", "icon", "dark", "documents.svg") 78 | }; 79 | } 80 | 81 | // @ts-ignore 82 | get tooltip(): string { 83 | return this.name; 84 | } 85 | } 86 | 87 | export class ValueItem extends SqlppItem { 88 | constructor(value: any, command?: Command) { 89 | super(value ? value.toString() : "(null)", value ? value.toString() : "(null)", TreeItemCollapsibleState.None, command); 90 | 91 | this.contextValue = "cblite.valueItem"; 92 | } 93 | 94 | // @ts-ignore 95 | get tooltip(): string { 96 | return this.name; 97 | } 98 | } 99 | 100 | export class CollectionItem extends SqlppItem { 101 | constructor(key: string, command?: Command) { 102 | super(key, key, TreeItemCollapsibleState.Collapsed, command); 103 | 104 | this.contextValue = "cblite.collectionItem"; 105 | } 106 | 107 | // @ts-ignore 108 | get tooltip(): string { 109 | return this.name; 110 | } 111 | } 112 | 113 | export class KeyItem extends SqlppItem { 114 | constructor(key: string, command?: Command) { 115 | super(key, key, TreeItemCollapsibleState.Collapsed, command); 116 | 117 | this.contextValue = "cblite.keyItem"; 118 | } 119 | 120 | // @ts-ignore 121 | get tooltip(): string { 122 | return this.name; 123 | } 124 | } 125 | 126 | export class ShowMoreItem extends TreeItem { 127 | static readonly batchSize = 50; 128 | 129 | readonly parent: SchemaCollection; 130 | readonly tree: ExplorerTreeProvider; 131 | 132 | constructor(parent: SchemaCollection, tree: ExplorerTreeProvider) { 133 | super("Show More...", TreeItemCollapsibleState.None); 134 | 135 | this.parent = parent; 136 | this.tree = tree; 137 | this.contextValue = "cblite.showMoreItem"; 138 | this.command = {title: "", command: "cblite.explorer.showMoreItems", arguments: [this] }; 139 | } 140 | 141 | showMore() { 142 | this.parent.limit += ShowMoreItem.batchSize; 143 | this.tree.refresh(); 144 | } 145 | 146 | // @ts-ignore 147 | get tooltip(): string { 148 | return "Show More..."; 149 | } 150 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { commands, ExtensionContext, languages, Position, TextDocument, Uri, window, workspace } from "vscode"; 2 | import { getEditorQueryDocument as getEditorDocument, pickWorkspaceDatabase, createQueryDocument, createDocContentDocument, showErrorMessage } from "./vscodewrapper"; 3 | import CbliteWorkspace from "./cbliteworkspace"; 4 | import { Configuration, getConfiguration } from "./configuration"; 5 | import { logger } from "./logging/logger"; 6 | import { Constants } from "./constants/constants"; 7 | import Explorer from "./explorer"; 8 | import Clipboard from "./utils/clipboard"; 9 | import { SqlppProvider } from "./providers/sqlpp.provider"; 10 | import { ShowMoreItem } from "./explorer/treeItem"; 11 | import { buildDocumentSchema, buildSchema, SchemaCollection, SchemaDatabase, SchemaDocument, SchemaItem, SchemaValue } from "./common"; 12 | import { Collection, Database, MutableDocument, QueryLanguage } from "./native/binding"; 13 | import { openDbAtPath } from "./utils/files"; 14 | import { pickListCollection } from "./vscodewrapper/quickPick"; 15 | var JSONstrict = require('json-bigint')({ strict: true, useNativeBigInt: true }); 16 | 17 | export namespace Commands { 18 | export const showOutputChannel: string = "cblite.showOutputChannel"; 19 | export const runDocumentQuery: string = "cblite.runDocumentQuery"; 20 | export const useDatabase: string = 'cblite.useDatabase'; 21 | export const explorerAdd: string = 'cblite.explorer.add'; 22 | export const explorerRemove: string = 'cblite.explorer.remove'; 23 | export const explorerCopyKey: string = 'cblite.explorer.copyKey'; 24 | export const explorerCopyValue: string = 'cblite.explorer.copyValue'; 25 | export const explorerCopyPath: string = 'cblite.explorer.copyPath'; 26 | export const lookupDocument: string = 'cblite.explorer.lookupDocument'; 27 | export const explorerCopyRelativePath: string = 'cblite.explorer.copyRelativePath'; 28 | export const explorerGetDocument: string = 'cblite.explorer.getDocument'; 29 | export const explorerShowMoreItems: string = 'cblite.explorer.showMoreItems'; 30 | export const newCollection: string = 'cblite.newCollection'; 31 | export const deleteCollection: string = 'cblite.deleteCollection'; 32 | export const newQuerySqlpp: string = 'cblite.newQuerySqlpp'; 33 | export const newQueryJson: string = 'cblite.newQueryJson'; 34 | export const quickQuery: string = 'cblite.quickQuery'; 35 | export const createDocument: string = 'cblite.createDocument'; 36 | export const updateDocument: string = 'cblite.updateDocument'; 37 | } 38 | 39 | let configuration: Configuration; 40 | let cbliteWorkspace: CbliteWorkspace; 41 | let explorer: Explorer; 42 | 43 | // this method is called when your extension is activated 44 | // your extension is activated the very first time the command is executed 45 | export function activate(context: ExtensionContext): Promise { 46 | logger.info(`Activating extension ${Constants.extensionName} v${Constants.extensionVersion}...`); 47 | 48 | configuration = getConfiguration(); 49 | logger.setLogLevel(configuration.logLevel); 50 | 51 | context.subscriptions.push(workspace.onDidChangeConfiguration(() => { 52 | configuration = getConfiguration(); 53 | logger.setLogLevel(configuration.logLevel); 54 | })); 55 | 56 | cbliteWorkspace = new CbliteWorkspace(); 57 | explorer = new Explorer(context); 58 | 59 | context.subscriptions.push(commands.registerCommand(Commands.showOutputChannel, () => { 60 | logger.showOutput(); 61 | })); 62 | 63 | context.subscriptions.push(commands.registerCommand(Commands.runDocumentQuery, async () => { 64 | await runDocumentQuery(); 65 | })); 66 | 67 | context.subscriptions.push(commands.registerCommand(Commands.explorerAdd, (dbUri?: Uri) => { 68 | let dbPath = dbUri? dbUri.fsPath : undefined; 69 | return explorerAdd(dbPath); 70 | })); 71 | 72 | context.subscriptions.push(commands.registerCommand(Commands.explorerRemove, (item?: {obj: Database}) => { 73 | let dbPath = item?.obj; 74 | return explorerRemove(dbPath); 75 | })); 76 | 77 | // context.subscriptions.push(commands.registerCommand(Commands.explorerRefresh, () => { 78 | // explorerRefresh(); 79 | // })); 80 | 81 | context.subscriptions.push(commands.registerCommand(Commands.explorerCopyKey, (item: SchemaItem) => { 82 | let path = buildPath(item); 83 | if(path) { 84 | return copyToClipboard(path); 85 | } 86 | 87 | window.showWarningMessage("No information found to copy..."); 88 | return Promise.resolve(); 89 | })); 90 | 91 | context.subscriptions.push(commands.registerCommand(Commands.explorerCopyValue, (item: SchemaItem) => { 92 | if('value' in item) { 93 | return copyToClipboard(item.value.toString()); 94 | } 95 | 96 | window.showWarningMessage("No information found to copy..."); 97 | return Promise.resolve(); 98 | })); 99 | 100 | context.subscriptions.push(commands.registerCommand(Commands.explorerCopyPath, (item: {path: string}) => { 101 | let path = item.path; 102 | return copyToClipboard(path); 103 | })); 104 | 105 | context.subscriptions.push(commands.registerCommand(Commands.explorerCopyRelativePath, (item: {path: string}) => { 106 | let path = workspace.asRelativePath(item.path); 107 | return copyToClipboard(path); 108 | })); 109 | 110 | context.subscriptions.push(commands.registerCommand(Commands.lookupDocument, async (item: SchemaCollection) => { 111 | let newDocID = await window.showInputBox({prompt: "Specify the document ID"}); 112 | if(!newDocID) { 113 | return; 114 | } 115 | 116 | return await getDocument(item.parent.parent.obj, item.obj, newDocID); 117 | })); 118 | 119 | context.subscriptions.push(commands.registerCommand(Commands.useDatabase, () => { 120 | return useDatabase(); 121 | })); 122 | 123 | context.subscriptions.push(commands.registerCommand(Commands.newQuerySqlpp, (db?: SchemaDatabase) => { 124 | return newQuery(db?.obj, "select meta().id from _ limit 100"); 125 | })); 126 | 127 | context.subscriptions.push(commands.registerCommand(Commands.newQueryJson, (db?: SchemaDatabase) => { 128 | return newQuery(db?.obj, '{\n\t"LIMIT":100,\n\t"WHAT": [["._id"]]\n}'); 129 | })); 130 | 131 | context.subscriptions.push(commands.registerCommand(Commands.createDocument, () => { 132 | return saveDocument(false); 133 | })); 134 | 135 | context.subscriptions.push(commands.registerCommand(Commands.updateDocument, () => { 136 | return saveDocument(true); 137 | })); 138 | 139 | context.subscriptions.push(commands.registerCommand(Commands.explorerGetDocument, (doc: SchemaDocument) => { 140 | let docId = doc.id; 141 | return getDocument(doc.parent.parent.parent.obj, doc.parent.obj, docId); 142 | })); 143 | 144 | context.subscriptions.push(commands.registerCommand(Commands.explorerShowMoreItems, (item: ShowMoreItem) => { 145 | item.showMore(); 146 | })); 147 | 148 | context.subscriptions.push(commands.registerCommand(Commands.newCollection, async (db: SchemaDatabase) => { 149 | let scope = await window.showInputBox({prompt: "Specify the scope name (blank for default)..."}); 150 | let collection = await window.showInputBox({prompt: "Specify the collection name..."}) 151 | if(!collection) { 152 | return; 153 | } 154 | 155 | try { 156 | let coll = db.obj.createCollection(collection, scope); 157 | explorer.addCollection(db, coll); 158 | } catch(err: any) { 159 | showErrorMessage(`Failed to create collection: ${err.message}`, 160 | {title: "Show output", command: Commands.showOutputChannel}); 161 | } 162 | })); 163 | 164 | context.subscriptions.push(commands.registerCommand(Commands.deleteCollection, (c: SchemaCollection) => { 165 | try { 166 | c.parent.parent.obj.deleteCollection(c.obj.name, c.obj.scopeName); 167 | explorer.removeCollection(c); 168 | } catch(err: any) { 169 | showErrorMessage(`Failed to delete collection: ${err.message}`, 170 | {title: "Show output", command: Commands.showOutputChannel}); 171 | } 172 | })); 173 | 174 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""); 175 | languages.registerCompletionItemProvider("sqlpp", SqlppProvider, ...characters); 176 | 177 | 178 | logger.info("Extension activated."); 179 | return Promise.resolve(true); 180 | } 181 | 182 | async function useDatabase(): Promise { 183 | let queryDocument = getEditorDocument(); 184 | let dbPath = await pickWorkspaceDatabase(false); 185 | if(queryDocument && dbPath) { 186 | let db = await openDbAtPath(dbPath); 187 | if(!db) { 188 | return undefined; 189 | } 190 | cbliteWorkspace.bindDatabaseToDocument(db, queryDocument); 191 | } 192 | } 193 | 194 | async function getDocument(dbObj: Database, collection: Collection, docId: string): Promise { 195 | let doc = collection.getMutableDocument(docId); 196 | 197 | let contentDoc = await createDocContentDocument(doc.propertiesAsJSON()); 198 | cbliteWorkspace.bindDatabaseToDocument(dbObj, contentDoc); 199 | cbliteWorkspace.bindDocToDocument(doc, contentDoc); 200 | return contentDoc; 201 | } 202 | 203 | async function saveDocument(update: boolean) { 204 | let vscodeDoc = getEditorDocument(); 205 | if(!vscodeDoc) { 206 | return; 207 | } 208 | 209 | let db = cbliteWorkspace.getDocumentDatabase(vscodeDoc); 210 | if(!db) { 211 | await useDatabase(); 212 | db = cbliteWorkspace.getDocumentDatabase(vscodeDoc); 213 | if(!db) { 214 | return; 215 | } 216 | } 217 | 218 | let doc: MutableDocument; 219 | let collection: Collection 220 | if(update) { 221 | let tmp = cbliteWorkspace.getDocumentDoc(vscodeDoc); 222 | if(!tmp) { 223 | showErrorMessage("Cannot update this document, it has not been saved yet"); 224 | return; 225 | } 226 | 227 | doc = tmp; 228 | collection = doc.collection; 229 | } else { 230 | let newDocID = await window.showInputBox({prompt: "Specify the document ID"}); 231 | if(!newDocID) { 232 | return; 233 | } 234 | 235 | let collectionPick = await pickListCollection(db); 236 | collection = db.getCollection(collectionPick.name, collectionPick.scope); 237 | 238 | let tmp = new MutableDocument(newDocID); 239 | doc = tmp; 240 | } 241 | 242 | doc.setPropertiesAsJSON(vscodeDoc.getText()); 243 | 244 | try { 245 | collection.saveDocument(doc); 246 | } catch(err: any) { 247 | showErrorMessage(`Failed to save document: ${err.message}`, {title: "Show output", command: Commands.showOutputChannel}); 248 | return; 249 | } 250 | 251 | if(!update) { 252 | cbliteWorkspace.bindDocToDocument(doc, vscodeDoc); 253 | } 254 | 255 | explorerUpdateDocument(db, collection, doc); 256 | explorer.refresh(); 257 | window.setStatusBarMessage(`Save completed!`, 2000); 258 | } 259 | 260 | async function explorerAdd(dbPath?: string): Promise { 261 | if(dbPath) { 262 | let schema = await buildSchema(dbPath); 263 | if(!schema) { 264 | return; 265 | } 266 | 267 | explorer.add(schema); 268 | } else { 269 | try { 270 | dbPath = await pickWorkspaceDatabase(false); 271 | await explorerAdd(dbPath); 272 | } catch(err: any) { 273 | // No database selected 274 | } 275 | } 276 | } 277 | 278 | function explorerUpdateDocument(dbObj: Database, collection: Collection, doc: MutableDocument): void { 279 | let schemaCollection = explorer.getCollection(dbObj, collection); 280 | if(!schemaCollection) { 281 | return; 282 | } 283 | 284 | let schema = buildDocumentSchema(schemaCollection, doc); 285 | if(!schema) { 286 | return; 287 | } 288 | 289 | explorer.update(schemaCollection, schema); 290 | } 291 | 292 | function explorerRemove(dbObj?: Database): Thenable { 293 | if(dbObj) { 294 | return Promise.resolve(explorer.remove(dbObj)); 295 | } 296 | 297 | return Promise.resolve(); 298 | } 299 | 300 | async function newQuery(dbObj?: Database, content: string = "", cursorPos: Position = new Position(0, 0)): Promise { 301 | var queryDoc = await createQueryDocument(content, cursorPos, true); 302 | if(dbObj) { 303 | cbliteWorkspace.bindDatabaseToDocument(dbObj, queryDoc); 304 | } 305 | 306 | return queryDoc; 307 | } 308 | 309 | async function runDocumentQuery() { 310 | let queryDocument = getEditorDocument(); 311 | if(queryDocument) { 312 | let db = cbliteWorkspace.getDocumentDatabase(queryDocument); 313 | if(db) { 314 | let query = queryDocument.getText(); 315 | await runQuery(db, query, true); 316 | } else { 317 | await useDatabase(); 318 | await runDocumentQuery(); 319 | } 320 | } 321 | } 322 | 323 | 324 | async function runQuery(dbObj: Database, query: string, display: boolean) { 325 | let language = window.activeTextEditor?.document.languageId !== "json" 326 | ? QueryLanguage.SQLPP 327 | : QueryLanguage.JSON; 328 | 329 | let queryObj = dbObj.createQuery(language, query); 330 | let results = queryObj.execute(); 331 | 332 | if(!results) { 333 | return; 334 | } 335 | 336 | if(display) { 337 | var doc = await workspace.openTextDocument({content: JSONstrict.stringify(results), language: "json"}); 338 | await window.showTextDocument(doc, {preview: false}); 339 | } 340 | } 341 | 342 | async function copyToClipboard(text: string) { 343 | await Clipboard.copy(text); 344 | window.setStatusBarMessage(`Copied '${text}' to clipboard.`, 2000); 345 | } 346 | 347 | function buildPath(item: SchemaItem): string { 348 | var current: SchemaItem|undefined = item; 349 | let components: string[] = []; 350 | while(current) { 351 | if('id' in current) { 352 | components.unshift(current.id); 353 | } else if('name' in current) { 354 | components.unshift(`["${current.name}"]`); 355 | } 356 | 357 | if('parent' in current) { 358 | if('array' in current.parent) { 359 | components.unshift(`[${current.parent.array!.indexOf(current as SchemaValue)}]`); 360 | } 361 | 362 | current = current.parent; 363 | } else { 364 | current = undefined; 365 | } 366 | } 367 | 368 | return components.join(""); 369 | } 370 | -------------------------------------------------------------------------------- /src/logging/logger.ts: -------------------------------------------------------------------------------- 1 | import { window, OutputChannel } from 'vscode'; 2 | import { Constants } from '../constants/constants'; 3 | 4 | export enum Level { 5 | DEBUG = "DEBUG", 6 | INFO = "INFO", 7 | WARN = "WARN", 8 | ERROR = "ERROR" 9 | } 10 | 11 | class Logger { 12 | private logLevel: string; 13 | private outputChannel: OutputChannel; 14 | 15 | constructor() { 16 | this.logLevel = Level.INFO; 17 | this.outputChannel = window.createOutputChannel(`${Constants.outputChannelName}`); 18 | } 19 | 20 | setLogLevel(logLevel: string) { 21 | this.logLevel = logLevel; 22 | } 23 | 24 | debug(msg: any) { 25 | this.log(`${msg.toString()}`, Level.DEBUG); 26 | } 27 | 28 | info(msg: any) { 29 | this.log(`${msg.toString()}`, Level.INFO); 30 | } 31 | 32 | warn(msg: any) { 33 | this.log(`${msg.toString()}`, Level.WARN); 34 | } 35 | 36 | error(msg: any) { 37 | this.log(`${msg.toString()}`, Level.ERROR); 38 | } 39 | 40 | output(msg: any) { 41 | this.outputChannel.appendLine(msg.toString()); 42 | } 43 | 44 | showOutput() { 45 | this.outputChannel.show(); 46 | } 47 | 48 | getOutputChannel(): OutputChannel { 49 | return this.outputChannel; 50 | } 51 | 52 | private log(msg: string, level: Level) { 53 | const time = new Date().toLocaleTimeString(); 54 | msg = `[${time}][${Constants.extensionName}][${level}] ${msg}`; 55 | switch(level) { 56 | case Level.ERROR: console.error(msg); break; 57 | case Level.WARN: console.warn(msg); break; 58 | case Level.INFO: console.info(msg); break; 59 | default: console.log(msg); break; 60 | } 61 | // log to output channel 62 | if (this.logLevel && logLevelGreaterThan(level, this.logLevel as Level)) { 63 | this.output(msg); 64 | } 65 | } 66 | } 67 | 68 | /** 69 | * Verify if log level l1 is greater than log level l2 70 | * DEBUG < INFO < WARN < ERROR 71 | */ 72 | function logLevelGreaterThan(l1: Level, l2: Level) { 73 | switch(l2) { 74 | case Level.ERROR: 75 | return (l1 === Level.ERROR); 76 | case Level.WARN: 77 | return (l1 === Level.WARN || l1 === Level.ERROR); 78 | case Level.INFO: 79 | return (l1 === Level.INFO || l1 === Level.WARN || l1 === Level.ERROR); 80 | case Level.DEBUG: 81 | return true; 82 | default: 83 | return (l1 === Level.INFO || l1 === Level.WARN || l1 === Level.ERROR); 84 | } 85 | } 86 | 87 | export const logger: Logger = new Logger(); -------------------------------------------------------------------------------- /src/native/Blob.cc: -------------------------------------------------------------------------------- 1 | #include "Blob.hh" 2 | 3 | Napi::Value Blob::isBlob(const Napi::CallbackInfo& info) { 4 | auto env = info.Env(); 5 | CBL_TYPE_ASSERT(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 6 | CBL_TYPE_ASSERT(env, info[0].IsObject(), CBL_ARGTYPE_ERR_MSG(0, object)); 7 | 8 | fleece::MutableDict dict = fleece::MutableDict::newDict(); 9 | serializeToFleeceDict(env, dict, info[0].As()); 10 | return Napi::Boolean::New(env, cbl::Blob::isBlob(dict)); 11 | } 12 | 13 | Blob::Blob(const Napi::CallbackInfo& info) 14 | :CouchbaseWrapper(info) 15 | { 16 | if(info.Length() == 0) return; // Shortcut for internal use 17 | 18 | auto env = info.Env(); 19 | CBL_TYPE_ASSERT(env, info.Length() == 2, CBL_ARGC_ERR_MSG(2)); 20 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 21 | CBL_TYPE_ASSERT(env, info[1].IsArrayBuffer() || info[1].IsTypedArray(), 22 | CBL_ARGTYPE_ERR_MSG(1, ArrayBuffer | Uint8Array)); 23 | 24 | std::string contentType = info[0].As(); 25 | if(info[1].IsArrayBuffer()) { 26 | auto ab = info[1].As(); 27 | _content = fleece::alloc_slice(ab.Data(), ab.ByteLength()); 28 | } else { 29 | auto arr = info[1].As(); 30 | _content = fleece::alloc_slice(arr.Data(), arr.ByteLength()); 31 | } 32 | 33 | _inner = cbl::Blob(contentType, _content); 34 | } 35 | 36 | Napi::Value Blob::get_length(const Napi::CallbackInfo& info) { 37 | auto env = info.Env(); 38 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 39 | return Napi::BigInt::New(env, _inner.length()); 40 | } 41 | 42 | Napi::Value Blob::get_contentType(const Napi::CallbackInfo& info) { 43 | auto env = info.Env(); 44 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 45 | 46 | return Napi::String::New(env, _inner.contentType()); 47 | } 48 | 49 | Napi::Value Blob::get_digest(const Napi::CallbackInfo& info) { 50 | auto env = info.Env(); 51 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 52 | 53 | return Napi::String::New(env, _inner.digest()); 54 | } 55 | 56 | #define MY_STATIC_METHOD(method) CBL_STATIC_METHOD(Blob, method) 57 | #define MY_GETTER(name) CBL_GETTER(Blob, name) 58 | 59 | Napi::Object Blob::Init(Napi::Env env, Napi::Object exports) { 60 | Napi::Function func = DefineClass(env, "Blob", { 61 | MY_STATIC_METHOD(isBlob), 62 | MY_GETTER(length), 63 | MY_GETTER(contentType), 64 | MY_GETTER(digest) 65 | }); 66 | 67 | cbl_init_object(exports, "Blob", func); 68 | return exports; 69 | } -------------------------------------------------------------------------------- /src/native/Blob.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cblite.hh" 4 | #include "CouchbaseWrapper.hh" 5 | 6 | namespace fleece { 7 | class alloc_slice; 8 | } 9 | 10 | class Blob : public CouchbaseWrapper { 11 | public: 12 | CBL_CLASS_BOILERPLATE(Blob); 13 | 14 | static Napi::Value isBlob(const Napi::CallbackInfo&); 15 | 16 | Napi::Value get_length(const Napi::CallbackInfo&); 17 | Napi::Value get_contentType(const Napi::CallbackInfo&); 18 | Napi::Value get_digest(const Napi::CallbackInfo&); 19 | 20 | private: 21 | fleece::alloc_slice _content; 22 | }; -------------------------------------------------------------------------------- /src/native/Collection.cc: -------------------------------------------------------------------------------- 1 | #include "Collection.hh" 2 | #include "Query.hh" 3 | #include "Database.hh" 4 | #include "Document.hh" 5 | 6 | #include 7 | 8 | Collection::Collection(const Napi::CallbackInfo& info) 9 | :CouchbaseWrapper(info) 10 | { 11 | if(info.Length() == 0) { 12 | // Shell for incoming already created 13 | // native object 14 | return; 15 | } 16 | 17 | auto env = info.Env(); 18 | CBL_TYPE_ASSERT(env, info.Length() >= 2 && info.Length() <= 4, 19 | CBL_ARGC_ERR_MSG(>= 2 or <= 4)); 20 | 21 | CBL_TYPE_ASSERT(env, info[0].IsObject(), CBL_ARGTYPE_ERR_MSG(0, object)); 22 | CBL_TYPE_ASSERT(env, info[1].IsBoolean(), CBL_ARGTYPE_ERR_MSG(1, boolean)); 23 | 24 | auto* db = ObjectWrap::Unwrap(info[0].As()); 25 | cbl::Database cblDb = *db; 26 | if(info.Length() == 2) { 27 | // Default collection, bool ignored 28 | try { 29 | _inner = cblDb.getDefaultCollection(); 30 | } CATCH_AND_ASSIGN_VOID(env); 31 | } else if(info.Length() == 3) { 32 | // Named collection in default scope 33 | CBL_TYPE_ASSERT(env, info[2].IsString(), CBL_ARGTYPE_ERR_MSG(2, string)); 34 | bool create = info[1].As(); 35 | std::string name = info[2].As(); 36 | try { 37 | _inner = create 38 | ? cblDb.createCollection(name) 39 | : cblDb.getCollection(name); 40 | } CATCH_AND_ASSIGN_VOID(env); 41 | } else { 42 | // Named collection in named scope 43 | CBL_TYPE_ASSERT(env, info[2].IsString(), CBL_ARGTYPE_ERR_MSG(2, string)); 44 | CBL_TYPE_ASSERT(env, info[3].IsString(), CBL_ARGTYPE_ERR_MSG(3, string)); 45 | bool create = info[1].As(); 46 | std::string name = info[2].As(); 47 | std::string scope = info[3].As(); 48 | try { 49 | _inner = create 50 | ? cblDb.createCollection(name, scope) 51 | : cblDb.getCollection(name, scope); 52 | } CATCH_AND_ASSIGN_VOID(env); 53 | } 54 | } 55 | 56 | Napi::Value Collection::get_name(const Napi::CallbackInfo& info) { 57 | auto env = info.Env(); 58 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 59 | 60 | return Napi::String::New(env, _inner.name().c_str()); 61 | } 62 | 63 | Napi::Value Collection::get_scopeName(const Napi::CallbackInfo& info) { 64 | auto env = info.Env(); 65 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 66 | 67 | return Napi::String::New(env, _inner.scopeName().c_str()); 68 | } 69 | 70 | Napi::Value Collection::get_count(const Napi::CallbackInfo& info) { 71 | auto env = info.Env(); 72 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 73 | 74 | return Napi::BigInt::New(env, _inner.count()); 75 | } 76 | 77 | Napi::Value Collection::getDocument(const Napi::CallbackInfo& info) { 78 | auto env = info.Env(); 79 | CBL_TYPE_ASSERT_RET(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 80 | CBL_TYPE_ASSERT_RET(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 81 | 82 | auto retVal = ObjectWrap::Unwrap(cbl_get_constructor(env, "Document").New({})); 83 | std::string id = info[0].As(); 84 | try { 85 | auto inner = _inner.getDocument(id); 86 | retVal->setInner(inner); 87 | retVal->syncFleeceProperties(env); 88 | return retVal->Value(); 89 | } CATCH_AND_ASSIGN(env) 90 | } 91 | 92 | Napi::Value Collection::getMutableDocument(const Napi::CallbackInfo& info) { 93 | auto env = info.Env(); 94 | CBL_TYPE_ASSERT_RET(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 95 | CBL_TYPE_ASSERT_RET(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 96 | 97 | auto retVal = ObjectWrap::Unwrap(cbl_get_constructor(env, "MutableDocument").New({})); 98 | std::string id = info[0].As(); 99 | try { 100 | auto inner = _inner.getMutableDocument(id); 101 | retVal->setInner(inner); 102 | retVal->syncFleeceProperties(env); 103 | return retVal->Value(); 104 | } CATCH_AND_ASSIGN(env) 105 | } 106 | 107 | void Collection::saveDocument(const Napi::CallbackInfo& info) { 108 | auto env = info.Env(); 109 | CBL_TYPE_ASSERT(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 110 | CBL_TYPE_ASSERT(env, info[0].IsObject(), CBL_ARGTYPE_ERR_MSG(0, object)); 111 | 112 | auto doc = ObjectWrap::Unwrap(info[0].As()); 113 | cbl::MutableDocument cblDoc = *doc; 114 | try { 115 | doc->syncJSProperties(env); 116 | _inner.saveDocument(cblDoc); 117 | } CATCH_AND_ASSIGN_VOID(env); 118 | } 119 | 120 | void Collection::deleteDocument(const Napi::CallbackInfo& info) { 121 | auto env = info.Env(); 122 | CBL_TYPE_ASSERT(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 123 | CBL_TYPE_ASSERT(env, info[0].IsObject(), CBL_ARGTYPE_ERR_MSG(0, object)); 124 | 125 | auto doc = ObjectWrap::Unwrap(info[0].As()); 126 | cbl::Document cblDoc = *doc; 127 | try { 128 | _inner.deleteDocument(cblDoc); 129 | } CATCH_AND_ASSIGN_VOID(env) 130 | } 131 | 132 | void Collection::createValueIndex(const Napi::CallbackInfo& info) { 133 | auto env = info.Env(); 134 | CBL_TYPE_ASSERT(env, info.Length() == 2, CBL_ARGC_ERR_MSG(2)); 135 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 136 | CBL_TYPE_ASSERT(env, info[1].IsObject(), CBL_ARGTYPE_ERR_MSG(1, object)); 137 | 138 | std::string name = info[0].As(); 139 | auto* config = ObjectWrap::Unwrap(info[1].As()); 140 | try { 141 | _inner.createValueIndex(name, *config); 142 | } CATCH_AND_ASSIGN_VOID(env) 143 | } 144 | 145 | void Collection::createFullTextIndex(const Napi::CallbackInfo& info) { 146 | auto env = info.Env(); 147 | CBL_TYPE_ASSERT(env, info.Length() == 2, CBL_ARGC_ERR_MSG(2)); 148 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 149 | CBL_TYPE_ASSERT(env, info[1].IsObject(), CBL_ARGTYPE_ERR_MSG(1, object)); 150 | 151 | std::string name = info[0].As(); 152 | auto* config = ObjectWrap::Unwrap(info[1].As()); 153 | try { 154 | _inner.createFullTextIndex(name, *config); 155 | } CATCH_AND_ASSIGN_VOID(env) 156 | } 157 | 158 | void Collection::deleteIndex(const Napi::CallbackInfo& info) { 159 | auto env = info.Env(); 160 | CBL_TYPE_ASSERT(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 161 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 162 | 163 | std::string name = info[0].As(); 164 | try { 165 | _inner.deleteIndex(name); 166 | } CATCH_AND_ASSIGN_VOID(env) 167 | } 168 | 169 | Napi::Value Collection::getIndexNames(const Napi::CallbackInfo& info) { 170 | auto env = info.Env(); 171 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 172 | auto names = _inner.getIndexNames(); 173 | return toJSValue(env, names); 174 | } 175 | 176 | #define MY_INSTANCE_METHOD(method) CBL_INSTANCE_METHOD(Collection, method) 177 | #define MY_GETTER(method) CBL_GETTER(Collection, method) 178 | 179 | Napi::Object Collection::Init(Napi::Env env, Napi::Object exports) { 180 | Napi::Function func = DefineClass(env, "Collection", { 181 | MY_GETTER(name), 182 | MY_GETTER(scopeName), 183 | MY_GETTER(count), 184 | MY_INSTANCE_METHOD(getDocument), 185 | MY_INSTANCE_METHOD(getMutableDocument), 186 | MY_INSTANCE_METHOD(saveDocument), 187 | MY_INSTANCE_METHOD(deleteDocument), 188 | MY_INSTANCE_METHOD(createValueIndex), 189 | MY_INSTANCE_METHOD(createFullTextIndex), 190 | MY_INSTANCE_METHOD(deleteIndex), 191 | MY_INSTANCE_METHOD(getIndexNames) 192 | }); 193 | 194 | cbl_init_object(exports, "Collection", func); 195 | return exports; 196 | } -------------------------------------------------------------------------------- /src/native/Collection.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cblite.hh" 4 | #include "CouchbaseWrapper.hh" 5 | 6 | class Collection : public CouchbaseWrapper { 7 | public: 8 | CBL_CLASS_BOILERPLATE(Collection); 9 | 10 | Napi::Value get_name(const Napi::CallbackInfo&); 11 | Napi::Value get_scopeName(const Napi::CallbackInfo &); 12 | Napi::Value get_count(const Napi::CallbackInfo &); 13 | 14 | Napi::Value getDocument(const Napi::CallbackInfo&); 15 | Napi::Value getMutableDocument(const Napi::CallbackInfo&); 16 | void saveDocument(const Napi::CallbackInfo&); 17 | void deleteDocument(const Napi::CallbackInfo&); 18 | 19 | void createValueIndex(const Napi::CallbackInfo&); 20 | void createFullTextIndex(const Napi::CallbackInfo&); 21 | void deleteIndex(const Napi::CallbackInfo&); 22 | Napi::Value getIndexNames(const Napi::CallbackInfo&); 23 | }; -------------------------------------------------------------------------------- /src/native/CouchbaseWrapper.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | template 6 | class CouchbaseWrapper : public Napi::ObjectWrap { 7 | public: 8 | CouchbaseWrapper(const Napi::CallbackInfo& info) 9 | :Napi::ObjectWrap(info) 10 | { 11 | 12 | } 13 | 14 | void setInner(const WrappedType& inner) { _inner = inner; } 15 | 16 | operator WrappedType() { return _inner; } 17 | 18 | protected: 19 | WrappedType _inner{}; 20 | }; -------------------------------------------------------------------------------- /src/native/Database.cc: -------------------------------------------------------------------------------- 1 | #include "Database.hh" 2 | #include "Collection.hh" 3 | #include "DatabaseConfiguration.hh" 4 | #include "Document.hh" 5 | #include "Query.hh" 6 | 7 | #include 8 | 9 | Database::Database(const Napi::CallbackInfo& info) 10 | :CouchbaseWrapper(info) 11 | { 12 | auto env = info.Env(); 13 | CBL_TYPE_ASSERT(env, info.Length() == 1 || info.Length() == 2, 14 | CBL_ARGC_ERR_MSG(1 or 2)); 15 | 16 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 17 | std::string name = info[0].As(); 18 | 19 | try { 20 | if(info.Length() == 2) { 21 | CBL_TYPE_ASSERT(env, info[1].IsObject(), CBL_ARGTYPE_ERR_MSG(1, object)); 22 | auto* config = ObjectWrap::Unwrap(info[1].As()); 23 | _config = config; 24 | _inner = cbl::Database(name, *config); 25 | } else { 26 | _inner = cbl::Database(name); 27 | } 28 | } CATCH_AND_ASSIGN_VOID(env) 29 | } 30 | 31 | Napi::Value Database::exists(const Napi::CallbackInfo& info) { 32 | auto env = info.Env(); 33 | CBL_TYPE_ASSERT_RET(env, info.Length() == 2, CBL_ARGC_ERR_MSG(2)); 34 | CBL_TYPE_ASSERT_RET(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 35 | CBL_TYPE_ASSERT_RET(env, info[1].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 36 | 37 | std::string name = info[0].As(); 38 | std::string inDir = info[1].As(); 39 | return Napi::Boolean::New(env, cbl::Database::exists(name, inDir)); 40 | } 41 | 42 | void Database::copyDatabase(const Napi::CallbackInfo& info) { 43 | auto env = info.Env(); 44 | CBL_TYPE_ASSERT(env, info.Length() == 2 || info.Length() == 3, 45 | CBL_ARGC_ERR_MSG(2 or 3)); 46 | 47 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 48 | CBL_TYPE_ASSERT(env, info[1].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 49 | std::string from = info[0].As(); 50 | std::string to = info[1].As(); 51 | try { 52 | if(info.Length() == 3) { 53 | CBL_TYPE_ASSERT(env, info[2].IsObject(), CBL_ARGTYPE_ERR_MSG(2, object)); 54 | auto* config = Napi::ObjectWrap::Unwrap(info[2].As()); 55 | cbl::Database::copyDatabase(from, to, *config); 56 | } else { 57 | cbl::Database::copyDatabase(from, to); 58 | } 59 | } CATCH_AND_ASSIGN_VOID(env) 60 | } 61 | 62 | void Database::deleteDatabase(const Napi::CallbackInfo& info) { 63 | auto env = info.Env(); 64 | CBL_TYPE_ASSERT(env, info.Length() == 2, CBL_ARGC_ERR_MSG(2)); 65 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 66 | CBL_TYPE_ASSERT(env, info[1].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 67 | 68 | std::string name = info[0].As(); 69 | std::string inDir = info[1].As(); 70 | try { 71 | cbl::Database::deleteDatabase(name, inDir); 72 | } CATCH_AND_ASSIGN_VOID(env) 73 | } 74 | 75 | Napi::Value Database::get_name(const Napi::CallbackInfo& info) { 76 | auto env = info.Env(); 77 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 78 | 79 | return Napi::String::New(env, _inner.name()); 80 | } 81 | 82 | Napi::Value Database::get_path(const Napi::CallbackInfo& info) { 83 | auto env = info.Env(); 84 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 85 | 86 | return Napi::String::New(env, _inner.path()); 87 | } 88 | 89 | Napi::Value Database::get_config(const Napi::CallbackInfo& info) { 90 | auto env = info.Env(); 91 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 92 | 93 | return _config->Value(); 94 | } 95 | 96 | Napi::Value Database::getScopeNames(const Napi::CallbackInfo& info) { 97 | auto env = info.Env(); 98 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 99 | 100 | try { 101 | auto cblVal = _inner.getScopeNames(); 102 | return toJSValue(env, cblVal); 103 | } CATCH_AND_ASSIGN(env); 104 | } 105 | 106 | Napi::Value Database::getCollectionNames(const Napi::CallbackInfo& info) { 107 | auto env = info.Env(); 108 | CBL_TYPE_ASSERT_RET(env, info.Length() <= 1, CBL_ARGC_ERR_MSG(0 or 1)); 109 | 110 | fleece::MutableArray cblVal; 111 | try { 112 | if(info.Length() == 0) { 113 | cblVal = _inner.getCollectionNames(); 114 | } else { 115 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 116 | std::string scope = info[0].As(); 117 | cblVal = _inner.getCollectionNames(scope); 118 | } 119 | } CATCH_AND_ASSIGN(env); 120 | 121 | return toJSValue(env, cblVal); 122 | } 123 | 124 | Napi::Value Database::getCollection(const Napi::CallbackInfo& info) { 125 | auto env = info.Env(); 126 | CBL_TYPE_ASSERT_RET(env, info.Length() == 1 || info.Length() == 2, 127 | CBL_ARGC_ERR_MSG(1 or 2)); 128 | 129 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 130 | 131 | if(info.Length() == 1) { 132 | std::string name = info[0].As(); 133 | return cbl_get_constructor(env, "Collection").New({Value(), Napi::Boolean::New(env, false), info[0]}); 134 | } 135 | 136 | CBL_TYPE_ASSERT(env, info[1].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 137 | return cbl_get_constructor(env, "Collection").New({Value(), Napi::Boolean::New(env, false), info[0], info[1]}); 138 | } 139 | 140 | Napi::Value Database::createCollection(const Napi::CallbackInfo& info) { 141 | auto env = info.Env(); 142 | CBL_TYPE_ASSERT_RET(env, info.Length() == 1 || info.Length() == 2, 143 | CBL_ARGC_ERR_MSG(1 or 2)); 144 | 145 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 146 | 147 | if(info.Length() == 1) { 148 | std::string name = info[0].As(); 149 | return cbl_get_constructor(env, "Collection").New({Value(), Napi::Boolean::New(env, true), info[0]}); 150 | } 151 | 152 | CBL_TYPE_ASSERT(env, info[1].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 153 | return cbl_get_constructor(env, "Collection").New({Value(), Napi::Boolean::New(env, true), info[0], info[1]}); 154 | } 155 | 156 | void Database::deleteCollection(const Napi::CallbackInfo& info) { 157 | auto env = info.Env(); 158 | CBL_TYPE_ASSERT_RET(env, info.Length() == 1 || info.Length() == 2, 159 | CBL_ARGC_ERR_MSG(1 or 2)); 160 | 161 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 162 | std::string name = info[0].As(); 163 | try { 164 | if(info.Length() == 1) { 165 | _inner.deleteCollection(name); 166 | } else { 167 | CBL_TYPE_ASSERT(env, info[1].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 168 | std::string scope = info[1].As(); 169 | _inner.deleteCollection(name, scope); 170 | } 171 | } CATCH_AND_ASSIGN_VOID(env); 172 | } 173 | 174 | Napi::Value Database::getDefaultCollection(const Napi::CallbackInfo& info) { 175 | auto env = info.Env(); 176 | CBL_TYPE_ASSERT(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 177 | 178 | return cbl_get_constructor(env, "Collection").New({Value(), Napi::Boolean::New(env, false)}); 179 | } 180 | 181 | void Database::close(const Napi::CallbackInfo& info) { 182 | auto env = info.Env(); 183 | CBL_TYPE_ASSERT(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 184 | 185 | try { 186 | _inner.close(); 187 | } CATCH_AND_ASSIGN_VOID(env) 188 | } 189 | 190 | void Database::deleteDb(const Napi::CallbackInfo& info) { 191 | auto env = info.Env(); 192 | CBL_TYPE_ASSERT(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 193 | 194 | try { 195 | _inner.deleteDatabase(); 196 | } CATCH_AND_ASSIGN_VOID(env) 197 | } 198 | 199 | Napi::Value Database::createQuery(const Napi::CallbackInfo& info) { 200 | return cbl_get_constructor(info.Env(), "Query").New({Value(), info[0], info[1]}); 201 | } 202 | 203 | #define MY_STATIC_METHOD(method) CBL_STATIC_METHOD(Database, method) 204 | #define MY_INSTANCE_METHOD(method) CBL_INSTANCE_METHOD(Database, method) 205 | #define MY_GETTER(method) CBL_GETTER(Database, method) 206 | 207 | Napi::Object Database::Init(Napi::Env env, Napi::Object exports) { 208 | Napi::Function func = DefineClass(env, "Database", { 209 | MY_STATIC_METHOD(exists), 210 | MY_STATIC_METHOD(copyDatabase), 211 | MY_STATIC_METHOD(deleteDatabase), 212 | MY_GETTER(name), 213 | MY_GETTER(path), 214 | MY_GETTER(config), 215 | MY_INSTANCE_METHOD(getScopeNames), 216 | MY_INSTANCE_METHOD(getCollectionNames), 217 | MY_INSTANCE_METHOD(getCollection), 218 | MY_INSTANCE_METHOD(createCollection), 219 | MY_INSTANCE_METHOD(deleteCollection), 220 | MY_INSTANCE_METHOD(getDefaultCollection), 221 | MY_INSTANCE_METHOD(close), 222 | InstanceMethod<&Database::deleteDb>("deleteDatabase"), 223 | MY_INSTANCE_METHOD(createQuery) 224 | }); 225 | 226 | cbl_init_object(exports, "Database", func); 227 | 228 | return exports; 229 | } -------------------------------------------------------------------------------- /src/native/Database.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cblite.hh" 4 | #include "CouchbaseWrapper.hh" 5 | 6 | class DatabaseConfiguration; 7 | 8 | class Database : public CouchbaseWrapper { 9 | public: 10 | CBL_CLASS_BOILERPLATE(Database); 11 | 12 | static Napi::Value exists(const Napi::CallbackInfo& info); 13 | static void copyDatabase(const Napi::CallbackInfo& info); 14 | static void deleteDatabase(const Napi::CallbackInfo& info); 15 | 16 | Napi::Value get_name(const Napi::CallbackInfo&); 17 | Napi::Value get_path(const Napi::CallbackInfo&); 18 | Napi::Value get_config(const Napi::CallbackInfo&); 19 | 20 | Napi::Value getScopeNames(const Napi::CallbackInfo&); 21 | Napi::Value getCollectionNames(const Napi::CallbackInfo&); 22 | Napi::Value getCollection(const Napi::CallbackInfo&); 23 | Napi::Value createCollection(const Napi::CallbackInfo&); 24 | void deleteCollection(const Napi::CallbackInfo&); 25 | Napi::Value getDefaultCollection(const Napi::CallbackInfo&); 26 | 27 | void close(const Napi::CallbackInfo&); 28 | void deleteDb(const Napi::CallbackInfo&); 29 | 30 | Napi::Value createQuery(const Napi::CallbackInfo&); 31 | 32 | private: 33 | DatabaseConfiguration* _config; 34 | }; -------------------------------------------------------------------------------- /src/native/DatabaseConfiguration.cc: -------------------------------------------------------------------------------- 1 | #include "DatabaseConfiguration.hh" 2 | #include 3 | 4 | DatabaseConfiguration::DatabaseConfiguration(const Napi::CallbackInfo& info) 5 | : CouchbaseWrapper(info) 6 | { 7 | auto env = info.Env(); 8 | CBL_TYPE_ASSERT(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 9 | } 10 | 11 | Napi::Value DatabaseConfiguration::get_directory(const Napi::CallbackInfo& info) { 12 | auto env = info.Env(); 13 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 14 | 15 | if(_inner.directory) { 16 | return Napi::String::New(env, _directory); 17 | } 18 | 19 | return env.Undefined(); 20 | } 21 | 22 | void DatabaseConfiguration::set_directory(const Napi::CallbackInfo& info, const Napi::Value& val) { 23 | auto env = info.Env(); 24 | CBL_TYPE_ASSERT(env, val.IsString(), CBL_ACCESSOR_TYPE_ERR_MSG(string)); 25 | 26 | _directory = val.ToString(); 27 | _inner.directory = FLStr(_directory.c_str()); 28 | } 29 | 30 | Napi::Value DatabaseConfiguration::get_encryptionKey(const Napi::CallbackInfo& info) { 31 | #ifndef COUCHBASE_ENTERPRISE 32 | NAPI_THROW(Napi::Error::New(info.Env(), "Not supported in community edition")); 33 | #else 34 | if(_encryptionKey) { 35 | return _encryptionKey->Value(); 36 | } 37 | 38 | return info.Env().Undefined(); 39 | #endif 40 | } 41 | 42 | void DatabaseConfiguration::set_encryptionKey(const Napi::CallbackInfo& info, const Napi::Value& val) { 43 | auto env = info.Env(); 44 | #ifndef COUCHBASE_ENTERPRISE 45 | NAPI_THROW_VOID(Napi::Error::New(env, "Not supported in community edition")); 46 | #else 47 | CBL_TYPE_ASSERT(env, val.IsObject(), CBL_ACCESSOR_TYPE_ERR_MSG(object)); 48 | 49 | EncryptionKey* passed = Napi::ObjectWrap::Unwrap(val.As()); 50 | _encryptionKey = passed; 51 | _inner.encryptionKey = *passed; 52 | #endif 53 | } 54 | 55 | EncryptionKey::EncryptionKey(const Napi::CallbackInfo& info) 56 | #ifdef COUCHBASE_ENTERPRISE 57 | :CouchbaseWrapper(info) 58 | #else 59 | :ObjectWrap(info) 60 | #endif 61 | { 62 | auto env = info.Env(); 63 | #ifndef COUCHBASE_ENTERPRISE 64 | NAPI_THROW(Napi::Error::New(env, "Not supported in community edition")); 65 | #else 66 | CBL_TYPE_ASSERT(env, info.Length() == 0 || info.Length() == 2, CBL_ARGC_ERR_MSG(0 or 2)); 67 | if(info.Length() == 2) { 68 | CBL_TYPE_ASSERT(env, info[0].IsNumber(), CBL_ARGTYPE_ERR_MSG(0, number)); 69 | CBL_TYPE_ASSERT(env, info[1].IsTypedArray() || info[1].IsArrayBuffer(), 70 | CBL_ARGTYPE_ERR_MSG(1, Uint8Array | ArrayBuffer)); 71 | 72 | size_t len = setBytes(env, info[1]); 73 | CBLEncryptionAlgorithm algorithm = (CBLEncryptionAlgorithm)info[0].ToNumber().Int32Value(); 74 | switch(algorithm) { 75 | case kCBLEncryptionNone: 76 | CBL_ASSERT(env, len == 0, "Invalid byte array size (expected 0 for 'None')"); 77 | break; 78 | case kCBLEncryptionAES256: 79 | CBL_ASSERT(env, len == kCBLEncryptionKeySizeAES256, 80 | "Invalid byte array size (expected 32 for 'AES256')"); 81 | break; 82 | default: 83 | CBL_ASSERT(env, false, "Invalid algorithm type passed"); 84 | } 85 | 86 | _inner.algorithm = algorithm; 87 | } 88 | #endif 89 | } 90 | 91 | Napi::Value EncryptionKey::get_algorithm(const Napi::CallbackInfo& info) { 92 | auto env = info.Env(); 93 | #ifndef COUCHBASE_ENTERPRISE 94 | NAPI_THROW(Napi::Error::New(env, "Not supported in community edition")); 95 | #else 96 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 97 | 98 | return Napi::Number::New(env, _inner.algorithm); 99 | #endif 100 | } 101 | 102 | void EncryptionKey::set_algorithm(const Napi::CallbackInfo& info, const Napi::Value& value) { 103 | auto env = info.Env(); 104 | #ifndef COUCHBASE_ENTERPRISE 105 | NAPI_THROW_VOID(Napi::Error::New(env, "Not supported in community edition")); 106 | #else 107 | CBL_TYPE_ASSERT(env, value.IsNumber(), CBL_ACCESSOR_TYPE_ERR_MSG(number)); 108 | _inner.algorithm = (CBLEncryptionAlgorithm)info[0].ToNumber().Int32Value(); 109 | #endif 110 | } 111 | 112 | Napi::Value EncryptionKey::get_bytes(const Napi::CallbackInfo& info) { 113 | auto env = info.Env(); 114 | #ifndef COUCHBASE_ENTERPRISE 115 | NAPI_THROW(Napi::Error::New(env, "Not supported in community edition")); 116 | #else 117 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 118 | 119 | return Napi::ArrayBuffer::New(env, _inner.bytes, 32); 120 | #endif 121 | } 122 | 123 | void EncryptionKey::set_bytes(const Napi::CallbackInfo& info, const Napi::Value& val) { 124 | auto env = info.Env(); 125 | #ifndef COUCHBASE_ENTERPRISE 126 | NAPI_THROW_VOID(Napi::Error::New(env, "Not supported in community edition")); 127 | #else 128 | CBL_TYPE_ASSERT(env, val.IsTypedArray() || val.IsArrayBuffer(), 129 | CBL_ACCESSOR_TYPE_ERR_MSG(Uint8Array | ArrayBuffer)); 130 | 131 | setBytes(env, val); 132 | #endif 133 | } 134 | 135 | size_t EncryptionKey::setBytes(Napi::Env& env, Napi::Value val) { 136 | #ifndef COUCHBASE_ENTERPRISE 137 | return 0; 138 | #else 139 | uint8_t* buffer; 140 | size_t len; 141 | if(val.IsTypedArray()) { 142 | buffer = val.As().Data(); 143 | len = val.As().ByteLength(); 144 | } else if(val.IsArrayBuffer()) { 145 | buffer = (uint8_t *)val.As().Data(); 146 | len = val.As().ByteLength(); 147 | } else { 148 | CBL_ASSERT_RET1(env, false, "Invalid parameter type", (size_t)-1); 149 | } 150 | 151 | CBL_ASSERT_RET1(env, len <= 32, "Passed buffer is too large", (size_t)-1); 152 | memcpy(_inner.bytes, buffer, len); 153 | return len; 154 | #endif 155 | } 156 | 157 | Napi::Value EncryptionKey::createFromPassword(const Napi::CallbackInfo& info) { 158 | auto env = info.Env(); 159 | #ifndef COUCHBASE_ENTERPRISE 160 | NAPI_THROW(Napi::Error::New(env, "Not supported in community edition")); 161 | #else 162 | CBL_TYPE_ASSERT(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 163 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 164 | auto pw = (std::string)info[0].As(); 165 | 166 | CBLEncryptionKey key; 167 | if(!CBLEncryptionKey_FromPassword(&key, FLStr(pw.c_str()))) { 168 | NAPI_THROW(Napi::Error::New(env, "Failed to create key from password"), env.Undefined()); 169 | } 170 | 171 | Napi::Object retVal = cbl_get_constructor(env, "EncryptionKey").New({}); 172 | auto* unwrapped = ObjectWrap::Unwrap(retVal); 173 | unwrapped->setInner(key); 174 | return retVal; 175 | #endif 176 | } 177 | 178 | #if CBLITE_VERSION_NUMBER >= 3001000 179 | Napi::Value EncryptionKey::createFromPasswordOld(const Napi::CallbackInfo& info) { 180 | auto env = info.Env(); 181 | #ifndef COUCHBASE_ENTERPRISE 182 | NAPI_THROW(Napi::Error::New(env, "Not supported in community edition")); 183 | #else 184 | CBL_TYPE_ASSERT(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 185 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(1, string)); 186 | auto pw = (std::string)info[0].As(); 187 | 188 | CBLEncryptionKey key; 189 | if(!CBLEncryptionKey_FromPasswordOld(&key, FLStr(pw.c_str()))) { 190 | NAPI_THROW(Napi::Error::New(env, "Failed to create key from password"), env.Undefined()); 191 | } 192 | 193 | Napi::Object retVal = cbl_get_constructor(env, "EncryptionKey").New({}); 194 | auto* unwrapped = ObjectWrap::Unwrap(retVal); 195 | unwrapped->setInner(key); 196 | return retVal; 197 | #endif 198 | } 199 | #endif 200 | 201 | 202 | #define MY_GETTER_SETTER(x) CBL_GETTER_SETTER(EncryptionKey, x) 203 | #define MY_STATIC_METHOD(x) CBL_STATIC_METHOD(EncryptionKey, x) 204 | 205 | Napi::Object EncryptionKey::Init(Napi::Env env, Napi::Object exports) { 206 | Napi::Function func = DefineClass(env, "EncryptionKey", { 207 | MY_GETTER_SETTER(algorithm), 208 | MY_GETTER_SETTER(bytes), 209 | MY_STATIC_METHOD(createFromPassword), 210 | #if CBLITE_VERSION_NUMBER >= 3001000 211 | MY_STATIC_METHOD(createFromPasswordOld) 212 | #endif 213 | }); 214 | 215 | cbl_init_object(exports, "EncryptionKey", func); 216 | return exports; 217 | } 218 | 219 | #undef MY_INSTANCE_METHOD 220 | #undef MY_GETTER_SETTER 221 | #define MY_GETTER_SETTER(x) CBL_GETTER_SETTER(DatabaseConfiguration, x) 222 | 223 | Napi::Object DatabaseConfiguration::Init(Napi::Env env, Napi::Object exports) { 224 | Napi::Function func = DefineClass(env, "DatabaseConfiguration", { 225 | MY_GETTER_SETTER(directory), 226 | MY_GETTER_SETTER(encryptionKey) 227 | }); 228 | 229 | cbl_init_object(exports, "DatabaseConfiguration", func); 230 | return exports; 231 | } 232 | -------------------------------------------------------------------------------- /src/native/DatabaseConfiguration.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cblite.hh" 4 | #include "CouchbaseWrapper.hh" 5 | 6 | class EncryptionKey 7 | #ifdef COUCHBASE_ENTERPRISE 8 | : public CouchbaseWrapper { 9 | #else 10 | : public Napi::ObjectWrap { 11 | #endif 12 | public: 13 | CBL_CLASS_BOILERPLATE(EncryptionKey); 14 | 15 | CBL_GETSET(algorithm); 16 | CBL_GETSET(bytes); 17 | 18 | static Napi::Value createFromPassword(const Napi::CallbackInfo&); 19 | static Napi::Value createFromPasswordOld(const Napi::CallbackInfo&); 20 | private: 21 | size_t setBytes(Napi::Env& env, Napi::Value val); 22 | }; 23 | 24 | class DatabaseConfiguration : public CouchbaseWrapper { 25 | public: 26 | CBL_CLASS_BOILERPLATE(DatabaseConfiguration); 27 | 28 | CBL_GETSET(directory); 29 | CBL_GETSET(encryptionKey); 30 | private: 31 | std::string _directory; 32 | 33 | #ifdef COUCHBASE_ENTERPRISE 34 | EncryptionKey* _encryptionKey {nullptr}; 35 | #endif 36 | }; -------------------------------------------------------------------------------- /src/native/Document.cc: -------------------------------------------------------------------------------- 1 | #include "Document.hh" 2 | #include "Collection.hh" 3 | #include 4 | 5 | template 6 | static Napi::Value get_id(T instance, const Napi::CallbackInfo& info) { 7 | auto env = info.Env(); 8 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 9 | 10 | return Napi::String::New(env, instance.id()); 11 | } 12 | 13 | template 14 | static Napi::Value get_revisionID(T instance, const Napi::CallbackInfo& info) { 15 | auto env = info.Env(); 16 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 17 | 18 | return Napi::String::New(env, instance.revisionID()); 19 | } 20 | 21 | template 22 | static Napi::Value get_sequence(T instance, const Napi::CallbackInfo& info) { 23 | auto env = info.Env(); 24 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 25 | 26 | return Napi::BigInt::New(env, instance.sequence()); 27 | } 28 | 29 | template 30 | static Napi::Value get_collection(T instance, const Napi::CallbackInfo& info) { 31 | auto env = info.Env(); 32 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 33 | 34 | auto* retVal = Napi::ObjectWrap::Unwrap(cbl_get_constructor(env, "Collection").New({})); 35 | retVal->setInner(instance.collection()); 36 | return retVal->Value(); 37 | } 38 | 39 | template 40 | static Napi::Value propertiesAsJSON(T instance, const Napi::CallbackInfo& info) { 41 | auto env = info.Env(); 42 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 43 | 44 | auto retVal = instance.propertiesAsJSON(); 45 | return Napi::String::New(env, (const char *)retVal.buf, retVal.size); 46 | } 47 | 48 | Document::Document(const Napi::CallbackInfo& info) 49 | :CouchbaseWrapper(info) 50 | { 51 | CBL_TYPE_ASSERT(info.Env(), info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 52 | } 53 | 54 | Napi::Value Document::get_id(const Napi::CallbackInfo& info) { 55 | return ::get_id(_inner, info); 56 | } 57 | 58 | Napi::Value Document::get_revisionID(const Napi::CallbackInfo& info) { 59 | return ::get_revisionID(_inner, info); 60 | } 61 | 62 | Napi::Value Document::get_sequence(const Napi::CallbackInfo& info) { 63 | return ::get_sequence(_inner, info); 64 | } 65 | 66 | Napi::Value Document::get_collection(const Napi::CallbackInfo& info) { 67 | return ::get_collection(_inner, info); 68 | } 69 | 70 | Napi::Value Document::propertiesAsJSON(const Napi::CallbackInfo& info) { 71 | return ::propertiesAsJSON(_inner, info); 72 | } 73 | 74 | void Document::syncFleeceProperties(Napi::Env env) { 75 | auto fleeceProperties = _inner.properties(); 76 | for(fleece::Dict::iterator i(fleeceProperties); i; ++i) { 77 | std::string nextKey = i.keyString().asString(); 78 | Value().Set(nextKey, toJSValue(env, i.value())); 79 | } 80 | } 81 | 82 | MutableDocument::MutableDocument(const Napi::CallbackInfo& info) 83 | :CouchbaseWrapper(info) 84 | { 85 | auto env = info.Env(); 86 | CBL_TYPE_ASSERT(env, info.Length() <= 1, CBL_ARGC_ERR_MSG(<= 1)); 87 | if(info.Length() == 1) { 88 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 89 | std::string id = info[0].As(); 90 | _inner = cbl::MutableDocument(id); 91 | } else { 92 | _inner = cbl::MutableDocument(nullptr); 93 | } 94 | } 95 | 96 | Napi::Value MutableDocument::get_id(const Napi::CallbackInfo& info) { 97 | return ::get_id(_inner, info); 98 | } 99 | 100 | Napi::Value MutableDocument::get_revisionID(const Napi::CallbackInfo& info) { 101 | return ::get_revisionID(_inner, info); 102 | } 103 | 104 | Napi::Value MutableDocument::get_sequence(const Napi::CallbackInfo& info) { 105 | return ::get_sequence(_inner, info); 106 | } 107 | 108 | Napi::Value MutableDocument::get_collection(const Napi::CallbackInfo& info) { 109 | return ::get_collection(_inner, info); 110 | } 111 | 112 | Napi::Value MutableDocument::propertiesAsJSON(const Napi::CallbackInfo& info) { 113 | return ::propertiesAsJSON(_inner, info); 114 | } 115 | 116 | void MutableDocument::setPropertiesAsJSON(const Napi::CallbackInfo& info) { 117 | auto env = info.Env(); 118 | CBL_TYPE_ASSERT(env, info.Length() == 1, CBL_ARGC_ERR_MSG(1)); 119 | CBL_TYPE_ASSERT(env, info[0].IsString(), CBL_ARGTYPE_ERR_MSG(0, string)); 120 | 121 | std::string json = info[0].As(); 122 | try { 123 | _inner.setPropertiesAsJSON(json); 124 | syncFleeceProperties(env); 125 | } CATCH_AND_ASSIGN_VOID(env) 126 | } 127 | 128 | void MutableDocument::syncJSProperties(Napi::Env env) { 129 | auto fleeceProperties = _inner.properties().mutableCopy(); 130 | serializeToFleeceDict(env, fleeceProperties, Value()); 131 | _inner.setProperties(fleeceProperties); 132 | } 133 | 134 | void MutableDocument::syncFleeceProperties(Napi::Env env) { 135 | auto fleeceProperties = _inner.properties(); 136 | for(fleece::Dict::iterator i(fleeceProperties); i; ++i) { 137 | Value().Set((std::string)i.keyString(), toJSValue(env, i.value())); 138 | } 139 | } 140 | 141 | #define MY_INSTANCE_METHOD(method) CBL_INSTANCE_METHOD(Document, method) 142 | #define MY_GETTER(method) CBL_GETTER(Document, method) 143 | 144 | Napi::Object Document::Init(Napi::Env env, Napi::Object exports) { 145 | Napi::Function func = DefineClass(env, "Document", { 146 | MY_GETTER(id), 147 | MY_GETTER(revisionID), 148 | MY_GETTER(sequence), 149 | MY_GETTER(collection), 150 | MY_INSTANCE_METHOD(propertiesAsJSON) 151 | }); 152 | 153 | cbl_init_object(exports, "Document", func); 154 | 155 | return exports; 156 | } 157 | 158 | #undef MY_INSTANCE_METHOD 159 | #undef MY_GETTER 160 | #define MY_INSTANCE_METHOD(method) CBL_INSTANCE_METHOD(MutableDocument, method) 161 | #define MY_GETTER(method) CBL_GETTER(MutableDocument, method) 162 | 163 | Napi::Object MutableDocument::Init(Napi::Env env, Napi::Object exports) { 164 | Napi::Function func = DefineClass(env, "MutableDocument", { 165 | MY_GETTER(id), 166 | MY_GETTER(revisionID), 167 | MY_GETTER(sequence), 168 | MY_GETTER(collection), 169 | MY_INSTANCE_METHOD(propertiesAsJSON), 170 | MY_INSTANCE_METHOD(setPropertiesAsJSON) 171 | }); 172 | 173 | cbl_init_object(exports, "MutableDocument", func); 174 | 175 | return exports; 176 | } -------------------------------------------------------------------------------- /src/native/Document.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cblite.hh" 4 | #include "CouchbaseWrapper.hh" 5 | 6 | class MutableDocument; 7 | 8 | class Document : public CouchbaseWrapper { 9 | public: 10 | CBL_CLASS_BOILERPLATE(Document); 11 | 12 | Napi::Value get_id(const Napi::CallbackInfo&); 13 | Napi::Value get_revisionID(const Napi::CallbackInfo&); 14 | Napi::Value get_sequence(const Napi::CallbackInfo&); 15 | Napi::Value get_collection(const Napi::CallbackInfo&); 16 | 17 | Napi::Value propertiesAsJSON(const Napi::CallbackInfo&); 18 | 19 | void syncFleeceProperties(Napi::Env env); 20 | }; 21 | 22 | class MutableDocument : public CouchbaseWrapper { 23 | public: 24 | CBL_CLASS_BOILERPLATE(MutableDocument); 25 | 26 | Napi::Value get_id(const Napi::CallbackInfo&); 27 | Napi::Value get_revisionID(const Napi::CallbackInfo&); 28 | Napi::Value get_sequence(const Napi::CallbackInfo&); 29 | Napi::Value get_collection(const Napi::CallbackInfo&); 30 | 31 | Napi::Value propertiesAsJSON(const Napi::CallbackInfo&); 32 | void setPropertiesAsJSON(const Napi::CallbackInfo&); 33 | 34 | void syncJSProperties(Napi::Env env); 35 | void syncFleeceProperties(Napi::Env env); 36 | }; -------------------------------------------------------------------------------- /src/native/Query.cc: -------------------------------------------------------------------------------- 1 | #include "Query.hh" 2 | #include "Database.hh" 3 | #include 4 | 5 | template 6 | static Napi::Value get_expressionLanguage(T& instance, const Napi::CallbackInfo& info) { 7 | auto env = info.Env(); 8 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 9 | return Napi::Number::New(env, instance.expressionLanguage); 10 | } 11 | 12 | template 13 | void set_expressionLanguage(T& instance, const Napi::CallbackInfo& info, const Napi::Value& val) { 14 | auto env = info.Env(); 15 | CBL_TYPE_ASSERT(env, val.IsNumber(), CBL_ACCESSOR_TYPE_ERR_MSG(number)); 16 | instance.expressionLanguage = (CBLQueryLanguage)val.ToNumber().Int32Value(); 17 | } 18 | 19 | ValueIndexConfiguration::ValueIndexConfiguration(const Napi::CallbackInfo& info) 20 | :CouchbaseWrapper(info) 21 | { 22 | CBL_TYPE_ASSERT(info.Env(), info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 23 | } 24 | 25 | Napi::Value ValueIndexConfiguration::get_expressionLanguage(const Napi::CallbackInfo& info) { 26 | return ::get_expressionLanguage(_inner, info); 27 | } 28 | 29 | void ValueIndexConfiguration::set_expressionLanguage(const Napi::CallbackInfo& info, const Napi::Value& val) { 30 | ::set_expressionLanguage(_inner, info, val); 31 | } 32 | 33 | Napi::Value ValueIndexConfiguration::get_expressions(const Napi::CallbackInfo& info) { 34 | auto env = info.Env(); 35 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 36 | 37 | return Napi::String::New(env, _expressions); 38 | } 39 | 40 | void ValueIndexConfiguration::set_expressions(const Napi::CallbackInfo& info, const Napi::Value& val) { 41 | auto env = info.Env(); 42 | CBL_TYPE_ASSERT(env, val.IsString(), CBL_ACCESSOR_TYPE_ERR_MSG(string)); 43 | 44 | _expressions = val.As(); 45 | _inner.expressions = FLStr(_expressions.c_str()); 46 | } 47 | 48 | FullTextIndexConfiguration::FullTextIndexConfiguration(const Napi::CallbackInfo& info) 49 | :CouchbaseWrapper(info) 50 | { 51 | CBL_TYPE_ASSERT(info.Env(), info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 52 | } 53 | 54 | Napi::Value FullTextIndexConfiguration::get_expressionLanguage(const Napi::CallbackInfo& info) { 55 | return ::get_expressionLanguage(_inner, info); 56 | } 57 | 58 | void FullTextIndexConfiguration::set_expressionLanguage(const Napi::CallbackInfo& info, const Napi::Value& val) { 59 | ::set_expressionLanguage(_inner, info, val); 60 | } 61 | 62 | Napi::Value FullTextIndexConfiguration::get_expressions(const Napi::CallbackInfo& info) { 63 | auto env = info.Env(); 64 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 65 | 66 | return Napi::String::New(env, _expressions); 67 | } 68 | 69 | void FullTextIndexConfiguration::set_expressions(const Napi::CallbackInfo& info, const Napi::Value& val) { 70 | auto env = info.Env(); 71 | CBL_TYPE_ASSERT(env, val.IsString(), CBL_ACCESSOR_TYPE_ERR_MSG(string)); 72 | 73 | _expressions = val.As(); 74 | _inner.expressions = FLStr(_expressions.c_str()); 75 | } 76 | 77 | Napi::Value FullTextIndexConfiguration::get_ignoreAccents(const Napi::CallbackInfo& info) { 78 | auto env = info.Env(); 79 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 80 | return Napi::Boolean::New(env, _inner.ignoreAccents); 81 | } 82 | 83 | void FullTextIndexConfiguration::set_ignoreAccents(const Napi::CallbackInfo& info, const Napi::Value& val) { 84 | auto env = info.Env(); 85 | CBL_TYPE_ASSERT(env, val.IsBoolean(), CBL_ACCESSOR_TYPE_ERR_MSG(boolean)); 86 | _inner.ignoreAccents = val.As(); 87 | } 88 | 89 | Napi::Value FullTextIndexConfiguration::get_language(const Napi::CallbackInfo& info) { 90 | auto env = info.Env(); 91 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 92 | 93 | return Napi::String::New(env, _language);; 94 | } 95 | 96 | void FullTextIndexConfiguration::set_language(const Napi::CallbackInfo& info, const Napi::Value& val) { 97 | auto env = info.Env(); 98 | CBL_TYPE_ASSERT(env, val.IsString(), CBL_ACCESSOR_TYPE_ERR_MSG(string)); 99 | 100 | _language = val.As(); 101 | _inner.language = FLStr(_language.c_str()); 102 | } 103 | 104 | Query::Query(const Napi::CallbackInfo& info) 105 | :CouchbaseWrapper(info) 106 | { 107 | auto env = info.Env(); 108 | CBL_TYPE_ASSERT(env, info.Length() == 3, CBL_ARGC_ERR_MSG(3)); 109 | CBL_TYPE_ASSERT(env, info[0].IsObject(), CBL_ARGTYPE_ERR_MSG(0, object)); 110 | CBL_TYPE_ASSERT(env, info[1].IsNumber(), CBL_ARGTYPE_ERR_MSG(1, number)); 111 | CBL_TYPE_ASSERT(env, info[2].IsString(), CBL_ARGTYPE_ERR_MSG(2, string)); 112 | 113 | auto* db = ObjectWrap::Unwrap(info[0].As()); 114 | auto language = (CBLQueryLanguage)info[1].ToNumber().Int32Value(); 115 | auto expression = (std::string)info[2].As(); 116 | try { 117 | _inner = cbl::Query(*db, language, expression); 118 | } CATCH_AND_ASSIGN_VOID(env) 119 | } 120 | 121 | Napi::Value Query::columnNames(const Napi::CallbackInfo& info) { 122 | auto env = info.Env(); 123 | auto columns = _inner.columnNames(); 124 | auto retVal = Napi::Array::New(env, columns.size()); 125 | int index = 0; 126 | for(const auto& c : columns) { 127 | retVal.Set(index++, Napi::String::New(env, c)); 128 | } 129 | 130 | return retVal; 131 | } 132 | 133 | Napi::Value Query::explain(const Napi::CallbackInfo& info) { 134 | return Napi::String::New(info.Env(), _inner.explain()); 135 | } 136 | 137 | Napi::Value Query::execute(const Napi::CallbackInfo& info) { 138 | auto env = info.Env(); 139 | CBL_TYPE_ASSERT_RET(env, info.Length() == 0, CBL_ARGC_ERR_MSG(0)); 140 | 141 | cbl::ResultSet results; 142 | try { 143 | results = _inner.execute(); 144 | } CATCH_AND_ASSIGN(env); 145 | 146 | Napi::Array retVal = Napi::Array::New(env); 147 | int index = 0; 148 | for(const auto& result : results) { 149 | Napi::Object next = Napi::Object::New(env); 150 | for(const auto& c : _inner.columnNames()) { 151 | auto val = result.valueForKey(c); 152 | next.Set(c, toJSValue(env, val)); 153 | } 154 | 155 | retVal.Set(index++, next); 156 | } 157 | 158 | return retVal; 159 | } 160 | 161 | #define MY_GETTER_SETTER(name) CBL_GETTER_SETTER(ValueIndexConfiguration, name) 162 | 163 | Napi::Object ValueIndexConfiguration::Init(Napi::Env env, Napi::Object exports) { 164 | Napi::Function func = DefineClass(env, "ValueIndexConfiguration", { 165 | MY_GETTER_SETTER(expressionLanguage), 166 | MY_GETTER_SETTER(expressions) 167 | }); 168 | 169 | cbl_init_object(exports, "ValueIndexConfiguration", func); 170 | return exports; 171 | } 172 | 173 | #undef MY_GETTER_SETTER 174 | #define MY_GETTER_SETTER(name) CBL_GETTER_SETTER(FullTextIndexConfiguration, name) 175 | 176 | Napi::Object FullTextIndexConfiguration::Init(Napi::Env env, Napi::Object exports) { 177 | Napi::Function func = DefineClass(env, "FullTextIndexConfiguration", { 178 | MY_GETTER_SETTER(expressionLanguage), 179 | MY_GETTER_SETTER(expressions), 180 | MY_GETTER_SETTER(ignoreAccents), 181 | MY_GETTER_SETTER(language) 182 | }); 183 | 184 | cbl_init_object(exports, "FullTextIndexConfiguration", func); 185 | return exports; 186 | } 187 | 188 | #undef MY_GETTER_SETTER 189 | #define MY_INSTANCE_METHOD(method) CBL_INSTANCE_METHOD(Query, method) 190 | 191 | Napi::Object Query::Init(Napi::Env env, Napi::Object exports) { 192 | Napi::Function func = DefineClass(env, "Query", { 193 | MY_INSTANCE_METHOD(columnNames), 194 | MY_INSTANCE_METHOD(explain), 195 | MY_INSTANCE_METHOD(execute) 196 | }); 197 | 198 | cbl_init_object(exports, "Query", func); 199 | return exports; 200 | } -------------------------------------------------------------------------------- /src/native/Query.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "cblite.hh" 4 | #include "CouchbaseWrapper.hh" 5 | 6 | class Database; 7 | 8 | class ValueIndexConfiguration : public CouchbaseWrapper { 9 | public: 10 | CBL_CLASS_BOILERPLATE(ValueIndexConfiguration); 11 | 12 | CBL_GETSET(expressionLanguage); 13 | CBL_GETSET(expressions); 14 | 15 | private: 16 | std::string _expressions; 17 | }; 18 | 19 | class FullTextIndexConfiguration : public CouchbaseWrapper { 20 | public: 21 | CBL_CLASS_BOILERPLATE(FullTextIndexConfiguration); 22 | 23 | CBL_GETSET(expressionLanguage); 24 | CBL_GETSET(expressions); 25 | CBL_GETSET(ignoreAccents); 26 | CBL_GETSET(language); 27 | 28 | private: 29 | std::string _expressions; 30 | std::string _language; 31 | }; 32 | 33 | class Query : public CouchbaseWrapper { 34 | public: 35 | CBL_CLASS_BOILERPLATE(Query); 36 | 37 | Napi::Value columnNames(const Napi::CallbackInfo&); 38 | Napi::Value explain(const Napi::CallbackInfo&); 39 | Napi::Value execute(const Napi::CallbackInfo&); 40 | }; -------------------------------------------------------------------------------- /src/native/binding.ts: -------------------------------------------------------------------------------- 1 | let addon: any; 2 | const os = require("os"); 3 | let l: any; 4 | try { 5 | l = require("../logging/logger"); 6 | } catch(e) { 7 | console.log("VSCode logging unavailable"); 8 | } 9 | 10 | function print_load_error(e: any) { 11 | if(l) { 12 | l.logger.error("Failed to load..."); 13 | l.logger.error(e); 14 | } else { 15 | console.log("Failed to load..."); 16 | console.log(e); 17 | } 18 | } 19 | 20 | function print_msg(msg: string) { 21 | if(l) { 22 | l.logger.info(msg); 23 | } else { 24 | console.log(msg); 25 | } 26 | } 27 | 28 | if(os.platform() === "win32") { 29 | print_msg("Loading Windows NAPI addon..."); 30 | try { 31 | addon = require("../../out/Windows/cblite-js.node"); 32 | } catch(e) { 33 | print_load_error(e); 34 | throw e; 35 | } 36 | } else if(os.platform() === "darwin") { 37 | print_msg("Loading macOS NAPI addon..."); 38 | try { 39 | addon = require("../../out/Darwin/cblite-js.node"); 40 | } catch(e) { 41 | print_load_error(e); 42 | throw e; 43 | } 44 | } else { 45 | print_msg("Loading Linux NAPI addon..."); 46 | try { 47 | addon = require("../../out/Linux/cblite-js.node"); 48 | } catch(e) { 49 | print_load_error(e); 50 | throw e; 51 | } 52 | } 53 | 54 | export enum CBLErrorDomain { 55 | LITE_CORE = 1, 56 | POSIX, 57 | SQLITE, 58 | FLEECE, 59 | NETWORK, 60 | WEB_SOCKET 61 | } 62 | 63 | export enum LiteCoreErrorCode { 64 | ASSERTION_FAILED = 1, 65 | UNIMPLEMENTED, 66 | UNSUPPORTED_ENCRYPTION, 67 | BAD_REVISION_ID, 68 | CORRUPT_REVISION_DATA, 69 | NOT_OPEN, 70 | NOT_FOUND, 71 | CONFLICT, 72 | INVALID_PARAMETER, 73 | UNEXPECTED_ERROR, 74 | CANT_OPEN_FILE, 75 | IO_ERROR, 76 | MEMORY_ERROR, 77 | NOT_WRITEABLE, 78 | CORRUPT_DATA, 79 | BUSY, 80 | NOT_IN_TRANSACTION, 81 | TRANSACTION_NOT_CLOSED, 82 | UNSUPPORTED, 83 | NOT_A_DATABASE_FILE, 84 | WRONG_FORMAT, 85 | CRYPTO, 86 | INVALID_QUERY, 87 | MISSING_INDEX, 88 | INVALID_QUERY_PARAM, 89 | REMOTE_ERROR, 90 | DATABASE_TOO_OLD, 91 | DATABASE_TOO_NEW, 92 | BAD_DOC_ID, 93 | CANT_UPGRADE_DATABASE 94 | } 95 | 96 | export enum EncryptionAlgorithm { 97 | NONE, 98 | AES256 99 | } 100 | 101 | export interface EncryptionKey { 102 | algorithm: EncryptionAlgorithm 103 | bytes: ArrayBuffer | Uint8Array 104 | } 105 | 106 | export class EncryptionKeyMethods { 107 | public static createFromPassword(password: string) : EncryptionKey { 108 | return addon.EncryptionKey.createFromPassword(password); 109 | } 110 | 111 | public static createFromPasswordOld(password: string) : EncryptionKey | undefined { 112 | if(addon.EncryptionKey.createFromPasswordOld === null) { 113 | return undefined; 114 | } 115 | 116 | return addon.EncryptionKey.createFromPasswordOld(password); 117 | } 118 | } 119 | 120 | export var EncryptionKey: { 121 | new(): EncryptionKey 122 | new(algo: EncryptionAlgorithm, bytes: ArrayBuffer | Uint8Array): EncryptionKey 123 | } = addon.EncryptionKey; 124 | 125 | export interface DatabaseConfiguration { 126 | directory: string 127 | encryptionKey: EncryptionKey 128 | } 129 | 130 | export var DatabaseConfiguration: { 131 | new(): DatabaseConfiguration 132 | } = addon.DatabaseConfiguration; 133 | 134 | export class Database { 135 | private _inner: any; 136 | 137 | public static exists(name: string, inDirectory: string): boolean { 138 | return addon.Database.exists(name, inDirectory); 139 | } 140 | 141 | public static copyDatabase(fromPath: string, toName: string, 142 | config?: DatabaseConfiguration): void { 143 | if(config) { 144 | addon.Database.copyDatabase(fromPath, toName, config); 145 | } else { 146 | addon.Database.copyDatabase(fromPath, toName); 147 | } 148 | } 149 | 150 | public static deleteDatabase(name: string, inDirectory: string): void { 151 | addon.Database.deleteDatabase(name, inDirectory); 152 | } 153 | 154 | public get name(): string { 155 | return this._inner.name; 156 | } 157 | 158 | public get path(): string { 159 | return this._inner.path; 160 | } 161 | 162 | public get config(): DatabaseConfiguration { 163 | return this._inner.config; 164 | } 165 | 166 | public constructor(name: string, config?: DatabaseConfiguration) { 167 | if(!config) { 168 | this._inner = new addon.Database(name); 169 | } else { 170 | this._inner = new addon.Database(name, config); 171 | } 172 | } 173 | 174 | public getScopeNames(): Array { 175 | return this._inner.getScopeNames(); 176 | } 177 | 178 | public getCollectionNames(scope?: string): Array { 179 | return scope 180 | ? this._inner.getCollectionNames(scope) 181 | : this._inner.getCollectionNames(); 182 | } 183 | 184 | public getCollection(name: string, scope?: string): Collection { 185 | return scope 186 | ? this._inner.getCollection(name, scope) 187 | : this._inner.getCollection(name); 188 | } 189 | 190 | public createCollection(name: string, scope?: string): Collection { 191 | return scope 192 | ? this._inner.createCollection(name, scope) 193 | : this._inner.createCollection(name); 194 | } 195 | 196 | public deleteCollection(name: string, scope?: string): void { 197 | if(scope) { 198 | this._inner.deleteCollection(name, scope); 199 | } else { 200 | this._inner.deleteCollection(name); 201 | } 202 | } 203 | 204 | public getDefaultCollection(): Collection { 205 | return this._inner.getDefaultCollection(); 206 | } 207 | 208 | public close(): void { 209 | this._inner.close(); 210 | } 211 | 212 | public deleteDatabase(): void { 213 | this._inner.deleteDatabase(); 214 | } 215 | 216 | public createQuery(language: QueryLanguage, expressions: string): Query { 217 | return this._inner.createQuery(language, expressions); 218 | } 219 | } 220 | 221 | export interface Document { 222 | id: string 223 | revisionID: string 224 | sequence: bigint 225 | collection: Collection 226 | propertiesAsJSON(): string 227 | } 228 | 229 | export interface MutableDocument extends Document { 230 | setPropertiesAsJSON(properties: string): void 231 | } 232 | 233 | export var MutableDocument: { 234 | new(): MutableDocument 235 | new(id: string): MutableDocument 236 | } = addon.MutableDocument; 237 | 238 | export enum QueryLanguage { 239 | JSON, 240 | SQLPP 241 | } 242 | 243 | export interface ValueIndexConfiguration { 244 | expressionLanguage: QueryLanguage 245 | expressions: string 246 | } 247 | 248 | export var ValueIndexConfiguration: { 249 | new(): ValueIndexConfiguration 250 | } = addon.ValueIndexConfiguration; 251 | 252 | export interface FullTextIndexConfiguration { 253 | expressionLanguage: QueryLanguage 254 | expressions: string 255 | ignoreAccents: boolean 256 | language: string 257 | } 258 | 259 | export var FullTextIndexConfiguration: { 260 | new(): FullTextIndexConfiguration 261 | } = addon.FullTextIndexConfiguration; 262 | 263 | export interface Query { 264 | columnNames(): Array 265 | explain(): string 266 | execute(): Array> 267 | } 268 | 269 | export class BlobMethods { 270 | public static isBlob(dict: Record): boolean { 271 | return addon.Blob.isBlob(dict); 272 | } 273 | } 274 | 275 | export interface Blob { 276 | length: bigint 277 | contentType: string 278 | digest: string 279 | } 280 | 281 | export var Blob: { 282 | new(contentType: string, content: ArrayBuffer | Uint8Array): Blob 283 | } = addon.Blob; 284 | 285 | export interface Collection { 286 | name: string 287 | scopeName: string 288 | count: bigint 289 | getDocument(id: string) : Document 290 | getMutableDocument(id: string) : MutableDocument 291 | saveDocument(doc: MutableDocument) : void 292 | deleteDocument(doc: Document) : void 293 | createValueIndex(name: string, config: ValueIndexConfiguration) : void 294 | createFullTextIndex(name: string, config: FullTextIndexConfiguration) : void 295 | deleteIndex(name: string) : void 296 | getIndexNames() : Array 297 | } 298 | -------------------------------------------------------------------------------- /src/native/cblite.cc: -------------------------------------------------------------------------------- 1 | #include "cblite.hh" 2 | #include "DatabaseConfiguration.hh" 3 | #include "Database.hh" 4 | #include "Collection.hh" 5 | #include "Document.hh" 6 | #include "Query.hh" 7 | #include "fleece/Fleece.hh" 8 | #include "Blob.hh" 9 | 10 | #include 11 | #include 12 | 13 | std::map _Constructors; 14 | 15 | Napi::Object cbl_init_object(Napi::Object& exports, const char* name, Napi::Function& func) { 16 | exports.Set(name, func); 17 | auto constructor = Napi::Persistent(func); 18 | constructor.SuppressDestruct(); 19 | _Constructors[name] = std::move(constructor); 20 | 21 | return exports; 22 | } 23 | 24 | Napi::FunctionReference& cbl_get_constructor(const Napi::Env& env, const char* name) { 25 | auto it = _Constructors.find(name); 26 | #ifdef DEBUG 27 | if(it == _Constructors.end()) { 28 | std::string msg = std::string("Missing constructor for ") + name + "; did you forget to Init it?"; 29 | NAPI_THROW(Napi::Error::New(env, msg), env.Undefined()); 30 | } 31 | #endif 32 | 33 | return it->second; 34 | } 35 | 36 | Napi::Value toJSValue(Napi::Env env, const fleece::Value& value) { 37 | switch(value.type()) { 38 | case kFLArray: 39 | { 40 | auto retVal = Napi::Array::New(env, value.asArray().count()); 41 | int index = 0; 42 | for(fleece::Array::iterator i(value.asArray()); i; ++i) { 43 | retVal.Set(index++, toJSValue(env, i.value())); 44 | } 45 | 46 | return retVal; 47 | } 48 | case kFLDict: 49 | { 50 | auto dict = value.asDict(); 51 | if(cbl::Blob::isBlob(dict)) { 52 | Napi::Object retVal = cbl_get_constructor(env, "Blob").New({}); 53 | auto* blob = Napi::ObjectWrap::Unwrap(retVal); 54 | blob->setInner(cbl::Blob(dict)); 55 | return retVal; 56 | } 57 | 58 | auto retVal = Napi::Object::New(env); 59 | for(fleece::Dict::iterator i(dict); i; ++i) { 60 | std::string nextKey = i.keyString().asString(); 61 | retVal.Set(nextKey, toJSValue(env, i.value())); 62 | } 63 | 64 | return retVal; 65 | } 66 | case kFLBoolean: 67 | return Napi::Boolean::New(env, value.asBool()); 68 | case kFLNull: 69 | return env.Null(); 70 | case kFLNumber: 71 | if(value.isInteger()) { 72 | if(value.isUnsigned()) { 73 | return Napi::BigInt::New(env, value.asUnsigned()); 74 | } 75 | 76 | return Napi::BigInt::New(env, value.asInt()); 77 | } else { 78 | if(value.isDouble()) { 79 | return Napi::Number::New(env, value.asDouble()); 80 | } 81 | 82 | return Napi::Number::New(env, value.asFloat()); 83 | } 84 | case kFLString: 85 | return Napi::String::New(env, value.asstring()); 86 | default: 87 | break; 88 | } 89 | 90 | return env.Undefined(); 91 | } 92 | 93 | void serializeToFleeceArray(Napi::Env env, fleece::MutableArray& array, const Napi::Array& val) { 94 | for(int i = 0; i < val.Length(); i++) { 95 | auto next = val.Get(i); 96 | switch(next.Type()) { 97 | case napi_null: 98 | array.appendNull(); 99 | break; 100 | case napi_boolean: 101 | array.append((bool)next.As()); 102 | break; 103 | case napi_number: 104 | // We start to see Javascript's true colors here...no numeric information 105 | array.append(next.As().DoubleValue()); 106 | break; 107 | case napi_bigint: 108 | // No way I can tell to distinguish between signed and unsigned 109 | bool lossless; 110 | array.append(next.As().Int64Value(&lossless)); 111 | break; 112 | case napi_string: 113 | array.append((std::string)next.As()); 114 | break; 115 | case napi_object: 116 | if(next.IsArray()) { 117 | auto subArray = fleece::MutableArray::newArray(); 118 | serializeToFleeceArray(env, subArray, next.As()); 119 | array.append(subArray); 120 | } else { 121 | auto obj = next.As(); 122 | if(obj.InstanceOf(cbl_get_constructor(env, "Blob").Value())) { 123 | auto* blob = Napi::ObjectWrap::Unwrap(obj); 124 | array.append((cbl::Blob)*blob); 125 | } else { 126 | auto subDict = fleece::MutableDict::newDict(); 127 | serializeToFleeceDict(env, subDict, obj); 128 | array.append(subDict); 129 | } 130 | } 131 | break; 132 | default: 133 | CBL_ASSERT(env, false, "Invalid type in Javascript array"); 134 | } 135 | } 136 | } 137 | 138 | void serializeToFleeceDict(Napi::Env env, fleece::MutableDict& dict, const Napi::Object& val) { 139 | auto keys = val.GetPropertyNames(); 140 | for(int i = 0; i < keys.Length(); i++) { 141 | std::string key = keys.Get(i).As(); 142 | auto value = val.Get(key); 143 | switch(value.Type()) { 144 | case napi_null: 145 | dict.setNull(key); 146 | break; 147 | case napi_boolean: 148 | dict.set(key, (bool)value.As()); 149 | break; 150 | case napi_number: 151 | dict.set(key, value.ToNumber().DoubleValue()); 152 | break; 153 | case napi_bigint: 154 | { 155 | bool lossless; 156 | dict.set(key, value.As().Int64Value(&lossless)); 157 | break; 158 | } 159 | case napi_string: 160 | dict.set(key, (std::string)value.As()); 161 | break; 162 | case napi_object: 163 | if(value.IsArray()) { 164 | auto subArray = fleece::MutableArray::newArray(); 165 | serializeToFleeceArray(env, subArray, value.As()); 166 | dict.set(key, subArray); 167 | } else { 168 | auto obj = value.As(); 169 | if(obj.InstanceOf(cbl_get_constructor(env, "Blob").Value())) { 170 | auto* blob = Napi::ObjectWrap::Unwrap(obj); 171 | dict.set(key, (cbl::Blob)*blob); 172 | } else { 173 | auto subDict = fleece::MutableDict::newDict(); 174 | serializeToFleeceDict(env, subDict, obj); 175 | dict.set(key, subDict); 176 | } 177 | } 178 | break; 179 | default: 180 | CBL_ASSERT(env, false, "Invalid type in Javascript dict"); 181 | } 182 | } 183 | } 184 | 185 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 186 | DatabaseConfiguration::Init(env, exports); 187 | EncryptionKey::Init(env, exports); 188 | Database::Init(env, exports); 189 | Collection::Init(env, exports); 190 | Document::Init(env, exports); 191 | MutableDocument::Init(env, exports); 192 | ValueIndexConfiguration::Init(env, exports); 193 | FullTextIndexConfiguration::Init(env, exports); 194 | Query::Init(env, exports); 195 | Blob::Init(env, exports); 196 | 197 | return exports; 198 | } 199 | 200 | NODE_API_MODULE(addon, Init) 201 | -------------------------------------------------------------------------------- /src/native/cblite.hh: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "cbl++/CouchbaseLite.hh" 7 | 8 | namespace fleece { 9 | class Value; 10 | class MutableArray; 11 | class MutableDict; 12 | } 13 | 14 | #define CBL_CLASS_BOILERPLATE(name) \ 15 | static Napi::Object Init(Napi::Env env, Napi::Object exports); \ 16 | name(const Napi::CallbackInfo& info) 17 | 18 | #define CBL_GETSET(name) \ 19 | Napi::Value get_##name(const Napi::CallbackInfo&); \ 20 | void set_##name(const Napi::CallbackInfo&, const Napi::Value&) 21 | 22 | #define CBL_INSTANCE_METHOD(Class, Method) InstanceMethod<&Class::Method>(#Method) 23 | #define CBL_STATIC_METHOD(Class, Method) StaticMethod<&Class::Method>(#Method) 24 | #define CBL_GETTER_SETTER(Class, Name) InstanceAccessor<&Class::get_##Name, &Class::set_##Name>(#Name) 25 | #define CBL_GETTER(Class, Name) InstanceAccessor<&Class::get_##Name>(#Name) 26 | 27 | #define CBL_TYPE_ASSERT(env, condition, message) \ 28 | if(!(condition)) { \ 29 | NAPI_THROW_VOID(Napi::TypeError::New(env, message)); \ 30 | } 31 | 32 | #define CBL_TYPE_ASSERT_RET(env, condition, message) \ 33 | if(!(condition)) { \ 34 | NAPI_THROW(Napi::TypeError::New(env, message), env.Undefined()); \ 35 | } 36 | 37 | #define CBL_ASSERT(env, condition, message) \ 38 | if(!(condition)) { \ 39 | NAPI_THROW_VOID(Napi::Error::New(env, message)); \ 40 | } 41 | 42 | #define CBL_ASSERT_RET(env, condition, message) \ 43 | if(!(condition)) { \ 44 | NAPI_THROW(Napi::Error::New(env, message), env.Undefined()); \ 45 | } 46 | 47 | #define CBL_ASSERT_RET1(env, condition, message, retVal) \ 48 | if(!(condition)) { \ 49 | NAPI_THROW(Napi::Error::New(env, message), retVal); \ 50 | } 51 | 52 | Napi::Object cbl_init_object(Napi::Object& exports, const char* name, Napi::Function& func); 53 | Napi::FunctionReference& cbl_get_constructor(const Napi::Env& env, const char* name); 54 | Napi::Value toJSValue(Napi::Env env, const fleece::Value& val); 55 | void serializeToFleeceArray(Napi::Env env, fleece::MutableArray& array, const Napi::Array& val); 56 | void serializeToFleeceDict(Napi::Env env, fleece::MutableDict& dict, const Napi::Object& val); 57 | 58 | #define CBL_ARGC_ERR_MSG(x) "Incorrect number of arguments (expected " #x ")" 59 | #define CBL_ARGTYPE_ERR_MSG(p, t) "Incorrect type for argument " #p " (expected " #t ")" 60 | #define CBL_ACCESSOR_TYPE_ERR_MSG(t) "Incorrect type for accessor (expected " #t ")" 61 | 62 | #define CATCH_AND_ASSIGN_VOID(env) \ 63 | catch(CBLError& e) { \ 64 | auto msg = "Couchbase Lite Error " + std::to_string(e.domain) + " / " + \ 65 | std::to_string(e.code) + " " + (std::string)CBLError_Message(&e); \ 66 | auto err = Napi::Error::New(env, msg); \ 67 | err.Set("code", Napi::Number::New(env, e.code)); \ 68 | err.Set("domain", Napi::Number::New(env, e.domain)); \ 69 | NAPI_THROW_VOID(err); \ 70 | } catch(std::exception& e) { \ 71 | NAPI_THROW_VOID(Napi::Error::New(env, e.what())); \ 72 | } catch(...) { \ 73 | NAPI_THROW_VOID(Napi::Error::New(env, "Unknown non-standard error occurred")); \ 74 | } 75 | 76 | #define CATCH_AND_ASSIGN(env) \ 77 | catch(CBLError& e) { \ 78 | auto msg = "Couchbase Lite Error " + std::to_string(e.domain) + " / " + \ 79 | std::to_string(e.code) + " " + (std::string)CBLError_Message(&e); \ 80 | auto err = Napi::Error::New(env, msg); \ 81 | err.Set("code", Napi::Number::New(env, e.code)); \ 82 | err.Set("domain", Napi::Number::New(env, e.domain)); \ 83 | NAPI_THROW(err, env.Undefined()); \ 84 | } catch(std::exception& e) { \ 85 | NAPI_THROW(Napi::Error::New(env, e.what()), env.Undefined()); \ 86 | } catch(...) { \ 87 | NAPI_THROW(Napi::Error::New(env, "Unknown non-standard error occurred"), env.Undefined()); \ 88 | } 89 | -------------------------------------------------------------------------------- /src/providers/sqlpp.provider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | const kReservedWords = [ 4 | "AND", "ANY", "AS", "ASC", "BETWEEN", "BY", "CASE", "CROSS", "DESC", "DISTINCT", 5 | "ELSE", "END", "EVERY", "FALSE", "FROM", "GROUP", "HAVING", "IN", "INNER", "IS", 6 | "JOIN", "LEFT", "LIKE", "LIMIT", "MATCH", "META", "MISSING", "NATURAL", "NOT", 7 | "NULL", "MISSING", "OFFSET", "ON", "OR", "ORDER", "OUTER", "REGEX", "RIGHT", 8 | "SATISFIES", "SELECT", "THEN", "TRUE", "USING", "WHEN", "WHERE", 9 | "COLLATE" 10 | ]; 11 | 12 | const kFunctions = [ 13 | "array_avg", "array_contains", "array_count", "array_ifnull", "array_length", "array_max", 14 | "array_min", "array_of", "array_sum", 15 | "greatest", "least", 16 | "ifmissing", "ifnull", "ifmissingornull", "missingif", "nullif", 17 | "millis_to_str", "millis_to_utc", "str_to_millis", "str_to_utc", 18 | "abs", "acos", "asin", "atan", "atan2", "ceil", "cos", "degrees", "e", "exp", 19 | "floor", "ln", "log", "pi", "power", "radians", "round", "sign", "sin", "sqrt", 20 | "tan", "trunc", 21 | "regexp_contains", "regexp_like", "regexp_position", "regexp_replace", 22 | "contains", "length", "lower", "ltrim", "rtrim", "trim", "upper", 23 | "isarray", "isatom", "isboolean", "isnumber", "isobject", "isstring", "type", "toarray", 24 | "toatom", "toboolean", "tonumber", "toobject", "tostring", 25 | "rank", 26 | "avg", "count", "max", "min", "sum", 27 | "prediction", "euclidean_distance", "cosine_distance", 28 | ]; 29 | 30 | const kAllCompletions = kReservedWords.map(s => new vscode.CompletionItem(s, vscode.CompletionItemKind.Keyword)) 31 | .concat(kFunctions.map(s => new vscode.CompletionItem(s, vscode.CompletionItemKind.Function))); 32 | 33 | export const SqlppProvider = { 34 | provideCompletionItems, 35 | resolveCompletionItem 36 | }; 37 | 38 | export async function resolveCompletionItem(item: vscode.CompletionItem, token: vscode.CancellationToken) 39 | { 40 | if(item.kind === vscode.CompletionItemKind.Keyword) { 41 | return item; 42 | } 43 | 44 | item.insertText = item.label + "("; 45 | return item; 46 | } 47 | 48 | export async function provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) 49 | : Promise 50 | { 51 | return shouldProvide(document, position) ? 52 | provide(document, position) : 53 | Promise.resolve([]); 54 | } 55 | 56 | function shouldProvide(document: vscode.TextDocument, position: vscode.Position) { 57 | return true; 58 | } 59 | 60 | async function provide(document: vscode.TextDocument, position: vscode.Position) 61 | : Promise 62 | { 63 | return kAllCompletions; 64 | } -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | // import * as assert from 'assert'; 2 | 3 | // // You can import and use all API from the 'vscode' module 4 | // // as well as import your extension to test it 5 | // import * as vscode from 'vscode'; 6 | // // import * as myExtension from '../../extension'; 7 | 8 | // suite('Extension Test Suite', () => { 9 | // vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | // test('Sample test', () => { 12 | // assert.equal(-1, [1, 2, 3].indexOf(5)); 13 | // assert.equal(-1, [1, 2, 3].indexOf(0)); 14 | // }); 15 | // }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | // import * as path from 'path'; 2 | // import * as Mocha from 'mocha'; 3 | // import * as glob from 'glob'; 4 | 5 | // export function run(): Promise { 6 | // // Create the mocha test 7 | // const mocha = new Mocha({ 8 | // ui: 'tdd', 9 | // color: true 10 | // }); 11 | 12 | // const testsRoot = path.resolve(__dirname, '..'); 13 | 14 | // return new Promise((c, e) => { 15 | // glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | // if (err) { 17 | // return e(err); 18 | // } 19 | 20 | // // Add files to the test suite 21 | // files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | // try { 24 | // // Run the mocha test 25 | // mocha.run(failures => { 26 | // if (failures > 0) { 27 | // e(new Error(`${failures} tests failed.`)); 28 | // } else { 29 | // c(); 30 | // } 31 | // }); 32 | // } catch (err) { 33 | // console.error(err); 34 | // e(err); 35 | // } 36 | // }); 37 | // }); 38 | // } 39 | -------------------------------------------------------------------------------- /src/utils/clipboard.ts: -------------------------------------------------------------------------------- 1 | import { env } from "vscode"; 2 | 3 | namespace Clipboard { 4 | export function copy(text: string): Promise { 5 | return Promise.resolve(env.clipboard.writeText(text)); 6 | } 7 | 8 | export function read(): Promise { 9 | return Promise.resolve(env.clipboard.readText()); 10 | } 11 | } 12 | 13 | export default Clipboard; -------------------------------------------------------------------------------- /src/utils/files.ts: -------------------------------------------------------------------------------- 1 | import { lstatSync } from "fs"; 2 | import { basename, dirname } from "path"; 3 | import { window } from "vscode"; 4 | import { Commands } from "../extension"; 5 | import { CBLErrorDomain, Database, DatabaseConfiguration, EncryptionKeyMethods, LiteCoreErrorCode } from "../native/binding"; 6 | import { showErrorMessage } from "../vscodewrapper"; 7 | 8 | export async function openDbAtPath(filePath: string, password?: string, prev_err?: string): Promise { 9 | if(!isDirectorySync(filePath)) { 10 | let err: any; 11 | err.domain = CBLErrorDomain.LITE_CORE; 12 | err.code = LiteCoreErrorCode.NOT_A_DATABASE_FILE; 13 | err.message = `'${filePath}' is not a cblite2 folder.`; 14 | throw err; 15 | } 16 | 17 | let filename = basename(filePath); 18 | filename = filename.substring(0, filename.lastIndexOf(".")) || filename; 19 | let config = new DatabaseConfiguration(); 20 | config.directory = dirname(filePath); 21 | if(password) { 22 | if(prev_err) { 23 | let key = EncryptionKeyMethods.createFromPasswordOld(password); 24 | if(!key) { 25 | showErrorMessage(prev_err, {title: "Show output", command: Commands.showOutputChannel}); 26 | return undefined; 27 | } 28 | 29 | config.encryptionKey = key; 30 | } else { 31 | config.encryptionKey = EncryptionKeyMethods.createFromPassword(password); 32 | } 33 | } 34 | 35 | try { 36 | return new Database(filename, config); 37 | } catch(err: any) { 38 | let message = `Failed to open database: ${err.message}`; 39 | if(err.domain === CBLErrorDomain.LITE_CORE && 40 | err.code === LiteCoreErrorCode.NOT_A_DATABASE_FILE && 41 | !password) { 42 | password = await window.showInputBox({prompt: "Please enter the DB password", password: true}); 43 | if(!password) { 44 | showErrorMessage(message, {title: "Show output", command: Commands.showOutputChannel}); 45 | } else { 46 | return await openDbAtPath(filePath, password); 47 | } 48 | } else if(password && !prev_err) { 49 | return await openDbAtPath(filePath, password, err); 50 | } else { 51 | showErrorMessage(message, {title: "Show output", command: Commands.showOutputChannel}); 52 | } 53 | } 54 | 55 | return undefined; 56 | } 57 | 58 | export function isDirectorySync(filePath: string): boolean { 59 | try { 60 | var stat = lstatSync(filePath); 61 | return stat.isDirectory(); 62 | } catch(e) { 63 | // lstatSync throws an error if path doesn't exist 64 | return false; 65 | } 66 | } -------------------------------------------------------------------------------- /src/vscodewrapper/errorMessage.ts: -------------------------------------------------------------------------------- 1 | import { commands, window } from "vscode"; 2 | 3 | export interface ErrorMessageAction { 4 | title: string, 5 | command: string, 6 | args?: any[] 7 | } 8 | 9 | export function showErrorMessage(message: string, ...actions: ErrorMessageAction[]) { 10 | let items = actions.map(action => action.title); 11 | window.showErrorMessage(message, ...items).then(item => { 12 | actions.forEach(action => { 13 | if(action.title === item) { 14 | if(action.args) { 15 | commands.executeCommand(action.command, ...action.args); 16 | } else { 17 | commands.executeCommand(action.command); 18 | } 19 | } 20 | }); 21 | }); 22 | } -------------------------------------------------------------------------------- /src/vscodewrapper/index.ts: -------------------------------------------------------------------------------- 1 | import { showErrorMessage } from "./errorMessage"; 2 | import { pickWorkspaceDatabase, pickListDatabase } from "./quickPick"; 3 | import { getEditorQueryDocument, createQueryDocument, createDocContentDocument } from "./workspace"; 4 | 5 | export { 6 | pickWorkspaceDatabase, 7 | pickListDatabase, 8 | getEditorQueryDocument, 9 | createQueryDocument, 10 | createDocContentDocument, 11 | showErrorMessage 12 | }; -------------------------------------------------------------------------------- /src/vscodewrapper/quickPick.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from "fs"; 2 | import { basename, sep } from "path"; 3 | import { CancellationToken, CancellationTokenSource, QuickPickItem, window, workspace } from "vscode"; 4 | import { Database } from "../native/binding"; 5 | 6 | export namespace QuickPick { 7 | export class DatabaseItem implements QuickPickItem { 8 | path: string; 9 | label: string; 10 | description: string; 11 | detail?: string; 12 | picked?: boolean; 13 | 14 | constructor(path: string, description?: string) { 15 | this.path = sep === "/" ? path : path.replace(/\//g, sep); 16 | this.label = basename(path); 17 | this.description = description ?? this.path; 18 | } 19 | } 20 | 21 | export class CollectionItem implements QuickPickItem { 22 | label: string; 23 | detail?: string; 24 | picked?: boolean; 25 | 26 | constructor(readonly scope: string, readonly name: string) { 27 | this.label = `${scope}.${name}`; 28 | } 29 | } 30 | 31 | export class FileDialogItem implements QuickPickItem { 32 | label: string; 33 | description: string; 34 | detail?: string; 35 | picked?: boolean; 36 | 37 | constructor() { 38 | this.label = "Choose database from file"; 39 | this.description = ""; 40 | } 41 | } 42 | 43 | export class ErrorItem implements QuickPickItem { 44 | label: string; 45 | description?: string; 46 | detail?: string; 47 | picked?: boolean; 48 | 49 | constructor(label: string) { 50 | this.label = label; 51 | } 52 | } 53 | } 54 | 55 | export function pickWorkspaceDatabase(autopick: boolean, hint?: string): Thenable { 56 | const promise = new Promise>(resolve => { 57 | let items: Array = []; 58 | workspace.workspaceFolders?.forEach(wf => { 59 | items = items.concat(findDatabases(wf.uri.fsPath).map(item => new QuickPick.DatabaseItem(item))); 60 | }); 61 | 62 | items.push(new QuickPick.FileDialogItem); 63 | resolve(items); 64 | }); 65 | 66 | return new Promise((resolve, reject) => { 67 | hint = hint ? hint : "Choose a database."; 68 | showAutoQuickPick(autopick, promise, hint).then(item => { 69 | if(item instanceof QuickPick.DatabaseItem) { 70 | resolve(item.path); 71 | } else if(item instanceof QuickPick.FileDialogItem) { 72 | // eslint-disable-next-line @typescript-eslint/naming-convention 73 | window.showOpenDialog({filters: {"Database (.cblite2)": ["cblite2"]}, canSelectFiles: true, canSelectFolders: true}).then(fileUri => { 74 | if(fileUri) { 75 | resolve(fileUri[0].fsPath); 76 | } else { 77 | resolve(""); 78 | } 79 | }); 80 | } else { 81 | resolve(""); 82 | } 83 | }); 84 | }); 85 | } 86 | 87 | export function pickListCollection(dbObj: Database): Thenable<{scope: string, name: string}> { 88 | let items: QuickPick.CollectionItem[] = []; 89 | dbObj.getScopeNames().forEach(scope => { 90 | dbObj.getCollectionNames(scope).forEach(collection => { 91 | items.push(new QuickPick.CollectionItem(scope, collection)); 92 | }); 93 | }); 94 | 95 | return new Promise((resolve, reject) => { 96 | showAutoQuickPick(false, items, 'Choose a collection to use.').then(item => { 97 | if(item instanceof QuickPick.CollectionItem) { 98 | resolve({scope: item.scope, name: item.name}) 99 | } else { 100 | reject(); 101 | } 102 | }); 103 | }); 104 | } 105 | 106 | export function pickListDatabase(autopick: boolean, dbs: string[]): Thenable { 107 | let items: QuickPick.DatabaseItem[] | QuickPick.ErrorItem[]; 108 | if(dbs.length === 0) { 109 | items = []; 110 | } else { 111 | items = dbs.map(dbPath => new QuickPick.DatabaseItem(dbPath)); 112 | } 113 | 114 | return new Promise((resolve, reject) => { 115 | showAutoQuickPick(autopick, items, 'Choose a database to close.').then(item => { 116 | if(item instanceof QuickPick.DatabaseItem) { 117 | resolve(item.path); 118 | } else { 119 | reject(); 120 | } 121 | }); 122 | }); 123 | } 124 | 125 | export function showAutoQuickPick(autopick: boolean, items: QuickPickItem[] | Thenable, hint?: string): Thenable { 126 | if(autopick && items instanceof Array && items.length === 1) { 127 | let item = items[0]; 128 | return new Promise(resolve => resolve(item)); 129 | } 130 | 131 | return new Promise((resolve, reject) => { 132 | let cancTokenSource: CancellationTokenSource | undefined; 133 | let cancToken: CancellationToken | undefined; 134 | 135 | if(autopick && !(items instanceof Array)) { 136 | cancTokenSource = new CancellationTokenSource(); 137 | cancToken = cancTokenSource.token; 138 | 139 | items.then(items => { 140 | if (items.length === 1) { 141 | let item = items[0]; 142 | resolve(item); 143 | 144 | if (cancTokenSource) { 145 | cancTokenSource.cancel(); 146 | cancTokenSource.dispose(); 147 | } 148 | } 149 | }); 150 | } 151 | 152 | window.showQuickPick(items, {placeHolder: hint? hint : ''}, cancToken).then(item => { 153 | resolve(item!); 154 | 155 | if (cancTokenSource) { 156 | cancTokenSource.dispose(); 157 | } 158 | }); 159 | }); 160 | } 161 | 162 | function findDatabases(location: string) : string[] { 163 | let results: string[] = []; 164 | readdirSync(location).forEach(file => { 165 | if(file.startsWith(".")) { 166 | return; 167 | } 168 | 169 | let fullPath = location+'/'+file; 170 | let stat = statSync(fullPath); 171 | if(stat && stat.isDirectory()) { 172 | if(file.endsWith("cblite2")) { 173 | results.push(fullPath); 174 | } 175 | 176 | results = results.concat(findDatabases(fullPath)); 177 | } 178 | }); 179 | 180 | return results; 181 | } -------------------------------------------------------------------------------- /src/vscodewrapper/workspace.ts: -------------------------------------------------------------------------------- 1 | import { Position, Selection, TextDocument, ViewColumn, workspace } from "vscode"; 2 | import { window } from 'vscode'; 3 | 4 | export function getEditorQueryDocument(): TextDocument|undefined { 5 | let editor = window.activeTextEditor; 6 | if(editor) { 7 | return editor.document.languageId === 'sqlpp' || editor.document.languageId === 'json' ? editor.document : undefined; 8 | } 9 | 10 | return undefined; 11 | } 12 | 13 | export function createQueryDocument(content: string, cursorPos: Position, show?: boolean): Thenable { 14 | let json: boolean = !content.startsWith("select"); 15 | return workspace.openTextDocument({language: json ? 'json' : 'sqlpp', content: content}).then(doc => { 16 | if(show) { 17 | window.showTextDocument(doc, ViewColumn.One).then(editor => { 18 | editor.selection = new Selection(cursorPos, cursorPos); 19 | }); 20 | } 21 | 22 | return Promise.resolve(doc); 23 | }); 24 | } 25 | 26 | export function createDocContentDocument(json: string): Thenable { 27 | return workspace.openTextDocument({language: "json", content: json}).then(doc => { 28 | window.showTextDocument(doc, ViewColumn.One); 29 | return Promise.resolve(doc); 30 | }); 31 | } -------------------------------------------------------------------------------- /standalone_test/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"); 4 | const { EncryptionAlgorithm, EncryptionKey, DatabaseConfiguration, Database, MutableDocument, Blob, ValueIndexConfiguration, QueryLanguage } = require("../out/native/binding.js"); 5 | 6 | function testBasic() 7 | { 8 | const instance = new DatabaseConfiguration(); 9 | assert.strictEqual(instance.directory, undefined); 10 | instance.directory = "/tmp"; 11 | assert.strictEqual(instance.directory, "/tmp", "Unexpected value returned"); 12 | } 13 | 14 | function testEncryptionKey() 15 | { 16 | const instance = new DatabaseConfiguration(); 17 | assert.strictEqual(instance.encryptionKey, undefined); 18 | var key = new EncryptionKey(); 19 | assert.strictEqual(key.algorithm, EncryptionAlgorithm.NONE); 20 | 21 | const bytes = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 22 | 24, 25, 26, 27, 28, 29, 30, 31]); 23 | key = new EncryptionKey(EncryptionAlgorithm.AES256, bytes); 24 | assert.strictEqual(key.algorithm, EncryptionAlgorithm.AES256); 25 | assert.strictEqual(key.bytes.byteLength, 32); 26 | const bytes2 = key.bytes; 27 | assert.deepStrictEqual(bytes.buffer, bytes2); 28 | 29 | instance.encryptionKey = key; 30 | assert.strictEqual(instance.encryptionKey, key); 31 | } 32 | 33 | assert.doesNotThrow(testBasic, undefined, "testBasic threw an expection"); 34 | assert.doesNotThrow(testEncryptionKey, undefined, "testEncryptionKey threw an exception"); 35 | 36 | assert.strictEqual(Database.exists("invalid", "/tmp"), false); 37 | const dbConfig = new DatabaseConfiguration(); 38 | dbConfig.directory = "/tmp"; 39 | const db = new Database("test", dbConfig); 40 | console.log(db.name, db.path); 41 | 42 | const doc = new MutableDocument("test-doc"); 43 | console.log(doc.id); 44 | 45 | doc.cool = true; 46 | doc.answer = 42n; 47 | doc.name = "Jim"; 48 | doc.array = [1, 2, 3]; 49 | doc.blob = new Blob("application/octet-stream", new Uint8Array([0, 1, 2, 3, 4, 5])); 50 | console.log(doc); 51 | 52 | var coll = db.getDefaultCollection(); 53 | 54 | try { 55 | coll.saveDocument(doc); 56 | } catch(err) { 57 | console.log(err); 58 | } 59 | 60 | const gotDoc = coll.getDocument(doc.id); 61 | console.log(gotDoc.id, gotDoc.revisionID, gotDoc, gotDoc.blob.digest); 62 | 63 | var query = db.createQuery(QueryLanguage.SQLPP, "SELECT * FROM _"); 64 | console.log(query.columnNames()); 65 | console.log(query.execute()); 66 | 67 | coll.deleteDocument(doc); 68 | 69 | console.log("Indexes:", coll.getIndexNames()); 70 | var config = new ValueIndexConfiguration(); 71 | config.expressionLanguage = QueryLanguage.SQLPP; 72 | config.expressions = "name"; 73 | coll.createValueIndex("tmp", config); 74 | console.log("Indexes:", coll.getIndexNames()); 75 | coll.deleteIndex("tmp"); 76 | 77 | console.log("Tests passed- everything looks OK!"); -------------------------------------------------------------------------------- /syntaxes/sqlpp.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "scopeName": "source.sql.sqlpp", 3 | "patterns": [{ 4 | "include": "#comment" 5 | }, { 6 | "include": "#sqlpp-stmt" 7 | }], 8 | "repository": { 9 | "sqlpp-stmt": { 10 | "patterns": [{ 11 | "include": "#select-stmt" 12 | }] 13 | }, 14 | "select-stmt": { 15 | "patterns": [{ 16 | "include": "#select-stmt-select" 17 | }, { 18 | "include": "#select-stmt-values" 19 | }] 20 | }, 21 | "with-clause-as": { 22 | "begin": "(?i)\\b(as)\\b\\s*", 23 | "end": "(?<=\\))", 24 | "name": "meta.other.cte-as.sql.sqlpp", 25 | "beginCaptures": { 26 | "1": { 27 | "name": "keyword.other.sql.sqlpp" 28 | } 29 | }, 30 | "endCaptures": {}, 31 | "patterns": [{ 32 | "begin": "\\(", 33 | "end": "\\)", 34 | "name": "meta.cte-select-stmt.sql.sqlpp", 35 | "beginCaptures": {}, 36 | "endCaptures": {} 37 | }] 38 | }, 39 | "common-table-expression": { 40 | "patterns": [{ 41 | "include": "#with-clause-as" 42 | }, { 43 | "include": "#name" 44 | }, { 45 | "begin": "\\(", 46 | "end": "\\)", 47 | "beginCaptures": {}, 48 | "endCaptures": {}, 49 | "patterns": [{ 50 | "include": "#name" 51 | }] 52 | }] 53 | }, 54 | "select-stmt-select": { 55 | "begin": "(?i)\\b(select)\\b\\s*", 56 | "end": "(?=;)", 57 | "name": "meta.statement.select.sql.sqlpp", 58 | "beginCaptures": { 59 | "1": { 60 | "name": "keyword.other.DML.sql.sqlpp" 61 | } 62 | }, 63 | "patterns": [{ 64 | "include": "#expr" 65 | }] 66 | }, 67 | "select-stmt-values": { 68 | "begin": "(?i)\\b(values)\\s*", 69 | "end": "(?=;)", 70 | "name": "meta.statement.values.sql.sqlpp", 71 | "beginCaptures": { 72 | "1": { 73 | "name": "keyword.other.DML.sql.sqlpp" 74 | } 75 | }, 76 | "patterns": [{ 77 | "include": "#rowvalue" 78 | }] 79 | }, 80 | "rowvalue": { 81 | "begin": "\\(", 82 | "end": "\\)", 83 | "beginCaptures": { 84 | "0": { 85 | "name": "punctuation.section.rowvalue.values.begin" 86 | } 87 | }, 88 | "endCaptures": { 89 | "0": { 90 | "name": "punctuation.section.rowvalue.values.end" 91 | } 92 | }, 93 | "name": "meta.rowvalue.sql.sqlpp", 94 | "patterns": [{ 95 | "include": "#expr" 96 | }, { 97 | "match": ",", 98 | "name": "punctuation.separator.rowvalue" 99 | }] 100 | }, 101 | "expr": { 102 | "patterns": [{ 103 | "include": "#comment" 104 | }, { 105 | "include": "#keyword" 106 | }, { 107 | "include": "#expr-parens" 108 | }, { 109 | "include": "#function" 110 | }, { 111 | "include": "#literal-value" 112 | }, { 113 | "include": "#name" 114 | }] 115 | }, 116 | "expr-parens": { 117 | "begin": "\\(", 118 | "end": "\\)", 119 | "name": "meta.parens", 120 | "patterns": [{ 121 | "include": "#keyword" 122 | }, { 123 | "include": "#expr-parens" 124 | }, { 125 | "include": "#function" 126 | }, { 127 | "include": "#literal-value" 128 | }, { 129 | "include": "#name" 130 | }] 131 | }, 132 | "literal-value": { 133 | "patterns": [{ 134 | "match": "(?i)\\b(null|true|false|missing)\\b", 135 | "name": "constant.language.sql.sqlpp" 136 | }, { 137 | "include": "#numeric-literal" 138 | }, { 139 | "include": "#string-literal" 140 | }] 141 | }, 142 | "function": { 143 | "begin": "(?i)\\b(array_avg|array_contains|array_count|array_ifnull|array_length|array_max|array_min|array_of|array_sum|greatest|least|ifmissing|ifnull|ifmissingornull|missingif|nullif|millis_to_str|millis_to_utc|str_to_millis|str_to_utc|abs|acos|asin|atan|atan2|ceil|cos|degrees|e|exp|floor|ln|log|pi|power|radians|round|sign|sin|sqrt|tan|trunc|regexp_contains|regexp_like|regexp_position|regexp_replace|contains|length|lower|ltrim|rtrim|trim|upper|isarray|isatom|isboolean|isnumber|isobject|isstring|type|toarray|toatom|toboolean|tonumber|toobject|tostring|rank|avg|count|max|min|sum)\\b\\s*(\\()", 144 | "end": "\\)", 145 | "name": "meta.function-call.sql.sqlpp", 146 | "beginCaptures": { 147 | "1": { 148 | "name": "entity.name.function.sql.sqlpp" 149 | } 150 | }, 151 | "patterns": [{ 152 | "include": "#expr" 153 | }] 154 | }, 155 | "keyword": { 156 | "match": "(?i)\\b(and|any|as|asc|between|by|case|collate|cross|desc|distinct|else|end|every|from|group|having|in|inner|is|join|left|like|limit|natural|not|offset|on|or|order|outer|regex|right|satisfies|select|then|using|when|where)\\b", 157 | "name": "keyword.other.sql.sqlpp" 158 | }, 159 | "name": { 160 | "patterns": [{ 161 | "include": "#doublequoted-name" 162 | }, { 163 | "include": "#backquoted-name" 164 | }, { 165 | "include": "#bare-name" 166 | }] 167 | }, 168 | "bare-name": { 169 | "match": "\\b(?i)(?!and\\b|any\\b|as\\b|asc\\b|between\\b|by\\b|case\\b|collate\\b|cross\\b|desc\\b|distinct\\b|else\\b|end\\b|every\\b|from\\b|group\\b|having\\b|in\\b|inner\\b|is\\b|join\\b|left\\b|like\\b|limit\\b|natural\\b|not\\b|offset\\b|on\\b|or\\b|order\\b|outer\\b|regex\\b|right\\b|satisfies\\b|select\\b|then\\b|using\\b|when\\b|where\\b)\\w+\\b", 170 | "name": "variable.other.bare" 171 | }, 172 | "doublequoted-name": { 173 | "begin": "\"", 174 | "end": "\"(?!\")", 175 | "name": "string.quoted.double.sql.sqlpp", 176 | "patterns": [{ 177 | "match": "\"\"", 178 | "name": "constant.character.escape.doublequote.sql.sqlpp" 179 | }] 180 | }, 181 | "backquoted-name": { 182 | "begin": "`", 183 | "end": "`(?!`)", 184 | "name": "string.quoted.backtick.sql.sqlpp", 185 | "patterns": [{ 186 | "match": "``", 187 | "name": "constant.character.escape.backquote.sql.sqlpp" 188 | }] 189 | }, 190 | "numeric-literal": { 191 | "match": "(?i)(?", ret); 46 | return ret; 47 | } 48 | } 49 | } 50 | ] 51 | } 52 | }; 53 | 54 | module.exports = config; --------------------------------------------------------------------------------