initial commit

This commit is contained in:
alexweininger 2021-04-19 13:11:56 -07:00
commit 8ac13c28fd
85 changed files with 9665 additions and 0 deletions

22
.eslintrc.json Normal file
View file

@ -0,0 +1,22 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/semi": "warn",
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"semi": "off"
},
"ignorePatterns": [
"**/*.d.ts"
]
}

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
out
dist
node_modules
.vscode-test/
*.vsix

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"tabWidth": 4,
"useTabs": false,
"printWidth": 140,
"proseWrap": "never"
}

8
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"eamodio.tsl-problem-matcher"
]
}

34
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,34 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles": [
"${workspaceFolder}/out/test/**/*.js"
],
"preLaunchTask": "npm: test-watch"
}
]
}

11
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,11 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"out": false // set this to true to hide the "out" folder with the compiled JS files
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
}

33
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,33 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": [
"$ts-webpack-watch",
"$tslint-webpack-watch"
],
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "test-watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": "build"
}
]
}

11
.vscodeignore Normal file
View file

@ -0,0 +1,11 @@
.vscode/**
.vscode-test/**
out/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts

9
CHANGELOG.md Normal file
View file

@ -0,0 +1,9 @@
# Change Log
All notable changes to the "vscode-appwrite" extension will be documented in this file.
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
## [Unreleased]
- Initial release

70
README.md Normal file
View file

@ -0,0 +1,70 @@
# vscode-appwrite README
This is the README for your extension "vscode-appwrite". After writing up a brief description, we recommend including the following sections.
## Features
Describe specific features of your extension including screenshots of your extension in action. Image paths are relative to this README file.
For example if there is an image subfolder under your extension project workspace:
\!\[feature X\]\(images/feature-x.png\)
> Tip: Many popular extensions utilize animations. This is an excellent way to show off your extension! We recommend short, focused animations that are easy to follow.
## Requirements
If you have any requirements or dependencies, add a section describing those and how to install and configure them.
## Extension Settings
Include if your extension adds any VS Code settings through the `contributes.configuration` extension point.
For example:
This extension contributes the following settings:
* `myExtension.enable`: enable/disable this extension
* `myExtension.thing`: set to `blah` to do something
## Known Issues
Calling out known issues can help limit users opening duplicate issues against your extension.
## Release Notes
Users appreciate release notes as you update your extension.
### 1.0.0
Initial release of ...
### 1.0.1
Fixed issue #.
### 1.1.0
Added features X, Y, and Z.
-----------------------------------------------------------------------------------------------------------
## Following extension guidelines
Ensure that you've read through the extensions guidelines and follow the best practices for creating your extension.
* [Extension Guidelines](https://code.visualstudio.com/api/references/extension-guidelines)
## Working with Markdown
**Note:** You can author your README using Visual Studio Code. Here are some useful editor keyboard shortcuts:
* Split the editor (`Cmd+\` on macOS or `Ctrl+\` on Windows and Linux)
* Toggle preview (`Shift+CMD+V` on macOS or `Shift+Ctrl+V` on Windows and Linux)
* Press `Ctrl+Space` (Windows, Linux) or `Cmd+Space` (macOS) to see a list of Markdown snippets
### For more information
* [Visual Studio Code's Markdown Support](http://code.visualstudio.com/docs/languages/markdown)
* [Markdown Syntax Reference](https://help.github.com/articles/markdown-basics/)
**Enjoy!**

6867
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

342
package.json Normal file
View file

@ -0,0 +1,342 @@
{
"name": "vscode-appwrite",
"displayName": "vscode-appwrite",
"description": "Manage your Appwrite resources right from VS Code!",
"version": "0.0.1",
"engines": {
"vscode": "^1.55.0"
},
"categories": [
"Other"
],
"activationEvents": [
"onCommand:vscode-appwrite.Connect",
"onView:Appwrite",
"onView:Users",
"onView:Database",
"onView:Health",
"onCommand:vscode-appwrite.AddProject"
],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "vscode-appwrite.Connect",
"title": "Connect to Appwrite"
},
{
"command": "vscode-appwrite.AddProject",
"title": "Add an Appwrite project"
},
{
"command": "vscode-appwrite.SelectProject",
"title": "Select an Appwrite project"
},
{
"command": "vscode-appwrite.CreateUser",
"title": "Create user",
"icon": "$(add)"
},
{
"command": "vscode-appwrite.DeleteUser",
"title": "Delete user",
"icon": "$(trash)"
},
{
"command": "vscode-appwrite.GetUserLogs",
"title": "Get user logs",
"icon": "$(output)"
},
{
"command": "vscode-appwrite.openUserInConsole",
"title": "View user in Appwrite console",
"icon": "$(link-external)"
},
{
"command": "vscode-appwrite.viewUserPrefs",
"title": "View user preferences",
"icon": "$(json)"
},
{
"command": "vscode-appwrite.refreshEntry",
"title": "Refresh",
"icon": "$(refresh)"
},
{
"command": "vscode-appwrite.copyUserId",
"title": "Copy user ID",
"icon": "$(clippy)"
},
{
"command": "vscode-appwrite.copyUserEmail",
"title": "Copy user email",
"icon": "$(clippy)"
},
{
"command": "vscode-appwrite.refreshUsersList",
"title": "Refresh users list",
"icon": "$(refresh)"
},
{
"command": "vscode-appwrite.OpenDocumentation",
"title": "Open documentation",
"icon": "$(book)"
},
{
"command": "vscode-appwrite.viewDocumentAsJson",
"title": "View as JSON"
},
{
"command": "vscode-appwrite.viewCollectionAsJson",
"title": "View collection as JSON"
},
{
"command": "vscode-appwrite.refreshCollection",
"title": "Refresh",
"icon": "$(refresh)"
},
{
"command": "vscode-appwrite.createRule",
"title": "Create collection rule",
"icon": "$(add)"
},
{
"command": "vscode-appwrite.removeRule",
"title": "Remove collection rule"
},
{
"command": "vscode-appwrite.deleteDocument",
"title": "Delete document"
},
{
"command": "vscode-appwrite.deleteCollection",
"title": "Delete collection"
},
{
"command": "vscode-appwrite.refreshCollectionsList",
"title": "Refresh",
"icon": "$(refresh)"
},
{
"command": "vscode-appwrite.createCollection",
"title": "Create collection",
"icon": "$(add)"
},
{
"command": "vscode-appwrite.createPermission",
"title": "Create permission",
"icon": "$(add)"
},
{
"command": "vscode-appwrite.deletePermission",
"title": "Delete permission",
"icon": "$(trash)"
},
{
"command": "vscode-appwrite.editPermission",
"title": "Edit permission",
"icon": "$(edit)"
}
],
"views": {
"Appwrite": [
{
"id": "Users",
"name": "Users"
},
{
"id": "Database",
"name": "Database"
},
{
"id": "Storage",
"name": "Storage"
},
{
"id": "Functions",
"name": "Functions"
},
{
"id": "Health",
"name": "Health"
}
]
},
"viewsWelcome": [
{
"view": "Users",
"contents": "Connect to Appwrite to get started.\n[Connect to Appwrite](command:vscode-appwrite.Connect)"
}
],
"menus": {
"view/title": [
{
"command": "vscode-appwrite.OpenDocumentation",
"when": "view == Users",
"group": "navigation"
},
{
"command": "vscode-appwrite.refreshUsersList",
"when": "view == Users",
"group": "navigation"
},
{
"command": "vscode-appwrite.CreateUser",
"when": "view == Users",
"group": "navigation"
},
{
"command": "vscode-appwrite.refreshCollectionsList",
"when": "view == Database",
"group": "navigation"
},
{
"command": "vscode-appwrite.createCollection",
"when": "view == Database",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "vscode-appwrite.viewUserPrefs",
"when": "viewItem == users.prefs",
"group": "inline"
},
{
"command": "vscode-appwrite.DeleteUser",
"when": "viewItem == user"
},
{
"command": "vscode-appwrite.GetUserLogs",
"when": "viewItem == user"
},
{
"command": "vscode-appwrite.openUserInConsole",
"when": "view == Users && viewItem == user",
"group": "inline"
},
{
"command": "vscode-appwrite.copyUserId",
"when": "viewItem == user.id",
"group": "inline"
},
{
"command": "vscode-appwrite.copyUserEmail",
"when": "viewItem == user.email",
"group": "inline"
},
{
"command": "vscode-appwrite.viewDocumentAsJson",
"when": "viewItem == document"
},
{
"command": "vscode-appwrite.viewCollectionAsJson",
"when": "viewItem == collection"
},
{
"command": "vscode-appwrite.deleteCollection",
"when": "viewItem == collection"
},
{
"command": "vscode-appwrite.createRule",
"when": "viewItem == collection.rules",
"group": "inline"
},
{
"command": "vscode-appwrite.createRule",
"when": "viewItem == collection.rules"
},
{
"command": "vscode-appwrite.removeRule",
"when": "viewItem == collection.rule"
},
{
"command": "vscode-appwrite.refreshCollection",
"when": "viewItem == collection"
},
{
"command": "vscode-appwrite.deleteDocument",
"when": "viewItem == document"
},
{
"command": "vscode-appwrite.createPermission",
"when": "viewItem =~ /^(permissions)$/"
},
{
"command": "vscode-appwrite.createPermission",
"when": "viewItem =~ /^(permissions)$/",
"group": "inline"
},
{
"command": "vscode-appwrite.editPermission",
"when": "viewItem =~ /^(permission)$/",
"group": "inline"
},
{
"command": "vscode-appwrite.editPermission",
"when": "viewItem =~ /^(permission)$/"
},
{
"command": "vscode-appwrite.deletePermission",
"when": "viewItem =~ /^(permission)$/"
},
{
"command": "vscode-appwrite.deletePermission",
"when": "viewItem =~ /^(permission)$/",
"group": "inline"
}
]
},
"viewsContainers": {
"activitybar": [
{
"id": "Appwrite",
"title": "Appwrite",
"icon": "resources/vscode-appwrite.svg"
}
]
},
"configuration": {
"title": "Appwrite",
"properties": {
"appwrite.projects": {
"type": "array",
"default": [],
"description": "List of Appwrite projects"
}
}
}
},
"scripts": {
"vscode:prepublish": "npm run package",
"compile": "webpack",
"watch": "webpack --watch",
"package": "webpack --mode production --devtool hidden-source-map",
"test-compile": "tsc -p ./",
"test-watch": "tsc -watch -p ./",
"pretest": "npm run test-compile && npm run lint",
"lint": "eslint src --ext ts",
"test": "node ./out/test/runTest.js"
},
"devDependencies": {
"@types/fs-extra": "^9.0.11",
"@types/glob": "^7.1.3",
"@types/mocha": "^8.0.4",
"@types/node": "^12.11.7",
"@types/vscode": "^1.55.0",
"@typescript-eslint/eslint-plugin": "^4.14.1",
"@typescript-eslint/parser": "^4.14.1",
"eslint": "^7.19.0",
"glob": "^7.1.6",
"mocha": "^8.2.1",
"ts-loader": "^8.0.14",
"typescript": "^4.1.3",
"vscode-test": "^1.5.0",
"webpack": "^5.19.0",
"webpack-cli": "^4.4.0"
},
"dependencies": {
"fs-extra": "^9.1.0",
"node-appwrite": "^2.1.0"
}
}

View file

@ -0,0 +1,5 @@
<svg width="272" height="182" viewBox="0 0 272 182" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M271.058 91.03C271.058 140.752 230.752 181.058 181.03 181.058C157.934 181.058 136.871 172.361 120.934 158.065C146.465 146.572 164.239 120.908 164.239 91.094C164.239 61.242 146.421 35.552 120.839 24.08C136.788 9.734 157.889 1.002 181.03 1.002C230.752 1.002 271.058 41.308 271.058 91.03Z" fill="white"/>
<path d="M145.043 19.167C125.984 4.78431 101.992 -1.4381 78.3449 1.86864C54.698 5.17537 33.3332 17.7404 18.9505 36.7995C4.56781 55.8586 -1.6546 79.8506 1.65214 103.498C4.95887 127.144 17.5239 148.509 36.583 162.892C55.6421 177.275 79.6341 183.497 103.281 180.19C126.928 176.884 148.293 164.319 162.676 145.259C177.058 126.2 183.281 102.208 179.974 78.5614C176.667 54.9145 164.102 33.5497 145.043 19.167V19.167ZM135.03 32.434C150.522 44.1786 160.723 61.5889 163.394 80.8452C166.065 100.101 160.988 119.631 149.278 135.149C137.568 150.667 120.18 160.906 100.93 163.62C81.6795 166.333 62.1388 161.3 46.595 149.624C31.103 137.879 20.9024 120.469 18.2312 101.213C15.5601 81.9566 20.6365 62.4271 32.3469 46.9091C44.0572 31.3911 61.4449 21.1521 80.6952 18.4384C99.9455 15.7247 119.486 20.758 135.03 32.434V32.434Z" fill="white"/>
<path d="M93.225 59.882C93.117 60.148 91.74 65.554 90.258 71.966C88.72 78.379 86.282 88.501 84.907 94.49C82.254 105.459 80.664 112.667 80.664 113.622C80.664 113.885 82.308 114.099 84.321 114.099H87.979L89.62 106.784C90.576 102.81 92.696 93.694 94.391 86.54C96.088 79.385 98.153 70.693 98.95 67.195C99.743 63.696 100.538 60.519 100.699 60.148C100.858 59.671 99.955 59.513 97.2 59.513C95.132 59.513 93.329 59.671 93.225 59.882V59.882ZM64.764 82.829L59.889 88.13L61.321 89.825C62.113 90.778 64.289 93.164 66.143 95.126L69.535 98.728H79.181L74.624 93.799C72.131 91.152 70.064 88.606 70.064 88.289C70.064 87.918 71.973 85.534 74.305 82.99C76.636 80.394 78.545 78.166 78.545 77.901C78.545 77.689 76.531 77.53 74.094 77.53H69.695L64.764 82.829ZM101.862 77.847C101.862 78.008 102.764 79.013 103.875 80.127C108.012 84.26 110.926 87.76 110.767 88.447C110.661 88.82 108.646 91.308 106.21 93.905L101.812 98.728H106.739L111.668 98.676L116.171 93.748C118.664 90.994 120.677 88.552 120.677 88.235C120.677 87.972 118.558 85.48 115.907 82.673L111.139 77.53H106.526C103.929 77.53 101.862 77.689 101.862 77.847V77.847Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

370
src/appwrite.d.ts vendored Normal file
View file

@ -0,0 +1,370 @@
export type Token = {
/**
* Token ID.
*/
$id: string;
/**
* User ID.
*/
userId: string;
/**
* 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.
*/
secret: string;
/**
* Token expiration date in Unix timestamp.
*/
expire: number;
};
export type User = {
/**
* User ID.
*/
$id: string;
/**
* User name.
*/
name: string;
/**
* User registration date in Unix timestamp.
*/
registration: number;
/**
* User status. 0 for Unactivated, 1 for active and 2 is blocked.
* @deprecated according to developers
*/
status: number;
/**
* User email address.
*/
email: string;
/**
* Email verification status.
*/
emailVerification: boolean;
/**
* User preferences as a key-value object
*/
prefs: Record<string, unknown>;
};
export type UsersList = {
/**
* Total sum of items in the list.
*/
sum: number;
/**
* List of users.
*/
users: User[];
};
export type Error = {
/**
* Error message.
*/
message: string;
/**
* Error code.
*/
code: string;
/**
* Server version number.
*/
version: string;
};
export type Session = {
/**
* Session ID.
*/
$id: string;
/**
* User ID.
*/
userId: string;
/**
* Session expiration date in Unix timestamp.
*/
expire: number;
/**
* IP in use when the session was created.
*/
ip: string;
/**
* Returns true if this the current user session.
*/
current: boolean;
} & ClientInfo;
export type Log = {
/**
* Event name.
*/
event: string;
/**
* IP session in use when the session was created.
*/
ip: string;
/**
* Log creation time in Unix timestamp.
*/
time: number;
} & ClientInfo;
type ClientInfo = {
/**
* Operating system code name. View list of possible values:
* https://github.com/appwrite/appwrite/blob/master/docs/lists/os.json
*/
osCode: string;
/**
* Operating system name.
*/
osName: string;
/**
* Operating system version.
*/
osVersion: string;
/**
* Client type.
*/
clientType: string;
/**
* Client code name. View list of possible values:
* https://github.com/appwrite/appwrite/blob/master/docs/lists/clients.json
*/
clientCode: string;
/**
* Client name.
*/
clientName: string;
/**
* Client version.
*/
clientVersion: string;
/**
* Client engine name.
*/
clientEngine: string;
/**
* Client engine version.
*/
clientEngineVersion: string;
/**
* Device name.
*/
deviceName: string;
/**
* Device brand name.
*/
deviceBrand: string;
/**
* Device model name.
*/
deviceModel: string;
/**
* Country two-character ISO 3166-1 alpha code.
*/
countryCode: string;
/**
* Country name.
*/
countryName: string;
};
export type Team = {
/**
* Team ID.
*/
$id: string;
/**
* Team name.
*/
name: string;
/**
* Team creation date in Unix timestamp.
*/
dateCreated: number;
/**
* Total sum of team members.
*/
sum: number;
};
type Membership = {
/**
* Membership ID.
*/
$id: string;
/**
* User ID.
*/
userId: string;
/**
* Team ID.
*/
teamId: string;
/**
* User name.
*/
name: string;
/**
* User email address.
*/
email: string;
/**
* Date, the user has been invited to join the team in Unix timestamp.
*/
invited: number;
/**
* Date, the user has accepted the invitation to join the team in Unix timestamp.
*/
joined: number;
/**
* User confirmation status, true if the user has joined the team or false otherwise.
*/
confirm: boolean;
/**
* User list of roles
*/
roles: string[];
};
export type FilesList = {
sum: number;
files: File[];
};
export type File = {
'$id': string;
'$permissions': Permissions;
name: string;
dateCreated: number;
signature: string;
mimeType: string;
sizeOriginal: number;
};
export type Collection = {
$id: string;
$permissions: Permissions;
name: string;
dateCreated: number;
dateUpdated: number;
rules: Rule[];
};
export type CreatedCollection = Partial<Collection> & Pick<Collection, 'name'>;
export type CollectionsList = {
sum: number;
collections: Collection[];
};
export type CreatedRule = Omit<Rule, '$id' | '$collection' | 'default' | 'list'>;
export type Rule = {
$id: string;
$collection: string;
type: string;
key: string;
label: string;
default: string;
array: boolean;
required: boolean;
list: string[];
};
interface Permissions {
read: string[];
write: string[];
}
export type Client = {
// Your API Endpoint
setEndpoint: (endpoint: string) => Client;
// Your project ID
setProject: (projectId: string) => Client;
// Your secret API key
setKey: (key: string) => Client;
};
export type UsersClient = {
deleteUser: (id: string) => Promise<any>;
deleteSessions: (id: string) => Promise<any>;
create: (email: string, password: string, name?: string) => Promise<any>;
getLogs: (id: string) => Promise<Log[]>;
};
export type DatabaseClient = {
listCollections: () => Promise<any>;
listDocuments: (collectionId: string) => Promise<any>;
updateCollection: (collectionId: string, name: string, read: string[], write: string[], rules?: (Rule | CreatedRule)[]) => Promise<any>;
deleteCollection: (collectionId: string) => Promise<any>;
getCollection: (collectionId: string) => Promise<any>;
deleteDocument: (collectionId: string, documentId: string) => Promise<any>;
createCollection: (name: string, read: string[], write: string[], rules: Rule[]) => Promise<any>;
};
export type DocumentsList = {
sum: number;
documents: any[];
};
type GetHealth = () => any;
export type HealthClient = {
get: GetHealth;
getDB: GetHealth;
getCache: GetHealth;
getTime: GetHealth;
getQueueWebhooks: GetHealth;
getQueueTasks: GetHealth;
getQueueLogs: GetHealth;
getQueueUsage: GetHealth;
getQueueCertificates: GetHealth;
getQueueFunctions: GetHealth;
getStorageLocal: GetHealth;
getAntiVirus: GetHealth;
};
export type AppwriteHealth = {
HTTP: any;
DB: any;
Cache: any;
Time: any;
QueueWebhooks: any;
QueueTasks: any;
QueueLogs: any;
QueueUsage: any;
QueueCertificates: any;
QueueFunctions: any;
StorageLocal: any;
AntiVirus: any;
};
export type StorageClient = {
createFile: (file: any, read: string[], write: string[]) => Promise<any>;
listFiles: () => Promise<any>;
getFile: (fileId: string) => Promise<any>;
}
export type SDK = {
Client: new () => Client;
Users: new (client: Client) => UsersClient;
Health: new (client: Client) => HealthClient;
Database: new (client: Client) => DatabaseClient;
Storage: new (client: Client) => StorageClient;
};

