├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ └── bug.yaml └── workflows │ ├── ciBuild.yml │ └── lint.yml ├── .gitignore ├── .prettierrc ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── media ├── features │ ├── database │ │ ├── scr1.png │ │ └── scr2.png │ ├── functions │ │ ├── CreateTag.gif │ │ └── functionsOverview.png │ ├── health │ │ └── scr1.png │ ├── projects │ │ └── projectsView1.gif │ └── users │ │ └── scr1.png └── readmeCoverImage.png ├── package-lock.json ├── package.json ├── resources ├── AppwriteIcon.png └── vscode-appwrite.svg ├── src ├── appwrite.d.ts ├── appwrite │ ├── Database.ts │ ├── Functions.ts │ ├── Health.ts │ ├── Storage.ts │ └── Users.ts ├── client.ts ├── commands │ ├── common │ │ ├── editValue.ts │ │ └── viewMore.ts │ ├── connectAppwrite.ts │ ├── database │ │ ├── createCollection.ts │ │ ├── createRule.ts │ │ ├── deleteCollection.ts │ │ ├── deleteDocument.ts │ │ ├── openDocument.ts │ │ ├── permissions │ │ │ ├── createPermission.ts │ │ │ ├── deletePermission.ts │ │ │ └── editPermission.ts │ │ ├── refreshCollection.ts │ │ ├── refreshCollectionsList.ts │ │ ├── removeRule.ts │ │ └── viewCollectionAsJson.ts │ ├── functions │ │ ├── activateTag.ts │ │ ├── copyExecutionErrors.ts │ │ ├── copyExecutionOutput.ts │ │ ├── createExecution.ts │ │ ├── createFunction.ts │ │ ├── createFunctionVar.ts │ │ ├── createTag.ts │ │ ├── deleteFunction.ts │ │ ├── deleteFunctionVar.ts │ │ ├── deleteTag.ts │ │ ├── openExecutionsInBrowser.ts │ │ ├── openFunctionSettingsInBrowser.ts │ │ ├── openFunctionTagsInBrowser.ts │ │ ├── viewExecutionErrors.ts │ │ └── viewExecutionOutput.ts │ ├── openDocumentation.ts │ ├── project │ │ ├── addProject.ts │ │ ├── removeProject.ts │ │ └── setActiveProject.ts │ ├── registerCommands.ts │ └── users │ │ ├── copyUserEmail.ts │ │ ├── copyUserId.ts │ │ ├── createUser.ts │ │ ├── deleteUser.ts │ │ ├── getUserLogs.ts │ │ ├── openUserInConsole.ts │ │ ├── refreshUsersList.ts │ │ └── viewUserPrefs.ts ├── constants.ts ├── extension.ts ├── extensionVariables.ts ├── settings.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts ├── tree │ ├── ChildTreeItem.ts │ ├── CollapsableTreeItem.ts │ ├── CommandTreeItem.ts │ ├── common │ │ ├── EditableTreeItemBase.ts │ │ ├── EnumEditableTreeItem.ts │ │ ├── SimpleEditableTreeItem.ts │ │ ├── StringEditableTreeItem.ts │ │ └── editable │ │ │ ├── EditableTreeItemBase.ts │ │ │ ├── EnumEditableTreeItem.ts │ │ │ ├── SimpleEditableTreeItem.ts │ │ │ └── StringEditableTreeItem.ts │ ├── database │ │ ├── CollectionTreeItem.ts │ │ ├── DatabaseTreeItemProvider.ts │ │ ├── DocumentTreeItem.ts │ │ ├── DocumentsTreeItem.ts │ │ └── settings │ │ │ ├── PermissionTreeItem.ts │ │ │ ├── PermissionsTreeItem.ts │ │ │ ├── RuleTreeItem.ts │ │ │ └── RulesTreeItem.ts │ ├── functions │ │ ├── FunctionTreeItem.ts │ │ ├── FunctionsTreeItemProvider.ts │ │ ├── executions │ │ │ ├── ExecutionTreeItem.ts │ │ │ └── ExecutionsTreeItem.ts │ │ ├── settings │ │ │ ├── EventsTreeItem.ts │ │ │ ├── FunctionSettingsTreeItem.ts │ │ │ ├── NameTreeItem.ts │ │ │ ├── ScheduleTreeItem.ts │ │ │ ├── TimeoutTreeItem.ts │ │ │ ├── VarTreeItem.ts │ │ │ └── VarsTreeItem.ts │ │ └── tags │ │ │ ├── TagTreeItem.ts │ │ │ └── TagsTreeItem.ts │ ├── health │ │ ├── HealthTreeItem.ts │ │ └── HealthTreeItemProvider.ts │ ├── projects │ │ ├── ProjectTreeItem.ts │ │ └── ProjectsTreeItemProvider.ts │ ├── storage │ │ ├── FileTreeItem.ts │ │ └── StorageTreeItemProvider.ts │ └── users │ │ ├── UserPropertyTreeItemBase.ts │ │ ├── UserTreeItem.ts │ │ ├── UserTreeItemProvider.ts │ │ └── properties │ │ └── UserPrefsTreeItem.ts ├── ui │ ├── AddProjectWizard.ts │ ├── AppwriteOutputChannel.ts │ ├── AppwriteTreeItemBase.ts │ ├── BaseEditor.ts │ ├── DialogResponses.ts │ ├── confirmDialog.ts │ ├── createRuleWizard.ts │ ├── createTemporaryFile.ts │ └── openReadonlyContent.ts └── utils │ ├── AppwriteCall.ts │ ├── date.ts │ ├── nonNullUtils.ts │ ├── openUrl.ts │ ├── promiseWithTimeout.ts │ ├── refreshTree.ts │ ├── sleep.ts │ ├── tar.ts │ ├── types.d.ts │ ├── validation.ts │ └── workspace.ts ├── tsconfig.json ├── vsc-extension-quickstart.md └── 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 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended" 14 | ], 15 | "rules": { 16 | "@typescript-eslint/naming-convention": "off", 17 | "@typescript-eslint/semi": "warn", 18 | "curly": "warn", 19 | "eqeqeq": "warn", 20 | "no-throw-literal": "warn", 21 | "semi": "off", 22 | "@typescript-eslint/no-unused-vars": [ 23 | "error", 24 | { 25 | "argsIgnorePattern": "^_" 26 | } 27 | ], 28 | "no-unused-vars": "off", 29 | "no-useless-escape": "off", 30 | "no-inner-declarations": "off", 31 | "no-case-declarations": "off", 32 | "@typescript-eslint/prefer-regexp-exec": "off", 33 | "@typescript-eslint/no-inferrable-types": "off", 34 | "@typescript-eslint/unbound-method": "off", 35 | "@typescript-eslint/no-unnecessary-type-assertion": "off", 36 | "@typescript-eslint/restrict-template-expressions": "off", 37 | "@typescript-eslint/no-namespace": "off", 38 | "@typescript-eslint/ban-types": "off", 39 | "@typescript-eslint/no-explicit-any": "off" 40 | }, 41 | "ignorePatterns": [ 42 | "**/*.d.ts" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug] " 4 | labels: [bug] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | - type: textarea 11 | id: repro 12 | attributes: 13 | label: Reproduction steps 14 | description: "How do you trigger this bug? Please walk us through it step by step." 15 | value: | 16 | 1. 17 | 2. 18 | 3. 19 | ... 20 | render: bash 21 | validations: 22 | required: true 23 | - type: dropdown 24 | id: version 25 | attributes: 26 | label: Version 27 | description: What version of VS Code are you using? 28 | options: 29 | - Stable (Default) 30 | - Insiders 31 | validations: 32 | required: true 33 | - type: dropdown 34 | id: os 35 | attributes: 36 | label: What operating system are you seeing the problem on? 37 | multiple: true 38 | options: 39 | - All 40 | - Windows 41 | - macOS 42 | - Linux 43 | - type: textarea 44 | id: logs 45 | attributes: 46 | label: Relevant log output 47 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 48 | render: shell 49 | -------------------------------------------------------------------------------- /.github/workflows/ciBuild.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CIBuild 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npx vsce package 30 | 31 | - uses: actions/upload-artifact@v2 32 | with: 33 | name: vsix 34 | path: '*.vsix' 35 | if-no-files-found: error # 'warn' or 'ignore' are also available, defaults to `warn` 36 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Lint 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm ci 29 | - run: npm run lint 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "printWidth": 140, 5 | "proseWrap": "never" 6 | } -------------------------------------------------------------------------------- /.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 | // A launch configuration that compiles the extension and then opens it inside a new window 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 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "npm: test-watch" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": 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 | } -------------------------------------------------------------------------------- /.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": [ 10 | "$ts-webpack-watch", 11 | "$tslint-webpack-watch" 12 | ], 13 | "isBackground": true, 14 | "presentation": { 15 | "reveal": "never" 16 | }, 17 | "group": { 18 | "kind": "build", 19 | "isDefault": true 20 | } 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "test-watch", 25 | "problemMatcher": "$tsc-watch", 26 | "isBackground": true, 27 | "presentation": { 28 | "reveal": "never" 29 | }, 30 | "group": "build" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-appwrite" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.1.3] - 2021-6-17 10 | ## Added 11 | - New feature! JSON strings inside documents will now be automatically formatted as JSON when viewing documents. You can turn this feature off via the `appwrite.formatJsonStrings` setting. 12 | 13 | ## [0.1.2] - 2021-6-03 14 | ## Added 15 | - Ability to set the `list`, `default` and `array` properties when creating a new collection. | [Issue #20](https://github.com/streamlux/vscode-appwrite/issues/20) | [PR #21](https://github.com/streamlux/vscode-appwrite/pull/21) | Thanks [@Maatteogekko](https://github.com/Maatteogekko)! 16 | 17 | ## [0.1.1] - 2021-5-31 18 | ## Added 19 | - You can now easily create function tags from multiple places in the extension. [PR #19](https://github.com/streamlux/vscode-appwrite/pull/19) 20 | 21 | ## Fixed 22 | - Fixed an error when deleting a user. [Issue #17](https://github.com/streamlux/vscode-appwrite/issues/17) [PR #18](https://github.com/streamlux/vscode-appwrite/pull/18) Thanks [@aadarshadhakalg](https://github.com/aadarshadhakalg)! 23 | 24 | ## [0.1.0] - 2021-5-29 25 | 26 | ## Functions! 27 | ### Added 28 | - Ability to create and delete Appwrite functions 29 | - Edit function settings 30 | - View, and delete tags (creating tags is broken currently) 31 | - Create and view function executions 32 | - View execution output and errors 33 | 34 | ## [0.0.9] - 2021-5-21 35 | - Remove temporary fix for Appwrite https://github.com/appwrite/appwrite/issues/1171. Upstream issue was resolved. 36 | 37 | ## [0.0.8] - 2021-5-21 38 | - Temp fix for Appwrite https://github.com/appwrite/appwrite/issues/1171 39 | 40 | ## [0.0.7] - 2021-5-14 41 | ### Fixed 42 | - Fixed a bug where the password validation for creating a new user did not follow the Appwrite spec. [Issue](https://github.com/streamlux/vscode-appwrite/issues/11) 43 | - Show nicer message when Appwrite project can't be found. [Issue](https://github.com/streamlux/vscode-appwrite/pull/14) 44 | 45 | ## [0.0.6] - 2021-4-30 46 | ### Fixed 47 | - Fixed a bug where the extension could not connect to Appwrite instances over localhost beacuse of self-signed certificates. 48 | 49 | ## [0.0.5] - 2021-4-30 50 | ### Fixed 51 | - Sometimes views would not refresh after adding/removing a project [PR](https://github.com/streamlux/vscode-appwrite/pull/7) 52 | 53 | ## [0.0.4] - 2021-4-30 54 | 55 | ### Fixed 56 | - Hotfix 57 | 58 | ## [0.0.3] - 2021-4-30 59 | 60 | ### Fixed 61 | - Errors when user has no projects 62 | 63 | ## [0.0.2] - 2021-4-30 64 | 65 | ### Added 66 | - Projects view 67 | - Ability to set active project 68 | - Refresh storage command 69 | 70 | 71 | ## [0.0.1] - 2021-4-29 72 | 73 | - Initial release 74 | - View and manage collections and documents 75 | - View and manage users 76 | - Monitor Appwrite health 77 | - View files in storage 78 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 Streamlux LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Cover image](media/readmeCoverImage.png) 2 | 3 | # Appwrite for Visual Studio Code 4 | 5 | Use the Appwrite extension to quickly monitor, manage, and interact with your Appwrite instance directly from VS Code. 6 | 7 | [![Version](https://vsmarketplacebadge.apphb.com/version/streamlux.vscode-appwrite.svg)](https://marketplace.visualstudio.com/items?itemName=streamlux.vscode-appwrite) [![Installs](https://vsmarketplacebadge.apphb.com/installs-short/streamlux.vscode-appwrite.svg)](https://marketplace.visualstudio.com/items?itemName=streamlux.vscode-appwrite) 8 | 9 | ## What is Appwrite? 10 | 11 | From [appwrite.io](https://appwrite.io) 12 | 13 | > Secure Open-Source Backend Server for Web, Mobile & Flutter Developers 14 | > 15 | > Appwrite is a self-hosted solution that provides developers with a set of easy-to-use and integrate REST APIs to manage their core backend needs. 16 | 17 | ## Features 18 | 19 | ### Connect to multiple Appwrite projects 20 | 21 | 22 | 23 | ### Creating function tags with ease! 24 | 25 | ![Functions feature](media/features/functions/CreateTag.gif) 26 | 27 | ### Create and manage Appwrite cloud functions, upload tags, and view function output 28 | 29 | 30 | 31 | ### View database documents right inside VS Code. 32 | 33 | ![Database feature](media/features/database/scr2.png) 34 | 35 | ### Manage database collection permissions and rules. 36 | 37 | ![Database feature](media/features/database/scr1.png) 38 | 39 | ### Create and view users, user preferences, and more. 40 | ![Users feature](media/features/users/scr1.png) 41 | 42 | ### Quickly and easily check the health of all the Appwrite services. 43 | 44 | ![Health feature](media/features/health/scr1.png) 45 | 46 | ## Requirements 47 | 48 | This extension does not provide features for setting up or installing Appwrite. Only managing and interacting with Appwrite once it's running. 49 | 50 | ## Extension Settings 51 | 52 | Include if your extension adds any VS Code settings through the `contributes.configuration` extension point. 53 | 54 | For example: 55 | 56 | This extension contributes the following settings: 57 | 58 | * `appwrite.projects`: List of Appwrite project configurations. To set up a project configuration, run the `Connect to Appwrite` command. Search commands by hitting F1, then search `Connect to Appwrite`. 59 | 60 | After connecting to an Appwrite project, your `appwrite.projects` setting will contain: 61 | 62 | ```json 63 | { 64 | "endpoint": "https://[Domain]/v1", 65 | "projectId": "[Project ID]", 66 | "secret": "API key with all scopes", 67 | "selfSigned": "boolean", // set to true if you're connecting to Appwrite over localhost 68 | "nickname": "My project" 69 | } 70 | ``` 71 | 72 | We plan on adding better multi-project support in the future. 73 | 74 | ## Known Issues 75 | 76 | If you find issues, or want to suggest features, please file them in the issues section of the repository. 77 | 78 | This extension has not been tested with large >1000 users or documents so it may hang or slow down if you try to load collections with large amounts of documents. If this is the case please report it! 79 | 80 | ----------------------------------------------------------------------------------------------------------- 81 | 82 | ## Contributing 83 | 84 | There are a couple of ways you can contribute to this repo: 85 | 86 | * **Ideas, feature requests and bugs**: We are open to all ideas and we want to get rid of bugs! Use the Issues section to either report a new issue, provide your ideas or contribute to existing threads. 87 | * **Documentation**: Found a typo or strangely worded sentences? Submit a PR! 88 | * **Code**: Contribute bug fixes, features or design changes: 89 | * Clone the repository locally and open in VS Code. 90 | * Open the terminal (press CTRL+ \`) and run `npm install`. 91 | * Debug: press F5 to start debugging the extension. 92 | 93 | ## License 94 | 95 | [MIT](LICENSE.md) 96 | 97 | **Enjoy!** 98 | -------------------------------------------------------------------------------- /media/features/database/scr1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/features/database/scr1.png -------------------------------------------------------------------------------- /media/features/database/scr2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/features/database/scr2.png -------------------------------------------------------------------------------- /media/features/functions/CreateTag.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/features/functions/CreateTag.gif -------------------------------------------------------------------------------- /media/features/functions/functionsOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/features/functions/functionsOverview.png -------------------------------------------------------------------------------- /media/features/health/scr1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/features/health/scr1.png -------------------------------------------------------------------------------- /media/features/projects/projectsView1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/features/projects/projectsView1.gif -------------------------------------------------------------------------------- /media/features/users/scr1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/features/users/scr1.png -------------------------------------------------------------------------------- /media/readmeCoverImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/media/readmeCoverImage.png -------------------------------------------------------------------------------- /resources/AppwriteIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/resources/AppwriteIcon.png -------------------------------------------------------------------------------- /resources/vscode-appwrite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/appwrite.d.ts: -------------------------------------------------------------------------------- 1 | import { ReadStream } from 'fs'; 2 | import { Stream } from 'node:stream'; 3 | 4 | export type Token = { 5 | /** 6 | * Token ID. 7 | */ 8 | $id: string; 9 | /** 10 | * User ID. 11 | */ 12 | userId: string; 13 | /** 14 | * Token secret key. This will return an empty string unless the response is returned using an API key or as part of a webhook payload. 15 | */ 16 | secret: string; 17 | /** 18 | * Token expiration date in Unix timestamp. 19 | */ 20 | expire: number; 21 | }; 22 | 23 | export type User = { 24 | /** 25 | * User ID. 26 | */ 27 | $id: string; 28 | /** 29 | * User name. 30 | */ 31 | name: string; 32 | /** 33 | * User registration date in Unix timestamp. 34 | */ 35 | registration: number; 36 | /** 37 | * User status. 0 for Unactivated, 1 for active and 2 is blocked. 38 | * @deprecated according to developers 39 | */ 40 | status: number; 41 | /** 42 | * User email address. 43 | */ 44 | email: string; 45 | /** 46 | * Email verification status. 47 | */ 48 | emailVerification: boolean; 49 | /** 50 | * User preferences as a key-value object 51 | */ 52 | prefs: Record; 53 | }; 54 | 55 | export type UsersList = { 56 | /** 57 | * Total sum of items in the list. 58 | */ 59 | sum: number; 60 | /** 61 | * List of users. 62 | */ 63 | users: User[]; 64 | }; 65 | 66 | export type Error = { 67 | /** 68 | * Error message. 69 | */ 70 | message: string; 71 | 72 | /** 73 | * Error code. 74 | */ 75 | code: string; 76 | 77 | /** 78 | * Server version number. 79 | */ 80 | version: string; 81 | }; 82 | 83 | export type Session = { 84 | /** 85 | * Session ID. 86 | */ 87 | $id: string; 88 | /** 89 | * User ID. 90 | */ 91 | userId: string; 92 | /** 93 | * Session expiration date in Unix timestamp. 94 | */ 95 | expire: number; 96 | /** 97 | * IP in use when the session was created. 98 | */ 99 | ip: string; 100 | /** 101 | * Returns true if this the current user session. 102 | */ 103 | current: boolean; 104 | } & ClientInfo; 105 | 106 | export type Log = { 107 | /** 108 | * Event name. 109 | */ 110 | event: string; 111 | /** 112 | * IP session in use when the session was created. 113 | */ 114 | ip: string; 115 | /** 116 | * Log creation time in Unix timestamp. 117 | */ 118 | time: number; 119 | } & ClientInfo; 120 | 121 | type ClientInfo = { 122 | /** 123 | * Operating system code name. View list of possible values: 124 | * https://github.com/appwrite/appwrite/blob/master/docs/lists/os.json 125 | */ 126 | osCode: string; 127 | /** 128 | * Operating system name. 129 | */ 130 | osName: string; 131 | /** 132 | * Operating system version. 133 | */ 134 | osVersion: string; 135 | /** 136 | * Client type. 137 | */ 138 | clientType: string; 139 | /** 140 | * Client code name. View list of possible values: 141 | * https://github.com/appwrite/appwrite/blob/master/docs/lists/clients.json 142 | */ 143 | clientCode: string; 144 | /** 145 | * Client name. 146 | */ 147 | clientName: string; 148 | /** 149 | * Client version. 150 | */ 151 | clientVersion: string; 152 | /** 153 | * Client engine name. 154 | */ 155 | clientEngine: string; 156 | /** 157 | * Client engine version. 158 | */ 159 | clientEngineVersion: string; 160 | /** 161 | * Device name. 162 | */ 163 | deviceName: string; 164 | /** 165 | * Device brand name. 166 | */ 167 | deviceBrand: string; 168 | /** 169 | * Device model name. 170 | */ 171 | deviceModel: string; 172 | /** 173 | * Country two-character ISO 3166-1 alpha code. 174 | */ 175 | countryCode: string; 176 | /** 177 | * Country name. 178 | */ 179 | countryName: string; 180 | }; 181 | 182 | export type Team = { 183 | /** 184 | * Team ID. 185 | */ 186 | $id: string; 187 | /** 188 | * Team name. 189 | */ 190 | name: string; 191 | /** 192 | * Team creation date in Unix timestamp. 193 | */ 194 | dateCreated: number; 195 | /** 196 | * Total sum of team members. 197 | */ 198 | sum: number; 199 | }; 200 | 201 | type Membership = { 202 | /** 203 | * Membership ID. 204 | */ 205 | $id: string; 206 | 207 | /** 208 | * User ID. 209 | */ 210 | userId: string; 211 | 212 | /** 213 | * Team ID. 214 | */ 215 | teamId: string; 216 | 217 | /** 218 | * User name. 219 | */ 220 | name: string; 221 | 222 | /** 223 | * User email address. 224 | */ 225 | email: string; 226 | 227 | /** 228 | * Date, the user has been invited to join the team in Unix timestamp. 229 | */ 230 | invited: number; 231 | 232 | /** 233 | * Date, the user has accepted the invitation to join the team in Unix timestamp. 234 | */ 235 | joined: number; 236 | 237 | /** 238 | * User confirmation status, true if the user has joined the team or false otherwise. 239 | */ 240 | confirm: boolean; 241 | 242 | /** 243 | * User list of roles 244 | */ 245 | roles: string[]; 246 | }; 247 | 248 | export type FilesList = { 249 | sum: number; 250 | files: File[]; 251 | }; 252 | 253 | export type File = { 254 | $id: string; 255 | $permissions: Permissions; 256 | name: string; 257 | dateCreated: number; 258 | signature: string; 259 | mimeType: string; 260 | sizeOriginal: number; 261 | }; 262 | 263 | export type Collection = { 264 | $id: string; 265 | $permissions: Permissions; 266 | name: string; 267 | dateCreated: number; 268 | dateUpdated: number; 269 | rules: Rule[]; 270 | }; 271 | 272 | export type CreatedCollection = Partial & Pick; 273 | 274 | export type CollectionsList = { 275 | sum: number; 276 | collections: Collection[]; 277 | }; 278 | 279 | export type CreatedRule = Omit; 280 | 281 | export type Rule = { 282 | $id: string; 283 | $collection: string; 284 | type: string; 285 | key: string; 286 | label: string; 287 | default?: any; 288 | required: boolean; 289 | array: boolean; 290 | list?: string[]; 291 | }; 292 | 293 | export type Permissions = { 294 | read: string[]; 295 | write: string[]; 296 | } 297 | 298 | export type Client = { 299 | // Your API Endpoint 300 | setEndpoint: (endpoint: string) => Client; 301 | // Your project ID 302 | setProject: (projectId: string) => Client; 303 | // Your secret API key 304 | setKey: (key: string) => Client; 305 | setSelfSigned: (value: boolean) => void; 306 | }; 307 | export type UsersClient = { 308 | delete: (id: string) => Promise; 309 | deleteSessions: (id: string) => Promise; 310 | create: (email: string, password: string, name?: string) => Promise; 311 | getLogs: (id: string) => Promise; 312 | }; 313 | 314 | export type DatabaseClient = { 315 | listCollections: () => Promise; 316 | listDocuments: (collectionId: string) => Promise; 317 | updateCollection: (collectionId: string, name: string, read: string[], write: string[], rules?: (Rule | CreatedRule)[]) => Promise; 318 | deleteCollection: (collectionId: string) => Promise; 319 | getCollection: (collectionId: string) => Promise; 320 | deleteDocument: (collectionId: string, documentId: string) => Promise; 321 | createCollection: (name: string, read: string[], write: string[], rules: Rule[]) => Promise; 322 | }; 323 | 324 | export type DocumentsList = { 325 | sum: number; 326 | documents: any[]; 327 | }; 328 | 329 | type GetHealth = () => any; 330 | 331 | export type HealthClient = { 332 | get: GetHealth; 333 | getDB: GetHealth; 334 | getCache: GetHealth; 335 | getTime: GetHealth; 336 | getQueueWebhooks: GetHealth; 337 | getQueueTasks: GetHealth; 338 | getQueueLogs: GetHealth; 339 | getQueueUsage: GetHealth; 340 | getQueueCertificates: GetHealth; 341 | getQueueFunctions: GetHealth; 342 | getStorageLocal: GetHealth; 343 | getAntiVirus: GetHealth; 344 | }; 345 | 346 | export type AppwriteHealth = { 347 | HTTP: any; 348 | DB: any; 349 | Cache: any; 350 | Time: any; 351 | QueueWebhooks: any; 352 | QueueTasks: any; 353 | QueueLogs: any; 354 | QueueUsage: any; 355 | QueueCertificates: any; 356 | QueueFunctions: any; 357 | StorageLocal: any; 358 | AntiVirus: any; 359 | }; 360 | 361 | export type StorageClient = { 362 | createFile: (file: any, read?: string[], write?: string[]) => Promise; 363 | listFiles: () => Promise; 364 | getFile: (fileId: string) => Promise; 365 | }; 366 | 367 | type Vars = Record; 368 | 369 | export type Function = { 370 | '$id': string; 371 | '$permissions': Permissions; 372 | name: string; 373 | dateCreated: number; 374 | dateUpdated: number; 375 | status: string; 376 | env: string; 377 | tag: string; 378 | vars: Vars; 379 | events: string[]; 380 | schedule: string; 381 | scheduleNext: number; 382 | schedulePrevious: number; 383 | timeout: number; 384 | } 385 | 386 | export type FunctionsList = { 387 | sum: number; 388 | functions: Function[]; 389 | } 390 | 391 | export type Tag = { 392 | '$id': string; 393 | functionId: string; 394 | dateCreated: number; 395 | command: string; 396 | size: string; 397 | }; 398 | 399 | export type TagList = { 400 | sum: number; 401 | tags: Tag[]; 402 | } 403 | 404 | export type ExecutionStatus = "waiting" | "processing" | "completed" | "failed"; 405 | 406 | export type Execution = { 407 | '$id': string; 408 | functionId: string; 409 | dateCreated: number; 410 | trigger: string; 411 | status: ExecutionStatus; 412 | exitCode: number; 413 | stdout: string; 414 | stderr: string; 415 | time: number; 416 | }; 417 | 418 | export type ExecutionList = { 419 | sum: number; 420 | executions: Execution[]; 421 | }; 422 | 423 | export type Search = { 424 | search?: string; 425 | limit?: number; 426 | offset?: number; 427 | orderType?: 'ASC' | 'DESC'; 428 | }; 429 | 430 | export type FunctionsClient = { 431 | create: (name: string, execute: string[], env: string, vars?: Vars, events?: string[], schedule?: string, timeout?: number) => Promise; 432 | list: (search?: string, offset?: number, limit?: number, orderType?: 'ASC' | 'DESC') => Promise; 433 | get: (functionId: string) => Promise; 434 | update: (functionId: string, name: string, execute: string[], vars?: Vars, events?: string[], schedule?: string, timeout?: number) => Promise; 435 | updateTag: (functionId: string, tagId: string) => Promise; 436 | delete: (functionId: string) => Promise; 437 | createTag: (id: string, command: string, code: ReadStream) => Promise; 438 | listTags: (id: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC') => Promise; 439 | getTag: (functionId: string, tagId: string) => Promise; 440 | deleteTag: (functionId: string, tagId: string) => Promise; 441 | createExecution: (functionId: string, data?: string) => Promise; 442 | listExecutions: (functionId: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC') => Promise; 443 | getExecution: (functionId: string, executionId: string) => Promise; 444 | } 445 | 446 | export type SDK = { 447 | Client: new () => Client; 448 | 449 | Users: new (client: Client) => UsersClient; 450 | Health: new (client: Client) => HealthClient; 451 | Database: new (client: Client) => DatabaseClient; 452 | Storage: new (client: Client) => StorageClient; 453 | Functions: new (client: Client) => FunctionsClient; 454 | }; 455 | -------------------------------------------------------------------------------- /src/appwrite/Database.ts: -------------------------------------------------------------------------------- 1 | import { Client, Collection, CreatedCollection, CreatedRule, DatabaseClient, Rule } from "../appwrite"; 2 | import { AppwriteSDK } from '../constants'; 3 | import AppwriteCall from "../utils/AppwriteCall"; 4 | 5 | export class Database { 6 | private readonly database: DatabaseClient; 7 | 8 | constructor(client: Client) { 9 | this.database = new AppwriteSDK.Database(client); 10 | } 11 | 12 | public async getCollection(collectionId: string): Promise { 13 | return await AppwriteCall(this.database.getCollection(collectionId)); 14 | } 15 | 16 | public async deleteDocument(collectionId: string, documentId: string): Promise { 17 | await AppwriteCall(this.database.deleteDocument(collectionId, documentId)); 18 | } 19 | 20 | public async deleteCollection(collectionId: string): Promise { 21 | await AppwriteCall(this.database.deleteCollection(collectionId)); 22 | } 23 | 24 | public async createCollection(collection: CreatedCollection): Promise { 25 | await AppwriteCall( 26 | this.database.createCollection( 27 | collection.name, 28 | collection.$permissions?.read ?? [], 29 | collection.$permissions?.write ?? [], 30 | collection.rules ?? [] 31 | ) 32 | ); 33 | } 34 | 35 | public async updatePermissions(collection: Collection, read: string[], write: string[]): Promise { 36 | await AppwriteCall(this.database.updateCollection(collection.$id, collection.name, read, write, collection.rules)); 37 | } 38 | 39 | public async createRule(collection: Collection, newRule: CreatedRule): Promise { 40 | await AppwriteCall( 41 | this.database.updateCollection(collection.$id, collection.name, collection.$permissions.read, collection.$permissions.write, [ 42 | ...collection.rules, 43 | newRule, 44 | ]) 45 | ); 46 | } 47 | 48 | public async removeRule(collection: Collection, ruleToRemove: Rule): Promise { 49 | const rules = collection.rules.filter((rule) => rule.$id !== ruleToRemove.$id); 50 | 51 | await AppwriteCall( 52 | this.database.updateCollection( 53 | collection.$id, 54 | collection.name, 55 | collection.$permissions.read, 56 | collection.$permissions.write, 57 | rules 58 | ) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/appwrite/Functions.ts: -------------------------------------------------------------------------------- 1 | import { Client, Execution, ExecutionList, FunctionsClient, FunctionsList, Tag, TagList, Vars } from "../appwrite"; 2 | import { AppwriteSDK } from '../constants'; 3 | import AppwriteCall from '../utils/AppwriteCall'; 4 | import { ReadStream } from 'node:fs'; 5 | 6 | export class Functions { 7 | public readonly functions: FunctionsClient; 8 | 9 | constructor(client: Client) { 10 | this.functions = new AppwriteSDK.Functions(client); 11 | } 12 | 13 | public async create(name: string, execute: string[], env: string, vars?: Vars, events?: string[], schedule?: string, timeout?: number): Promise { 14 | return await AppwriteCall(this.functions.create(name, execute, env, vars, events, schedule, timeout)); 15 | } 16 | public async list(search?: string, offset?: number, limit?: number, orderType?: 'ASC' | 'DESC'): Promise { 17 | return await AppwriteCall(this.functions.list(search, offset, limit, orderType)); 18 | } 19 | public async get(functionId: string): Promise { 20 | return await AppwriteCall(this.functions.get(functionId)); 21 | } 22 | public async update(functionId: string, name: string, execute: string[], vars?: Vars, events?: string[], schedule?: string, timeout?: number): Promise { 23 | return await AppwriteCall(this.functions.update(functionId, name, execute, vars, events, schedule, timeout)); 24 | } 25 | public async updateTag(functionId: string, tagId: string): Promise { 26 | return await AppwriteCall(this.functions.updateTag(functionId, tagId)); 27 | } 28 | public async delete(functionId: string): Promise { 29 | return await AppwriteCall(this.functions.delete(functionId)); 30 | } 31 | public async createTag(functionId: string, command: string, code: ReadStream): Promise { 32 | return await AppwriteCall(this.functions.createTag(functionId, command, code)); 33 | } 34 | public async listTags(id: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC'): Promise { 35 | return await AppwriteCall(this.functions.listTags(id, search, offset, limit, orderType)); 36 | } 37 | public async getTag(functionId: string, tagId: string): Promise { 38 | return await AppwriteCall(this.functions.getTag(functionId, tagId)); 39 | } 40 | public async deleteTag(functionId: string, tagId: string): Promise { 41 | return await AppwriteCall(this.functions.deleteTag(functionId, tagId)); 42 | } 43 | public async createExecution(functionId: string, data?: string): Promise { 44 | return await AppwriteCall(this.functions.createExecution(functionId, data)); 45 | } 46 | public async listExecutions(functionId: string, search?: string, limit?: number, offset?: number, orderType?: 'ASC' | 'DESC'): Promise { 47 | return await AppwriteCall(this.functions.listExecutions(functionId, search, limit, offset, orderType)); 48 | } 49 | public async getExecution(functionId: string, executionId: string): Promise { 50 | return await AppwriteCall(this.functions.getExecution(functionId, executionId)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/appwrite/Health.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString } from 'vscode'; 2 | import { AppwriteHealth, Client, HealthClient } from "../appwrite"; 3 | import { AppwriteSDK } from '../constants'; 4 | export class Health { 5 | private readonly health: HealthClient; 6 | 7 | constructor(client: Client) { 8 | this.health = new AppwriteSDK.Health(client); 9 | } 10 | 11 | /** 12 | * @returns The health of all Appwrite services. 13 | */ 14 | public async checkup(): Promise> { 15 | return { 16 | HTTP: await this.health.get(), 17 | DB: await this.health.getDB(), 18 | Cache: await this.health.getCache(), 19 | Time: await this.health.getTime(), 20 | QueueWebhooks: await this.health.getQueueWebhooks(), 21 | QueueTasks: await this.health.getQueueTasks(), 22 | QueueLogs: await this.health.getQueueLogs(), 23 | QueueUsage: await this.health.getQueueUsage(), 24 | QueueCertificates: await this.health.getQueueCertificates(), 25 | QueueFunctions: await this.health.getQueueFunctions(), 26 | StorageLocal: await this.health.getStorageLocal(), 27 | AntiVirus: await this.health.getAntiVirus(), 28 | }; 29 | } 30 | } 31 | 32 | export const healthTooltips: Record = { 33 | HTTP: "Check the Appwrite HTTP server is up and responsive.", 34 | DB: "Check the Appwrite in-memory cache server is up and connection is successful.", 35 | Cache: "Check the Appwrite in-memory cache server is up and connection is successful.", 36 | Time: 37 | new MarkdownString("Check the Appwrite server time is synced with Google remote NTP server. We use this technology to smoothly handle leap seconds with no disruptive events. The [Network Time Protocol (NTP)](https://en.wikipedia.org/wiki/Network_Time_Protocol) is used by hundreds of millions of computers and devices to synchronize their clocks over the Internet. If your computer sets its own clock, it likely uses NTP."), 38 | QueueWebhooks: "The number of webhooks that are waiting to be processed in the Appwrite internal queue server.", 39 | QueueTasks: "The number of tasks that are waiting to be processed in the Appwrite internal queue server.", 40 | QueueLogs: "The number of logs that are waiting to be processed in the Appwrite internal queue server.", 41 | QueueUsage: "The number of usage stats that are waiting to be processed in the Appwrite internal queue server.", 42 | QueueCertificates: 43 | new MarkdownString("The number of certificates that are waiting to be issued against [Letsencrypt](https://letsencrypt.org/) in the Appwrite internal queue server."), 44 | QueueFunctions: "The number of functions waiting to be executed.", 45 | StorageLocal: "Check the Appwrite local storage device is up and connection is successful.", 46 | AntiVirus: "Check the Appwrite Anti Virus server is up and connection is successful.", 47 | }; 48 | -------------------------------------------------------------------------------- /src/appwrite/Storage.ts: -------------------------------------------------------------------------------- 1 | import { ReadStream } from 'node:fs'; 2 | import { Client, FilesList, StorageClient } from "../appwrite"; 3 | import { AppwriteSDK } from '../constants'; 4 | import AppwriteCall from "../utils/AppwriteCall"; 5 | 6 | export class Storage { 7 | private readonly storage: StorageClient; 8 | 9 | constructor(client: Client) { 10 | this.storage = new AppwriteSDK.Storage(client); 11 | } 12 | 13 | public async listFiles(): Promise { 14 | return await AppwriteCall(this.storage.listFiles()); 15 | } 16 | 17 | public async createFile(file: ReadStream): Promise { 18 | return await AppwriteCall(this.storage.createFile(file)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/appwrite/Users.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { Client, Log, User, UsersClient } from "../appwrite"; 3 | import { AppwriteSDK } from "../constants"; 4 | import AppwriteCall from "../utils/AppwriteCall"; 5 | 6 | export class Users { 7 | private readonly users: UsersClient; 8 | 9 | constructor(client: Client) { 10 | this.users = new AppwriteSDK.Users(client); 11 | } 12 | public async createNewUser(context: CreateUserContext): Promise { 13 | await AppwriteCall(this.users.create(context.email, context.password, context.name), (user) => { 14 | window.showInformationMessage(`Created user with id: ${user.$id}`); 15 | }); 16 | } 17 | 18 | public async delete(userId: string): Promise { 19 | await AppwriteCall(this.users.delete(userId), () => { 20 | window.showInformationMessage(`Deleted user with id: ${userId}.`); 21 | }); 22 | } 23 | 24 | public async getLogs(userId: string): Promise { 25 | return (await AppwriteCall(this.users.getLogs(userId))) ?? []; 26 | } 27 | } 28 | 29 | type CreateUserContext = { 30 | email: string; 31 | password: string; 32 | name?: string; 33 | }; 34 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "./appwrite"; 2 | import { Database } from "./appwrite/Database"; 3 | import { Functions } from './appwrite/Functions'; 4 | import { Health } from "./appwrite/Health"; 5 | import { Storage } from "./appwrite/Storage"; 6 | import { Users } from "./appwrite/Users"; 7 | import { AppwriteSDK } from "./constants"; 8 | import { AppwriteProjectConfiguration } from "./settings"; 9 | 10 | export let client: Client; 11 | export let clientConfig: { endpoint: string; projectId: string; secret: string }; 12 | export let usersClient: Users | undefined; 13 | export let healthClient: Health | undefined; 14 | export let databaseClient: Database | undefined; 15 | export let storageClient: Storage | undefined; 16 | export let functionsClient: Functions | undefined; 17 | 18 | 19 | function initAppwriteClient({ endpoint, projectId, secret, selfSigned }: AppwriteProjectConfiguration) { 20 | client = new AppwriteSDK.Client(); 21 | clientConfig = { endpoint, projectId, secret }; 22 | client.setEndpoint(endpoint).setProject(projectId).setKey(secret).setSelfSigned(selfSigned); 23 | 24 | usersClient = new Users(client); 25 | healthClient = new Health(client); 26 | databaseClient = new Database(client); 27 | storageClient = new Storage(client); 28 | functionsClient = new Functions(client); 29 | 30 | return client; 31 | } 32 | 33 | export function createAppwriteClient(config?: AppwriteProjectConfiguration): void { 34 | if (config) { 35 | initAppwriteClient(config); 36 | return; 37 | } 38 | 39 | usersClient = undefined; 40 | healthClient = undefined; 41 | databaseClient = undefined; 42 | storageClient = undefined; 43 | functionsClient = undefined; 44 | } 45 | -------------------------------------------------------------------------------- /src/commands/common/editValue.ts: -------------------------------------------------------------------------------- 1 | import { EditableTreeItem } from '../../tree/common/editable/SimpleEditableTreeItem'; 2 | 3 | export async function editValue(treeItem: EditableTreeItem): Promise { 4 | if (treeItem === undefined) { 5 | return; 6 | } 7 | 8 | await treeItem.prompt(); 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/common/viewMore.ts: -------------------------------------------------------------------------------- 1 | import { AppwriteTreeItemBase } from '../../ui/AppwriteTreeItemBase'; 2 | 3 | export async function viewMore(treeItem: AppwriteTreeItemBase): Promise { 4 | await treeItem.viewMore(); 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/connectAppwrite.ts: -------------------------------------------------------------------------------- 1 | import { createAppwriteClient } from "../client"; 2 | import { addProjectConfiguration } from '../settings'; 3 | import { addProjectWizard } from "../ui/AddProjectWizard"; 4 | import { refreshTree } from '../utils/refreshTree'; 5 | 6 | export async function connectAppwrite(): Promise { 7 | const projectConfiguration = await addProjectWizard(); 8 | if (projectConfiguration) { 9 | addProjectConfiguration(projectConfiguration); 10 | createAppwriteClient(projectConfiguration); 11 | refreshTree(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/database/createCollection.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { databaseClient } from "../../client"; 3 | 4 | export async function createCollection(): Promise { 5 | if (!databaseClient) { 6 | return; 7 | } 8 | 9 | const name = await window.showInputBox({ 10 | prompt: "Collection name", 11 | }); 12 | 13 | if (name && name.length > 0) { 14 | await databaseClient.createCollection({ name }); 15 | window.showInformationMessage(`Created collection "${name}".`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/commands/database/createRule.ts: -------------------------------------------------------------------------------- 1 | import { CreatedRule } from "../../appwrite"; 2 | import { databaseClient } from "../../client"; 3 | import { RulesTreeItem } from "../../tree/database/settings/RulesTreeItem"; 4 | import { createRuleWizard } from "../../ui/createRuleWizard"; 5 | import { refreshTree } from '../../utils/refreshTree'; 6 | 7 | export async function createRule(rulesTreeItem: RulesTreeItem): Promise { 8 | 9 | if (!databaseClient) { 10 | return; 11 | } 12 | 13 | const collection = rulesTreeItem.parent.collection; 14 | const ruleContext = await createRuleWizard(collection); 15 | 16 | if (ruleContext) { 17 | const newRule: CreatedRule = { 18 | ...ruleContext, 19 | type: ruleContext.type, 20 | }; 21 | 22 | databaseClient.createRule(collection, newRule); 23 | 24 | await rulesTreeItem.refresh(); 25 | refreshTree("database"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/commands/database/deleteCollection.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { databaseClient } from "../../client"; 3 | import { CollectionTreeItem } from "../../tree/database/CollectionTreeItem"; 4 | import { confirmDialog } from "../../ui/confirmDialog"; 5 | 6 | export async function deleteCollection(collectionTreeItem: CollectionTreeItem): Promise { 7 | if (!databaseClient) { 8 | return; 9 | } 10 | const collection = collectionTreeItem.collection; 11 | try { 12 | const shouldDelete = await confirmDialog(`Delete collection "${collection.name}"?`); 13 | if (shouldDelete) { 14 | await databaseClient.deleteCollection(collection.$id); 15 | window.showInformationMessage(`Deleted collection "${collection.name}".`); 16 | } 17 | } catch (e) { 18 | window.showErrorMessage(e); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/commands/database/deleteDocument.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { databaseClient } from "../../client"; 3 | import { DocumentTreeItem } from "../../tree/database/DocumentTreeItem"; 4 | import { confirmDialog } from "../../ui/confirmDialog"; 5 | 6 | export async function deleteDocument(documentTreeItem: DocumentTreeItem): Promise { 7 | if (!databaseClient) { 8 | return; 9 | } 10 | const document = documentTreeItem.document; 11 | const collection = documentTreeItem.parent.parent.collection; 12 | try { 13 | const shouldDelete = await confirmDialog(`Delete document "${document["$id"]}" from ${collection.name}?`); 14 | if (shouldDelete) { 15 | await databaseClient.deleteDocument(collection.$id, document["$id"]); 16 | window.showInformationMessage(`Deleted document "${document["$id"]}" from ${collection.name}.`); 17 | } 18 | } catch (e) { 19 | window.showErrorMessage(e); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/database/openDocument.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | import { DocumentTreeItem } from "../../tree/database/DocumentTreeItem"; 3 | import { openReadOnlyJson } from "../../ui/openReadonlyContent"; 4 | 5 | function parseJSONString(str: string): { valid: boolean; value: any } { 6 | try { 7 | return { value: JSON.parse(str), valid: true }; 8 | } catch (e) { 9 | return { value: str, valid: false }; 10 | } 11 | } 12 | 13 | export async function viewDocumentAsJson(documentTreeItem: DocumentTreeItem): Promise { 14 | const document = documentTreeItem.document; 15 | const documentId = document["$id"]; 16 | 17 | const formatJsonStrings = workspace.getConfiguration("appwrite").get("formatJsonStrings"); 18 | 19 | if (formatJsonStrings) { 20 | Object.entries(document).forEach(([key, value]) => { 21 | if (typeof value === "string") { 22 | const result = parseJSONString(value); 23 | document[key] = result.value; 24 | } 25 | }); 26 | } 27 | 28 | await openReadOnlyJson( 29 | { 30 | label: documentId, 31 | fullId: documentId, 32 | }, 33 | documentTreeItem.document 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/commands/database/permissions/createPermission.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { databaseClient } from "../../../client"; 3 | import { PermissionsTreeItem } from "../../../tree/database/settings/PermissionsTreeItem"; 4 | 5 | export type CreatePermissionWizardContext = { 6 | kind: "read" | "write"; 7 | permission: string; 8 | }; 9 | export async function createPermissionWizard(kind?: "read" | "write"): Promise { 10 | const permissionKind = kind ?? (await window.showQuickPick(["read", "write"])); 11 | if (permissionKind && (permissionKind === "read" || permissionKind === "write")) { 12 | const permission = await window.showInputBox({ prompt: "Add * for wildcard access", placeHolder: "User ID, Team ID, or Role" }); 13 | if (permission === undefined) { 14 | return undefined; 15 | } 16 | 17 | return { 18 | kind: permissionKind, 19 | permission, 20 | }; 21 | } 22 | return undefined; 23 | } 24 | 25 | export async function createPermission(treeItem: PermissionsTreeItem): Promise { 26 | if (!databaseClient) { 27 | return; 28 | } 29 | 30 | const collection = treeItem.parent.collection; 31 | const context = await createPermissionWizard(undefined); 32 | 33 | if (context === undefined) { 34 | return; 35 | } 36 | 37 | const read = Array.from(collection.$permissions.read); 38 | const write = Array.from(collection.$permissions.write); 39 | 40 | if (context.kind === "read") { 41 | read.push(context.permission); 42 | } else { 43 | write.push(context.permission); 44 | } 45 | 46 | await databaseClient.updatePermissions(collection, read, write); 47 | } 48 | -------------------------------------------------------------------------------- /src/commands/database/permissions/deletePermission.ts: -------------------------------------------------------------------------------- 1 | import { databaseClient } from "../../../client"; 2 | import { PermissionTreeItem } from "../../../tree/database/settings/PermissionTreeItem"; 3 | 4 | export async function deletePermission(treeItem: PermissionTreeItem): Promise { 5 | if (!databaseClient) { 6 | return; 7 | } 8 | const collection = treeItem.parent.parent.collection; 9 | const kind = treeItem.kind; 10 | 11 | let read = Array.from(collection.$permissions.read); 12 | let write = Array.from(collection.$permissions.write); 13 | 14 | if (kind === "read") { 15 | read = read.filter((item) => item !== treeItem.permission); 16 | } else { 17 | write = write.filter((item) => item !== treeItem.permission); 18 | } 19 | 20 | await databaseClient.updatePermissions(collection, read, write); 21 | } 22 | -------------------------------------------------------------------------------- /src/commands/database/permissions/editPermission.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { databaseClient } from "../../../client"; 3 | import { PermissionTreeItem } from "../../../tree/database/settings/PermissionTreeItem"; 4 | 5 | export async function editPermission(treeItem: PermissionTreeItem): Promise { 6 | if (!databaseClient) { 7 | return; 8 | } 9 | const editedPermission = await window.showInputBox({ 10 | value: treeItem.permission, 11 | }); 12 | 13 | if (editedPermission === undefined) { 14 | return; 15 | } 16 | 17 | const collection = treeItem.parent.parent.collection; 18 | const kind = treeItem.kind; 19 | 20 | let read = Array.from(collection.$permissions.read); 21 | let write = Array.from(collection.$permissions.write); 22 | 23 | if (kind === "read") { 24 | read = read.filter((item) => item !== treeItem.permission); 25 | read.push(editedPermission); 26 | } else { 27 | write = write.filter((item) => item !== treeItem.permission); 28 | write.push(editedPermission); 29 | } 30 | 31 | await databaseClient.updatePermissions(collection, read, write); 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/database/refreshCollection.ts: -------------------------------------------------------------------------------- 1 | import { CollectionTreeItem } from "../../tree/database/CollectionTreeItem"; 2 | 3 | export async function refreshCollection(collectionTreeItem: CollectionTreeItem): Promise { 4 | await collectionTreeItem.refresh(); 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/database/refreshCollectionsList.ts: -------------------------------------------------------------------------------- 1 | import { refreshTree } from '../../utils/refreshTree'; 2 | 3 | export async function refreshCollectionsList(): Promise { 4 | refreshTree('database'); 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/database/removeRule.ts: -------------------------------------------------------------------------------- 1 | import { databaseClient } from '../../client'; 2 | import { RuleTreeItem } from '../../tree/database/settings/RuleTreeItem'; 3 | import { refreshTree } from '../../utils/refreshTree'; 4 | 5 | export async function removeRule(ruleItem: RuleTreeItem): Promise { 6 | if (!databaseClient) { 7 | return; 8 | } 9 | const rule = ruleItem.rule; 10 | const collection = ruleItem.parent.parent.collection; 11 | await databaseClient.removeRule(collection, rule); 12 | refreshTree('database'); 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/database/viewCollectionAsJson.ts: -------------------------------------------------------------------------------- 1 | import { CollectionTreeItem } from "../../tree/database/CollectionTreeItem"; 2 | import { openReadOnlyJson } from "../../ui/openReadonlyContent"; 3 | 4 | export async function viewCollectionAsJson(collectionTreeItem: CollectionTreeItem): Promise { 5 | const collection = collectionTreeItem.collection; 6 | await openReadOnlyJson({ label: collection.name, fullId: collection.$id }, collection); 7 | } 8 | -------------------------------------------------------------------------------- /src/commands/functions/activateTag.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from '../../appwrite'; 2 | import { functionsClient } from '../../client'; 3 | import { TagTreeItem } from '../../tree/functions/tags/TagTreeItem'; 4 | 5 | export async function activateTag(tagItem: TagTreeItem | Tag): Promise { 6 | const tag = tagItem instanceof TagTreeItem ? tagItem.tag : tagItem; 7 | await functionsClient?.updateTag(tag.functionId, tag.$id); 8 | } 9 | -------------------------------------------------------------------------------- /src/commands/functions/copyExecutionErrors.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'vscode'; 2 | import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; 3 | 4 | export async function copyExecutionErrors(executionItem: ExecutionTreeItem): Promise { 5 | if (executionItem === undefined) { 6 | return; 7 | } 8 | 9 | const execution = executionItem.execution; 10 | env.clipboard.writeText(execution.stderr); 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/functions/copyExecutionOutput.ts: -------------------------------------------------------------------------------- 1 | import { env } from 'vscode'; 2 | import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; 3 | 4 | export async function copyExecutionOutput(executionItem: ExecutionTreeItem): Promise { 5 | if (executionItem === undefined) { 6 | return; 7 | } 8 | 9 | const execution = executionItem.execution; 10 | env.clipboard.writeText(execution.stdout); 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/functions/createExecution.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | import { Execution } from '../../appwrite'; 3 | import { functionsClient } from '../../client'; 4 | import { ext } from '../../extensionVariables'; 5 | import { FunctionTreeItem } from '../../tree/functions/FunctionTreeItem'; 6 | import { sleep } from '../../utils/sleep'; 7 | import { viewExecutionErrors } from './viewExecutionErrors'; 8 | import { viewExecutionOutput } from './viewExecutionOutput'; 9 | 10 | export async function createExecution(functionTreeItem: FunctionTreeItem): Promise { 11 | const func = functionTreeItem.func; 12 | await executeFunction(func.$id); 13 | } 14 | 15 | export async function executeFunction(functionId: string): Promise { 16 | ext.outputChannel.appendLog(`Creating execution for function with ID: ${functionId}`); 17 | let execution = await functionsClient?.createExecution(functionId); 18 | ext.outputChannel.appendLog(JSON.stringify(execution, null, 2)); 19 | await ext.tree?.functions?.refresh(); 20 | 21 | if (execution === undefined) { 22 | return; 23 | } 24 | 25 | execution = await waitForExecution(execution); 26 | ext.tree?.functions?.refresh(); 27 | 28 | if (execution === undefined) { 29 | return; 30 | } 31 | 32 | const failed = execution.status === "failed"; 33 | const item = !failed ? "View output" : "View errors"; 34 | const action = await window.showInformationMessage(`Execution ${failed ? "failed" : "completed"} in ${execution.time.toFixed(2)}s.`, item); 35 | if (action === item) { 36 | if (item === "View output") { 37 | await viewExecutionOutput(execution); 38 | return; 39 | } 40 | await viewExecutionErrors(execution); 41 | return; 42 | } 43 | } 44 | 45 | async function waitForExecution(execution: Execution | undefined): Promise { 46 | if (execution === undefined) { 47 | return; 48 | } 49 | if (execution.status === "processing" || execution.status === "waiting") { 50 | await sleep(5000); 51 | 52 | ext.outputChannel.appendLog("Execution still ..."); 53 | return await waitForExecution(await functionsClient?.getExecution(execution.functionId, execution.$id)); 54 | } 55 | 56 | return execution; 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/functions/createFunction.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | import { functionsClient } from '../../client'; 3 | import { appwriteFunctionRuntimes } from '../../constants'; 4 | import { validateFunctionName } from '../../tree/functions/settings/NameTreeItem'; 5 | 6 | export async function createFunction(): Promise { 7 | 8 | const name = await window.showInputBox({ prompt: 'Function name', validateInput: validateFunctionName }); 9 | if (name === undefined) { 10 | return; 11 | } 12 | 13 | const env: string | undefined = await window.showQuickPick(appwriteFunctionRuntimes); 14 | if (env === undefined) { 15 | return; 16 | } 17 | 18 | await functionsClient?.create(name, [], env); 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/functions/createFunctionVar.ts: -------------------------------------------------------------------------------- 1 | import { functionsClient } from '../../client'; 2 | import { VarsTreeItem } from '../../tree/functions/settings/VarsTreeItem'; 3 | import { keyValuePrompt } from '../../tree/functions/settings/VarTreeItem'; 4 | 5 | export async function createFunctionVar(treeItem: VarsTreeItem): Promise { 6 | if (treeItem === undefined) { 7 | return; 8 | } 9 | const func = treeItem.parent.func; 10 | const keyval = await keyValuePrompt(); 11 | if (keyval) { 12 | const newVars = {...func.vars}; 13 | newVars[keyval.key] = keyval.value; 14 | await functionsClient?.update(func.$id, func.name, [], newVars, func.events, func.schedule, func.timeout); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/functions/createTag.ts: -------------------------------------------------------------------------------- 1 | import { ProgressLocation, QuickPickItem, Uri, window, workspace } from "vscode"; 2 | import { functionsClient } from "../../client"; 3 | import { getTarReadStream } from "../../utils/tar"; 4 | import { ext } from "../../extensionVariables"; 5 | import * as fs from "fs"; 6 | import { TagsTreeItem } from "../../tree/functions/tags/TagsTreeItem"; 7 | import { selectWorkspaceFolder } from "../../utils/workspace"; 8 | import { ProgressMessage } from "../../utils/types"; 9 | import { Tag } from "../../appwrite"; 10 | import { activateTag } from "./activateTag"; 11 | 12 | export async function createTag(item?: TagsTreeItem | Uri): Promise { 13 | if (item instanceof Uri) { 14 | const functions = await functionsClient?.list(); 15 | if (functions === undefined) { 16 | return; 17 | } 18 | const pick = await window.showQuickPick( 19 | functions.functions.map( 20 | (func): QuickPickItem => ({ label: func.name, description: func.env, detail: func.$id }) 21 | ), 22 | { placeHolder: "Select a function to create tag" } 23 | ); 24 | if (pick === undefined || pick.detail === undefined) { 25 | return; 26 | } 27 | const tags = await functionsClient?.listTags(pick.detail); 28 | let value; 29 | if (tags && tags.tags.length > 0) { 30 | value = tags.tags[tags.tags.length - 1].command; 31 | } 32 | const command = await window.showInputBox({ value, prompt: "Command to run your code" }); 33 | if (command === undefined) { 34 | return; 35 | } 36 | const tag = await window.withProgress( 37 | { location: ProgressLocation.Notification, title: "Creating tag..." }, 38 | async (progress, _token) => { 39 | if (pick.detail === undefined) { 40 | return; 41 | } 42 | return await createTagFromUri(pick.detail, command, item, progress); 43 | } 44 | ); 45 | if (tag) { 46 | await tagNotification(tag); 47 | } 48 | 49 | return; 50 | } 51 | 52 | if (item instanceof TagsTreeItem) { 53 | const func = item.parent.func; 54 | const folder = await selectWorkspaceFolder("Select folder of your function code."); 55 | if (folder === undefined || folder === "") { 56 | return; 57 | } 58 | const tags = await functionsClient?.listTags(func.$id); 59 | let value; 60 | if (tags && tags.tags.length > 0) { 61 | value = tags.tags[tags.tags.length - 1].command; 62 | } 63 | const command = await window.showInputBox({ value, prompt: "Command to run your code" }); 64 | if (command === undefined) { 65 | return; 66 | } 67 | const tag = await window.withProgress( 68 | { location: ProgressLocation.Notification, title: "Creating tag..." }, 69 | async (progress, _token) => { 70 | return await createTagFromUri(func.$id, command, Uri.parse(folder), progress); 71 | } 72 | ); 73 | 74 | if (tag) { 75 | await tagNotification(tag); 76 | return; 77 | } 78 | } 79 | 80 | if (item === undefined) { 81 | const functions = await functionsClient?.list(); 82 | if (functions === undefined) { 83 | return; 84 | } 85 | const pick = await window.showQuickPick( 86 | functions.functions.map( 87 | (func): QuickPickItem => ({ label: func.name, description: func.env, detail: func.$id }) 88 | ), 89 | { placeHolder: "Select a function to create tag" } 90 | ); 91 | if (pick === undefined || pick.detail === undefined) { 92 | return; 93 | } 94 | const funcId = pick.detail; 95 | const folder = await selectWorkspaceFolder("Select folder of your function code."); 96 | const tags = await functionsClient?.listTags(funcId); 97 | let value; 98 | if (tags && tags.tags.length > 0) { 99 | value = tags.tags[tags.tags.length - 1].command; 100 | } 101 | const command = await window.showInputBox({ value, prompt: "Command to run your code" }); 102 | if (command === undefined) { 103 | return; 104 | } 105 | const tag = await window.withProgress( 106 | { location: ProgressLocation.Notification, title: "Creating tag..." }, 107 | async (progress, _token) => { 108 | return await createTagFromUri(funcId, command, Uri.parse(folder), progress); 109 | } 110 | ); 111 | 112 | if (tag) { 113 | await tagNotification(tag); 114 | return; 115 | } 116 | } 117 | } 118 | 119 | async function createTagFromUri(functionId: string, command: string, uri: Uri, progress: ProgressMessage): Promise { 120 | progress.report({ message: "Creating tarball", increment: 10 }); 121 | 122 | if (functionsClient === undefined) { 123 | return; 124 | } 125 | 126 | let tarFilePath; 127 | try { 128 | tarFilePath = await getTarReadStream(uri); 129 | } catch (e) { 130 | window.showErrorMessage("Error creating tar file.\n" + e); 131 | return; 132 | } 133 | if (tarFilePath === undefined) { 134 | window.showErrorMessage("Failed to create tar file."); 135 | ext.outputChannel.appendLog("Failed to create tar file."); 136 | return; 137 | } 138 | // somehow makes the upload work 139 | await workspace.fs.readFile(Uri.file(tarFilePath)); 140 | progress.report({ message: "Uploading tag", increment: 60 }); 141 | try { 142 | return await functionsClient.createTag(functionId, command, fs.createReadStream(tarFilePath)); 143 | } catch (e) { 144 | ext.outputChannel.appendLog("Creating tag error: " + e); 145 | } 146 | } 147 | 148 | async function tagNotification(tag: Tag): Promise { 149 | ext.tree?.functions?.refresh(); 150 | if (tag) { 151 | const action = await window.showInformationMessage( 152 | `Successfully created tag with size ${tag.size}B.`, 153 | "Activate tag", 154 | "View in console" 155 | ); 156 | if (action === "Activate tag") { 157 | await activateTag(tag); 158 | } 159 | if (action === "View in console") { 160 | // 161 | } 162 | return; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/commands/functions/deleteFunction.ts: -------------------------------------------------------------------------------- 1 | import { functionsClient } from '../../client'; 2 | import { FunctionTreeItem } from '../../tree/functions/FunctionTreeItem'; 3 | 4 | export async function deleteFunction(treeItem: FunctionTreeItem): Promise { 5 | if (!treeItem) { 6 | return; 7 | } 8 | await functionsClient?.delete(treeItem.func.$id); 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/functions/deleteFunctionVar.ts: -------------------------------------------------------------------------------- 1 | import { functionsClient } from '../../client'; 2 | import { VarTreeItem } from '../../tree/functions/settings/VarTreeItem'; 3 | 4 | export async function deleteFunctionVar(treeItem: VarTreeItem): Promise { 5 | if (treeItem === undefined) { 6 | return; 7 | } 8 | 9 | const func = treeItem.func; 10 | const newVars = {...func.vars}; 11 | delete newVars[treeItem.key]; 12 | await functionsClient?.update(func.$id, func.name, [], newVars, func.events, func.schedule, func.timeout); 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/functions/deleteTag.ts: -------------------------------------------------------------------------------- 1 | import { functionsClient } from "../../client"; 2 | import { TagTreeItem } from "../../tree/functions/tags/TagTreeItem"; 3 | 4 | export async function deleteTag(tagItem: TagTreeItem): Promise { 5 | if (tagItem === undefined) { 6 | return; 7 | } 8 | 9 | const func = tagItem.parent.parent.func; 10 | await functionsClient?.deleteTag(func.$id, tagItem.tag.$id); 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/functions/openExecutionsInBrowser.ts: -------------------------------------------------------------------------------- 1 | import { clientConfig } from '../../client'; 2 | import { ExecutionsTreeItem } from '../../tree/functions/executions/ExecutionsTreeItem'; 3 | import { openUrl } from '../../utils/openUrl'; 4 | import { getConsoleUrlFromEndpoint } from '../users/openUserInConsole'; 5 | 6 | export async function openExecutionsInBrowser(treeItem: ExecutionsTreeItem): Promise { 7 | 8 | const func = treeItem.parent.func; 9 | 10 | const consoleUrl = getConsoleUrlFromEndpoint(clientConfig.endpoint); 11 | // https://console.streamlux.com/console/functions/function/logs?id=60b1836a8e5d9&project=605ce39a30c01 12 | 13 | const url = `${consoleUrl}/functions/function/logs?id=${func.$id}&project=${clientConfig.projectId}`; 14 | openUrl(url); 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/functions/openFunctionSettingsInBrowser.ts: -------------------------------------------------------------------------------- 1 | import { clientConfig } from '../../client'; 2 | import { ExecutionsTreeItem } from '../../tree/functions/executions/ExecutionsTreeItem'; 3 | import { openUrl } from '../../utils/openUrl'; 4 | import { getConsoleUrlFromEndpoint } from '../users/openUserInConsole'; 5 | 6 | export async function openFunctionSettingsInBrowser(treeItem: ExecutionsTreeItem): Promise { 7 | 8 | const func = treeItem.parent.func; 9 | 10 | const consoleUrl = getConsoleUrlFromEndpoint(clientConfig.endpoint); 11 | // https://console.streamlux.com/console/functions/function/settings?id=60b1836a8e5d9&project=605ce39a30c01 12 | 13 | const url = `${consoleUrl}/functions/function/settings?id=${func.$id}&project=${clientConfig.projectId}`; 14 | openUrl(url); 15 | } 16 | -------------------------------------------------------------------------------- /src/commands/functions/openFunctionTagsInBrowser.ts: -------------------------------------------------------------------------------- 1 | import { clientConfig } from '../../client'; 2 | import { ExecutionsTreeItem } from '../../tree/functions/executions/ExecutionsTreeItem'; 3 | import { openUrl } from '../../utils/openUrl'; 4 | import { getConsoleUrlFromEndpoint } from '../users/openUserInConsole'; 5 | 6 | export async function openFunctionTagsInBrowser(treeItem: ExecutionsTreeItem): Promise { 7 | const func = treeItem.parent.func; 8 | 9 | const consoleUrl = getConsoleUrlFromEndpoint(clientConfig.endpoint); 10 | // https://console.streamlux.com/console/functions/function?id=60b1836a8e5d9&project=605ce39a30c01 11 | const url = `${consoleUrl}/functions/function?id=${func.$id}&project=${clientConfig.projectId}`; 12 | openUrl(url); 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/functions/viewExecutionErrors.ts: -------------------------------------------------------------------------------- 1 | import { Execution } from '../../appwrite'; 2 | import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; 3 | import { openReadOnlyContent } from "../../ui/openReadonlyContent"; 4 | 5 | export async function viewExecutionErrors(executionItem: ExecutionTreeItem | Execution): Promise { 6 | if (executionItem === undefined) { 7 | return; 8 | } 9 | 10 | let execution = executionItem as Execution; 11 | 12 | if (executionItem instanceof ExecutionTreeItem) { 13 | execution = executionItem.execution; 14 | } 15 | await openReadOnlyContent({ label: `Execution stderr`, fullId: `${execution.$id}-errors.txt` }, execution.stderr, '.txt'); 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/functions/viewExecutionOutput.ts: -------------------------------------------------------------------------------- 1 | import { Execution } from '../../appwrite'; 2 | import { ExecutionTreeItem } from "../../tree/functions/executions/ExecutionTreeItem"; 3 | import { openReadOnlyContent } from "../../ui/openReadonlyContent"; 4 | 5 | export async function viewExecutionOutput(executionItem: ExecutionTreeItem | Execution): Promise { 6 | if (executionItem === undefined) { 7 | return; 8 | } 9 | 10 | 11 | let execution = executionItem as Execution; 12 | 13 | if (executionItem instanceof ExecutionTreeItem) { 14 | execution = executionItem.execution; 15 | } 16 | console.log(execution.dateCreated); 17 | 18 | await openReadOnlyContent({ label: `Execution stdout`, fullId: `${execution.$id}-output.txt` }, execution.stdout, '.txt'); 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/openDocumentation.ts: -------------------------------------------------------------------------------- 1 | import { openUrl } from '../utils/openUrl'; 2 | 3 | const documentationLinks = { 4 | home: 'https://appwrite.io/docs', 5 | users: 'https://appwrite.io/docs/server/users', 6 | database: 'https://appwrite.io/docs/client/database', 7 | health: 'https://appwrite.io/docs/server/health', 8 | storage: 'https://appwrite.io/docs/client/storage', 9 | functions: 'https://appwrite.io/docs/server/functions' 10 | }; 11 | 12 | type DocsPage = keyof typeof documentationLinks; 13 | 14 | export async function openDocumentation(page?: DocsPage): Promise { 15 | await openUrl(documentationLinks[page || 'home']); 16 | } 17 | -------------------------------------------------------------------------------- /src/commands/project/addProject.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { createAppwriteClient } from '../../client'; 3 | import { addProjectWizard } from "../../ui/AddProjectWizard"; 4 | 5 | export async function addProject(): Promise { 6 | const projectConfiguration = await addProjectWizard(); 7 | 8 | if (projectConfiguration) { 9 | createAppwriteClient(projectConfiguration); 10 | } 11 | 12 | window.showInformationMessage("Connected to Appwrite project."); 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/project/removeProject.ts: -------------------------------------------------------------------------------- 1 | import { removeProjectConfig } from "../../settings"; 2 | import { ProjectTreeItem } from "../../tree/projects/ProjectTreeItem"; 3 | 4 | export async function removeProject(project: ProjectTreeItem | string): Promise { 5 | if (typeof project === "string") { 6 | await removeProjectConfig(project); 7 | return; 8 | } 9 | 10 | await removeProjectConfig(project.project.projectId); 11 | } 12 | -------------------------------------------------------------------------------- /src/commands/project/setActiveProject.ts: -------------------------------------------------------------------------------- 1 | import { setActiveProjectId } from '../../settings'; 2 | import { ProjectTreeItem } from "../../tree/projects/ProjectTreeItem"; 3 | 4 | export async function setActiveProject(treeItem: ProjectTreeItem): Promise { 5 | if (treeItem === undefined) { 6 | return; 7 | } 8 | 9 | if (!(treeItem instanceof ProjectTreeItem)) { 10 | return; 11 | } 12 | 13 | await setActiveProjectId(treeItem.project.projectId); 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/registerCommands.ts: -------------------------------------------------------------------------------- 1 | import { commands, ExtensionContext } from "vscode"; 2 | import { AppwriteTree } from "../extensionVariables"; 3 | import { refreshAllViews, refreshTree } from "../utils/refreshTree"; 4 | import { connectAppwrite } from "./connectAppwrite"; 5 | import { createCollection } from "./database/createCollection"; 6 | import { createPermission } from "./database/permissions/createPermission"; 7 | import { createRule } from "./database/createRule"; 8 | import { deleteCollection } from "./database/deleteCollection"; 9 | import { deleteDocument } from "./database/deleteDocument"; 10 | import { deletePermission } from "./database/permissions/deletePermission"; 11 | import { viewDocumentAsJson } from "./database/openDocument"; 12 | import { refreshCollection } from "./database/refreshCollection"; 13 | import { refreshCollectionsList } from "./database/refreshCollectionsList"; 14 | import { removeRule } from "./database/removeRule"; 15 | import { viewCollectionAsJson } from "./database/viewCollectionAsJson"; 16 | import { openDocumentation } from "./openDocumentation"; 17 | import { copyUserEmail } from "./users/copyUserEmail"; 18 | import { copyUserId } from "./users/copyUserId"; 19 | import { createUser } from "./users/createUser"; 20 | import { deleteUser } from "./users/deleteUser"; 21 | import { getUserLogs } from "./users/getUserLogs"; 22 | import { openUserInConsole } from "./users/openUserInConsole"; 23 | import { refreshUsersList } from "./users/refreshUsersList"; 24 | import { viewUserPrefs } from "./users/viewUserPrefs"; 25 | import { editPermission } from "./database/permissions/editPermission"; 26 | import { setActiveProject } from "./project/setActiveProject"; 27 | import { removeProject } from "./project/removeProject"; 28 | import { createTag } from './functions/createTag'; 29 | import { createExecution } from './functions/createExecution'; 30 | import { activateTag } from './functions/activateTag'; 31 | import { editValue } from './common/editValue'; 32 | import { deleteFunction } from './functions/deleteFunction'; 33 | import { createFunction } from './functions/createFunction'; 34 | import { createFunctionVar } from './functions/createFunctionVar'; 35 | import { deleteFunctionVar } from './functions/deleteFunctionVar'; 36 | import { deleteTag } from './functions/deleteTag'; 37 | import { viewExecutionErrors } from './functions/viewExecutionErrors'; 38 | import { viewExecutionOutput } from './functions/viewExecutionOutput'; 39 | import { copyExecutionErrors } from './functions/copyExecutionErrors'; 40 | import { copyExecutionOutput } from './functions/copyExecutionOutput'; 41 | import { openExecutionsInBrowser } from './functions/openExecutionsInBrowser'; 42 | import { openFunctionSettingsInBrowser } from './functions/openFunctionSettingsInBrowser'; 43 | import { openFunctionTagsInBrowser } from './functions/openFunctionTagsInBrowser'; 44 | 45 | import { viewMore } from './common/viewMore'; 46 | 47 | class CommandRegistrar { 48 | constructor(private readonly context: ExtensionContext) {} 49 | 50 | registerCommand(commandId: string, callback: (...args: any[]) => any): void { 51 | const disposable = commands.registerCommand(commandId, callback); 52 | this.context.subscriptions.push(disposable); 53 | } 54 | } 55 | 56 | export function registerCommands(context: ExtensionContext): void { 57 | const registrar = new CommandRegistrar(context); 58 | const registerCommand = ( 59 | commandId: string, 60 | callback?: (...args: any[]) => any, 61 | refresh?: keyof AppwriteTree | (keyof AppwriteTree)[] | "all" 62 | ) => { 63 | registrar.registerCommand(`vscode-appwrite.${commandId}`, async (...args: any[]) => { 64 | await callback?.(...args); 65 | if (refresh !== undefined) { 66 | if (refresh === "all") { 67 | refreshAllViews(); 68 | } else if (typeof refresh === "string") { 69 | refreshTree(refresh); 70 | } else { 71 | refreshTree(...refresh); 72 | } 73 | } 74 | }); 75 | }; 76 | 77 | /** Common **/ 78 | registerCommand("editValue", editValue); 79 | registerCommand("viewMore", viewMore); 80 | 81 | /** General **/ 82 | registerCommand("Connect", connectAppwrite, "all"); 83 | 84 | /** Users **/ 85 | registerCommand("openUserInConsole", openUserInConsole); 86 | registerCommand("viewUserPrefs", viewUserPrefs); 87 | registerCommand("copyUserId", copyUserId); 88 | registerCommand("copyUserEmail", copyUserEmail); 89 | registerCommand("CreateUser", createUser); 90 | registerCommand("refreshUsersList", refreshUsersList); 91 | registerCommand("DeleteUser", deleteUser); 92 | registerCommand("OpenUsersDocumentation", () => openDocumentation("users")); 93 | registerCommand("GetUserLogs", getUserLogs); 94 | 95 | /** Database **/ 96 | registerCommand("OpenDatabaseDocumentation", () => openDocumentation("database")); 97 | registerCommand("viewDocumentAsJson", viewDocumentAsJson); 98 | registerCommand("viewCollectionAsJson", viewCollectionAsJson); 99 | registerCommand("createRule", createRule); 100 | registerCommand("removeRule", removeRule); 101 | registerCommand("deleteDocument", deleteDocument, "database"); 102 | registerCommand("deleteCollection", deleteCollection, "database"); 103 | registerCommand("refreshCollection", refreshCollection); 104 | registerCommand("refreshCollectionsList", refreshCollectionsList); 105 | registerCommand("createCollection", createCollection, "database"); 106 | registerCommand("createPermission", createPermission, "database"); 107 | registerCommand("deletePermission", deletePermission, "database"); 108 | registerCommand("editPermission", editPermission, "database"); 109 | 110 | /** Health **/ 111 | registerCommand("refreshHealth", undefined, "health"); 112 | registerCommand("openHealthDocumentation", () => openDocumentation("health")); 113 | 114 | /** Storage **/ 115 | registerCommand("refreshStorage", undefined, "storage"); 116 | registerCommand("openStorageDocumentation", () => openDocumentation("storage")); 117 | 118 | /** Projects **/ 119 | registerCommand("addProject", connectAppwrite, "all"); 120 | registerCommand("setActiveProject", setActiveProject, "all"); 121 | registerCommand("refreshProjects", undefined, "projects"); 122 | registerCommand("removeProject", removeProject, "all"); 123 | 124 | /** Functions **/ 125 | registerCommand("refreshFunctions", undefined, "functions"); 126 | registerCommand("CreateExecution", createExecution, "functions"); 127 | registerCommand("CreateTag", createTag, "functions"); 128 | registerCommand("activateTag", activateTag, "functions"); 129 | registerCommand("deleteTag", deleteTag, "functions"); 130 | registerCommand("deleteFunction", deleteFunction, "functions"); 131 | registerCommand("openFunctionsDocumentation", () => openDocumentation("functions")); 132 | registerCommand("createFunction", createFunction, "functions"); 133 | registerCommand("createFunctionVar", createFunctionVar, "functions"); 134 | registerCommand("deleteFunctionVar", deleteFunctionVar, "functions"); 135 | registerCommand("viewExecutionErrors", viewExecutionErrors); 136 | registerCommand("viewExecutionOutput", viewExecutionOutput); 137 | registerCommand("copyExecutionOutput", copyExecutionOutput); 138 | registerCommand("copyExecutionErrors", copyExecutionErrors); 139 | registerCommand("openExecutionsInBrowser", openExecutionsInBrowser); 140 | registerCommand("openFunctionTagsInBrowser", openFunctionTagsInBrowser); 141 | registerCommand("openFunctionSettingsInBrowser", openFunctionSettingsInBrowser); 142 | } 143 | -------------------------------------------------------------------------------- /src/commands/users/copyUserEmail.ts: -------------------------------------------------------------------------------- 1 | import { ChildTreeItem } from '../../tree/ChildTreeItem'; 2 | import { env, window} from 'vscode'; 3 | import { UserTreeItem } from '../../tree/users/UserTreeItem'; 4 | 5 | export async function copyUserEmail(item: ChildTreeItem): Promise { 6 | const email = item.parent.user.email; 7 | await env.clipboard.writeText(email); 8 | window.showInformationMessage('User email copied to clipboard'); 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/users/copyUserId.ts: -------------------------------------------------------------------------------- 1 | import { ChildTreeItem } from '../../tree/ChildTreeItem'; 2 | import { env, window} from 'vscode'; 3 | import { UserTreeItem } from '../../tree/users/UserTreeItem'; 4 | 5 | export async function copyUserId(item: ChildTreeItem): Promise { 6 | const id = item.parent.user.$id; 7 | await env.clipboard.writeText(id); 8 | window.showInformationMessage('User id copied to clipboard'); 9 | } 10 | -------------------------------------------------------------------------------- /src/commands/users/createUser.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { usersClient } from '../../client'; 3 | import { ext } from '../../extensionVariables'; 4 | 5 | export async function createUser(): Promise { 6 | if (!usersClient) { 7 | return; 8 | } 9 | const email = await window.showInputBox({ 10 | ignoreFocusOut: true, 11 | placeHolder: "jane.doe@hotmail.com", 12 | prompt: "New user email address", 13 | }); 14 | if (email === undefined) { 15 | return; 16 | } 17 | const password = await window.showInputBox({ 18 | ignoreFocusOut: true, 19 | password: true, 20 | prompt: "Enter user password", 21 | validateInput: (value) => { 22 | if (value.length < 6) { 23 | return "Password must be at least 6 characters long."; 24 | } 25 | if (value.length > 32) { 26 | return "Password must be less than 32 characters long."; 27 | } 28 | }, 29 | }); 30 | if (password === undefined) { 31 | return; 32 | } 33 | const name = await window.showInputBox({ 34 | ignoreFocusOut: true, 35 | placeHolder: "", 36 | prompt: "New user name (optional)", 37 | }); 38 | 39 | await usersClient.createNewUser({ 40 | email, 41 | password, 42 | name: name === "" ? undefined : name, 43 | }); 44 | 45 | ext.tree?.users?.refresh(); 46 | } 47 | -------------------------------------------------------------------------------- /src/commands/users/deleteUser.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { usersClient } from "../../client"; 3 | import { UserTreeItem } from '../../tree/users/UserTreeItem'; 4 | import { DialogResponses } from "../../ui/DialogResponses"; 5 | import { refreshTree } from "../../utils/refreshTree"; 6 | 7 | export async function deleteUser(userTreeItem: UserTreeItem): Promise { 8 | if (!usersClient) { 9 | return; 10 | } 11 | const user = userTreeItem.user; 12 | const userId = user.$id; 13 | const shouldDeleteUser = await window.showWarningMessage( 14 | `Are you sure you want to delete user with email: "${user.email}"?`, 15 | { 16 | modal: true, 17 | }, 18 | DialogResponses.yes, 19 | DialogResponses.cancel 20 | ); 21 | 22 | if (shouldDeleteUser === DialogResponses.yes) { 23 | await usersClient.delete(userId); 24 | refreshTree("users"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/users/getUserLogs.ts: -------------------------------------------------------------------------------- 1 | import { usersClient } from "../../client"; 2 | import { UserTreeItem } from "../../tree/users/UserTreeItem"; 3 | import { openReadOnlyJson } from '../../ui/openReadonlyContent'; 4 | 5 | export async function getUserLogs(treeItem: UserTreeItem): Promise { 6 | if (!usersClient) { 7 | return; 8 | } 9 | const userId = treeItem.user.$id; 10 | 11 | const logs = await usersClient.getLogs(userId); 12 | 13 | await openReadOnlyJson({ label: `Logs for ${treeItem.user.email}`, fullId: `${treeItem.user.$id}-UserLogs` }, logs); 14 | } 15 | -------------------------------------------------------------------------------- /src/commands/users/openUserInConsole.ts: -------------------------------------------------------------------------------- 1 | import { URL } from "url"; 2 | import { commands, Uri } from "vscode"; 3 | import { clientConfig } from "../../client"; 4 | import { UserTreeItem } from "../../tree/users/UserTreeItem"; 5 | 6 | export function getConsoleUrlFromEndpoint(endpoint: string): string { 7 | const url = new URL(endpoint); 8 | return `${url.origin}/console`; 9 | } 10 | 11 | function getUserUrl(userId: string, endpoint: string, projectId: string): string { 12 | return `${getConsoleUrlFromEndpoint(endpoint)}/users/user?id=${userId}&project=${projectId}`; 13 | } 14 | 15 | export async function openUserInConsole(item: UserTreeItem): Promise { 16 | const url = getUserUrl(item.user.$id, clientConfig.endpoint, clientConfig.projectId); 17 | 18 | await commands.executeCommand("vscode.open", Uri.parse(url)); 19 | } 20 | -------------------------------------------------------------------------------- /src/commands/users/refreshUsersList.ts: -------------------------------------------------------------------------------- 1 | import { ext } from "../../extensionVariables"; 2 | 3 | export async function refreshUsersList(): Promise { 4 | ext.tree?.users?.refresh(); 5 | } 6 | -------------------------------------------------------------------------------- /src/commands/users/viewUserPrefs.ts: -------------------------------------------------------------------------------- 1 | import { UserPrefsTreeItem } from "../../tree/users/properties/UserPrefsTreeItem"; 2 | import { UserTreeItem } from '../../tree/users/UserTreeItem'; 3 | import { openReadOnlyJson } from "../../ui/openReadonlyContent"; 4 | 5 | export async function viewUserPrefs(item: UserPrefsTreeItem | UserTreeItem): Promise { 6 | const userItem = item instanceof UserPrefsTreeItem ? item.parent : item; 7 | const prefs = userItem.user.prefs; 8 | 9 | await openReadOnlyJson( 10 | { label: `prefs`, fullId: `${userItem.user.$id}.prefs` }, 11 | prefs 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { SDK } from "./appwrite"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | export const AppwriteSDK: SDK = require("node-appwrite") as SDK; 5 | 6 | export const appwriteSystemEvents = [ 7 | { 8 | name: "account.create", 9 | description: "This event triggers when the account is created.", 10 | }, 11 | { 12 | name: "account.update.email", 13 | description: "This event triggers when the account email address is updated.", 14 | }, 15 | { 16 | name: "account.update.name", 17 | description: "This event triggers when the account name is updated.", 18 | }, 19 | { 20 | name: "account.update.password", 21 | description: "This event triggers when the account password is updated.", 22 | }, 23 | { 24 | name: "account.update.prefs", 25 | description: "This event triggers when the account preferences are updated.", 26 | }, 27 | { 28 | name: "account.recovery.create", 29 | description: "This event triggers when the account recovery token is created.", 30 | }, 31 | { 32 | name: "account.recovery.update", 33 | description: "This event triggers when the account recovery token is validated.", 34 | }, 35 | { 36 | name: "account.verification.create", 37 | description: "This event triggers when the account verification token is created.", 38 | }, 39 | { 40 | name: "account.verification.update", 41 | description: "This event triggers when the account verification token is validated.", 42 | }, 43 | { 44 | name: "account.delete", 45 | description: "This event triggers when the account is deleted.", 46 | }, 47 | { 48 | name: "account.sessions.create", 49 | description: "This event triggers when the account session is created.", 50 | }, 51 | { 52 | name: "account.delete", 53 | description: "This event triggers when the account is deleted.", 54 | }, 55 | { 56 | name: "account.sessions.create", 57 | description: "This event triggers when the account session is created.", 58 | }, 59 | { 60 | name: "account.sessions.delete", 61 | description: "This event triggers when the account session is deleted.", 62 | }, 63 | { 64 | name: "database.collections.create", 65 | description: "This event triggers when a database collection is created.", 66 | }, 67 | { 68 | name: "database.collections.update", 69 | description: "This event triggers when a database collection is updated.", 70 | }, 71 | { 72 | name: "database.collections.delete", 73 | description: "This event triggers when a database collection is deleted.", 74 | }, 75 | { 76 | name: "database.documents.create", 77 | description: "This event triggers when a database document is created.", 78 | }, 79 | { 80 | name: "database.documents.update", 81 | description: "This event triggers when a database document is updated.", 82 | }, 83 | { 84 | name: "database.documents.delete", 85 | description: "This event triggers when a database document is deleted.", 86 | }, 87 | { 88 | name: "functions.create", 89 | description: "This event triggers when a function is created.", 90 | }, 91 | { 92 | name: "functions.update", 93 | description: "This event triggers when a function is updated.", 94 | }, 95 | { 96 | name: "functions.delete", 97 | description: "This event triggers when a function is deleted.", 98 | }, 99 | { 100 | name: "functions.tags.create", 101 | description: "This event triggers when a function tag is created.", 102 | }, 103 | { 104 | name: "functions.tags.update", 105 | description: "This event triggers when a function tag is updated.", 106 | }, 107 | { 108 | name: "functions.tags.delete", 109 | description: "This event triggers when a function tag is deleted.", 110 | }, 111 | { 112 | name: "functions.executions.create", 113 | description: "This event triggers when a function execution is created.", 114 | }, 115 | { 116 | name: "functions.executions.update", 117 | description: "This event triggers when a function execution is updated.", 118 | }, 119 | { 120 | name: "storage.files.create", 121 | description: "This event triggers when a storage file is created.", 122 | }, 123 | { 124 | name: "storage.files.update", 125 | description: "This event triggers when a storage file is updated.", 126 | }, 127 | { 128 | name: "storage.files.delete", 129 | description: "This event triggers when a storage file is deleted.", 130 | }, 131 | { 132 | name: "users.create", 133 | description: "This event triggers when a user is created from the users API.", 134 | }, 135 | { 136 | name: "users.update.prefs", 137 | description: "This event triggers when a user preference is updated from the users API.", 138 | }, 139 | { 140 | name: "users.update.status", 141 | description: "This event triggers when a user status is updated from the users API.", 142 | }, 143 | { 144 | name: "users.delete", 145 | description: "This event triggers when a user is deleted from users API.", 146 | }, 147 | { 148 | name: "users.sessions.delete", 149 | description: "This event triggers when a user session is deleted from users API.", 150 | }, 151 | { 152 | name: "teams.create", 153 | description: "This event triggers when a team is created.", 154 | }, 155 | { 156 | name: "teams.update", 157 | description: "This event triggers when a team is updated.", 158 | }, 159 | { 160 | name: "teams.delete", 161 | description: "This event triggers when a team is deleted.", 162 | }, 163 | { 164 | name: "teams.memberships.create", 165 | description: "This event triggers when a team memberships is created.", 166 | }, 167 | { 168 | name: "teams.memberships.update", 169 | description: "This event triggers when a team membership is updated.", 170 | }, 171 | { 172 | name: "teams.memberships.update.status", 173 | description: "This event triggers when a team memberships status is updated.", 174 | }, 175 | { 176 | name: "teams.memberships.delete", 177 | description: "This event triggers when a team memberships is deleted.", 178 | }, 179 | ]; 180 | 181 | export const appwriteFunctionRuntimes = [ 182 | "dotnet-3.1", 183 | "dotnet-5.0", 184 | "dart-2.10", 185 | "dart-2.12", 186 | "deno-1.5", 187 | "deno-1.6", 188 | "deno-1.8", 189 | "python-3.8", 190 | "python-3.9", 191 | "ruby-2.7", 192 | "ruby-3.0", 193 | "php-7.4", 194 | "php-8.0", 195 | "node-14.5", 196 | "node-15.5", 197 | ]; 198 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { createAppwriteClient } from "./client"; 3 | import { registerCommands } from "./commands/registerCommands"; 4 | import { ext } from "./extensionVariables"; 5 | import { getActiveProjectConfiguration } from "./settings"; 6 | import { DatabaseTreeItemProvider } from "./tree/database/DatabaseTreeItemProvider"; 7 | import { FunctionsTreeItemProvider } from './tree/functions/FunctionsTreeItemProvider'; 8 | import { HealthTreeItemProvider } from "./tree/health/HealthTreeItemProvider"; 9 | import { ProjectsTreeItemProvider } from "./tree/projects/ProjectsTreeItemProvider"; 10 | import { StorageTreeItemProvider } from "./tree/storage/StorageTreeItemProvider"; 11 | import { UserTreeItemProvider } from "./tree/users/UserTreeItemProvider"; 12 | import { createAppwriteOutputChannel } from "./ui/AppwriteOutputChannel"; 13 | 14 | export async function activate(context: vscode.ExtensionContext): Promise { 15 | const userTreeItemProvider = new UserTreeItemProvider(); 16 | const healthTreeItemProvider = new HealthTreeItemProvider(); 17 | const databaseTreeItemProvider = new DatabaseTreeItemProvider(); 18 | const storageTreeItemProvider = new StorageTreeItemProvider(); 19 | const projectsTreeItemProvider = new ProjectsTreeItemProvider(); 20 | const functionsTreeItemProvider = new FunctionsTreeItemProvider(); 21 | 22 | vscode.window.registerTreeDataProvider("Users", userTreeItemProvider); 23 | vscode.window.registerTreeDataProvider("Health", healthTreeItemProvider); 24 | vscode.window.registerTreeDataProvider("Database", databaseTreeItemProvider); 25 | vscode.window.registerTreeDataProvider("Storage", storageTreeItemProvider); 26 | vscode.window.registerTreeDataProvider("Projects", projectsTreeItemProvider); 27 | vscode.window.registerTreeDataProvider("Functions", functionsTreeItemProvider); 28 | 29 | const activeProject = await getActiveProjectConfiguration(); 30 | createAppwriteClient(activeProject); 31 | 32 | ext.context = context; 33 | ext.outputChannel = createAppwriteOutputChannel("Appwrite", "appwrite"); 34 | 35 | ext.tree = { 36 | users: userTreeItemProvider, 37 | health: healthTreeItemProvider, 38 | database: databaseTreeItemProvider, 39 | storage: storageTreeItemProvider, 40 | projects: projectsTreeItemProvider, 41 | functions: functionsTreeItemProvider 42 | }; 43 | 44 | registerCommands(context); 45 | } 46 | -------------------------------------------------------------------------------- /src/extensionVariables.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionContext } from "vscode"; 2 | import { DatabaseTreeItemProvider } from './tree/database/DatabaseTreeItemProvider'; 3 | import { FunctionsTreeItemProvider } from './tree/functions/FunctionsTreeItemProvider'; 4 | import { HealthTreeItemProvider } from './tree/health/HealthTreeItemProvider'; 5 | import { ProjectsTreeItemProvider } from './tree/projects/ProjectsTreeItemProvider'; 6 | import { StorageTreeItemProvider } from './tree/storage/StorageTreeItemProvider'; 7 | import { UserTreeItemProvider } from './tree/users/UserTreeItemProvider'; 8 | import { AppwriteOutputChannel } from './ui/AppwriteOutputChannel'; 9 | 10 | export type AppwriteTree = { 11 | users?: UserTreeItemProvider; 12 | health?: HealthTreeItemProvider; 13 | database?: DatabaseTreeItemProvider; 14 | storage?: StorageTreeItemProvider; 15 | projects?: ProjectsTreeItemProvider; 16 | functions?: FunctionsTreeItemProvider; 17 | }; 18 | 19 | export type Ext = { 20 | context: ExtensionContext; 21 | outputChannel: AppwriteOutputChannel; 22 | tree?: AppwriteTree; 23 | }; 24 | 25 | export const ext: Ext = {} as Ext; 26 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | import { createAppwriteClient } from "./client"; 3 | 4 | export type AppwriteProjectConfiguration = { 5 | nickname?: string; 6 | endpoint: string; 7 | console?: string; 8 | projectId: string; 9 | selfSigned: boolean; 10 | secret: string; 11 | }; 12 | 13 | export async function getDefaultProject(): Promise { 14 | const projects = await getAppwriteProjects(); 15 | return projects?.[0] ?? undefined; 16 | } 17 | 18 | export async function getAppwriteProjects(): Promise { 19 | const configuration = workspace.getConfiguration("appwrite"); 20 | const projects = configuration.get("projects"); 21 | if (projects === undefined) { 22 | configuration.update("projects", []); 23 | return []; 24 | } 25 | return projects as AppwriteProjectConfiguration[]; 26 | } 27 | 28 | export async function addProjectConfiguration(projectConfig: AppwriteProjectConfiguration): Promise { 29 | const configuration = workspace.getConfiguration("appwrite"); 30 | const projects = await getAppwriteProjects(); 31 | 32 | await configuration.update("projects", [...projects, projectConfig], true); 33 | await setActiveProjectId(projectConfig.projectId); 34 | } 35 | 36 | export async function getActiveProjectId(): Promise { 37 | const configuration = workspace.getConfiguration("appwrite"); 38 | const projectId = configuration.get("activeProjectId"); 39 | return projectId ?? ""; 40 | } 41 | 42 | export async function getActiveProjectConfiguration(): Promise { 43 | const configurations = await getAppwriteProjects(); 44 | const activeConfigId = await getActiveProjectId(); 45 | let activeConfig; 46 | 47 | if (configurations === undefined || configurations?.length === 0) { 48 | return undefined; 49 | } 50 | 51 | configurations.forEach((config) => { 52 | if (config.projectId === activeConfigId) { 53 | activeConfig = config; 54 | } 55 | }); 56 | 57 | if (activeConfig === undefined) { 58 | activeConfig = configurations[0]; 59 | setActiveProjectId(configurations[0].projectId); 60 | } 61 | return activeConfig; 62 | } 63 | 64 | export async function setActiveProjectId(projectId: string): Promise { 65 | const configuration = workspace.getConfiguration("appwrite"); 66 | await configuration.update("activeProjectId", projectId, true); 67 | const active = await getActiveProjectConfiguration(); 68 | createAppwriteClient(active); 69 | } 70 | 71 | export async function updateActiveProjectId(): Promise { 72 | const projects = await getAppwriteProjects(); 73 | if (projects.length > 0) { 74 | const configuration = workspace.getConfiguration("appwrite"); 75 | await configuration.update("activeProjectId", projects[0].projectId, true); 76 | const active = await getActiveProjectConfiguration(); 77 | createAppwriteClient(active); 78 | } 79 | } 80 | 81 | export async function removeProjectConfig(projectId: string): Promise { 82 | const projects = await getAppwriteProjects(); 83 | const updatedProjects = projects.filter((project) => project.projectId !== projectId); 84 | const configuration = workspace.getConfiguration("appwrite"); 85 | await configuration.update("projects", updatedProjects, true); 86 | await updateActiveProjectId(); 87 | } 88 | -------------------------------------------------------------------------------- /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.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-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/tree/ChildTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem } from "vscode"; 2 | 3 | export class ChildTreeItem extends TreeItem { 4 | constructor(public readonly parent: Parent, item: TreeItem) { 5 | super(item.label || 'Please provide label'); 6 | Object.assign(this, item); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/tree/CollapsableTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { AppwriteTreeItemBase } from "../ui/AppwriteTreeItemBase"; 3 | 4 | export class CollapsableTreeItem extends AppwriteTreeItemBase { 5 | constructor(parent: Parent, item: Partial & { label: string }, private readonly children: TreeItem[], public readonly brand?: string) { 6 | super(parent, item.label); 7 | Object.assign(this, item); 8 | } 9 | 10 | public async getChildren(): Promise { 11 | return this.children; 12 | } 13 | 14 | collapsibleState = TreeItemCollapsibleState.Collapsed; 15 | } 16 | -------------------------------------------------------------------------------- /src/tree/CommandTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { Command, TreeItem } from 'vscode'; 2 | 3 | export class CommandTreeItem extends TreeItem { 4 | constructor(item: TreeItem, command: Command) { 5 | super(item.label || 'Add label'); 6 | Object.assign(this, item); 7 | this.command = command; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/tree/common/EditableTreeItemBase.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem } from "vscode"; 2 | 3 | export abstract class EditableTreeItemBase extends TreeItem { 4 | public abstract setValue(value: T): Promise; 5 | 6 | constructor(contextValuePrefix: string, public readonly value: T, description?: string) { 7 | super(typeof value === "string" ? value : "No label"); 8 | this.contextValue = `editable_${contextValuePrefix}`; 9 | this.description = description ?? contextValuePrefix; 10 | } 11 | 12 | public abstract prompt(): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/tree/common/EnumEditableTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem, QuickPickOptions, window } from "vscode"; 2 | import { EditableTreeItemBase } from "./EditableTreeItemBase"; 3 | 4 | export abstract class EnumEditableTreeItemBase extends EditableTreeItemBase { 5 | public abstract options: string[] | QuickPickItem[]; 6 | 7 | public quickPickOptions: QuickPickOptions; 8 | 9 | constructor(contextValuePrefix: string, public readonly value: string[], description?: string) { 10 | super(contextValuePrefix, value, description); 11 | this.quickPickOptions = {}; 12 | } 13 | 14 | public async prompt(): Promise { 15 | 16 | const value = await window.showQuickPick( 17 | this.options.map((option: QuickPickItem | string): QuickPickItem => { 18 | if (typeof option === "string") { 19 | return { label: option, picked: this.value.includes(option) }; 20 | } 21 | const picked = this.value.includes(option.label); 22 | return { ...option, picked, alwaysShow: picked }; 23 | }).sort((a, b) => { 24 | if (a.picked) { 25 | return -1; 26 | } 27 | if (b.picked) { 28 | return 1; 29 | } 30 | return 0; 31 | }), 32 | { ...this.quickPickOptions, canPickMany: true } 33 | ); 34 | if (value !== undefined) { 35 | this.setValue(value.map((item) => item.label)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tree/common/SimpleEditableTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, window } from "vscode"; 2 | 3 | export class EditableTreeItem extends TreeItem { 4 | public readonly setValue: (value: string) => Promise; 5 | 6 | constructor(label: string, contextValuePrefix: string, public readonly value: string, setValue: (value: string) => Promise) { 7 | super(label); 8 | this.setValue = setValue; 9 | this.contextValue = `editable_${contextValuePrefix}`; 10 | } 11 | 12 | public async prompt(): Promise { 13 | const value = await window.showInputBox({ value: this.value }); 14 | if (value !== undefined) { 15 | this.setValue(value); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tree/common/StringEditableTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { InputBoxOptions, window } from "vscode"; 2 | import { EditableTreeItemBase } from "./EditableTreeItemBase"; 3 | 4 | export abstract class StringEditableTreeItemBase extends EditableTreeItemBase { 5 | public abstract setValue(value: string): Promise; 6 | public inputBoxOptions: InputBoxOptions; 7 | 8 | constructor(contextValuePrefix: string, public readonly value: string, description?: string) { 9 | super(contextValuePrefix, value, description); 10 | 11 | this.inputBoxOptions = { 12 | prompt: description, 13 | }; 14 | } 15 | 16 | public async prompt(): Promise { 17 | const value = await window.showInputBox({ value: this.value, ...this.inputBoxOptions }); 18 | if (value !== undefined) { 19 | this.setValue(value); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/tree/common/editable/EditableTreeItemBase.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem } from "vscode"; 2 | 3 | export abstract class EditableTreeItemBase extends TreeItem { 4 | public abstract setValue(value: T): Promise; 5 | 6 | constructor(contextValuePrefix: string, public readonly value: T, description?: string) { 7 | super(typeof value === "string" ? value : "No label"); 8 | this.contextValue = `editable_${contextValuePrefix}`; 9 | this.description = description ?? contextValuePrefix; 10 | } 11 | 12 | public abstract prompt(): Promise; 13 | } 14 | -------------------------------------------------------------------------------- /src/tree/common/editable/EnumEditableTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem, QuickPickOptions, window } from "vscode"; 2 | import { EditableTreeItemBase } from "./EditableTreeItemBase"; 3 | 4 | export abstract class EnumEditableTreeItemBase extends EditableTreeItemBase { 5 | public abstract options: string[] | QuickPickItem[]; 6 | 7 | public quickPickOptions: QuickPickOptions; 8 | 9 | constructor(contextValuePrefix: string, public readonly value: string[], description?: string) { 10 | super(contextValuePrefix, value, description); 11 | this.quickPickOptions = {}; 12 | } 13 | 14 | public async prompt(): Promise { 15 | 16 | const value = await window.showQuickPick( 17 | this.options.map((option: QuickPickItem | string): QuickPickItem => { 18 | if (typeof option === "string") { 19 | return { label: option, picked: this.value.includes(option) }; 20 | } 21 | const picked = this.value.includes(option.label); 22 | return { ...option, picked, alwaysShow: picked }; 23 | }).sort((a, b) => { 24 | if (a.picked) { 25 | return -1; 26 | } 27 | if (b.picked) { 28 | return 1; 29 | } 30 | return 0; 31 | }), 32 | { ...this.quickPickOptions, canPickMany: true } 33 | ); 34 | if (value !== undefined) { 35 | this.setValue(value.map((item) => item.label)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/tree/common/editable/SimpleEditableTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, window } from "vscode"; 2 | 3 | export class EditableTreeItem extends TreeItem { 4 | public readonly setValue: (value: string) => Promise; 5 | 6 | constructor(label: string, contextValuePrefix: string, public readonly value: string, setValue: (value: string) => Promise) { 7 | super(label); 8 | this.setValue = setValue; 9 | this.contextValue = `editable_${contextValuePrefix}`; 10 | } 11 | 12 | public async prompt(): Promise { 13 | const value = await window.showInputBox({ value: this.value }); 14 | if (value !== undefined) { 15 | this.setValue(value); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tree/common/editable/StringEditableTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { InputBoxOptions, window } from "vscode"; 2 | import { EditableTreeItemBase } from "./EditableTreeItemBase"; 3 | 4 | export abstract class StringEditableTreeItemBase extends EditableTreeItemBase { 5 | public abstract setValue(value: string): Promise; 6 | public inputBoxOptions: InputBoxOptions; 7 | 8 | constructor(contextValuePrefix: string, public readonly value: string, description?: string) { 9 | super(contextValuePrefix, value, description); 10 | 11 | this.inputBoxOptions = { 12 | prompt: description, 13 | }; 14 | } 15 | 16 | public async prompt(): Promise { 17 | const value = await window.showInputBox({ value: this.value, ...this.inputBoxOptions }); 18 | if (value !== undefined) { 19 | this.setValue(value); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/tree/database/CollectionTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { Collection } from "../../appwrite"; 3 | import { databaseClient } from "../../client"; 4 | import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase"; 5 | import { DatabaseTreeItemProvider } from "./DatabaseTreeItemProvider"; 6 | import { DocumentsTreeItem } from "./DocumentsTreeItem"; 7 | import { PermissionsTreeItem } from "./settings/PermissionsTreeItem"; 8 | import { RulesTreeItem } from "./settings/RulesTreeItem"; 9 | 10 | export class CollectionTreeItem extends AppwriteTreeItemBase { 11 | constructor(public collection: Collection, public readonly provider: DatabaseTreeItemProvider) { 12 | super(undefined, collection.name); 13 | } 14 | 15 | public async getChildren(): Promise { 16 | return [new RulesTreeItem(this), new PermissionsTreeItem(this), new DocumentsTreeItem(this)]; 17 | } 18 | 19 | public async refresh(): Promise { 20 | if (!databaseClient) { 21 | return; 22 | } 23 | this.collection = (await databaseClient.getCollection(this.collection.$id)) ?? this.collection; 24 | this.provider.refreshChild(this); 25 | } 26 | 27 | collapsibleState = TreeItemCollapsibleState.Collapsed; 28 | 29 | contextValue = "collection"; 30 | 31 | iconPath = new ThemeIcon("folder"); 32 | } 33 | -------------------------------------------------------------------------------- /src/tree/database/DatabaseTreeItemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { client } from "../../client"; 3 | import AppwriteCall from "../../utils/AppwriteCall"; 4 | import { Collection, CollectionsList } from "../../appwrite"; 5 | import { CollectionTreeItem } from "./CollectionTreeItem"; 6 | import { AppwriteSDK } from "../../constants"; 7 | import { ext } from '../../extensionVariables'; 8 | import { AppwriteTreeItemBase } from '../../ui/AppwriteTreeItemBase'; 9 | 10 | export class DatabaseTreeItemProvider implements vscode.TreeDataProvider { 11 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 12 | CollectionTreeItem | undefined | void 13 | >(); 14 | 15 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 16 | 17 | refresh(): void { 18 | ext.outputChannel?.appendLine('refresh database'); 19 | this._onDidChangeTreeData.fire(); 20 | } 21 | 22 | refreshChild(child: vscode.TreeItem): void { 23 | this._onDidChangeTreeData.fire(child); 24 | } 25 | 26 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem { 27 | return element; 28 | } 29 | 30 | async getChildren(parent?: vscode.TreeItem): Promise { 31 | ext.outputChannel?.appendLine('getChildren for: ' + parent?.label); 32 | if (client === undefined) { 33 | return Promise.resolve([]); 34 | } 35 | 36 | if (parent instanceof AppwriteTreeItemBase) { 37 | return await parent.getChildren?.() ?? []; 38 | } 39 | 40 | const databaseSdk = new AppwriteSDK.Database(client); 41 | 42 | const collectionsList = await AppwriteCall(databaseSdk.listCollections()); 43 | if (collectionsList) { 44 | const collectionTreeItems = collectionsList.collections.map((collection: Collection) => new CollectionTreeItem(collection, this)) ?? []; 45 | const headerItem: vscode.TreeItem = { 46 | label: `Total collections: ${collectionsList.sum}`, 47 | }; 48 | return [headerItem, ...collectionTreeItems]; 49 | } 50 | 51 | return [{ label: "No collections found" }]; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/tree/database/DocumentTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon } from 'vscode'; 2 | import { ChildTreeItem } from '../ChildTreeItem'; 3 | import { DocumentsTreeItem } from './DocumentsTreeItem'; 4 | 5 | export class DocumentTreeItem extends ChildTreeItem { 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | constructor(parent: DocumentsTreeItem, public readonly document: Record) { 8 | super(parent, { 9 | label: document['$id'], 10 | }); 11 | } 12 | iconPath = new ThemeIcon('json'); 13 | contextValue = 'document'; 14 | } 15 | -------------------------------------------------------------------------------- /src/tree/database/DocumentsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { DocumentsList } from "../../appwrite"; 3 | import { client } from "../../client"; 4 | import { AppwriteSDK } from "../../constants"; 5 | import { ext } from "../../extensionVariables"; 6 | import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase"; 7 | import AppwriteCall from "../../utils/AppwriteCall"; 8 | import { CollectionTreeItem } from "./CollectionTreeItem"; 9 | import { DocumentTreeItem } from "./DocumentTreeItem"; 10 | 11 | export class DocumentsTreeItem extends AppwriteTreeItemBase { 12 | constructor(parent: CollectionTreeItem) { 13 | super(parent, "Documents"); 14 | } 15 | 16 | public async getChildren(): Promise { 17 | const databaseSdk = new AppwriteSDK.Database(client); 18 | const documentList = await AppwriteCall(databaseSdk.listDocuments(this.parent.collection.$id)); 19 | if (documentList === undefined) { 20 | return []; 21 | } 22 | 23 | ext.outputChannel?.append(JSON.stringify(documentList, null, 4)); 24 | 25 | const documentTreeItems = documentList.documents.map((document) => new DocumentTreeItem(this, document)); 26 | const headerItem: TreeItem = { 27 | label: `Total documents: ${documentTreeItems.length}`, 28 | }; 29 | return [headerItem, ...documentTreeItems]; 30 | } 31 | 32 | collapsibleState = TreeItemCollapsibleState.Collapsed; 33 | 34 | contextValue = "documents"; 35 | 36 | iconPath = new ThemeIcon("database"); 37 | } 38 | -------------------------------------------------------------------------------- /src/tree/database/settings/PermissionTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ChildTreeItem } from "../../ChildTreeItem"; 2 | import { PermissionsTreeItem } from "./PermissionsTreeItem"; 3 | 4 | export class PermissionTreeItem extends ChildTreeItem { 5 | constructor(parent: PermissionsTreeItem, public readonly permission: string, public readonly kind: "read" | "write") { 6 | super(parent, { label: permission }); 7 | } 8 | 9 | contextValue = 'permission'; 10 | } 11 | -------------------------------------------------------------------------------- /src/tree/database/settings/PermissionsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { Permissions } from "../../../appwrite"; 3 | import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; 4 | import { CollapsableTreeItem } from "../../CollapsableTreeItem"; 5 | import { CollectionTreeItem } from "../CollectionTreeItem"; 6 | import { PermissionTreeItem } from "./PermissionTreeItem"; 7 | 8 | export class PermissionsTreeItem extends AppwriteTreeItemBase { 9 | public readonly permissions: Permissions; 10 | 11 | constructor(parent: CollectionTreeItem) { 12 | super(parent, "Permissions"); 13 | this.permissions = parent.collection.$permissions; 14 | } 15 | 16 | public async getChildren(): Promise { 17 | const readPermissions = this.permissions.read.map((perm) => new PermissionTreeItem(this, perm, "read")); 18 | const writePermissions = this.permissions.write.map((perm) => new PermissionTreeItem(this, perm, "write")); 19 | return [ 20 | new CollapsableTreeItem(this, { label: "Read" }, readPermissions, "read"), 21 | new CollapsableTreeItem(this, { label: "Write" }, writePermissions, "write"), 22 | ]; 23 | } 24 | 25 | iconPath = new ThemeIcon("shield"); 26 | contextValue = "permissions"; 27 | collapsibleState = TreeItemCollapsibleState.Collapsed; 28 | } 29 | -------------------------------------------------------------------------------- /src/tree/database/settings/RuleTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { Rule } from "../../../appwrite"; 2 | import { ChildTreeItem } from "../../ChildTreeItem"; 3 | import { RulesTreeItem } from "./RulesTreeItem"; 4 | 5 | export class RuleTreeItem extends ChildTreeItem { 6 | constructor(parent: RulesTreeItem, public readonly rule: Rule) { 7 | super(parent, { label: rule.label, description: rule.type }); 8 | } 9 | contextValue = "collection.rule"; 10 | } 11 | -------------------------------------------------------------------------------- /src/tree/database/settings/RulesTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { Rule } from "../../../appwrite"; 3 | import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; 4 | import { CommandTreeItem } from "../../CommandTreeItem"; 5 | import { CollectionTreeItem } from "../CollectionTreeItem"; 6 | import { RuleTreeItem } from "./RuleTreeItem"; 7 | 8 | export class RulesTreeItem extends AppwriteTreeItemBase { 9 | public readonly rules: Rule[]; 10 | 11 | constructor(parent: CollectionTreeItem) { 12 | super(parent, "Rules"); 13 | this.rules = parent.collection.rules; 14 | } 15 | 16 | public async getChildren(): Promise { 17 | if (this.rules.length === 0) { 18 | const addRuleItem = new CommandTreeItem( 19 | { label: "Add rule", iconPath: new ThemeIcon("add") }, 20 | { command: "vscode-appwrite.createRule", arguments: [this], title: "Create rule", tooltip: "Create collection rule" } 21 | ); 22 | return [addRuleItem]; 23 | } 24 | return this.rules.map((rule) => new RuleTreeItem(this, rule)); 25 | } 26 | 27 | public async refresh(): Promise { 28 | await this.parent.refresh(); 29 | } 30 | 31 | iconPath = new ThemeIcon("symbol-property"); 32 | contextValue = "collection.rules"; 33 | collapsibleState = TreeItemCollapsibleState.Collapsed; 34 | } 35 | -------------------------------------------------------------------------------- /src/tree/functions/FunctionTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { Function } from "../../appwrite"; 3 | import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase"; 4 | import { msToDate } from '../../utils/date'; 5 | import { ExecutionsTreeItem } from './executions/ExecutionsTreeItem'; 6 | import { FunctionsTreeItemProvider } from './FunctionsTreeItemProvider'; 7 | import { FunctionSettingsTreeItem } from './settings/FunctionSettingsTreeItem'; 8 | import { TagsTreeItem } from './tags/TagsTreeItem'; 9 | 10 | export class FunctionTreeItem extends AppwriteTreeItemBase { 11 | constructor(public func: Function, public readonly provider: FunctionsTreeItemProvider) { 12 | super(undefined, func.name); 13 | this.tooltip = new MarkdownString(`ID: ${func.$id} \nLast updated: ${msToDate(func.dateUpdated)} \nCreated: ${msToDate(func.dateCreated)}`); 14 | this.description = func.env; 15 | } 16 | 17 | public async getChildren(): Promise { 18 | return [new FunctionSettingsTreeItem(this), new TagsTreeItem(this), new ExecutionsTreeItem(this)]; 19 | } 20 | 21 | public async refresh(): Promise { 22 | this.provider.refreshChild(this); 23 | } 24 | 25 | collapsibleState = TreeItemCollapsibleState.Collapsed; 26 | 27 | contextValue = "function"; 28 | 29 | iconPath = new ThemeIcon("symbol-event"); 30 | } 31 | -------------------------------------------------------------------------------- /src/tree/functions/FunctionsTreeItemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { client } from "../../client"; 3 | import { Function, FunctionsList } from "../../appwrite"; 4 | import { AppwriteSDK } from "../../constants"; 5 | import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase"; 6 | import { ext } from "../../extensionVariables"; 7 | import { EventEmitter, TreeItem } from "vscode"; 8 | import { FunctionTreeItem } from "./FunctionTreeItem"; 9 | 10 | export class FunctionsTreeItemProvider implements vscode.TreeDataProvider { 11 | private _onDidChangeTreeData: EventEmitter = new EventEmitter(); 12 | 13 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 14 | 15 | refresh(): void { 16 | ext.outputChannel?.appendLine("Refreshing functions tree provider..."); 17 | this._onDidChangeTreeData.fire(); 18 | } 19 | 20 | refreshChild(child: vscode.TreeItem): void { 21 | this._onDidChangeTreeData.fire(child); 22 | } 23 | 24 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem { 25 | return element; 26 | } 27 | 28 | async getChildren(parent?: AppwriteTreeItemBase | TreeItem): Promise { 29 | if (client === undefined) { 30 | return Promise.resolve([]); 31 | } 32 | 33 | if (parent === undefined) { 34 | const functionsSdk = new AppwriteSDK.Functions(client); 35 | 36 | const list: FunctionsList = await functionsSdk.list(); 37 | 38 | if (list) { 39 | const functionTreeItems = list.functions.map((func: Function) => new FunctionTreeItem(func, this)) ?? []; 40 | return functionTreeItems; 41 | } 42 | 43 | return [{ label: "No functions found" }]; 44 | } 45 | 46 | if (parent instanceof AppwriteTreeItemBase) { 47 | return await parent.getChildren?.() ?? []; 48 | } 49 | 50 | return []; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/tree/functions/executions/ExecutionTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, ThemeColor, ThemeIcon, TreeItem } from "vscode"; 2 | import { Execution, ExecutionStatus } from "../../../appwrite"; 3 | import { msToDate } from "../../../utils/date"; 4 | import { ExecutionsTreeItem } from "./ExecutionsTreeItem"; 5 | 6 | const executionStatusIcons: Record = { 7 | processing: new ThemeIcon("loading"), 8 | waiting: new ThemeIcon("circle-outline"), 9 | completed: new ThemeIcon("circle-filled", new ThemeColor("testing.iconPassed")), 10 | failed: new ThemeIcon("circle-filled", new ThemeColor("testing.iconFailed")), 11 | }; 12 | 13 | export class ExecutionTreeItem extends TreeItem { 14 | public isAutoRefreshing: boolean = false; 15 | private refreshCount: number = 0; 16 | 17 | constructor(public readonly parent: ExecutionsTreeItem, public execution: Execution) { 18 | super(execution.$id); 19 | this.label = this.getLabel(execution); 20 | this.iconPath = executionStatusIcons[execution.status]; 21 | const md = `Id: ${execution.$id} \nCreated: ${this.getCreated(execution)} \nTrigger: ${execution.trigger}`; 22 | this.tooltip = new MarkdownString(md); 23 | this.description = execution.trigger; 24 | this.contextValue = this.getContextValue(execution); 25 | this.isAutoRefreshing = execution.status === "processing" || execution.status === "waiting"; 26 | // if (this.isAutoRefreshing) { 27 | // this.autoRefresh(); 28 | // } 29 | } 30 | 31 | // async autoRefresh(): Promise { 32 | // if (!this.isAutoRefreshing && this.refreshCount < 5) { 33 | // return; 34 | // } 35 | // this.refreshCount++; 36 | // ext.outputChannel.appendLog("Refreshing execution."); 37 | // const execution = await functionsClient?.getExecution(this.parent.parent.func.$id, this.execution.$id); 38 | 39 | // if (!execution) { 40 | // ext.outputChannel.appendLog("Execution is undefined"); 41 | // this.isAutoRefreshing = false; 42 | // return; 43 | // } 44 | // this.execution = execution; 45 | // this.contextValue = this.getContextValue(execution); 46 | // this.iconPath = executionStatusIcons[execution.status]; 47 | // this.label = this.getLabel(execution); 48 | // this.isAutoRefreshing = execution.status === "processing" || execution.status === "waiting"; 49 | // ext.tree?.functions?.refreshChild(this); 50 | // await sleep(1000); 51 | // this.autoRefresh(); 52 | // } 53 | 54 | getLabel(execution: Execution): string { 55 | if (execution.status === "completed" || execution.status === "failed") { 56 | return `${this.getCreated(execution)} (${this.getExecutionTime(execution)}s)`; 57 | } 58 | return `${this.getCreated(execution)} (${execution.status})`; 59 | } 60 | 61 | getExecutionTime(execution: Execution): string { 62 | return execution.time.toPrecision(2); 63 | } 64 | 65 | getContextValue(execution: Execution): string { 66 | if (execution.status === "completed" || execution.status === "failed") { 67 | if (execution.stderr === "" && execution.stdout === "") { 68 | return "execution_noErrorOrOutput"; 69 | } 70 | if (execution.stderr === "") { 71 | return "execution_outputOnly"; 72 | } 73 | if (execution.stdout === "") { 74 | return "execution_errorOnly"; 75 | } 76 | } 77 | return "execution"; 78 | } 79 | 80 | getCreated(execution: Execution): string { 81 | return msToDate(execution.dateCreated); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/tree/functions/executions/ExecutionsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { Execution, ExecutionList } from "../../../appwrite"; 3 | import { functionsClient } from "../../../client"; 4 | import { ExecutionTreeItem } from "./ExecutionTreeItem"; 5 | import { FunctionTreeItem } from "../FunctionTreeItem"; 6 | import { ext } from "../../../extensionVariables"; 7 | import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase"; 8 | 9 | export class ExecutionsTreeItem extends AppwriteTreeItemBase { 10 | constructor(public readonly parent: FunctionTreeItem) { 11 | super(parent, "Executions"); 12 | } 13 | 14 | private executionsToShow = 10; 15 | 16 | public async getChildren(): Promise { 17 | if (!functionsClient) { 18 | return []; 19 | } 20 | const executions: ExecutionList | undefined = await functionsClient.listExecutions( 21 | this.parent.func.$id, 22 | undefined, 23 | this.executionsToShow, 24 | undefined, 25 | "DESC" 26 | ); 27 | const children = executions?.executions.map((execution: Execution) => new ExecutionTreeItem(this, execution)) ?? [ 28 | new TreeItem("No executions."), 29 | ]; 30 | if (children.length === 0) { 31 | children.push(new TreeItem("No executions.")); 32 | } 33 | ext.outputChannel.appendLog(`Found ${executions?.sum} executions`); 34 | if (executions?.sum ?? (0 > this.executionsToShow && this.executionsToShow !== 100)) { 35 | const viewMoreItem: TreeItem = { 36 | command: { 37 | command: "vscode-appwrite.viewMore", 38 | arguments: [this], 39 | title: "View more", 40 | }, 41 | label: "View more...", 42 | }; 43 | children.push(viewMoreItem); 44 | } 45 | return children; 46 | } 47 | 48 | collapsibleState = TreeItemCollapsibleState.Collapsed; 49 | 50 | contextValue = "executions"; 51 | 52 | iconPath = new ThemeIcon("history"); 53 | 54 | async viewMore(): Promise { 55 | this.executionsToShow += 10; 56 | if (this.executionsToShow > 100) { 57 | this.executionsToShow = 100; 58 | } 59 | ext.tree?.functions?.refreshChild(this); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tree/functions/settings/EventsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem, QuickPickOptions } from "vscode"; 2 | import { Function } from "../../../appwrite"; 3 | import { functionsClient } from "../../../client"; 4 | import { appwriteSystemEvents } from "../../../constants"; 5 | import { ext } from "../../../extensionVariables"; 6 | import { EnumEditableTreeItemBase } from "../../common/editable/EnumEditableTreeItem"; 7 | import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; 8 | 9 | export class EventsTreeItem extends EnumEditableTreeItemBase { 10 | public quickPickOptions: QuickPickOptions = { 11 | placeHolder: "Select which system events should trigger this function.", 12 | matchOnDescription: true 13 | }; 14 | public options: string[] | QuickPickItem[] = appwriteSystemEvents.map((event) => ({ 15 | label: event.name, 16 | description: event.description.replace("This event t", "T") 17 | })); 18 | 19 | public readonly func: Function; 20 | 21 | constructor(public readonly parent: FunctionSettingsTreeItem) { 22 | super("System events", parent.func.events); 23 | this.func = parent.func; 24 | this.label = parent.func.events.length === 0 ? 'None' : `${parent.func.events.length} active`; 25 | } 26 | 27 | public async setValue(value: string[]): Promise { 28 | await functionsClient?.update(this.func.$id, this.func.name, [], this.func.vars, value, this.func.schedule, this.func.timeout); 29 | ext.tree?.functions?.refresh(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/tree/functions/settings/FunctionSettingsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { Function } from "../../../appwrite"; 3 | import { functionsClient } from "../../../client"; 4 | import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; 5 | import { ChildTreeItem } from "../../ChildTreeItem"; 6 | import { FunctionTreeItem } from "../FunctionTreeItem"; 7 | import { EventsTreeItem } from "./EventsTreeItem"; 8 | import { NameTreeItem } from "./NameTreeItem"; 9 | import { ScheduleTreeItem } from "./ScheduleTreeItem"; 10 | import { TimeoutTreeItem } from "./TimeoutTreeItem"; 11 | import { VarsTreeItem } from "./VarsTreeItem"; 12 | 13 | export class FunctionSettingsTreeItem extends AppwriteTreeItemBase { 14 | public readonly func: Function; 15 | 16 | constructor(public readonly parent: FunctionTreeItem) { 17 | super(parent, "Settings"); 18 | this.func = parent.func; 19 | } 20 | 21 | public async getChildren(): Promise { 22 | if (!functionsClient) { 23 | return []; 24 | } 25 | 26 | const children = [ 27 | new NameTreeItem(this), 28 | new ScheduleTreeItem(this), 29 | new TimeoutTreeItem(this.func), 30 | new EventsTreeItem(this), 31 | new VarsTreeItem(this), 32 | ]; 33 | return children; 34 | } 35 | 36 | labelItem(label: string, value: string): TreeItem { 37 | return new ChildTreeItem(this, { label: value === "" ? "None" : value, description: label }); 38 | } 39 | 40 | collapsibleState = TreeItemCollapsibleState.Collapsed; 41 | 42 | contextValue = "functionSettings"; 43 | 44 | iconPath = new ThemeIcon("settings"); 45 | } 46 | -------------------------------------------------------------------------------- /src/tree/functions/settings/NameTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { InputBoxOptions, MarkdownString } from "vscode"; 2 | import { Function } from "../../../appwrite"; 3 | import { functionsClient } from "../../../client"; 4 | import { ext } from "../../../extensionVariables"; 5 | import { StringEditableTreeItemBase } from '../../common/editable/StringEditableTreeItem'; 6 | import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; 7 | 8 | const tooltip = "Function name"; 9 | const description = "Function name. Max length: 128 chars."; 10 | const tooLongInvalid = "Value exceeds maximum length of 128 characters."; 11 | 12 | export function validateFunctionName(value: string): string | undefined { 13 | if (value.length > 128) { 14 | return tooLongInvalid; 15 | } 16 | } 17 | 18 | export class NameTreeItem extends StringEditableTreeItemBase { 19 | public readonly func: Function; 20 | 21 | inputBoxOptions: InputBoxOptions = { 22 | validateInput: (value) => { 23 | if (value.length > 128) { 24 | return tooLongInvalid; 25 | } 26 | }, 27 | prompt: description, 28 | }; 29 | 30 | public async setValue(value: string): Promise { 31 | if (value.length === 0) { 32 | return; 33 | } 34 | await functionsClient?.update(this.func.$id, value, [], this.func.vars, this.func.events, this.func.schedule, this.func.timeout); 35 | ext.tree?.functions?.refresh(); 36 | } 37 | 38 | constructor(private readonly parent: FunctionSettingsTreeItem) { 39 | super("Name", parent.func.name); 40 | this.func = parent.func; 41 | this.tooltip = new MarkdownString(tooltip); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/tree/functions/settings/ScheduleTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { InputBoxOptions, MarkdownString } from "vscode"; 2 | import { Function } from "../../../appwrite"; 3 | import { functionsClient } from "../../../client"; 4 | import { ext } from "../../../extensionVariables"; 5 | import cron from "cron-validate"; 6 | import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; 7 | import cronstrue from "cronstrue"; 8 | import { StringEditableTreeItemBase } from '../../common/editable/StringEditableTreeItem'; 9 | 10 | export class ScheduleTreeItem extends StringEditableTreeItemBase { 11 | private readonly func: Function; 12 | 13 | inputBoxOptions: InputBoxOptions = { 14 | validateInput: (value) => { 15 | if (value === "") { 16 | return; 17 | } 18 | const cronResult = cron(value); 19 | if (!cronResult.isValid()) { 20 | return cronResult.getError().join(", "); 21 | } 22 | }, 23 | value: this.value === "" ? "0 0 * * *" : this.value, 24 | prompt: "Function execution schedule in CRON format. Leave blank for no schedule. https://crontab.guru/examples.html", 25 | }; 26 | 27 | public async setValue(value: string): Promise { 28 | await functionsClient?.update(this.func.$id, this.func.name, [], this.func.vars, this.func.events, value === "" ? undefined : value, this.func.timeout); 29 | ext.tree?.functions?.refresh(); 30 | } 31 | 32 | constructor(private readonly parent: FunctionSettingsTreeItem) { 33 | super("Schedule", parent.func.schedule); 34 | this.func = parent.func; 35 | this.tooltip = new MarkdownString(`Function execution schedule in CRON format`); 36 | this.label = `${this.value}`; 37 | const cronResult = cron(parent.func.schedule); 38 | if (cronResult.isValid()) { 39 | this.label = cronstrue.toString(this.value, { verbose: true }); 40 | } else { 41 | this.label = this.value === "" ? "None" : "Invalid CRON"; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/tree/functions/settings/TimeoutTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { InputBoxOptions, MarkdownString } from "vscode"; 2 | import { Function } from "../../../appwrite"; 3 | import { functionsClient } from "../../../client"; 4 | import { ext } from "../../../extensionVariables"; 5 | import { StringEditableTreeItemBase } from "../../common/editable/StringEditableTreeItem"; 6 | 7 | function isNumeric(str: string) { 8 | console.log("here"); 9 | return !isNaN(+str); 10 | } 11 | 12 | export class TimeoutTreeItem extends StringEditableTreeItemBase { 13 | inputBoxOptions: InputBoxOptions = { 14 | validateInput: (value) => { 15 | if (!isNumeric(value)) { 16 | return "Input must be an integer."; 17 | } 18 | 19 | if (+value > 900) { 20 | return "Value exceeds the maximum of 900 seconds (15 minutes)"; 21 | } 22 | 23 | if (+value < 0) { 24 | return "Value cannot be negative"; 25 | } 26 | }, 27 | prompt: "Function maximum execution time in seconds. Maximum of 900 seconds (15 minutes).", 28 | }; 29 | 30 | public async setValue(value: string): Promise { 31 | await functionsClient?.update( 32 | this.func.$id, 33 | this.func.name, 34 | [], 35 | this.func.vars, 36 | this.func.events, 37 | this.func.schedule, 38 | parseInt(value) 39 | ); 40 | ext.tree?.functions?.refresh(); 41 | } 42 | 43 | constructor(private readonly func: Function) { 44 | super("Timeout", func.timeout.toString()); 45 | this.tooltip = new MarkdownString(`Function maximum execution time in seconds.`); 46 | this.label = `${this.value}s`; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/tree/functions/settings/VarTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { InputBoxOptions, MarkdownString, window } from "vscode"; 2 | import { Function } from "../../../appwrite"; 3 | import { functionsClient } from "../../../client"; 4 | import { ext } from "../../../extensionVariables"; 5 | import { StringEditableTreeItemBase } from "../../common/editable/StringEditableTreeItem"; 6 | import { VarsTreeItem } from "./VarsTreeItem"; 7 | 8 | const tooltip = "Environment var"; 9 | const description = "Function name. Max length: 128 chars."; 10 | const tooLongInvalid = "Value exceeds maximum length of 128 characters."; 11 | 12 | export async function keyValuePrompt(keyInit?: string, valueInit?: string): Promise<{ key: string; value: string } | undefined> { 13 | const key = await window.showInputBox({ value: keyInit, prompt: "Environment variable name" }); 14 | if (key === undefined) { 15 | return; 16 | } 17 | const value = await window.showInputBox({ value: valueInit, prompt: "Environment variable value" }); 18 | if (value === undefined) { 19 | return; 20 | } 21 | return { key, value }; 22 | } 23 | 24 | export class VarTreeItem extends StringEditableTreeItemBase { 25 | public readonly func: Function; 26 | 27 | inputBoxOptions: InputBoxOptions = { 28 | validateInput: (value) => { 29 | if (value.length > 128) { 30 | return tooLongInvalid; 31 | } 32 | }, 33 | prompt: description, 34 | }; 35 | 36 | public async setValue(value: string, key?: string): Promise { 37 | if (value.length === 0) { 38 | return; 39 | } 40 | const newVars = { ...this.func.vars }; 41 | newVars[this.key] = value; 42 | if (key) { 43 | delete newVars[this.key]; 44 | newVars[key] = value; 45 | } 46 | await functionsClient?.update(this.func.$id, this.func.name, [], newVars, this.func.events, this.func.schedule, this.func.timeout); 47 | ext.tree?.functions?.refresh(); 48 | } 49 | 50 | constructor(public readonly parent: VarsTreeItem, public readonly key: string, value: string) { 51 | super("var", value); 52 | this.func = parent.parent.func; 53 | this.tooltip = new MarkdownString(tooltip); 54 | this.label = `${key}=${value}`; 55 | this.description = undefined; 56 | } 57 | 58 | public async prompt(): Promise { 59 | const keyval = await keyValuePrompt(this.key, this.value); 60 | if (keyval) { 61 | this.setValue(keyval.value, keyval.key); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/tree/functions/settings/VarsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { Vars } from "../../../appwrite"; 3 | import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; 4 | import { FunctionSettingsTreeItem } from "./FunctionSettingsTreeItem"; 5 | import { VarTreeItem } from "./VarTreeItem"; 6 | 7 | export class VarsTreeItem extends AppwriteTreeItemBase { 8 | public readonly vars: Vars; 9 | 10 | constructor(parent: FunctionSettingsTreeItem) { 11 | super(parent, "Environment variables"); 12 | this.vars = parent.func.vars; 13 | this.description = undefined; 14 | } 15 | 16 | public async getChildren(): Promise { 17 | return Object.keys(this.vars).map((key) => new VarTreeItem(this, key, this.vars[key])); 18 | } 19 | contextValue = "vars"; 20 | collapsibleState = TreeItemCollapsibleState.Collapsed; 21 | } 22 | -------------------------------------------------------------------------------- /src/tree/functions/tags/TagTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownString, ThemeIcon, TreeItem } from "vscode"; 2 | import { Tag } from '../../../appwrite'; 3 | import { msToDate } from '../../../utils/date'; 4 | import { TagsTreeItem } from './TagsTreeItem'; 5 | 6 | export class TagTreeItem extends TreeItem { 7 | constructor(public readonly parent: TagsTreeItem, public readonly tag: Tag) { 8 | super(tag.$id); 9 | const func = parent.parent.func; 10 | const active = func.tag === tag.$id; 11 | this.label = `${msToDate(tag.dateCreated)}${active ? ' (Active)' : ''}`; 12 | this.description = tag.$id; 13 | this.iconPath = new ThemeIcon(active ? 'circle-large-filled' : 'circle-large-outline'); 14 | this.contextValue = `tag${active ? '_active' : ''}`; 15 | this.tooltip = new MarkdownString(`ID: ${tag.$id} \nCreated: ${msToDate(tag.dateCreated)} \nCommand: ${tag.command} \nSize: ${tag.size}B`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/tree/functions/tags/TagsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode"; 2 | import { functionsClient } from "../../../client"; 3 | import { AppwriteTreeItemBase } from '../../../ui/AppwriteTreeItemBase'; 4 | import { FunctionTreeItem } from '../FunctionTreeItem'; 5 | import { TagTreeItem } from './TagTreeItem'; 6 | 7 | export class TagsTreeItem extends AppwriteTreeItemBase { 8 | constructor(public readonly parent: FunctionTreeItem) { 9 | super(parent, "Tags"); 10 | } 11 | 12 | public async getChildren(): Promise { 13 | if (!functionsClient) { 14 | return []; 15 | } 16 | const tags = await functionsClient.listTags(this.parent.func.$id); 17 | const children = tags?.tags.sort((a, b) => b.dateCreated - a.dateCreated).map((tag) => new TagTreeItem(this, tag)) ?? [new TreeItem('No tags.')]; 18 | 19 | if (children.length === 0) { 20 | const noTagsItem: TreeItem = { 21 | command: { 22 | command: "vscode-appwrite.CreateTag", 23 | title: "Create tag", 24 | arguments: [this], 25 | tooltip: "Create a tag" 26 | }, 27 | label: "Create a tag", 28 | iconPath: new ThemeIcon("cloud-upload"), 29 | }; 30 | children.push(noTagsItem); 31 | } 32 | return children; 33 | } 34 | 35 | collapsibleState = TreeItemCollapsibleState.Collapsed; 36 | 37 | contextValue = "tags"; 38 | 39 | iconPath = new ThemeIcon("tag"); 40 | } 41 | -------------------------------------------------------------------------------- /src/tree/health/HealthTreeItem.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { MarkdownString } from 'vscode'; 3 | 4 | export class HealthTreeItem extends vscode.TreeItem { 5 | constructor(public readonly label: string, status: "check" | "error" | any, tooltip?: string | MarkdownString | undefined) { 6 | super(label); 7 | console.log(status); 8 | this.label = label; 9 | this.iconPath = new vscode.ThemeIcon(status ? "check" : "error", new vscode.ThemeColor(status ? "#00ff00" : "list.errorForeground")); 10 | this.contextValue = `health.${label}`; 11 | this.description = "" + Object.values(status)[0]; 12 | this.tooltip = tooltip; 13 | } 14 | 15 | contextValue = "health"; 16 | } 17 | -------------------------------------------------------------------------------- /src/tree/health/HealthTreeItemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { healthClient } from "../../client"; 3 | import { ext } from "../../extensionVariables"; 4 | import { HealthTreeItem } from "./HealthTreeItem"; 5 | import * as dayjs from "dayjs"; 6 | import * as localizedFormat from "dayjs/plugin/localizedFormat"; 7 | import { healthTooltips } from "../../appwrite/Health"; 8 | import { AppwriteHealth } from "../../appwrite"; 9 | import { promiseWithTimeout } from "../../utils/promiseWithTimeout"; 10 | // dayjs.extend(relativeTime); 11 | dayjs.extend(localizedFormat); 12 | 13 | export class HealthTreeItemProvider implements vscode.TreeDataProvider { 14 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 15 | HealthTreeItem | undefined | void 16 | >(); 17 | 18 | private lastChecked: Date = new Date(); 19 | 20 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 21 | 22 | refresh(): void { 23 | this._onDidChangeTreeData.fire(); 24 | } 25 | 26 | getTreeItem(element: HealthTreeItem): vscode.TreeItem { 27 | return element; 28 | } 29 | 30 | async getChildren(element?: HealthTreeItem): Promise { 31 | if (healthClient === undefined) { 32 | return []; 33 | } 34 | 35 | // get children for root 36 | if (element === undefined) { 37 | try { 38 | const health = await promiseWithTimeout | undefined>( 39 | 10000, 40 | async () => { 41 | try { 42 | return await healthClient?.checkup(); 43 | } catch (e) { 44 | ext.outputChannel?.append('Error: ' + e.message); 45 | vscode.window.showErrorMessage('Could not connect to Appwrite project'); 46 | } 47 | }, 48 | "Health request timed out" 49 | ); 50 | if (health === undefined) { 51 | return []; 52 | } 53 | ext.outputChannel?.append(JSON.stringify(health, null, 4)); 54 | const healthItems = Object.entries(health).map(([service, status]) => { 55 | return new HealthTreeItem(service, status, healthTooltips[service as keyof AppwriteHealth]); 56 | }); 57 | this.lastChecked = new Date(); 58 | return [ 59 | { 60 | label: `Updated at ${dayjs(this.lastChecked).format("LTS")}`, 61 | }, 62 | ...healthItems, 63 | ]; 64 | } catch (e) { 65 | // 66 | } 67 | } 68 | return []; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/tree/projects/ProjectTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem } from "vscode"; 2 | import { AppwriteProjectConfiguration } from "../../settings"; 3 | 4 | export class ProjectTreeItem extends TreeItem { 5 | constructor(public readonly project: AppwriteProjectConfiguration, active: boolean) { 6 | super("Project"); 7 | this.iconPath = new ThemeIcon("rocket"); 8 | const name = project.nickname ?? "Project"; 9 | this.label = `${name} ${active ? "(Active)" : ""}`; 10 | this.contextValue = `appwriteProject${active ? "_active" : ""}`; 11 | if (!active) { 12 | this.command = { command: "vscode-appwrite.setActiveProject", title: "Set active", arguments: [this] }; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tree/projects/ProjectsTreeItemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { getActiveProjectId, getAppwriteProjects } from "../../settings"; 3 | import { ProjectTreeItem } from "./ProjectTreeItem"; 4 | 5 | export class ProjectsTreeItemProvider implements vscode.TreeDataProvider { 6 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 7 | vscode.TreeItem | undefined | void 8 | >(); 9 | 10 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 11 | 12 | constructor() { 13 | vscode.workspace.onDidChangeConfiguration((e) => { 14 | if (e.affectsConfiguration("appwrite")) { 15 | this.refresh(); 16 | } 17 | }); 18 | } 19 | 20 | refresh(): void { 21 | this._onDidChangeTreeData.fire(); 22 | } 23 | 24 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem { 25 | return element; 26 | } 27 | 28 | async getChildren(_element?: vscode.TreeItem): Promise { 29 | const configs = await getAppwriteProjects(); 30 | if (configs === undefined || configs.length === 0) { 31 | return []; 32 | } 33 | const activeProjectId = await getActiveProjectId(); 34 | return configs.map((config) => new ProjectTreeItem(config, config.projectId === activeProjectId)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/tree/storage/FileTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon, TreeItem } from "vscode"; 2 | import { File } from "../../appwrite"; 3 | 4 | export class FileTreeItem extends TreeItem { 5 | constructor(public readonly file: File) { 6 | super(file.name); 7 | } 8 | 9 | iconPath = new ThemeIcon("file"); 10 | 11 | contextValue = "file"; 12 | } 13 | -------------------------------------------------------------------------------- /src/tree/storage/StorageTreeItemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { storageClient } from "../../client"; 3 | import { FileTreeItem } from "./FileTreeItem"; 4 | 5 | export class StorageTreeItemProvider implements vscode.TreeDataProvider { 6 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 7 | vscode.TreeItem | undefined | void 8 | >(); 9 | 10 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 11 | 12 | refresh(): void { 13 | this._onDidChangeTreeData.fire(); 14 | } 15 | 16 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem { 17 | return element; 18 | } 19 | 20 | async getChildren(_element?: vscode.TreeItem): Promise { 21 | if (storageClient === undefined) { 22 | return []; 23 | } 24 | 25 | const files = await storageClient.listFiles(); 26 | if (files === undefined || files?.files.length === 0) { 27 | const noStorage = new vscode.TreeItem("No files found"); 28 | return [noStorage]; 29 | } 30 | return files.files.map((file) => new FileTreeItem(file)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/tree/users/UserPropertyTreeItemBase.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem } from 'vscode'; 2 | import { UserTreeItem } from './UserTreeItem'; 3 | 4 | export abstract class UserPropertyTreeItemBase extends TreeItem { 5 | constructor(public readonly parent: UserTreeItem, label: string) { 6 | super(label); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/tree/users/UserTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { User } from "../../appwrite"; 2 | import * as vscode from "vscode"; 3 | 4 | export class UserTreeItem extends vscode.TreeItem { 5 | constructor(public readonly user: User) { 6 | super(user.email); 7 | console.log(user); 8 | this.label = `${user.email}`; 9 | this.tooltip = user.emailVerification ? "Verified" : "Unverified"; 10 | this.iconPath = new vscode.ThemeIcon(user.emailVerification ? "verified" : "unverified"); 11 | } 12 | 13 | collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; 14 | contextValue = "user"; 15 | } 16 | -------------------------------------------------------------------------------- /src/tree/users/UserTreeItemProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import { client } from "../../client"; 3 | import AppwriteCall from "../../utils/AppwriteCall"; 4 | import { User, UsersList } from "../../appwrite"; 5 | import { ThemeIcon } from "vscode"; 6 | import { UserPrefsTreeItem } from "./properties/UserPrefsTreeItem"; 7 | import { ChildTreeItem } from "../ChildTreeItem"; 8 | import { UserTreeItem } from "./UserTreeItem"; 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const sdk = require("node-appwrite"); 11 | 12 | export class UserTreeItemProvider implements vscode.TreeDataProvider { 13 | private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter< 14 | UserTreeItem | undefined | void 15 | >(); 16 | 17 | readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; 18 | 19 | refresh(): void { 20 | this._onDidChangeTreeData.fire(); 21 | } 22 | 23 | getTreeItem(element: UserTreeItem): vscode.TreeItem { 24 | return element; 25 | } 26 | 27 | async getChildren(element?: UserTreeItem): Promise { 28 | if (client === undefined) { 29 | return Promise.resolve([]); 30 | } 31 | 32 | if (element instanceof UserTreeItem) { 33 | const regDate = new Date(); 34 | regDate.setMilliseconds(element.user.registration); 35 | const items: vscode.TreeItem[] = [ 36 | new ChildTreeItem(element, { 37 | contextValue: "user.name", 38 | label: element.user.name || "Unfilled", 39 | iconPath: new ThemeIcon("person"), 40 | description: "Name", 41 | }), 42 | new ChildTreeItem(element, { 43 | contextValue: "user.email", 44 | label: element.user.email, 45 | iconPath: new ThemeIcon("mail"), 46 | description: "Email", 47 | }), 48 | new ChildTreeItem(element, { 49 | contextValue: "user.registration", 50 | label: regDate.toDateString(), 51 | iconPath: new vscode.ThemeIcon("calendar"), 52 | description: "Joined", 53 | }), 54 | new ChildTreeItem(element, { 55 | label: element.user.$id, 56 | description: "User ID", 57 | iconPath: new vscode.ThemeIcon("key"), 58 | contextValue: "user.id", 59 | }), 60 | new UserPrefsTreeItem(element), 61 | ]; 62 | return Promise.resolve(items); 63 | } 64 | 65 | const usersSdk = new sdk.Users(client); 66 | const usersList = await AppwriteCall(usersSdk.list()); 67 | if (usersList) { 68 | const userTreeItems = usersList.users.map((user: User) => new UserTreeItem(user)) ?? []; 69 | const headerItem: vscode.TreeItem = { 70 | label: `Total users: ${usersList.sum}`, 71 | }; 72 | return [headerItem, ...userTreeItems]; 73 | } 74 | 75 | return [{ label: "No users found" }]; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/tree/users/properties/UserPrefsTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { ThemeIcon } from "vscode"; 2 | import { UserPropertyTreeItemBase } from "../UserPropertyTreeItemBase"; 3 | import { UserTreeItem } from '../UserTreeItem'; 4 | 5 | export class UserPrefsTreeItem extends UserPropertyTreeItemBase { 6 | 7 | constructor(parent: UserTreeItem) { 8 | super(parent, 'View preferences'); 9 | this.command = { 10 | command: 'vscode-appwrite.viewUserPrefs', 11 | title: 'View user preferences', 12 | arguments: [this] 13 | }; 14 | } 15 | 16 | iconPath = new ThemeIcon("json"); 17 | contextValue = 'users.prefs'; 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/AddProjectWizard.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { AppwriteProjectConfiguration, getActiveProjectConfiguration } from "../settings"; 3 | 4 | export async function addProjectWizard(): Promise { 5 | const config = await getActiveProjectConfiguration(); 6 | const endpoint = await window.showInputBox({ 7 | placeHolder: "Endpoint", 8 | value: config?.endpoint ?? "https://localhost/v1", 9 | valueSelection: undefined, 10 | prompt: "Enter your Appwrite API endpoint (ex: https://localhost/v1)", 11 | ignoreFocusOut: true, 12 | }); 13 | if (endpoint === undefined) { 14 | return; 15 | } 16 | const projectId = await window.showInputBox({ 17 | placeHolder: "Project Id", 18 | prompt: "Enter your Appwrite project id (ex: 5df5acd0d48c2)", 19 | ignoreFocusOut: true, 20 | }); 21 | if (projectId === undefined) { 22 | return; 23 | } 24 | const secret = await window.showInputBox({ 25 | placeHolder: "API key secret", 26 | prompt: "Enter your Appwrite API key secret (with all scopes)", 27 | ignoreFocusOut: true, 28 | }); 29 | if (secret === undefined) { 30 | return; 31 | } 32 | const selfSigned = await window.showQuickPick( 33 | [ 34 | { label: "Yes", description: "If running Appwrite on localhost, or local IP" }, 35 | { label: "No", description: "If connecting to a remote Appwrite instance" }, 36 | ], 37 | { 38 | placeHolder: "Allow communication with self-signed SSL certificates? (Select 'Yes' for connecting to Appwrite on localhost)", 39 | ignoreFocusOut: true, 40 | } 41 | ); 42 | if (selfSigned === undefined) { 43 | return; 44 | } 45 | const nickname = await window.showInputBox({ 46 | prompt: "(Optional) Project name", 47 | ignoreFocusOut: true, 48 | }); 49 | 50 | if (endpoint && projectId && secret) { 51 | return { endpoint, projectId, secret, nickname, selfSigned: selfSigned.label === "Yes" }; 52 | } 53 | return undefined; 54 | } 55 | -------------------------------------------------------------------------------- /src/ui/AppwriteOutputChannel.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { OutputChannel, ViewColumn, window, workspace, WorkspaceConfiguration } from "vscode"; 7 | 8 | // tslint:disable-next-line: export-name 9 | export function createAppwriteOutputChannel(name: string, extensionPrefix: string): AppwriteOutputChannel { 10 | return new AppwriteOutputChannel(name, extensionPrefix); 11 | } 12 | 13 | export class AppwriteOutputChannel { 14 | public readonly name: string; 15 | public readonly extensionPrefix: string; 16 | private _outputChannel: OutputChannel; 17 | 18 | constructor(name: string, extensionPrefix: string) { 19 | this.name = name; 20 | this.extensionPrefix = extensionPrefix; 21 | this._outputChannel = window.createOutputChannel(this.name); 22 | } 23 | 24 | public append(value: string): void { 25 | this._outputChannel.append(value); 26 | } 27 | 28 | public appendLine(value: string): void { 29 | this._outputChannel.appendLine(value); 30 | } 31 | 32 | public appendLog(value: string, options?: { resourceName?: string, date?: Date }): void { 33 | const enableOutputTimestampsSetting: string = 'enableOutputTimestamps'; 34 | const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(this.extensionPrefix); 35 | const result: boolean | undefined = projectConfiguration.get(enableOutputTimestampsSetting); 36 | 37 | if (!result) { 38 | this.appendLine(value); 39 | } else { 40 | // tslint:disable: strict-boolean-expressions 41 | options = options || {}; 42 | const date: Date = options.date || new Date(); 43 | this.appendLine(`${date.toLocaleTimeString()}${options.resourceName ? ' '.concat(options.resourceName) : ''}: ${value}`); 44 | } 45 | } 46 | 47 | public clear(): void { 48 | this._outputChannel.clear(); 49 | } 50 | 51 | public show(preserveFocus?: boolean | undefined): void; 52 | public show(column?: ViewColumn | undefined, preserveFocus?: boolean | undefined): void; 53 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 54 | public show(_column?: unknown, preserveFocus?: boolean | undefined): void { 55 | this._outputChannel.show(preserveFocus); 56 | } 57 | 58 | public hide(): void { 59 | this._outputChannel.hide(); 60 | } 61 | 62 | public dispose(): void { 63 | this._outputChannel.dispose(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/ui/AppwriteTreeItemBase.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem } from "vscode"; 2 | 3 | export abstract class AppwriteTreeItemBase extends TreeItem { 4 | constructor(public readonly parent: Parent, label: string) { 5 | super(label); 6 | } 7 | 8 | abstract getChildren?(): Promise; 9 | 10 | viewMore(): Promise { 11 | return Promise.resolve(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ui/BaseEditor.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as fse from "fs-extra"; 7 | import * as path from "path"; 8 | import * as vscode from "vscode"; 9 | import { ext } from '../extensionVariables'; 10 | import { createTemporaryFile } from "./createTemporaryFile"; 11 | import { DialogResponses } from "./DialogResponses"; 12 | 13 | // tslint:disable-next-line:no-unsafe-any 14 | export abstract class BaseEditor implements vscode.Disposable { 15 | private fileMap: { [key: string]: [vscode.TextDocument, ContextT] } = {}; 16 | private ignoreSave: boolean = false; 17 | 18 | constructor(private readonly showSavePromptKey: string) {} 19 | 20 | public abstract getData(context: ContextT): Promise; 21 | public abstract updateData(context: ContextT, data: string): Promise; 22 | public abstract getFilename(context: ContextT): Promise; 23 | public abstract getResourceName(context: ContextT): Promise; 24 | public abstract getSaveConfirmationText(context: ContextT): Promise; 25 | public abstract getSize(context: ContextT): Promise; 26 | 27 | public async showEditor(context: ContextT): Promise { 28 | const fileName: string = await this.getFilename(context); 29 | const resourceName: string = await this.getResourceName(context); 30 | this.appendLineToOutput(`Opening ${resourceName}...`, { resourceName }); 31 | const localFilePath: string = await createTemporaryFile(fileName); 32 | const document: vscode.TextDocument = await vscode.workspace.openTextDocument(localFilePath); 33 | this.fileMap[localFilePath] = [document, context]; 34 | const data: string = await this.getData(context); 35 | const textEditor: vscode.TextEditor = await vscode.window.showTextDocument(document); 36 | await this.updateEditor(data, textEditor); 37 | } 38 | 39 | public async updateMatchingContext(doc: vscode.Uri): Promise { 40 | const filePath: string | undefined = Object.keys(this.fileMap).find((fsPath: string) => path.relative(doc.fsPath, fsPath) === ""); 41 | if (filePath) { 42 | const [textDocument, context]: [vscode.TextDocument, ContextT] = this.fileMap[filePath]; 43 | await this.updateRemote(context, textDocument); 44 | } 45 | } 46 | 47 | public async dispose(): Promise { 48 | Object.keys(this.fileMap).forEach(async (key: string) => await fse.remove(path.dirname(key))); 49 | } 50 | 51 | public async onDidSaveTextDocument(globalState: vscode.Memento, doc: vscode.TextDocument): Promise { 52 | const filePath: string | undefined = Object.keys(this.fileMap).find( 53 | (fsPath: string) => path.relative(doc.uri.fsPath, fsPath) === "" 54 | ); 55 | if (!this.ignoreSave && filePath) { 56 | const context: ContextT = this.fileMap[filePath][1]; 57 | const showSaveWarning: boolean | undefined = vscode.workspace.getConfiguration().get(this.showSavePromptKey); 58 | 59 | if (showSaveWarning) { 60 | const message: string = await this.getSaveConfirmationText(context); 61 | const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage( 62 | message, 63 | DialogResponses.upload, 64 | DialogResponses.alwaysUpload, 65 | DialogResponses.dontUpload 66 | ); 67 | if (result === DialogResponses.alwaysUpload) { 68 | await vscode.workspace.getConfiguration().update(this.showSavePromptKey, false, vscode.ConfigurationTarget.Global); 69 | await globalState.update(this.showSavePromptKey, true); 70 | } 71 | } 72 | await this.updateRemote(context, doc); 73 | } 74 | } 75 | 76 | protected appendLineToOutput(value: string, options?: { resourceName?: string; date?: Date }): void { 77 | ext.outputChannel?.appendLog(value, options); 78 | ext.outputChannel?.show(true); 79 | } 80 | 81 | private async updateRemote(context: ContextT, doc: vscode.TextDocument): Promise { 82 | const filename: string = await this.getFilename(context); 83 | const resourceName: string = await this.getResourceName(context); 84 | this.appendLineToOutput(`Updating "${filename}" ...', filename)`, { resourceName }); 85 | const updatedData: string = await this.updateData(context, doc.getText()); 86 | this.appendLineToOutput(`Updated "${filename}".`, { resourceName }); 87 | if (doc.isClosed !== true) { 88 | const visibleDocument: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find((ed) => ed.document === doc); 89 | if (visibleDocument) { 90 | await this.updateEditor(updatedData, visibleDocument); 91 | } 92 | } 93 | } 94 | 95 | private async updateEditor(data: string, textEditor?: vscode.TextEditor): Promise { 96 | if (textEditor) { 97 | await BaseEditor.writeToEditor(textEditor, data); 98 | this.ignoreSave = true; 99 | try { 100 | await textEditor.document.save(); 101 | } finally { 102 | this.ignoreSave = false; 103 | } 104 | } 105 | } 106 | // tslint:disable-next-line:member-ordering 107 | private static async writeToEditor(editor: vscode.TextEditor, data: string): Promise { 108 | await editor.edit((editBuilder: vscode.TextEditorEdit) => { 109 | if (editor.document.lineCount > 0) { 110 | const lastLine: vscode.TextLine = editor.document.lineAt(editor.document.lineCount - 1); 111 | editBuilder.delete( 112 | new vscode.Range( 113 | new vscode.Position(0, 0), 114 | new vscode.Position(lastLine.range.start.line, lastLine.range.end.character) 115 | ) 116 | ); 117 | } 118 | editBuilder.insert(new vscode.Position(0, 0), data); 119 | }); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/ui/DialogResponses.ts: -------------------------------------------------------------------------------- 1 | import { MessageItem } from "vscode"; 2 | 3 | export namespace DialogResponses { 4 | export const yes: MessageItem = { title: "Yes" }; 5 | export const no: MessageItem = { title: "No" }; 6 | export const cancel: MessageItem = { title: "Cancel", isCloseAffordance: true }; 7 | export const deleteResponse: MessageItem = { title: "Delete" }; 8 | export const learnMore: MessageItem = { title: "Learn more" }; 9 | export const dontWarnAgain: MessageItem = { title: "Don't warn again" }; 10 | export const skipForNow: MessageItem = { title: "Skip fo now" }; 11 | export const upload: MessageItem = { title: "Upload" }; 12 | export const alwaysUpload: MessageItem = { title: "Always uploa" }; 13 | export const dontUpload: MessageItem = { title: "Don' upload", isCloseAffordance: true }; 14 | export const reportAnIssue: MessageItem = { title: "Report a issue" }; 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/confirmDialog.ts: -------------------------------------------------------------------------------- 1 | import { window } from "vscode"; 2 | import { DialogResponses } from "./DialogResponses"; 3 | 4 | export async function confirmDialog(text: string): Promise { 5 | const response = await window.showWarningMessage(text, { modal: true }, DialogResponses.yes, DialogResponses.cancel); 6 | return response === DialogResponses.yes; 7 | } 8 | -------------------------------------------------------------------------------- /src/ui/createRuleWizard.ts: -------------------------------------------------------------------------------- 1 | import { QuickPickItem, window } from "vscode"; 2 | import { Collection, CollectionsList } from "../appwrite"; 3 | import { client } from "../client"; 4 | import { AppwriteSDK } from "../constants"; 5 | import AppwriteCall from "../utils/AppwriteCall"; 6 | 7 | export type CreateRuleWizardContext = { 8 | label: string; 9 | key: string; 10 | type: keyof typeof ruleTypes; 11 | default: any; 12 | required: boolean; 13 | array: boolean; 14 | list?: string[]; 15 | }; 16 | 17 | const ruleTypes = { 18 | text: "Any string value.", 19 | numeric: "Any integer or float value.", 20 | boolean: "Any boolean value.", 21 | wildcard: "Accept any value.", 22 | url: "Any valid URL.", 23 | email: "Any valid email address.", 24 | ip: "Any valid IP v4 or v6 address.", 25 | document: "Accept a valid child document from specified collection(s).", 26 | }; 27 | 28 | type RuleType = keyof typeof ruleTypes; 29 | 30 | export async function createRuleWizard(collection: Collection): Promise { 31 | const label = await window.showInputBox({ 32 | placeHolder: "Attribute label", 33 | prompt: "Attribute internal display name", 34 | validateInput: (value) => { 35 | if (value === "") { 36 | return "Label cannot be empty."; 37 | } 38 | }, 39 | }); 40 | if (label === undefined) { 41 | return; 42 | } 43 | const key = await window.showInputBox({ 44 | placeHolder: "Attribute key name", 45 | prompt: "Attribute key name. Used as the document JSON key in the Database API.", 46 | ignoreFocusOut: true, 47 | validateInput: (value) => { 48 | if (value === "") { 49 | return "Key name cannot be empty."; 50 | } 51 | }, 52 | }); 53 | if (key === undefined) { 54 | return; 55 | } 56 | const ruleTypeItems: QuickPickItem[] = Object.entries(ruleTypes).map(([label, description]) => ({ 57 | label, 58 | detail: description, 59 | })); 60 | 61 | const typeItem = await window.showQuickPick(ruleTypeItems, { placeHolder: "Rule value type." }); 62 | const type: RuleType | undefined = (typeItem?.label as RuleType) ?? undefined; 63 | 64 | if (typeItem === undefined || type === undefined) { 65 | return; 66 | } 67 | 68 | let list: string[] | undefined = undefined; 69 | 70 | if (type === "document") { 71 | const databaseSdk = new AppwriteSDK.Database(client); 72 | const collectionsList = await AppwriteCall(databaseSdk.listCollections()); 73 | 74 | if (collectionsList === undefined) { 75 | window.showErrorMessage("Could not get collections list."); 76 | return; 77 | } 78 | 79 | if (collectionsList) { 80 | const collections = collectionsList.collections.filter((c) => c.$id !== collection.$id); 81 | const qpItems: QuickPickItem[] = collections.map((collection) => ({ 82 | label: collection.name, 83 | description: collection.$id, 84 | })); 85 | 86 | const listInput = await window.showQuickPick(qpItems, { 87 | canPickMany: true, 88 | placeHolder: "Collections which contain valid child documents for this document attribute.", 89 | ignoreFocusOut: true, 90 | }); 91 | list = listInput?.map((item) => item.description as string) ?? []; 92 | } 93 | } 94 | 95 | if (label === "document" && list === undefined) { 96 | return; 97 | } 98 | 99 | const array = await window.showQuickPick(["Primitive", "Array"], { 100 | placeHolder: "Decide if this rule is a primitive or an array of values.", 101 | ignoreFocusOut: true, 102 | }); 103 | 104 | if (array === undefined) { 105 | return; 106 | } 107 | 108 | const required = await window.showQuickPick(["Required", "Optional"], { 109 | placeHolder: "Decide if this rule value is required in order to pass document validation.", 110 | ignoreFocusOut: true, 111 | }); 112 | 113 | if (required === undefined) { 114 | return; 115 | } 116 | 117 | const defaultValue = await window.showInputBox({ 118 | placeHolder: "Default value (press Enter to skip)", 119 | prompt: "Default value for this rule type. Make sure that the default value is able to pass validation in order to avoid errors when skipping optional values.", 120 | ignoreFocusOut: true, 121 | }); 122 | 123 | if (defaultValue === undefined) { 124 | return; 125 | } 126 | 127 | if (label && key && type) { 128 | return { 129 | label, 130 | key, 131 | type, 132 | default: defaultValue, 133 | array: array === "Array", 134 | required: required === "Required", 135 | list, 136 | }; 137 | } 138 | 139 | return undefined; 140 | } 141 | -------------------------------------------------------------------------------- /src/ui/createTemporaryFile.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as crypto from "crypto"; 7 | import * as fse from 'fs-extra'; 8 | import * as os from 'os'; 9 | import * as path from 'path'; 10 | 11 | export async function createTemporaryFile(fileName: string): Promise { 12 | const randomFolderNameLength: number = 12; 13 | const buffer: Buffer = crypto.randomBytes(Math.ceil(randomFolderNameLength / 2)); 14 | const folderName: string = buffer.toString('hex').slice(0, randomFolderNameLength); 15 | const filePath: string = path.join(os.tmpdir(), folderName, fileName); 16 | await fse.ensureFile(filePath); 17 | return filePath; 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/openReadonlyContent.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See LICENSE.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { isNumber } from "util"; 7 | import { 8 | CancellationToken, 9 | Event, 10 | EventEmitter, 11 | TextDocumentContentProvider, 12 | TextDocumentShowOptions, 13 | Uri, 14 | window, 15 | workspace, 16 | WorkspaceConfiguration, 17 | } from "vscode"; 18 | import { ext } from "../extensionVariables"; 19 | import { nonNullValue } from '../utils/nonNullUtils'; 20 | 21 | let contentProvider: ReadOnlyContentProvider | undefined; 22 | const scheme: string = "azureextensionuiReadonly"; 23 | 24 | export async function openReadOnlyJson( 25 | node: { label: string; fullId: string }, 26 | data: {} 27 | ): Promise { 28 | let tab: string = " "; 29 | const config: WorkspaceConfiguration = workspace.getConfiguration("editor"); 30 | const insertSpaces: boolean = !!config.get("insertSpaces"); 31 | if (insertSpaces) { 32 | let tabSize: number | undefined = config.get("tabSize"); 33 | if (!isNumber(tabSize) || tabSize < 0) { 34 | tabSize = 4; 35 | } 36 | 37 | tab = " ".repeat(tabSize); 38 | } 39 | 40 | const content: string = JSON.stringify(data, undefined, tab); 41 | await openReadOnlyContent(node, content, ".json"); 42 | } 43 | 44 | export async function openReadOnlyContent( 45 | node: { label: string; fullId: string }, 46 | content: string, 47 | fileExtension: string, 48 | options?: TextDocumentShowOptions 49 | ): Promise { 50 | if (!contentProvider) { 51 | contentProvider = new ReadOnlyContentProvider(); 52 | ext.context?.subscriptions.push( 53 | workspace.registerTextDocumentContentProvider( 54 | scheme, 55 | contentProvider 56 | ) 57 | ); 58 | } 59 | 60 | return await contentProvider.openReadOnlyContent( 61 | node, 62 | content, 63 | fileExtension, 64 | options 65 | ); 66 | } 67 | 68 | export class ReadOnlyContent { 69 | private _uri: Uri; 70 | private _emitter: EventEmitter; 71 | private _content: string; 72 | 73 | constructor(uri: Uri, emitter: EventEmitter, content: string) { 74 | this._uri = uri; 75 | this._emitter = emitter; 76 | this._content = content; 77 | } 78 | 79 | public get content(): string { 80 | return this._content; 81 | } 82 | 83 | public async append(content: string): Promise { 84 | this._content += content; 85 | this._emitter.fire(this._uri); 86 | } 87 | 88 | public clear(): void { 89 | this._content = ""; 90 | this._emitter.fire(this._uri); 91 | } 92 | } 93 | 94 | class ReadOnlyContentProvider implements TextDocumentContentProvider { 95 | private _onDidChangeEmitter: EventEmitter = new EventEmitter(); 96 | private _contentMap: Map = new Map< 97 | string, 98 | ReadOnlyContent 99 | >(); 100 | 101 | public get onDidChange(): Event { 102 | return this._onDidChangeEmitter.event; 103 | } 104 | 105 | public async openReadOnlyContent( 106 | node: { label: string; fullId: string }, 107 | content: string, 108 | fileExtension: string, 109 | options?: TextDocumentShowOptions 110 | ): Promise { 111 | const idHash: string = Math.random().toString(); 112 | // in a URI, # means fragment and ? means query and is parsed in that way, so they should be removed to not break the path 113 | const uri: Uri = Uri.parse( 114 | `${scheme}:///${idHash}/${node.label.replace( 115 | /\#|\?/g, 116 | "_" 117 | )}${fileExtension}` 118 | ); 119 | const readOnlyContent: ReadOnlyContent = new ReadOnlyContent( 120 | uri, 121 | this._onDidChangeEmitter, 122 | content 123 | ); 124 | this._contentMap.set(uri.toString(), readOnlyContent); 125 | await window.showTextDocument(uri, options); 126 | this._onDidChangeEmitter.fire(uri); 127 | return readOnlyContent; 128 | } 129 | 130 | public async provideTextDocumentContent( 131 | uri: Uri, 132 | _token: CancellationToken 133 | ): Promise { 134 | const readOnlyContent: ReadOnlyContent = nonNullValue( 135 | this._contentMap.get(uri.toString()), 136 | "ReadOnlyContentProvider._contentMap.get" 137 | ); 138 | return readOnlyContent.content; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/utils/AppwriteCall.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-types */ 2 | import { window } from "vscode"; 3 | import { Error } from "../appwrite"; 4 | import { ext } from "../extensionVariables"; 5 | 6 | export default function AppwriteCall( 7 | promise: Promise, 8 | onSuccess?: (success: T) => R, 9 | onError?: (error: Error) => R 10 | ): Promise { 11 | return promise.then( 12 | (successResp) => { 13 | ext.outputChannel?.appendLog(`Appwrite call success:`); 14 | if (onSuccess) { 15 | return onSuccess((successResp as unknown) as T); 16 | } 17 | return successResp as unknown as R; 18 | }, 19 | (errResp: Error) => { 20 | if (onError) { 21 | onError(errResp as Error); 22 | return undefined; 23 | } 24 | 25 | window.showErrorMessage(errResp.message); 26 | ext.outputChannel?.appendLog(errResp.message); 27 | return undefined; 28 | } 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/date.ts: -------------------------------------------------------------------------------- 1 | import dayjs = require("dayjs"); 2 | import utc = require("dayjs/plugin/utc"); 3 | import timezone = require("dayjs/plugin/timezone"); // dependent on utc plugin 4 | dayjs.extend(utc); 5 | dayjs.extend(timezone); 6 | 7 | export function msToDate(ms: number): string { 8 | return dayjs(ms * 1000).tz(dayjs.tz.guess()).format("LTS"); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/nonNullUtils.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.md in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { isNullOrUndefined } from "util"; 7 | 8 | /** 9 | * Retrieves a property by name from an object and checks that it's not null and not undefined. It is strongly typed 10 | * for the property and will give a compile error if the given name is not a property of the source. 11 | */ 12 | export function nonNullProp( 13 | source: TSource, 14 | name: TKey 15 | ): NonNullable { 16 | const value: NonNullable = >( 17 | source[name] 18 | ); 19 | return nonNullValue(value, name); 20 | } 21 | 22 | /** 23 | * Validates that a given value is not null and not undefined. 24 | */ 25 | export function nonNullValue( 26 | value: T | undefined, 27 | propertyNameOrMessage?: string 28 | ): T { 29 | if (isNullOrUndefined(value)) { 30 | throw new Error( 31 | // tslint:disable-next-line:prefer-template 32 | "Internal error: Expected value to be neither null nor undefined" + 33 | (propertyNameOrMessage ? `: ${propertyNameOrMessage}` : "") 34 | ); 35 | } 36 | 37 | return value; 38 | } 39 | 40 | /** 41 | * Validates that a given string is not null, undefined, nor empty 42 | */ 43 | export function nonNullOrEmptyValue( 44 | value: string | undefined, 45 | propertyNameOrMessage?: string 46 | ): string { 47 | if (!value) { 48 | throw new Error( 49 | // tslint:disable-next-line:prefer-template 50 | "Internal error: Expected value to be neither null, undefined, nor empty" + 51 | (propertyNameOrMessage ? `: ${propertyNameOrMessage}` : "") 52 | ); 53 | } 54 | 55 | return value; 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/openUrl.ts: -------------------------------------------------------------------------------- 1 | import { commands, Uri } from 'vscode'; 2 | 3 | export async function openUrl(url: string): Promise { 4 | await commands.executeCommand('vscode.open', Uri.parse(url)); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/promiseWithTimeout.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | 3 | export const promiseWithTimeout = (timeoutMs: number, promise: () => Promise, failureMessage?: string): Promise => { 4 | let timeoutHandle: NodeJS.Timeout; 5 | const timeoutPromise = new Promise(() => { 6 | timeoutHandle = setTimeout(() => window.showErrorMessage(failureMessage ?? 'Request timed out'), timeoutMs); 7 | }); 8 | 9 | return Promise.race([promise(), timeoutPromise]).then((result) => { 10 | clearTimeout(timeoutHandle); 11 | return result; 12 | }); 13 | }; 14 | -------------------------------------------------------------------------------- /src/utils/refreshTree.ts: -------------------------------------------------------------------------------- 1 | import { AppwriteTree, ext } from "../extensionVariables"; 2 | 3 | export function refreshTree(...trees: (keyof AppwriteTree)[]): void { 4 | trees.forEach((tree) => { 5 | ext.tree?.[tree]?.refresh(); 6 | }); 7 | } 8 | 9 | export function refreshAllViews(): void { 10 | if (ext.tree) { 11 | for (const tree in ext.tree) { 12 | refreshTree(tree as keyof AppwriteTree); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number): Promise { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, ms); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/tar.ts: -------------------------------------------------------------------------------- 1 | import tar = require("tar"); 2 | import { Uri, window, workspace } from "vscode"; 3 | import { ext } from "../extensionVariables"; 4 | import * as path from "path"; 5 | import * as fs from "fs"; 6 | import { sleep } from './sleep'; 7 | 8 | export async function getTarReadStream(folder: Uri): Promise { 9 | try { 10 | const folderName = path.basename(folder.path); 11 | 12 | const tarName = `${folderName}.tar.gz`; 13 | const cwd = path.resolve(folder.fsPath, '..'); 14 | if (cwd === undefined) { 15 | window.showErrorMessage("No workspace open."); 16 | return; 17 | } 18 | 19 | ext.outputChannel.appendLog(`Creating '${tarName}' in '${workspace.workspaceFolders?.[0].uri.fsPath}'...`); 20 | const tarFilePath = path.join(ext.context?.globalStorageUri.fsPath ?? '', tarName); 21 | await workspace.fs.createDirectory(ext.context.globalStorageUri); 22 | 23 | tar.create({ gzip: true, cwd: cwd, mode: 1777 }, [path.relative(cwd, folder.fsPath)]).pipe(fs.createWriteStream(tarFilePath)); 24 | 25 | await sleep(500); 26 | 27 | ext.outputChannel.appendLog(`Created ${tarFilePath}`); 28 | 29 | return tarFilePath; 30 | 31 | } catch (e) { 32 | ext.outputChannel?.appendLog("Error creating tar.gz: " + e); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Progress } from 'vscode'; 2 | 3 | export type ProgressMessage = Progress<{ 4 | message?: string | undefined; 5 | increment?: number | undefined; 6 | }>; 7 | -------------------------------------------------------------------------------- /src/utils/validation.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/streamlux/vscode-appwrite/8b286657b0fc71469c0d0224a691c7694895e785/src/utils/validation.ts -------------------------------------------------------------------------------- /src/utils/workspace.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from "path"; 7 | import * as vscode from "vscode"; 8 | import { QuickPickItem } from 'vscode'; 9 | 10 | export interface IAzureQuickPickItem extends QuickPickItem { 11 | /** 12 | * An optional id to uniquely identify this item across sessions, used in persisting previous selections 13 | * If not specified, a hash of the label will be used 14 | */ 15 | id?: string; 16 | 17 | data: T; 18 | 19 | /** 20 | * Callback to use when this item is picked, instead of returning the pick 21 | * Only applies when used as part of an `AzureWizard` 22 | * This is not compatible with `canPickMany` 23 | */ 24 | onPicked?: () => void | Promise; 25 | 26 | /** 27 | * The group that this pick belongs to. Set `IAzureQuickPickOptions.enableGrouping` for this property to take effect 28 | * Only applies when used as part of an `AzureWizard` 29 | */ 30 | group?: string; 31 | 32 | /** 33 | * Optionally used to suppress persistence for this item, defaults to `false` 34 | */ 35 | suppressPersistence?: boolean; 36 | } 37 | 38 | 39 | export async function selectWorkspaceFolder(placeHolder: string): Promise { 40 | return await selectWorkspaceItem(placeHolder, { 41 | canSelectFiles: false, 42 | canSelectFolders: true, 43 | canSelectMany: false, 44 | defaultUri: 45 | vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.length > 0 46 | ? vscode.workspace.workspaceFolders[0].uri 47 | : undefined, 48 | openLabel: "Select", 49 | }); 50 | } 51 | 52 | export async function selectWorkspaceFile(placeHolder: string, fileExtensions?: string[]): Promise { 53 | const filters: { [name: string]: string[] } = {}; 54 | if (fileExtensions) { 55 | filters.Artifacts = fileExtensions; 56 | } 57 | return await selectWorkspaceItem(placeHolder, { 58 | canSelectFiles: true, 59 | canSelectFolders: false, 60 | canSelectMany: false, 61 | openLabel: "Select", 62 | filters: filters, 63 | }); 64 | } 65 | 66 | export async function selectWorkspaceItem(placeHolder: string, options: vscode.OpenDialogOptions): Promise { 67 | let folder: IAzureQuickPickItem | undefined; 68 | if (vscode.workspace.workspaceFolders) { 69 | const folderPicks: IAzureQuickPickItem[] = await Promise.all( 70 | vscode.workspace.workspaceFolders.map((f: vscode.WorkspaceFolder) => { 71 | return { label: path.basename(f.uri.fsPath), description: f.uri.fsPath, data: f.uri.fsPath }; 72 | }) 73 | ); 74 | 75 | folderPicks.push({ label: "$(file-directory) Browse...", description: "", data: undefined }); 76 | folder = await vscode.window.showQuickPick(folderPicks, { placeHolder }); 77 | } 78 | 79 | if (folder?.data) { 80 | return folder.data; 81 | } else { 82 | return (await vscode.window.showOpenDialog(options))?.[0].fsPath ?? ''; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES5", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true, /* enable all strict type-checking options */ 12 | "strictNullChecks": true, 13 | /* Additional Checks */ 14 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 15 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 16 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 17 | "experimentalDecorators": true 18 | }, 19 | "exclude": [ 20 | "node_modules", 21 | ".vscode-test" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | 25 | ## Explore the API 26 | 27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 28 | 29 | ## Run tests 30 | 31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 32 | * Press `F5` to run the tests in a new window with your extension loaded. 33 | * See the output of the test result in the debug console. 34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 11 | 12 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 13 | output: { 14 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'extension.js', 17 | libraryTarget: 'commonjs2' 18 | }, 19 | devtool: 'nosources-source-map', 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader' 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | module.exports = config; --------------------------------------------------------------------------------