63
src/appwrite/Database.ts Normal file
View file

@ -0,0 +1,63 @@
import { Client, Collection, CreatedCollection, CreatedRule, DatabaseClient, Rule, SDK } from "../appwrite";
import { CreateRuleWizardContext } from "../ui/createRuleWizard";
import AppwriteCall from "../utils/AppwriteCall";
const sdk: SDK = require("node-appwrite");
export class Database {
private readonly database: DatabaseClient;
constructor(client: Client) {
this.database = new sdk.Database(client);
}
public async getCollection(collectionId: string): Promise<Collection | undefined> {
return await AppwriteCall<Collection>(this.database.getCollection(collectionId));
}
public async deleteDocument(collectionId: string, documentId: string): Promise<void> {
await AppwriteCall(this.database.deleteDocument(collectionId, documentId));
}
public async deleteCollection(collectionId: string): Promise<void> {
await AppwriteCall(this.database.deleteCollection(collectionId));
}
public async createCollection(collection: CreatedCollection): Promise<void> {
await AppwriteCall(
this.database.createCollection(
collection.name,
collection.$permissions?.read ?? [],
collection.$permissions?.write ?? [],
collection.rules ?? []
)
);
}
public async updatePermissions(collection: Collection, read: string[], write: string[]): Promise<void> {
await AppwriteCall(this.database.updateCollection(collection.$id, collection.name, read, write, collection.rules));
}
public async createRule(collection: Collection, newRule: CreatedRule): Promise<void> {
await AppwriteCall(
this.database.updateCollection(collection.$id, collection.name, collection.$permissions.read, collection.$permissions.write, [
...collection.rules,
newRule,
])
);
}
public async removeRule(collection: Collection, ruleToRemove: Rule): Promise<void> {
const rules = collection.rules.filter((rule) => rule.$id !== ruleToRemove.$id);
await AppwriteCall(
this.database.updateCollection(
collection.$id,
collection.name,
collection.$permissions.read,
collection.$permissions.write,
rules
)
);
}
}

30
src/appwrite/Health.ts Normal file
View file

@ -0,0 +1,30 @@
import { AppwriteHealth, Client, HealthClient, SDK } from "../appwrite";
const sdk: SDK = require("node-appwrite");
export class Health {
private readonly health: HealthClient;
constructor(client: Client) {
this.health = new sdk.Health(client);
}
/**
* @returns The health of all Appwrite services.
*/
public async checkup(): Promise<AppwriteHealth> {
return {
HTTP: await this.health.get(),
DB: await this.health.getDB(),
Cache: await this.health.getCache(),
Time: await this.health.getTime(),
QueueWebhooks: await this.health.getQueueWebhooks(),
QueueTasks: await this.health.getQueueTasks(),
QueueLogs: await this.health.getQueueLogs(),
QueueUsage: await this.health.getQueueUsage(),
QueueCertificates: await this.health.getQueueCertificates(),
QueueFunctions: await this.health.getQueueFunctions(),
StorageLocal: await this.health.getStorageLocal(),
AntiVirus: await this.health.getAntiVirus(),
};
}
}

17
src/appwrite/Storage.ts Normal file
View file

@ -0,0 +1,17 @@
import { Client, Collection, CreatedCollection, CreatedRule, DatabaseClient, File, FilesList, Rule, SDK, StorageClient } from "../appwrite";
import { CreateRuleWizardContext } from "../ui/createRuleWizard";
import AppwriteCall from "../utils/AppwriteCall";
const sdk: SDK = require("node-appwrite");
export class Storage {
private readonly storage: StorageClient;
constructor(client: Client) {
this.storage = new sdk.Storage(client);
}
public async listFiles(): Promise<FilesList | undefined> {
return await AppwriteCall(this.storage.listFiles());
}
}

35
src/appwrite/Users.ts Normal file
View file

@ -0,0 +1,35 @@
import { window } from "vscode";
import { Client, Log, SDK, User, UsersClient } from "../appwrite";
import { ext } from "../extensionVariables";
import AppwriteCall from "../utils/AppwriteCall";
const sdk: SDK = require("node-appwrite");
export class Users {
private readonly users: UsersClient;
constructor(client: Client) {
this.users = new sdk.Users(client);
}
public async createNewUser(context: CreateUserContext): Promise<void> {
ext.outputChannel?.appendLog("Creating new user" + JSON.stringify(context));
await AppwriteCall<User, void>(this.users.create(context.email, context.password, context.name), (user) => {
window.showInformationMessage(`Created user with id: ${user.$id}`);
});
}
public async delete(userId: string): Promise<void> {
await AppwriteCall(this.users.deleteUser(userId), () => {
window.showInformationMessage(`Deleted user with id: ${userId}.`);
});
}
public async getLogs(userId: string): Promise<Log[]> {
return await AppwriteCall<Log[], Log[]>(this.users.getLogs(userId)) ?? [];
}
}
type CreateUserContext = {
email: string;
password: string;
name?: string;
};

28
src/client.ts Normal file
View file

@ -0,0 +1,28 @@
import { Client, SDK } from "./appwrite";
import { Database } from "./appwrite/Database";
import { Health } from "./appwrite/Health";
import { Storage } from "./appwrite/Storage";
import { Users } from "./appwrite/Users";
import { AppwriteProjectConfiguration } from "./settings";
const sdk: SDK = require("node-appwrite");
export let client: Client;
export let clientConfig: { endpoint: string; projectId: string; secret: string };
export let usersClient: Users;
export let healthClient: Health;
export let databaseClient: Database;
export let storageClient: Storage;
export function initAppwriteClient({ endpoint, projectId, secret }: AppwriteProjectConfiguration) {
client = new sdk.Client();
clientConfig = { endpoint, projectId, secret };
client.setEndpoint(endpoint).setProject(projectId).setKey(secret);
usersClient = new Users(client);
healthClient = new Health(client);
databaseClient = new Database(client);
storageClient = new Storage(client);
return client;
}

View file

@ -0,0 +1,13 @@
import { initAppwriteClient } from "../client";
import { addProjectConfiguration } from '../settings';
import { addProjectWizard } from "../ui/AddProjectWizard";
import { refreshTree } from '../utils/refreshTree';
export async function connectAppwrite() {
const projectConfiguration = await addProjectWizard();
if (projectConfiguration) {
addProjectConfiguration(projectConfiguration);
initAppwriteClient(projectConfiguration);
refreshTree();
}
}

View file

@ -0,0 +1,13 @@
import { window } from 'vscode';
import { databaseClient } from '../../client';
export async function createCollection(): Promise<void> {
const name = await window.showInputBox({
prompt: 'Collection name'
});
if (name && name.length > 0) {
await databaseClient.createCollection({name});
window.showInformationMessage(`Created collection "${name}".`);
}
}

View file

@ -0,0 +1,47 @@
import { window } from "vscode";
import { databaseClient } from '../../client';
import { CollapsableTreeItem } from '../../tree/CollapsableTreeItem';
import { PermissionsTreeItem } from "../../tree/database/settings/PermissionsTreeItem";
import { PermissionTreeItem } from "../../tree/database/settings/PermissionTreeItem";
export type CreatePermissionWizardContext = {
kind: "read" | "write";
permission: string;
};
export async function createPermissionWizard(kind?: "read" | "write"): Promise<CreatePermissionWizardContext | undefined> {
const permissionKind = kind ?? (await window.showQuickPick(["read", "write"]));
if (permissionKind && (permissionKind === "read" || permissionKind === "write")) {
const permission = await window.showInputBox({ prompt: "Add * for wildcard access", placeHolder: "User ID, Team ID, or Role" });
if (permission === undefined) {
return undefined;
}
return {
kind: permissionKind,
permission,
};
}
return undefined;
}
export async function createPermission(treeItem: PermissionsTreeItem): Promise<void> {
const collection = treeItem.parent.collection;
const context = await createPermissionWizard(undefined);
if (context === undefined) {
return;
}
const read = Array.from(collection.$permissions.read);
const write = Array.from(collection.$permissions.write);
if (context.kind === 'read') {
read.push(context.permission);
} else {
write.push(context.permission);
}
await databaseClient.updatePermissions(collection, read, write);
}

View file

@ -0,0 +1,24 @@
import { CreatedRule } from "../../appwrite";
import { databaseClient } from "../../client";
import { RulesTreeItem } from "../../tree/database/settings/RulesTreeItem";
import { createRuleWizard } from "../../ui/createRuleWizard";
import { refreshTree } from '../../utils/refreshTree';
export async function createRule(rulesTreeItem: RulesTreeItem): Promise<void> {
const ruleContext = await createRuleWizard();
const collection = rulesTreeItem.parent.collection;
if (ruleContext) {
const newRule: CreatedRule = {
...ruleContext,
type: ruleContext.type,
required: true,
array: false,
};
databaseClient.createRule(collection, newRule);
await rulesTreeItem.refresh();
refreshTree("database");
}
}

View file

@ -0,0 +1,17 @@
import { window } from "vscode";
import { databaseClient } from "../../client";
import { CollectionTreeItem } from "../../tree/database/CollectionTreeItem";
import { confirmDialog } from "../../ui/confirmDialog";
export async function deleteCollection(collectionTreeItem: CollectionTreeItem): Promise<void> {
const collection = collectionTreeItem.collection;
try {
const shouldDelete = await confirmDialog(`Delete collection "${collection.name}"?`);
if (shouldDelete) {
await databaseClient.deleteCollection(collection.$id);
window.showInformationMessage(`Deleted collection "${collection.name}".`);
}
} catch (e) {
window.showErrorMessage(e);
}
}

View file

@ -0,0 +1,18 @@
import { window } from "vscode";
import { databaseClient } from "../../client";
import { DocumentTreeItem } from "../../tree/database/DocumentTreeItem";
import { confirmDialog } from "../../ui/confirmDialog";
export async function deleteDocument(documentTreeItem: DocumentTreeItem): Promise<void> {
const document = documentTreeItem.document;
const collection = documentTreeItem.parent.parent.collection;
try {
const shouldDelete = await confirmDialog(`Delete document "${document["$id"]}" from ${collection.name}?`);
if (shouldDelete) {
await databaseClient.deleteDocument(collection.$id, document["$id"]);
documentTreeItem.parent.window.showInformationMessage(`Deleted document "${document["$id"]}" from ${collection.name}.`);
}
} catch (e) {
window.showErrorMessage(e);
}
}

View file

@ -0,0 +1,18 @@
import { databaseClient } from "../../client";
import { PermissionTreeItem } from "../../tree/database/settings/PermissionTreeItem";
export async function deletePermission(treeItem: PermissionTreeItem): Promise<void> {
const collection = treeItem.parent.parent.collection;
const kind = treeItem.kind;
let read = Array.from(collection.$permissions.read);
let write = Array.from(collection.$permissions.write);
if (kind === "read") {
read = read.filter((item) => item !== treeItem.permission);
} else {
write = write.filter((item) => item !== treeItem.permission);
}
await databaseClient.updatePermissions(collection, read, write);
}

View file

@ -0,0 +1,13 @@
import { DocumentTreeItem } from "../../tree/database/DocumentTreeItem";
import { openReadOnlyJson } from "../../ui/openReadonlyContent";
export async function viewDocumentAsJson(documentTreeItem: DocumentTreeItem): Promise<void> {
const documentId = documentTreeItem.document["$id"];
await openReadOnlyJson(
{
label: documentId,
fullId: documentId,
},
documentTreeItem.document
);
}

View file

@ -0,0 +1,5 @@
import { CollectionTreeItem } from "../../tree/database/CollectionTreeItem";
export async function refreshCollection(collectionTreeItem: CollectionTreeItem) {
await collectionTreeItem.refresh();
}

View file

@ -0,0 +1,5 @@
import { refreshTree } from '../../utils/refreshTree';
export async function refreshCollectionsList(): Promise<void> {
refreshTree('database');
}

View file

@ -0,0 +1,10 @@
import { databaseClient } from '../../client';
import { RuleTreeItem } from '../../tree/database/settings/RuleTreeItem';
import { refreshTree } from '../../utils/refreshTree';
export async function removeRule(ruleItem: RuleTreeItem): Promise<void> {
const rule = ruleItem.rule;
const collection = ruleItem.parent.parent.collection;
await databaseClient.removeRule(collection, rule);
refreshTree('database');
}

View file

@ -0,0 +1,7 @@
import { CollectionTreeItem } from "../../tree/database/CollectionTreeItem";
import { openReadOnlyJson } from "../../ui/openReadonlyContent";
export async function viewCollectionAsJson(collectionTreeItem: CollectionTreeItem): Promise<void> {
const collection = collectionTreeItem.collection;
await openReadOnlyJson({ label: collection.name, fullId: collection.$id }, collection);
}

View file

@ -0,0 +1,7 @@
import { openUrl } from '../utils/openUrl';
const appwriteDocsUrl = 'https://appwrite.io/docs';
export async function openDocumentation(): Promise<void> {
await openUrl(appwriteDocsUrl);
}

View file

@ -0,0 +1,13 @@
import { window } from "vscode";
import { initAppwriteClient } from "../../client";
import { addProjectWizard } from "../../ui/AddProjectWizard";
export async function addProject() {
const projectConfiguration = await addProjectWizard();
if (projectConfiguration) {
initAppwriteClient(projectConfiguration);
}
window.showInformationMessage("Connected to Appwrite project.");
}

View file

View file

@ -0,0 +1,69 @@
import { commands, ExtensionContext } from "vscode";
import { AppwriteTree, ext } from "../extensionVariables";
import { refreshTree } from "../utils/refreshTree";
import { connectAppwrite } from "./connectAppwrite";
import { createCollection } from "./database/createCollection";
import { createPermission } from './database/createPermission';
import { createRule } from "./database/createRule";
import { deleteCollection } from "./database/deleteCollection";
import { deleteDocument } from "./database/deleteDocument";
import { deletePermission } from './database/deletePermission';
import { viewDocumentAsJson } from "./database/openDocument";
import { refreshCollection } from "./database/refreshCollection";
import { refreshCollectionsList } from "./database/refreshCollectionsList";
import { removeRule } from "./database/removeRule";
import { viewCollectionAsJson } from "./database/viewCollectionAsJson";
import { openDocumentation } from "./openDocumentation";
import { addProject } from "./project/addProject";
import { copyUserEmail } from "./users/copyUserEmail";
import { copyUserId } from "./users/copyUserId";
import { createUser } from "./users/createUser";
import { deleteUser } from "./users/deleteUser";
import { getUserLogs } from "./users/getUserLogs";
import { openUserInConsole } from "./users/openUserInConsole";
import { refreshUsersList } from "./users/refreshUsersList";
import { viewUserPrefs } from "./users/viewUserPrefs";
class CommandRegistrar {
constructor(private readonly context: ExtensionContext) {}
registerCommand(commandId: string, callback: (...args: any[]) => any): void {
const disposable = commands.registerCommand(commandId, callback);
this.context.subscriptions.push(disposable);
}
}
export function registerCommands(context: ExtensionContext): void {
const registrar = new CommandRegistrar(context);
const registerCommand = (commandId: string, callback: (...args: any[]) => any, refresh?: keyof AppwriteTree) => {
registrar.registerCommand(`vscode-appwrite.${commandId}`, async (...args: any[]) => {
await callback(...args);
if (refresh) {
refreshTree(refresh);
}
});
};
registerCommand("Connect", connectAppwrite);
registerCommand("openUserInConsole", openUserInConsole);
registerCommand("viewUserPrefs", viewUserPrefs);
registerCommand("copyUserId", copyUserId);
registerCommand("copyUserEmail", copyUserEmail);
registerCommand("CreateUser", createUser);
registerCommand("refreshUsersList", refreshUsersList);
registerCommand("DeleteUser", deleteUser);
registerCommand("OpenDocumentation", openDocumentation);
registerCommand("GetUserLogs", getUserLogs);
registerCommand("viewDocumentAsJson", viewDocumentAsJson);
registerCommand("AddProject", addProject);
registerCommand("viewCollectionAsJson", viewCollectionAsJson);
registerCommand("createRule", createRule);
registerCommand("removeRule", removeRule);
registerCommand("deleteDocument", deleteDocument, 'database');
registerCommand("deleteCollection", deleteCollection, 'database');
registerCommand("refreshCollection", refreshCollection);
registerCommand("refreshCollectionsList", refreshCollectionsList);
registerCommand("createCollection", createCollection, 'database');
registerCommand("createPermission", createPermission, 'database');
registerCommand("deletePermission", deletePermission, 'database');
}

View file

@ -0,0 +1,9 @@
import { ChildTreeItem } from '../../tree/ChildTreeItem';
import { env, window} from 'vscode';
import { UserTreeItem } from '../../tree/users/UserTreeItem';
export async function copyUserEmail(item: ChildTreeItem<UserTreeItem>): Promise<void> {
const email = item.parent.user.email;
await env.clipboard.writeText(email);
window.showInformationMessage('User email copied to clipboard');
}

View file

@ -0,0 +1,9 @@
import { ChildTreeItem } from '../../tree/ChildTreeItem';
import { env, window} from 'vscode';
import { UserTreeItem } from '../../tree/users/UserTreeItem';
export async function copyUserId(item: ChildTreeItem<UserTreeItem>): Promise<void> {
const id = item.parent.user.$id;
await env.clipboard.writeText(id);
window.showInformationMessage('User id copied to clipboard');
}

View file

@ -0,0 +1,43 @@
import { window } from "vscode";
import { usersClient } from '../../client';
import { ext } from '../../extensionVariables';
export async function createUser(): Promise<void> {
const email = await window.showInputBox({
ignoreFocusOut: true,
placeHolder: "jane.doe@hotmail.com",
prompt: "New user email address",
});
if (email === undefined) {
return;
}
const password = await window.showInputBox({
ignoreFocusOut: true,
password: true,
prompt: "Enter user password",
validateInput: (value) => {
if (value.length <= 6) {
return "Password must be at least 6 characters long.";
}
if (value.length > 32) {
return "Password must be less than 32 characters long.";
}
},
});
if (password === undefined) {
return;
}
const name = await window.showInputBox({
ignoreFocusOut: true,
placeHolder: "",
prompt: "New user name (optional)",
});
await usersClient.createNewUser({
email,
password,
name: name === "" ? undefined : name,
});
ext.tree?.users?.refresh();
}

View file

@ -0,0 +1,23 @@
import { window } from "vscode";
import { usersClient } from "../../client";
import { UserTreeItem } from '../../tree/users/UserTreeItem';
import { DialogResponses } from "../../ui/DialogResponses";
import { refreshTree } from "../../utils/refreshTree";
export async function deleteUser(userTreeItem: UserTreeItem): Promise<void> {
const user = userTreeItem.user;
const userId = user.$id;
const shouldDeleteUser = await window.showWarningMessage(
`Are you sure you want to delete user with email: "${user.email}"?`,
{
modal: true,
},
DialogResponses.yes,
DialogResponses.cancel
);
if (shouldDeleteUser === DialogResponses.yes) {
await usersClient.delete(userId);
refreshTree("users");
}
}

View file

@ -0,0 +1,11 @@
import { usersClient } from "../../client";
import { UserTreeItem } from "../../tree/users/UserTreeItem";
import { openReadOnlyJson } from '../../ui/openReadonlyContent';
export async function getUserLogs(treeItem: UserTreeItem): Promise<void> {
const userId = treeItem.user.$id;
const logs = await usersClient.getLogs(userId);
await openReadOnlyJson({ label: `Logs for ${treeItem.user.email}`, fullId: `${treeItem.user.$id}-UserLogs` }, logs);
}

View file

@ -0,0 +1,19 @@
import { URL } from "url";
import { commands, Uri } from "vscode";
import { clientConfig } from "../../client";
import { UserTreeItem } from "../../tree/users/UserTreeItem";
function getConsoleUrlFromEndpoint(endpoint: string): string {
const url = new URL(endpoint);
return `${url.origin}/console`;
}
function getUserUrl(userId: string, endpoint: string, projectId: string): string {
return `${getConsoleUrlFromEndpoint(endpoint)}/users/user?id=${userId}&project=${projectId}`;
}
export async function openUserInConsole(item: UserTreeItem) {
const url = getUserUrl(item.user.$id, clientConfig.endpoint, clientConfig.projectId);
await commands.executeCommand("vscode.open", Uri.parse(url));
}

View file

@ -0,0 +1,5 @@
import { ext } from "../../extensionVariables";
export async function refreshUsersList() {
ext.tree?.users?.refresh();
}

View file

@ -0,0 +1,11 @@
import { UserPrefsTreeItem } from "../../tree/users/properties/UserPrefsTreeItem";
import { openReadOnlyJson } from "../../ui/openReadonlyContent";
export async function viewUserPrefs(item: UserPrefsTreeItem) {
const prefs = item.parent.user.prefs;
await openReadOnlyJson(
{ label: `prefs`, fullId: `${item.parent.user.$id}.prefs` },
prefs
);
}

3
src/constants.ts Normal file
View file

@ -0,0 +1,3 @@
import type { SDK } from './appwrite';
export const AppwriteSDK: SDK = require('node-appwrite') as SDK;

41
src/extension.ts Normal file
View file

@ -0,0 +1,41 @@
import * as vscode from "vscode";
import { initAppwriteClient } from "./client";
import { registerCommands } from "./commands/registerCommands";
import { ext } from "./extensionVariables";
import { getDefaultProject } from "./settings";
import { DatabaseTreeItemProvider } from "./tree/database/DatabaseTreeItemProvider";
import { HealthTreeItemProvider } from "./tree/health/HealthTreeItemProvider";
import { StorageTreeItemProvider } from "./tree/storage/StorageTreeItemProvider";
import { UserTreeItemProvider } from "./tree/users/UserTreeItemProvider";
import { createAppwriteOutputChannel } from "./ui/AppwriteOutputChannel";
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const userTreeItemProvider = new UserTreeItemProvider();
const healthTreeItemProvider = new HealthTreeItemProvider();
const databaseTreeItemProvider = new DatabaseTreeItemProvider();
const storageTreeItemProvider = new StorageTreeItemProvider();
vscode.window.registerTreeDataProvider("Users", userTreeItemProvider);
vscode.window.registerTreeDataProvider("Health", healthTreeItemProvider);
vscode.window.registerTreeDataProvider("Database", databaseTreeItemProvider);
vscode.window.registerTreeDataProvider("Storage", storageTreeItemProvider);
const defaultProject = await getDefaultProject();
if (defaultProject) {
initAppwriteClient(defaultProject);
}
ext.context = context;
ext.outputChannel = createAppwriteOutputChannel("Appwrite", "appwrite");
ext.tree = {
users: userTreeItemProvider,
health: healthTreeItemProvider,
database: databaseTreeItemProvider,
storage: storageTreeItemProvider,
};
registerCommands(context);
}
export function deactivate() {}

21
src/extensionVariables.ts Normal file
View file

@ -0,0 +1,21 @@
import { ExtensionContext, OutputChannel } from "vscode";
import { DatabaseTreeItemProvider } from './tree/database/DatabaseTreeItemProvider';
import { HealthTreeItemProvider } from './tree/health/HealthTreeItemProvider';
import { StorageTreeItemProvider } from './tree/storage/StorageTreeItemProvider';
import { UserTreeItemProvider } from './tree/users/UserTreeItemProvider';
import { AppwriteOutputChannel } from './ui/AppwriteOutputChannel';
export type AppwriteTree = {
users?: UserTreeItemProvider;
health?: HealthTreeItemProvider;
database?: DatabaseTreeItemProvider;
storage?: StorageTreeItemProvider;
};
export type Ext = {
context?: ExtensionContext;
outputChannel?: AppwriteOutputChannel;
tree?: AppwriteTree;
};
export const ext: Ext = {};

30
src/settings.ts Normal file
View file

@ -0,0 +1,30 @@
import { workspace } from 'vscode';
export type AppwriteProjectConfiguration = {
nickname?: string;
endpoint: string;
console?: string;
projectId: string;
secret: string;
};
export async function getDefaultProject(): Promise<AppwriteProjectConfiguration | undefined> {
const projects = await getAppwriteProjects();
return projects?.[0] ?? undefined;
}
export async function getAppwriteProjects(): Promise<AppwriteProjectConfiguration[]> {
const configuration = workspace.getConfiguration('appwrite');
const projects = configuration.get('projects');
if (projects === undefined) {
configuration.update('projects', []);
return [];
}
return projects as AppwriteProjectConfiguration[];
}
export async function addProjectConfiguration(projectConfig: AppwriteProjectConfiguration): Promise<void> {
const configuration = workspace.getConfiguration('appwrite');
const projects = await getAppwriteProjects();
await configuration.update('projects', [...projects, projectConfig], true);
}

23
src/test/runTest.ts Normal file
View file

@ -0,0 +1,23 @@
import * as path from 'path';
import { runTests } from 'vscode-test';
async function main() {
try {
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
// The path to test runner
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname, './suite/index');
// Download VS Code, unzip it and run the integration test
await runTests({ extensionDevelopmentPath, extensionTestsPath });
} catch (err) {
console.error('Failed to run tests');
process.exit(1);
}
}
main();

View file

@ -0,0 +1,15 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});

38
src/test/suite/index.ts Normal file
View file

@ -0,0 +1,38 @@
import * as path from 'path';
import * as Mocha from 'mocha';
import * as glob from 'glob';
export function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
color: true
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
if (err) {
return e(err);
}
// Add files to the test suite
files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run(failures => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
});
});
}

View file

@ -0,0 +1,8 @@
import { TreeItem } from "vscode";
export class ChildTreeItem<Parent> extends TreeItem {
constructor(public readonly parent: Parent, item: TreeItem) {
super(item.label || 'Please provide label');
Object.assign(this, item);
}
}

View file

@ -0,0 +1,16 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { AppwriteTreeItemBase } from "../ui/AppwriteTreeItemBase";
import { ChildTreeItem } from "./ChildTreeItem";
export class CollapsableTreeItem<Parent> extends AppwriteTreeItemBase<Parent> {
constructor(parent: Parent, item: Partial<TreeItem> & { label: string }, private readonly children: TreeItem[], public readonly brand?: string) {
super(parent, item.label);
Object.assign(this, item);
}
public async getChildren(): Promise<TreeItem[]> {
return this.children;
}
collapsibleState = TreeItemCollapsibleState.Collapsed;
}

View file

@ -0,0 +1,9 @@
import { Command, TreeItem } from 'vscode';
export class CommandTreeItem extends TreeItem {
constructor(item: TreeItem, command: Command) {
super(item.label || 'Add label');
Object.assign(this, item);
this.command = command;
}
}

View file

@ -0,0 +1,29 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { Collection } from "../../appwrite";
import { databaseClient } from '../../client';
import { AppwriteTreeItemBase } from '../../ui/AppwriteTreeItemBase';
import { DatabaseTreeItemProvider } from './DatabaseTreeItemProvider';
import { DocumentsTreeItem } from './DocumentsTreeItem';
import { PermissionsTreeItem } from './settings/PermissionsTreeItem';
import { RulesTreeItem } from './settings/RulesTreeItem';
export class CollectionTreeItem extends AppwriteTreeItemBase {
constructor(public collection: Collection, public readonly provider: DatabaseTreeItemProvider) {
super(undefined, collection.name);
}
public async getChildren(): Promise<TreeItem[]> {
return [new RulesTreeItem(this), new PermissionsTreeItem(this), new DocumentsTreeItem(this)];
}
public async refresh(): Promise<void> {
this.collection = await databaseClient.getCollection(this.collection.$id) ?? this.collection;
this.provider.refreshChild(this);
}
collapsibleState = TreeItemCollapsibleState.Collapsed;
contextValue = "collection";
iconPath = new ThemeIcon('folder');
}

View file

@ -0,0 +1,55 @@
import * as vscode from "vscode";
import { client } from "../../client";
import AppwriteCall from "../../utils/AppwriteCall";
import { Collection, CollectionsList, DocumentsList } from "../../appwrite";
import { CollectionTreeItem } from "./CollectionTreeItem";
import { AppwriteSDK } from "../../constants";
import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase";
import { ext } from '../../extensionVariables';
export class DatabaseTreeItemProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined | void> = new vscode.EventEmitter<
CollectionTreeItem | undefined | void
>();
readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined | void> = this._onDidChangeTreeData.event;
constructor() {}
refresh(): void {
ext.outputChannel?.appendLine('refresh database');
this._onDidChangeTreeData.fire();
}
refreshChild(child: vscode.TreeItem): void {
this._onDidChangeTreeData.fire(child);
}
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
return element;
}
async getChildren(parent?: vscode.TreeItem): Promise<vscode.TreeItem[]> {
ext.outputChannel?.appendLine('getChildren for: ' + parent?.label);
if (client === undefined) {
return Promise.resolve([]);
}
if (parent instanceof AppwriteTreeItemBase) {
return parent.getChildren?.() ?? [];
}
let databaseSdk = new AppwriteSDK.Database(client);
const collectionsList = await AppwriteCall<CollectionsList, CollectionsList>(databaseSdk.listCollections());
if (collectionsList) {
const userTreeItems = collectionsList.collections.map((collection: Collection) => new CollectionTreeItem(collection, this)) ?? [];
const headerItem: vscode.TreeItem = {
label: `Total collections: ${collectionsList.sum}`,
};
return [headerItem, ...userTreeItems];
}
return [{ label: "No collections found" }];
}
}

View file

@ -0,0 +1,13 @@
import { ThemeIcon } from 'vscode';
import { ChildTreeItem } from '../ChildTreeItem';
import { DocumentsTreeItem } from './DocumentsTreeItem';
export class DocumentTreeItem extends ChildTreeItem<DocumentsTreeItem> {
constructor(parent: DocumentsTreeItem, public readonly document: Record<string, any>) {
super(parent, {
label: document['$id'],
});
}
iconPath = new ThemeIcon('json');
contextValue = 'document';
}

View file

@ -0,0 +1,38 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { DocumentsList } from "../../appwrite";
import { client } from "../../client";
import { AppwriteSDK } from "../../constants";
import { ext } from "../../extensionVariables";
import { AppwriteTreeItemBase } from "../../ui/AppwriteTreeItemBase";
import AppwriteCall from "../../utils/AppwriteCall";
import { CollectionTreeItem } from "./CollectionTreeItem";
import { DocumentTreeItem } from "./DocumentTreeItem";
export class DocumentsTreeItem extends AppwriteTreeItemBase<CollectionTreeItem> {
window: any;
constructor(parent: CollectionTreeItem) {
super(parent, "Documents");
}
public async getChildren(): Promise<TreeItem[]> {
let databaseSdk = new AppwriteSDK.Database(client);
const documentList = await AppwriteCall<DocumentsList>(databaseSdk.listDocuments(this.parent.collection.$id));
if (documentList === undefined) {
return [];
}
ext.outputChannel?.append(JSON.stringify(documentList, null, 4));
const documentTreeItems = documentList.documents.map((document) => new DocumentTreeItem(this, document));
const headerItem: TreeItem = {
label: `Total documents: ${documentTreeItems.length}`,
};
return [headerItem, ...documentTreeItems];
}
collapsibleState = TreeItemCollapsibleState.Collapsed;
contextValue = "documents";
iconPath = new ThemeIcon("database");
}

View file

@ -0,0 +1,11 @@
import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase";
import { ChildTreeItem } from "../../ChildTreeItem";
import { PermissionsTreeItem } from "./PermissionsTreeItem";
export class PermissionTreeItem extends ChildTreeItem<PermissionsTreeItem> {
constructor(parent: PermissionsTreeItem, public readonly permission: string, public readonly kind: "read" | "write") {
super(parent, { label: permission });
}
contextValue = 'permission';
}

View file

@ -0,0 +1,28 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { Permissions } from "../../../appwrite";
import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase";
import { CollapsableTreeItem } from "../../CollapsableTreeItem";
import { CollectionTreeItem } from "../CollectionTreeItem";
import { PermissionTreeItem } from "./PermissionTreeItem";
export class PermissionsTreeItem extends AppwriteTreeItemBase<CollectionTreeItem> {
public readonly permissions: Permissions;
constructor(parent: CollectionTreeItem) {
super(parent, "Permissions");
this.permissions = parent.collection.$permissions;
}
public async getChildren(): Promise<TreeItem[]> {
const readPermissions = this.permissions.read.map((perm) => new PermissionTreeItem(this, perm, "read"));
const writePermissions = this.permissions.write.map((perm) => new PermissionTreeItem(this, perm, "write"));
return [
new CollapsableTreeItem(this, { label: "Read" }, readPermissions, "read"),
new CollapsableTreeItem(this, { label: "Write" }, writePermissions, "write"),
];
}
iconPath = new ThemeIcon("shield");
contextValue = "permissions";
collapsibleState = TreeItemCollapsibleState.Collapsed;
}

View file

@ -0,0 +1,11 @@
import { TreeItem } from "vscode";
import { Rule } from "../../../appwrite";
import { ChildTreeItem } from "../../ChildTreeItem";
import { RulesTreeItem } from "./RulesTreeItem";
export class RuleTreeItem extends ChildTreeItem<RulesTreeItem> {
constructor(parent: RulesTreeItem, public readonly rule: Rule) {
super(parent, { label: rule.label, description: rule.type });
}
contextValue = "collection.rule";
}

View file

@ -0,0 +1,34 @@
import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from "vscode";
import { Rule } from "../../../appwrite";
import { AppwriteTreeItemBase } from "../../../ui/AppwriteTreeItemBase";
import { CommandTreeItem } from "../../CommandTreeItem";
import { CollectionTreeItem } from "../CollectionTreeItem";
import { RuleTreeItem } from "./RuleTreeItem";
export class RulesTreeItem extends AppwriteTreeItemBase<CollectionTreeItem> {
public readonly rules: Rule[];
constructor(parent: CollectionTreeItem) {
super(parent, "Rules");
this.rules = parent.collection.rules;
}
public async getChildren(): Promise<TreeItem[]> {
if (this.rules.length === 0) {
const addRuleItem = new CommandTreeItem(
{ label: "Add rule", iconPath: new ThemeIcon("add") },
{ command: "vscode-appwrite.createRule", arguments: [this], title: "Create rule", tooltip: "Create collection rule" }
);
return [addRuleItem];
}
return this.rules.map((rule) => new RuleTreeItem(this, rule));
}
public async refresh(): Promise<void> {
await this.parent.refresh();
}
iconPath = new ThemeIcon("symbol-property");
contextValue = "collection.rules";
collapsibleState = TreeItemCollapsibleState.Collapsed;
}

View file

@ -0,0 +1,12 @@
import * as vscode from "vscode";
export class HealthTreeItem extends vscode.TreeItem {
constructor(public readonly label: string, status: boolean) {
super(label);
this.label = label;
this.iconPath = new vscode.ThemeIcon(status ? "check" : "error", new vscode.ThemeColor(status ? "#00ff00" : "list.errorForeground"));
this.contextValue = `health.${label}`;
}
contextValue = "health";
}

View file

@ -0,0 +1,32 @@
import * as vscode from "vscode";
import { healthClient } from "../../client";
import { HealthTreeItem } from "./HealthTreeItem";
export class HealthTreeItemProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<HealthTreeItem | undefined | void> = new vscode.EventEmitter<
HealthTreeItem | undefined | void
>();
readonly onDidChangeTreeData: vscode.Event<HealthTreeItem | undefined | void> = this._onDidChangeTreeData.event;
constructor() {}
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: HealthTreeItem): vscode.TreeItem {
return element;
}
async getChildren(element?: HealthTreeItem): Promise<vscode.TreeItem[]> {
// get children for root
if (element === undefined) {
const health = await healthClient.checkup();
return Object.entries(health).map(([service, status]) => {
return new HealthTreeItem(service, status);
});
}
return [];
}
}

View file

@ -0,0 +1,12 @@
import { ThemeIcon, TreeItem } from "vscode";
import { File } from "../../appwrite";
export class FileTreeItem extends TreeItem {
constructor(public readonly file: File) {
super(file.name);
}
iconPath = new ThemeIcon("file");
contextValue = "file";
}

View file

@ -0,0 +1,29 @@
import * as vscode from "vscode";
import { storageClient } from "../../client";
import { FileTreeItem } from "./FileTreeItem";
export class StorageTreeItemProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<vscode.TreeItem | undefined | void> = new vscode.EventEmitter<
vscode.TreeItem | undefined | void
>();
readonly onDidChangeTreeData: vscode.Event<vscode.TreeItem | undefined | void> = this._onDidChangeTreeData.event;
constructor() {}
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: vscode.TreeItem): vscode.TreeItem {
return element;
}
async getChildren(element?: vscode.TreeItem): Promise<vscode.TreeItem[]> {
const files = await storageClient.listFiles();
if (files === undefined) {
return [];
}
return files.files.map((file) => new FileTreeItem(file));
}
}

View file

@ -0,0 +1,8 @@
import { TreeItem } from 'vscode';
import { UserTreeItem } from './UserTreeItem';
export abstract class UserPropertyTreeItemBase extends TreeItem {
constructor(public readonly parent: UserTreeItem, label: string) {
super(label);
}
}

View file

@ -0,0 +1,15 @@
import { User } from "../../appwrite";
import * as vscode from "vscode";
export class UserTreeItem extends vscode.TreeItem {
constructor(public readonly user: User) {
super(user.email);
console.log(user);
this.label = `${user.email}`;
this.tooltip = user.emailVerification ? "Verified" : "Unverified";
this.iconPath = new vscode.ThemeIcon(user.emailVerification ? "verified" : "unverified");
}
collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
contextValue = "user";
}

View file

@ -0,0 +1,78 @@
import * as vscode from "vscode";
import { client } from "../../client";
import AppwriteCall from "../../utils/AppwriteCall";
import { User, UsersList } from "../../appwrite";
import { ThemeIcon } from "vscode";
import { UserPrefsTreeItem } from "./properties/UserPrefsTreeItem";
import { ChildTreeItem } from "../ChildTreeItem";
import { UserTreeItem } from "./UserTreeItem";
const sdk = require("node-appwrite");
export class UserTreeItemProvider implements vscode.TreeDataProvider<vscode.TreeItem> {
private _onDidChangeTreeData: vscode.EventEmitter<UserTreeItem | undefined | void> = new vscode.EventEmitter<
UserTreeItem | undefined | void
>();
readonly onDidChangeTreeData: vscode.Event<UserTreeItem | undefined | void> = this._onDidChangeTreeData.event;
constructor() {}
refresh(): void {
this._onDidChangeTreeData.fire();
}
getTreeItem(element: UserTreeItem): vscode.TreeItem {
return element;
}
async getChildren(element?: UserTreeItem): Promise<vscode.TreeItem[]> {
if (client === undefined) {
return Promise.resolve([]);
}
if (element instanceof UserTreeItem) {
const regDate = new Date();
regDate.setMilliseconds(element.user.registration);
const items: vscode.TreeItem[] = [
new ChildTreeItem(element, {
contextValue: "user.name",
label: element.user.name || "Unfilled",
iconPath: new ThemeIcon("person"),
description: "Name",
}),
new ChildTreeItem(element, {
contextValue: "user.email",
label: element.user.email,
iconPath: new ThemeIcon("mail"),
description: "Email",
}),
new ChildTreeItem(element, {
contextValue: "user.registration",
label: regDate.toDateString(),
iconPath: new vscode.ThemeIcon("calendar"),
description: "Joined",
}),
new ChildTreeItem(element, {
label: element.user.$id,
description: "User ID",
iconPath: new vscode.ThemeIcon("key"),
contextValue: "user.id",
}),
new UserPrefsTreeItem(element),
];
return Promise.resolve(items);
}
let usersSdk = new sdk.Users(client);
const usersList = await AppwriteCall<UsersList, UsersList>(usersSdk.list());
if (usersList) {
const userTreeItems = usersList.users.map((user: any) => new UserTreeItem(user)) ?? [];
const headerItem: vscode.TreeItem = {
label: `Total users: ${usersList.sum}`,
};
return [headerItem, ...userTreeItems];
}
return [{ label: "No users found" }];
}
}

View file

@ -0,0 +1,13 @@
import { ThemeIcon } from "vscode";
import { UserPropertyTreeItemBase } from "../UserPropertyTreeItemBase";
import { UserTreeItem } from '../UserTreeItem';
export class UserPrefsTreeItem extends UserPropertyTreeItemBase {
constructor(parent: UserTreeItem) {
super(parent, 'View preferences');
}
iconPath = new ThemeIcon("json");
contextValue = 'users.prefs';
}

View file

@ -0,0 +1,29 @@
import { window } from "vscode";
import { AppwriteProjectConfiguration } from "../settings";
export async function addProjectWizard(): Promise<AppwriteProjectConfiguration | undefined> {
const endpoint = await window.showInputBox({
placeHolder: "Endpoint",
prompt: "Enter your Appwrite API endping",
ignoreFocusOut: true
});
const projectId = await window.showInputBox({
placeHolder: "Project Id",
prompt: "Enter your Appwrite project id",
ignoreFocusOut: true
});
const secret = await window.showInputBox({
placeHolder: "API key secret",
prompt: "Enter your Appwrite API key secret",
ignoreFocusOut: true
});
const nickname = await window.showInputBox({
prompt: "(Optional) Project nickname",
ignoreFocusOut: true
});
if (endpoint && projectId && secret) {
return { endpoint, projectId, secret, nickname };
}
return undefined;
}

View file

@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { OutputChannel, ViewColumn, window, workspace, WorkspaceConfiguration } from "vscode";
// tslint:disable-next-line: export-name
export function createAppwriteOutputChannel(name: string, extensionPrefix: string) {
return new AppwriteOutputChannel(name, extensionPrefix);
}
export class AppwriteOutputChannel {
public readonly name: string;
public readonly extensionPrefix: string;
private _outputChannel: OutputChannel;
constructor(name: string, extensionPrefix: string) {
this.name = name;
this.extensionPrefix = extensionPrefix;
this._outputChannel = window.createOutputChannel(this.name);
}
public append(value: string): void {
this._outputChannel.append(value);
}
public appendLine(value: string): void {
this._outputChannel.appendLine(value);
}
public appendLog(value: string, options?: { resourceName?: string, date?: Date }): void {
const enableOutputTimestampsSetting: string = 'enableOutputTimestamps';
const projectConfiguration: WorkspaceConfiguration = workspace.getConfiguration(this.extensionPrefix);
const result: boolean | undefined = projectConfiguration.get<boolean>(enableOutputTimestampsSetting);
if (!result) {
this.appendLine(value);
} else {
// tslint:disable: strict-boolean-expressions
options = options || {};
const date: Date = options.date || new Date();
this.appendLine(`${date.toLocaleTimeString()}${options.resourceName ? ' '.concat(options.resourceName) : ''}: ${value}`);
}
}
public clear(): void {
this._outputChannel.clear();
}
public show(preserveFocus?: boolean | undefined): void;
public show(column?: ViewColumn | undefined, preserveFocus?: boolean | undefined): void;
// tslint:disable-next-line: no-any
public show(_column?: any, preserveFocus?: boolean | undefined): void {
this._outputChannel.show(preserveFocus);
}
public hide(): void {
this._outputChannel.hide();
}
public dispose(): void {
this._outputChannel.dispose();
}
}

View file

@ -0,0 +1,10 @@
import { TreeDataProvider, TreeItem } from "vscode";
export abstract class AppwriteTreeItemBase<Parent = void> extends TreeItem {
constructor(public readonly parent: Parent, label: string) {
super(label);
}
abstract getChildren?(): Promise<TreeItem[]>;
}

121
src/ui/BaseEditor.ts Normal file
View file

@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fse from "fs-extra";
import * as path from "path";
import * as vscode from "vscode";
import { ext } from '../extensionVariables';
import { createTemporaryFile } from "./createTemporaryFile";
import { DialogResponses } from "./DialogResponses";
// tslint:disable-next-line:no-unsafe-any
export abstract class BaseEditor<ContextT> implements vscode.Disposable {
private fileMap: { [key: string]: [vscode.TextDocument, ContextT] } = {};
private ignoreSave: boolean = false;
constructor(private readonly showSavePromptKey: string) {}
public abstract getData(context: ContextT): Promise<string>;
public abstract updateData(context: ContextT, data: string): Promise<string>;
public abstract getFilename(context: ContextT): Promise<string>;
public abstract getResourceName(context: ContextT): Promise<string>;
public abstract getSaveConfirmationText(context: ContextT): Promise<string>;
public abstract getSize(context: ContextT): Promise<number>;
public async showEditor(context: ContextT): Promise<void> {
const fileName: string = await this.getFilename(context);
const resourceName: string = await this.getResourceName(context);
this.appendLineToOutput(`Opening ${resourceName}...`, { resourceName });
const localFilePath: string = await createTemporaryFile(fileName);
const document: vscode.TextDocument = await vscode.workspace.openTextDocument(localFilePath);
this.fileMap[localFilePath] = [document, context];
const data: string = await this.getData(context);
const textEditor: vscode.TextEditor = await vscode.window.showTextDocument(document);
await this.updateEditor(data, textEditor);
}
public async updateMatchingContext(doc: vscode.Uri): Promise<void> {
const filePath: string | undefined = Object.keys(this.fileMap).find((fsPath: string) => path.relative(doc.fsPath, fsPath) === "");
if (filePath) {
const [textDocument, context]: [vscode.TextDocument, ContextT] = this.fileMap[filePath];
await this.updateRemote(context, textDocument);
}
}
public async dispose(): Promise<void> {
Object.keys(this.fileMap).forEach(async (key: string) => await fse.remove(path.dirname(key)));
}
public async onDidSaveTextDocument(globalState: vscode.Memento, doc: vscode.TextDocument): Promise<void> {
const filePath: string | undefined = Object.keys(this.fileMap).find(
(fsPath: string) => path.relative(doc.uri.fsPath, fsPath) === ""
);
if (!this.ignoreSave && filePath) {
const context: ContextT = this.fileMap[filePath][1];
const showSaveWarning: boolean | undefined = vscode.workspace.getConfiguration().get(this.showSavePromptKey);
if (showSaveWarning) {
const message: string = await this.getSaveConfirmationText(context);
const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(
message,
DialogResponses.upload,
DialogResponses.alwaysUpload,
DialogResponses.dontUpload
);
if (result === DialogResponses.alwaysUpload) {
await vscode.workspace.getConfiguration().update(this.showSavePromptKey, false, vscode.ConfigurationTarget.Global);
await globalState.update(this.showSavePromptKey, true);
}
}
await this.updateRemote(context, doc);
}
}
protected appendLineToOutput(value: string, options?: { resourceName?: string; date?: Date }): void {
ext.outputChannel?.appendLog(value, options);
ext.outputChannel?.show(true);
}
private async updateRemote(context: ContextT, doc: vscode.TextDocument): Promise<void> {
const filename: string = await this.getFilename(context);
const resourceName: string = await this.getResourceName(context);
this.appendLineToOutput(`Updating "${filename}" ...', filename)`, { resourceName });
const updatedData: string = await this.updateData(context, doc.getText());
this.appendLineToOutput(`Updated "${filename}".`, { resourceName });
if (doc.isClosed !== true) {
const visibleDocument: vscode.TextEditor | undefined = vscode.window.visibleTextEditors.find((ed) => ed.document === doc);
if (visibleDocument) {
await this.updateEditor(updatedData, visibleDocument);
}
}
}
private async updateEditor(data: string, textEditor?: vscode.TextEditor): Promise<void> {
if (!!textEditor) {
await BaseEditor.writeToEditor(textEditor, data);
this.ignoreSave = true;
try {
await textEditor.document.save();
} finally {
this.ignoreSave = false;
}
}
}
// tslint:disable-next-line:member-ordering
private static async writeToEditor(editor: vscode.TextEditor, data: string): Promise<void> {
await editor.edit((editBuilder: vscode.TextEditorEdit) => {
if (editor.document.lineCount > 0) {
const lastLine: vscode.TextLine = editor.document.lineAt(editor.document.lineCount - 1);
editBuilder.delete(
new vscode.Range(
new vscode.Position(0, 0),
new vscode.Position(lastLine.range.start.line, lastLine.range.end.character)
)
);
}
editBuilder.insert(new vscode.Position(0, 0), data);
});
}
}

15
src/ui/DialogResponses.ts Normal file
View file

@ -0,0 +1,15 @@
import { MessageItem } from "vscode";
export namespace DialogResponses {
export const yes: MessageItem = { title: "Yes" };
export const no: MessageItem = { title: "No" };
export const cancel: MessageItem = { title: "Cancel", isCloseAffordance: true };
export const deleteResponse: MessageItem = { title: "Delete" };
export const learnMore: MessageItem = { title: "Learn more" };
export const dontWarnAgain: MessageItem = { title: "Don't warn again" };
export const skipForNow: MessageItem = { title: "Skip fo now" };
export const upload: MessageItem = { title: "Upload" };
export const alwaysUpload: MessageItem = { title: "Always uploa" };
export const dontUpload: MessageItem = { title: "Don' upload", isCloseAffordance: true };
export const reportAnIssue: MessageItem = { title: "Report a issue" };
}

7
src/ui/confirmDialog.ts Normal file
View file

@ -0,0 +1,7 @@
import { window } from "vscode";
import { DialogResponses } from "./DialogResponses";
export async function confirmDialog(text: string): Promise<boolean> {
const response = await window.showWarningMessage(text, { modal: true }, DialogResponses.yes, DialogResponses.cancel);
return response === DialogResponses.yes;
}

View file

@ -0,0 +1,51 @@
import { QuickPickItem, window } from "vscode";
import { AppwriteProjectConfiguration } from "../settings";
export type CreateRuleWizardContext = {
label: string;
key: string;
type: keyof typeof ruleTypes;
};
export async function createRuleWizard(): Promise<CreateRuleWizardContext | undefined> {
const label = await window.showInputBox({
placeHolder: "Label",
prompt: "Attribute internal display name",
});
if (label === undefined) {
return;
}
const key = await window.showInputBox({
placeHolder: "Key",
prompt: "Attribute key name. Used as the document JSON key in the Database API.",
});
if (key === undefined) {
return;
}
const ruleTypeItems: QuickPickItem[] = Object.entries(ruleTypes).map(([label, description]) => ({
label,
description,
}));
const type = await window.showQuickPick(ruleTypeItems);
if (type === undefined) {
return;
}
if (label && key && type) {
return { label, key, type: (type.label as unknown) as keyof typeof ruleTypes };
}
return undefined;
}
const ruleTypes = {
text: "Any string value.",
numeric: "Any integer or float value.",
boolean: "Any boolean value.",
wildcard: "Accept any value.",
url: "Any valid URL.",
email: "Any valid email address.",
ip: "Any valid IP v4 or v6 address.",
document:
"Accept a valid child document. When using this type you are also required to pass the 'list' parameter with an array of the collections UID values of the document types you want to accept.",
};

View file

@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as crypto from "crypto";
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
export async function createTemporaryFile(fileName: string): Promise<string> {
const randomFolderNameLength: number = 12;
const buffer: Buffer = crypto.randomBytes(Math.ceil(randomFolderNameLength / 2));
const folderName: string = buffer.toString('hex').slice(0, randomFolderNameLength);
const filePath: string = path.join(os.tmpdir(), folderName, fileName);
await fse.ensureFile(filePath);
return filePath;
}

View file

@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See LICENSE.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNumber } from "util";
import {
CancellationToken,
Event,
EventEmitter,
TextDocumentContentProvider,
TextDocumentShowOptions,
Uri,
window,
workspace,
WorkspaceConfiguration,
} from "vscode";
import { ext } from "../extensionVariables";
import { nonNullValue } from '../utils/nonNullUtils';
let contentProvider: ReadOnlyContentProvider | undefined;
const scheme: string = "azureextensionuiReadonly";
export async function openReadOnlyJson(
node: { label: string; fullId: string },
data: {}
): Promise<void> {
let tab: string = " ";
const config: WorkspaceConfiguration = workspace.getConfiguration("editor");
const insertSpaces: boolean = !!config.get<boolean>("insertSpaces");
if (insertSpaces) {
let tabSize: number | undefined = config.get<number>("tabSize");
if (!isNumber(tabSize) || tabSize < 0) {
tabSize = 4;
}
tab = " ".repeat(tabSize);
}
const content: string = JSON.stringify(data, undefined, tab);
await openReadOnlyContent(node, content, ".json");
}
export async function openReadOnlyContent(
node: { label: string; fullId: string },
content: string,
fileExtension: string,
options?: TextDocumentShowOptions
): Promise<ReadOnlyContent> {
if (!contentProvider) {
contentProvider = new ReadOnlyContentProvider();
ext.context?.subscriptions.push(
workspace.registerTextDocumentContentProvider(
scheme,
contentProvider
)
);
}
return await contentProvider.openReadOnlyContent(
node,
content,
fileExtension,
options
);
}
export class ReadOnlyContent {
private _uri: Uri;
private _emitter: EventEmitter<Uri>;
private _content: string;
constructor(uri: Uri, emitter: EventEmitter<Uri>, content: string) {
this._uri = uri;
this._emitter = emitter;
this._content = content;
}
public get content(): string {
return this._content;
}
public async append(content: string): Promise<void> {
this._content += content;
this._emitter.fire(this._uri);
}
public clear(): void {
this._content = "";
this._emitter.fire(this._uri);
}
}
class ReadOnlyContentProvider implements TextDocumentContentProvider {
private _onDidChangeEmitter: EventEmitter<Uri> = new EventEmitter<Uri>();
private _contentMap: Map<string, ReadOnlyContent> = new Map<
string,
ReadOnlyContent
>();
public get onDidChange(): Event<Uri> {
return this._onDidChangeEmitter.event;
}
public async openReadOnlyContent(
node: { label: string; fullId: string },
content: string,
fileExtension: string,
options?: TextDocumentShowOptions
): Promise<ReadOnlyContent> {
const idHash: string = Math.random().toString();
// in a URI, # means fragment and ? means query and is parsed in that way, so they should be removed to not break the path
const uri: Uri = Uri.parse(
`${scheme}:///${idHash}/${node.label.replace(
/\#|\?/g,
"_"
)}${fileExtension}`
);
const readOnlyContent: ReadOnlyContent = new ReadOnlyContent(
uri,
this._onDidChangeEmitter,
content
);
this._contentMap.set(uri.toString(), readOnlyContent);
await window.showTextDocument(uri, options);
this._onDidChangeEmitter.fire(uri);
return readOnlyContent;
}
public async provideTextDocumentContent(
uri: Uri,
_token: CancellationToken
): Promise<string> {
const readOnlyContent: ReadOnlyContent = nonNullValue(
this._contentMap.get(uri.toString()),
"ReadOnlyContentProvider._contentMap.get"
);
return readOnlyContent.content;
}
}

30
src/utils/AppwriteCall.ts Normal file
View file

@ -0,0 +1,30 @@
/* eslint-disable @typescript-eslint/ban-types */
import { window } from "vscode";
import { Error } from "../appwrite";
import { ext } from "../extensionVariables";
export default function AppwriteCall<T, R = T>(
promise: Promise<object>,
onSuccess?: (success: T) => R,
onError?: (error: Error) => R
): Promise<R | undefined> {
return promise.then(
(successResp) => {
ext.outputChannel?.appendLog("Appwrite call success");
if (onSuccess) {
return onSuccess((successResp as unknown) as T);
}
return successResp as unknown as R;
},
(errResp: Error) => {
if (onError) {
onError(errResp as Error);
return undefined;
}
window.showErrorMessage(errResp.message);
ext.outputChannel?.appendLog(errResp.message);
return undefined;
}
);
}

56
src/utils/nonNullUtils.ts Normal file
View file

@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.md in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { isNullOrUndefined } from "util";
/**
* Retrieves a property by name from an object and checks that it's not null and not undefined. It is strongly typed
* for the property and will give a compile error if the given name is not a property of the source.
*/
export function nonNullProp<TSource, TKey extends keyof TSource>(
source: TSource,
name: TKey
): NonNullable<TSource[TKey]> {
const value: NonNullable<TSource[TKey]> = <NonNullable<TSource[TKey]>>(
source[name]
);
return nonNullValue(value, <string>name);
}
/**
* Validates that a given value is not null and not undefined.
*/
export function nonNullValue<T>(
value: T | undefined,
propertyNameOrMessage?: string
): T {
if (isNullOrUndefined(value)) {
throw new Error(
// tslint:disable-next-line:prefer-template
"Internal error: Expected value to be neither null nor undefined" +
(propertyNameOrMessage ? `: ${propertyNameOrMessage}` : "")
);
}
return value;
}
/**
* Validates that a given string is not null, undefined, nor empty
*/
export function nonNullOrEmptyValue(
value: string | undefined,
propertyNameOrMessage?: string
): string {
if (!value) {
throw new Error(
// tslint:disable-next-line:prefer-template
"Internal error: Expected value to be neither null, undefined, nor empty" +
(propertyNameOrMessage ? `: ${propertyNameOrMessage}` : "")
);
}
return value;
}

5
src/utils/openUrl.ts Normal file
View file

@ -0,0 +1,5 @@
import { commands, Uri } from 'vscode';
export async function openUrl(url: string): Promise<void> {
await commands.executeCommand('vscode.open', Uri.parse(url));
}

14
src/utils/refreshTree.ts Normal file
View file

@ -0,0 +1,14 @@
import { AppwriteTree, ext } from "../extensionVariables";
export function refreshTree(tree?: keyof AppwriteTree): void {
if (tree !== undefined) {
ext.tree?.[tree]?.refresh();
return;
}
if (ext.tree) {
Object.values(ext.tree).forEach((treeView) => {
treeView?.refresh();
});
}
}

23
tsconfig.json Normal file
View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES5",
"outDir": "out",
"lib": [
"es6"
],
"sourceMap": true,
"rootDir": "src",
"strict": true, /* enable all strict type-checking options */
"strictNullChecks": true,
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
"experimentalDecorators": true
},
"exclude": [
"node_modules",
".vscode-test"
]
}

View file

@ -0,0 +1,42 @@
# Welcome to your VS Code Extension
## What's in the folder
* This folder contains all of the files necessary for your extension.
* `package.json` - this is the manifest file in which you declare your extension and command.
* 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 doesnt yet need to load the plugin.
* `src/extension.ts` - this is the main file where you will provide the implementation of your command.
* 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`.
* We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
## Get up and running straight away
* Press `F5` to open a new window with your extension loaded.
* Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`.
* Set breakpoints in your code inside `src/extension.ts` to debug your extension.
* Find output from your extension in the debug console.
## Make changes
* You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`.
* You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
## Explore the API
* You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`.
## Run tests
* Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.
* Press `F5` to run the tests in a new window with your extension loaded.
* See the output of the test result in the debug console.
* Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder.
* The provided test runner will only consider files matching the name pattern `**.test.ts`.
* You can create folders inside the `test` folder to structure your tests any way you want.
## Go further
* Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension).
* [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace.
* Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration).

41
webpack.config.js Normal file
View file

@ -0,0 +1,41 @@
//@ts-check
'use strict';
const path = require('path');
/**@type {import('webpack').Configuration}*/
const config = {
target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production')
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
output: {
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
path: path.resolve(__dirname, 'dist'),
filename: 'extension.js',
libraryTarget: 'commonjs2'
},
devtool: 'nosources-source-map',
externals: {
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/
},
resolve: {
// support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader
extensions: ['.ts', '.js']
},
module: {
rules: [
{
test: /\.ts$/,
exclude: /node_modules/,
use: [
{
loader: 'ts-loader'
}
]
}
]
}
};
module.exports = config;