From 0357a8ac4739ad64ddd05e0adb396d06bcbaa225 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 21 May 2024 11:23:48 -0400 Subject: [PATCH 01/89] feat(cli): Adding console for get methods --- templates/cli/base/requests/api.twig | 11 ++++ templates/cli/lib/commands/command.js.twig | 6 ++- templates/cli/lib/utils.js.twig | 62 +++++++++++++++++++++- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/templates/cli/base/requests/api.twig b/templates/cli/base/requests/api.twig index aee246c41..5a611dd92 100644 --- a/templates/cli/base/requests/api.twig +++ b/templates/cli/base/requests/api.twig @@ -17,8 +17,19 @@ fs.writeFileSync(destination, response); {%~ endif %} if (parseOutput) { + {%~ if method.name == 'get' and service.name not in ['health','migrations','locale'] %} + if(console) { + showConsoleLink('{{service.name}}', 'get' + {%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%}{%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%} + ); + } else { + parse(response) + success() + } + {%~ else %} parse(response) success() + {%~ endif %} } return response; diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 7751e1804..193014db7 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -4,7 +4,7 @@ const tar = require("tar"); const ignore = require("ignore"); const { promisify } = require('util'); const libClient = require('../client.js'); -const { getAllFiles } = require('../utils.js'); +const { getAllFiles, showConsoleLink } = require('../utils.js'); const { Command } = require('commander'); const { sdkForProject, sdkForConsole } = require('../sdks') const { parse, actionRunner, parseInteger, parseBool, commandDescriptions, success, log } = require('../parser') @@ -70,6 +70,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%} {%- if method.type == 'location' -%}, destination{%- endif -%} + {%- if method.name == 'get' -%}, console{%- endif -%} }) => { {%~ endblock %} let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : @@ -94,6 +95,9 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {% if method.type == 'location' %} .requiredOption(`--destination `, `output file path.`) {% endif %} +{% if method.name == 'get' %} + .option(`--console`, `View this resource in the console`) +{% endif %} {% endautoescape %} .action(actionRunner({{ service.name | caseLower }}{{ method.name | caseUcfirst }})) diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 289b1fa6e..63c1758b0 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -1,9 +1,13 @@ const fs = require("fs"); const path = require("path"); +const { localConfig, globalConfig } = require("./config"); +const { success, log } = require('./parser') +const readline = require('readline'); +const cp = require('child_process'); function getAllFiles(folder) { const files = []; - for(const pathDir of fs.readdirSync(folder)) { + for (const pathDir of fs.readdirSync(folder)) { const pathAbsolute = path.join(folder, pathDir); if (fs.statSync(pathAbsolute).isDirectory()) { files.push(...getAllFiles(pathAbsolute)); @@ -14,6 +18,60 @@ function getAllFiles(folder) { return files; } +function showConsoleLink(serviceName, action, id = '') { + let resource = ''; + let service = ''; + + switch (serviceName) { + case "account": + service = 'account'; + break; + case "databases": + resource = 'database'; + service = 'databases'; + break; + case "functions": + resource = 'function'; + service = 'functions'; + break; + case "projects": + service = `project-${id}`; + id = ''; + break; + case "teams": + resource = 'team'; + service = 'auth/teams'; + break; + + case "users": + resource = 'user'; + service = 'auth'; + break; + default: + return; + } + + const baseUrl = globalConfig.getEndpoint().replace('/v1', ''); + + const end = action === 'get' ? (id ? `/${resource}-${id}` : `/${resource}`) : ''; + const projectId = localConfig.getProject().projectId; + const middle = resource !== '' ? `/project-${projectId}` : ''; + const url = `${baseUrl}/console${middle}/${service}${end}` + + const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open'); + const key = (process.platform == 'darwin' ? 'Return' : 'Enter'); + + success(`\n ${url}\n Press <${key}> to open URL in your default browser, exising in 3 seconds`); + setTimeout(() => process.exit(0), 3000); + + const read = readline.createInterface({ input: process.stdin, output: process.stdout }); + read.on('line', () => { + cp.exec(`${start} ${url}`); + setTimeout(() => process.exit(0), 250); + }); +} + module.exports = { - getAllFiles + getAllFiles, + showConsoleLink }; From d9fa0bb9ff021718d1d43fe35eef739bc1159ba0 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 21 May 2024 14:45:50 -0400 Subject: [PATCH 02/89] feat(cli): Multiple accounts in CLI --- templates/cli/lib/commands/generic.js.twig | 169 +++++++++++++++------ templates/cli/lib/config.js.twig | 124 +++++++++++++-- templates/cli/lib/questions.js.twig | 28 +++- templates/cli/lib/sdks.js.twig | 1 + 4 files changed, 264 insertions(+), 58 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index ee87252e3..42b07a550 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -1,12 +1,82 @@ const inquirer = require("inquirer"); const { Command } = require("commander"); const Client = require("../client"); -const { sdkForConsole } = require("../sdks"); +const { sdkForConsole, questionGetEndpoint } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, drawTable } = require("../parser"); {% if sdk.test != "true" %} const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); +const ID = require("../id"); + +const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; + +const loginCommand = async ({ selfHosted }) => { + const answers = await inquirer.prompt(questionsLogin); + const oldCurrent = globalConfig.getCurrentLogin(); + const id = ID.unique(); + + globalConfig.setCurrentLogin(id); + globalConfig.addLogin(id, {}); + globalConfig.setEmail(answers.email); + globalConfig.setEndpoint(DEFAULT_ENDPOINT); + + if (selfHosted) { + const selfHostedAnswers = await inquirer.prompt(questionGetEndpoint); + + globalConfig.setEndpoint(selfHostedAnswers.endpoint); + } + + let client = await sdkForConsole(false); + + let account; + + try { + await accountCreateEmailPasswordSession({ + email: answers.email, + password: answers.password, + parseOutput: false, + sdk: client + }) + + client.setCookie(globalConfig.getCookie()); + + account = await accountGet({ + sdk: client, + parseOutput: false + }); + } catch (error) { + if (error.response === 'user_more_factors_required') { + const { factor } = await inquirer.prompt(questionsListFactors); + + const challenge = await accountCreateMfaChallenge({ + factor, + parseOutput: false, + sdk: client + }); + + const { otp } = await inquirer.prompt(questionsMfaChallenge); + + await accountUpdateMfaChallenge({ + challengeId: challenge.$id, + otp, + parseOutput: false, + sdk: client + }); + + account = await accountGet({ + sdk: client, + parseOutput: false + }); + } else { + globalConfig.removeLogin(id); + globalConfig.setCurrentLogin(oldCurrent); + throw error; + } + } + + success("Signed in as user with ID: " + account.$id); +}; const whoami = new Command("whoami") .description(commandDescriptions['whoami']) @@ -49,61 +119,74 @@ const whoami = new Command("whoami") const login = new Command("login") .description(commandDescriptions['login']) + .option(`-sa, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`) .configureHelp({ helpWidth: process.stdout.columns || 80 }) + .action(actionRunner(loginCommand)); + +login + .command('list') + .description("List available logged accounts.") .action(actionRunner(async () => { - const answers = await inquirer.prompt(questionsLogin) + const logins = globalConfig.getLogins(); + const current = globalConfig.getCurrentLogin(); - let client = await sdkForConsole(false); + const data = [...logins.map((login => { + return { + 'Current': login.id === current ? '*' : '', + 'ID': login.id, + 'Endpoint': login.endpoint, + 'Email': login.email + }; + }))]; - await accountCreateEmailPasswordSession({ - email: answers.email, - password: answers.password, - parseOutput: false, - sdk: client - }) + drawTable(data); - client.setCookie(globalConfig.getCookie()); + })); - let account; +login + .command('change') + .description("Change the current account") + .option(`-a, --accountId `, `Login ID`) + .action(actionRunner(async ({ accountId }) => { + const loginIds = globalConfig.getLoginIds(); - try { - account = await accountGet({ - sdk: client, - parseOutput: false - }); - } catch (error) { - if (error.response === 'user_more_factors_required') { - const { factor } = await inquirer.prompt(questionsListFactors); - - const challenge = await accountCreateMfaChallenge({ - factor, - parseOutput: false, - sdk: client - }); - - const { otp } = await inquirer.prompt(questionsMfaChallenge); - - await accountUpdateMfaChallenge({ - challengeId: challenge.$id, - otp, - parseOutput: false, - sdk: client - }); - - account = await accountGet({ - sdk: client, - parseOutput: false - }); - } else { - throw error; - } + if (!loginIds.includes(accountId)) { + throw Error('Login ID not found'); } - success("Signed in as user with ID: " + account.$id); + globalConfig.setCurrentLogin(accountId); + success(`Current account is ${accountId}`); })); +login + .command('migrate') + .description("Migrate existing login to new scheme") + .action(actionRunner(async ({ accountId }) => { + const endpoint = globalConfig.getEndpoint(); + const cookie = globalConfig.getCookie(); + + if (endpoint === '' || cookie === '') { + throw Error(`Couldn't find any existing account credentials`) + } + + const id = ID.unique(); + const data = { + endpoint, + cookie, + email: 'legacy' + }; + + globalConfig.addLogin(id, data); + globalConfig.setCurrentLogin(id); + globalConfig.delete('endpoint'); + globalConfig.delete('cookie'); + + success(`Account was migrated and it's the current account`); + })); + + const logout = new Command("logout") .description(commandDescriptions['logout']) .configureHelp({ diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 37c3899f6..5d5ad1b83 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -341,7 +341,9 @@ class Local extends Config { class Global extends Config { static CONFIG_FILE_PATH = ".{{ spec.title|caseLower }}/prefs.json"; + static PREFERENCE_CURRENT = "current"; static PREFERENCE_ENDPOINT = "endpoint"; + static PREFERENCE_EMAIL = "email"; static PREFERENCE_SELF_SIGNED = "selfSigned"; static PREFERENCE_COOKIE = "cookie"; static PREFERENCE_PROJECT = "project"; @@ -349,6 +351,8 @@ class Global extends Config { static PREFERENCE_LOCALE = "locale"; static PREFERENCE_MODE = "mode"; + static IGNORE_ATTRIBUTES = [Global.PREFERENCE_CURRENT, Global.PREFERENCE_SELF_SIGNED, Global.PREFERENCE_ENDPOINT, Global.PREFERENCE_COOKIE, Global.PREFERENCE_PROJECT, Global.PREFERENCE_KEY, Global.PREFERENCE_LOCALE, Global.PREFERENCE_MODE]; + static MODE_ADMIN = "admin"; static MODE_DEFAULT = "default"; @@ -359,59 +363,151 @@ class Global extends Config { super(`${homeDir}/${path}`); } + getCurrentLogin() { + if (!this.has(Global.PREFERENCE_CURRENT)) { + return ""; + } + return this.get(Global.PREFERENCE_CURRENT); + } + + setCurrentLogin(endpoint) { + this.set(Global.PREFERENCE_CURRENT, endpoint); + } + + getLoginIds() { + return Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)); + } + + getLogins() { + const logins = Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)) + + return logins.map((login) => { + + return { + id: login, + endpoint: this.data[login][Global.PREFERENCE_ENDPOINT], + email: this.data[login][Global.PREFERENCE_EMAIL] + } + }) + } + + addLogin(login, data) { + this.set(login, data); + } + + removeLogin(login, data) { + this.delete(login); + } + + getEmail() { + if (!this.hasFrom(Global.PREFERENCE_EMAIL)) { + return ""; + } + + return this.getFrom(Global.PREFERENCE_EMAIL); + } + + setEmail(email) { + this.setTo(Global.PREFERENCE_EMAIL, email); + } + getEndpoint() { - if (!this.has(Global.PREFERENCE_ENDPOINT)) { + if (!this.hasFrom(Global.PREFERENCE_ENDPOINT)) { return ""; } - return this.get(Global.PREFERENCE_ENDPOINT); + + return this.getFrom(Global.PREFERENCE_ENDPOINT); } setEndpoint(endpoint) { - this.set(Global.PREFERENCE_ENDPOINT, endpoint); + this.setTo(Global.PREFERENCE_ENDPOINT, endpoint); } getSelfSigned() { - if (!this.has(Global.PREFERENCE_SELF_SIGNED)) { + if (!this.hasFrom(Global.PREFERENCE_SELF_SIGNED)) { return false; } - return this.get(Global.PREFERENCE_SELF_SIGNED); + return this.getFrom(Global.PREFERENCE_SELF_SIGNED); } setSelfSigned(selfSigned) { - this.set(Global.PREFERENCE_SELF_SIGNED, selfSigned); + this.setTo(Global.PREFERENCE_SELF_SIGNED, selfSigned); } getCookie() { - if (!this.has(Global.PREFERENCE_COOKIE)) { + if (!this.hasFrom(Global.PREFERENCE_COOKIE)) { return ""; } - return this.get(Global.PREFERENCE_COOKIE); + return this.getFrom(Global.PREFERENCE_COOKIE); } setCookie(cookie) { - this.set(Global.PREFERENCE_COOKIE, cookie); + this.setTo(Global.PREFERENCE_COOKIE, cookie); } getProject() { - if (!this.has(Global.PREFERENCE_PROJECT)) { + if (!this.hasFrom(Global.PREFERENCE_PROJECT)) { return ""; } - return this.get(Global.PREFERENCE_PROJECT); + return this.getFrom(Global.PREFERENCE_PROJECT); } setProject(project) { - this.set(Global.PREFERENCE_PROJECT, project); + this.setTo(Global.PREFERENCE_PROJECT, project); } getKey() { - if (!this.has(Global.PREFERENCE_KEY)) { + if (!this.hasFrom(Global.PREFERENCE_KEY)) { return ""; } return this.get(Global.PREFERENCE_KEY); } setKey(key) { - this.set(Global.PREFERENCE_KEY, key); + this.setTo(Global.PREFERENCE_KEY, key); + } + + hasFrom(key) { + try { + const current = this.getCurrentLogin(); + + if (current) { + const config = this.get(current); + + return config[key] !== undefined; + } + } catch { + return this.has(key); + } + } + + getFrom(key) { + try { + const current = this.getCurrentLogin(); + + if (current) { + const config = this.get(current); + + return config[key]; + } + } catch { + return this.get(key); + } + } + + setTo(key, value) { + try { + const current = this.getCurrentLogin(); + + if (current) { + const config = this.get(current); + + config[key] = value; + this.write(); + } + } catch { + this.set(key, value); + } } } diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 67b5fd8ed..4b6613648 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -515,6 +515,31 @@ const questionsMfaChallenge = [ } ]; +const questionGetEndpoint = [ + { + type: "input", + name: "endpoint", + message: "Enter the endpoint of your {{ spec.title|caseUcfirst }} server", + default: "http://localhost/v1", + async validate(value) { + if (!value) { + return "Please enter a valid endpoint."; + } + let client = new Client().setEndpoint(value); + try { + let response = await client.call('get', '/health/version'); + if (response.version) { + return true; + } else { + throw new Error(); + } + } catch (error) { + return "Invalid endpoint or your Appwrite server is not running as expected."; + } + } + } +]; + module.exports = { questionsPullProject, questionsLogin, @@ -528,5 +553,6 @@ module.exports = { questionsPushTeams, questionsGetEntrypoint, questionsListFactors, - questionsMfaChallenge + questionsMfaChallenge, + questionGetEndpoint }; diff --git a/templates/cli/lib/sdks.js.twig b/templates/cli/lib/sdks.js.twig index e3f5f38dc..2e615f706 100644 --- a/templates/cli/lib/sdks.js.twig +++ b/templates/cli/lib/sdks.js.twig @@ -99,4 +99,5 @@ const sdkForProject = async () => { module.exports = { sdkForConsole, sdkForProject, + questionGetEndpoint, }; From 2b5a168e4df4def55d7dc8493560e48fcd4610a5 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 12:18:14 -0400 Subject: [PATCH 03/89] feat(cli): Console flow add an open flag --- templates/cli/base/requests/api.twig | 2 +- templates/cli/lib/commands/command.js.twig | 5 +++-- templates/cli/lib/utils.js.twig | 14 +++++--------- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/templates/cli/base/requests/api.twig b/templates/cli/base/requests/api.twig index 5a611dd92..f1611ad28 100644 --- a/templates/cli/base/requests/api.twig +++ b/templates/cli/base/requests/api.twig @@ -20,7 +20,7 @@ {%~ if method.name == 'get' and service.name not in ['health','migrations','locale'] %} if(console) { showConsoleLink('{{service.name}}', 'get' - {%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%}{%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%} + {%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%},open {%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%} ); } else { parse(response) diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 193014db7..13dee1543 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -70,7 +70,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%} {%- if method.type == 'location' -%}, destination{%- endif -%} - {%- if method.name == 'get' -%}, console{%- endif -%} + {%- if method.name == 'get' -%}, console, open{%- endif -%} }) => { {%~ endblock %} let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : @@ -96,7 +96,8 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ .requiredOption(`--destination `, `output file path.`) {% endif %} {% if method.name == 'get' %} - .option(`--console`, `View this resource in the console`) + .option(`--console`, `Get the resource console url`) + .option(`--open`, `Use with '--console' to open the using default browser`) {% endif %} {% endautoescape %} .action(actionRunner({{ service.name | caseLower }}{{ method.name | caseUcfirst }})) diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 63c1758b0..3f12e0d31 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -18,7 +18,7 @@ function getAllFiles(folder) { return files; } -function showConsoleLink(serviceName, action, id = '') { +function showConsoleLink(serviceName, action, open, id = '') { let resource = ''; let service = ''; @@ -58,17 +58,13 @@ function showConsoleLink(serviceName, action, id = '') { const middle = resource !== '' ? `/project-${projectId}` : ''; const url = `${baseUrl}/console${middle}/${service}${end}` - const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open'); - const key = (process.platform == 'darwin' ? 'Return' : 'Enter'); - success(`\n ${url}\n Press <${key}> to open URL in your default browser, exising in 3 seconds`); - setTimeout(() => process.exit(0), 3000); + success(url); - const read = readline.createInterface({ input: process.stdin, output: process.stdout }); - read.on('line', () => { + if (open) { + const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open'); cp.exec(`${start} ${url}`); - setTimeout(() => process.exit(0), 250); - }); + } } module.exports = { From fd78fadfbcf3e28ecae912f0a03ce4754d45554a Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 12:59:02 -0400 Subject: [PATCH 04/89] feat(cli): Hooking migration before any command --- templates/cli/index.js.twig | 3 +- templates/cli/lib/commands/generic.js.twig | 53 ++++++++++------------ templates/cli/lib/config.js.twig | 39 ++++++---------- 3 files changed, 41 insertions(+), 54 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 2126485c6..bfa4eaba7 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -11,7 +11,7 @@ const { version } = require("./package.json"); const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} -const { login, logout, whoami } = require("./lib/commands/generic"); +const { login, logout, whoami, migrate } = require("./lib/commands/generic"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} @@ -28,6 +28,7 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") + .hook('preAction',migrate) .on("option:json", () => { cliConfig.json = true; }) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 6f96db940..5bc2093ad 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -3,7 +3,7 @@ const { Command } = require("commander"); const Client = require("../client"); const { sdkForConsole, questionGetEndpoint } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); -const { actionRunner, success, parseBool, commandDescriptions, error, parse, drawTable } = require("../parser"); +const { actionRunner, success, parseBool, commandDescriptions, error, parse,log, drawTable } = require("../parser"); {% if sdk.test != "true" %} const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); @@ -161,33 +161,6 @@ login success(`Current account is ${accountId}`); })); -login - .command('migrate') - .description("Migrate existing login to new scheme") - .action(actionRunner(async ({ accountId }) => { - const endpoint = globalConfig.getEndpoint(); - const cookie = globalConfig.getCookie(); - - if (endpoint === '' || cookie === '') { - throw Error(`Couldn't find any existing account credentials`) - } - - const id = ID.unique(); - const data = { - endpoint, - cookie, - email: 'legacy' - }; - - globalConfig.addLogin(id, data); - globalConfig.setCurrentLogin(id); - globalConfig.delete('endpoint'); - globalConfig.delete('cookie'); - - success(`Account was migrated and it's the current account`); - })); - - const logout = new Command("logout") .description(commandDescriptions['logout']) .configureHelp({ @@ -280,11 +253,35 @@ const client = new Command("client") success() })); +const migrate = async ()=>{ + if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) { + return; + } + + const endpoint = globalConfig.get('endpoint'); + const cookie = globalConfig.get('cookie'); + + log("Old Appwrite login settings were detected, migrating..."); + const id = ID.unique(); + const data = { + endpoint, + cookie, + email: 'legacy' + }; + + globalConfig.addLogin(id, data); + globalConfig.setCurrentLogin(id); + globalConfig.delete('endpoint'); + globalConfig.delete('cookie'); + + success(`Account was migrated and it's the current account`); +} module.exports = { {% if sdk.test != "true" %} whoami, login, logout, + migrate, {% endif %} client }; diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 5d5ad1b83..fb38e40e7 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -467,46 +467,35 @@ class Global extends Config { this.setTo(Global.PREFERENCE_KEY, key); } + hasFrom(key) { - try { - const current = this.getCurrentLogin(); + const current = this.getCurrentLogin(); - if (current) { - const config = this.get(current); + if (current) { + const config = this.get(current); - return config[key] !== undefined; - } - } catch { - return this.has(key); + return config[key] !== undefined; } } getFrom(key) { - try { - const current = this.getCurrentLogin(); + const current = this.getCurrentLogin(); - if (current) { - const config = this.get(current); + if (current) { + const config = this.get(current); - return config[key]; - } - } catch { - return this.get(key); + return config[key]; } } setTo(key, value) { - try { - const current = this.getCurrentLogin(); + const current = this.getCurrentLogin(); - if (current) { - const config = this.get(current); + if (current) { + const config = this.get(current); - config[key] = value; - this.write(); - } - } catch { - this.set(key, value); + config[key] = value; + this.write(); } } } From 9387d95a457e5fa9e4781eae0c427bbcf25427f3 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 13:37:42 -0400 Subject: [PATCH 05/89] feat(cli): Interactive multiple account changing --- templates/cli/lib/commands/generic.js.twig | 33 +++++++-------- templates/cli/lib/questions.js.twig | 49 +++++++++++++++++++--- 2 files changed, 59 insertions(+), 23 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 5bc2093ad..b237b2565 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -3,7 +3,7 @@ const { Command } = require("commander"); const Client = require("../client"); const { sdkForConsole, questionGetEndpoint } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); -const { actionRunner, success, parseBool, commandDescriptions, error, parse,log, drawTable } = require("../parser"); +const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); {% if sdk.test != "true" %} const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); @@ -13,6 +13,20 @@ const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; const loginCommand = async ({ selfHosted }) => { const answers = await inquirer.prompt(questionsLogin); + + if (answers.method === 'select') { + const accountId = answers.accountId; + + if (!globalConfig.getLoginIds().includes(accountId)) { + throw Error('Login ID not found'); + } + + globalConfig.setCurrentLogin(accountId); + success(`Current account is ${accountId}`); + + return; + } + const oldCurrent = globalConfig.getCurrentLogin(); const id = ID.unique(); @@ -146,21 +160,6 @@ login })); -login - .command('change') - .description("Change the current account") - .option(`-a, --accountId `, `Login ID`) - .action(actionRunner(async ({ accountId }) => { - const loginIds = globalConfig.getLoginIds(); - - if (!loginIds.includes(accountId)) { - throw Error('Login ID not found'); - } - - globalConfig.setCurrentLogin(accountId); - success(`Current account is ${accountId}`); - })); - const logout = new Command("logout") .description(commandDescriptions['logout']) .configureHelp({ @@ -253,7 +252,7 @@ const client = new Command("client") success() })); -const migrate = async ()=>{ +const migrate = async () => { if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) { return; } diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 4b6613648..7e64252e1 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -1,4 +1,5 @@ -const { localConfig } = require('./config'); +const chalk = require("chalk"); +const { localConfig, globalConfig } = require('./config'); const { projectsList } = require('./commands/projects'); const { teamsList } = require('./commands/teams'); const { functionsListRuntimes } = require('./commands/functions'); @@ -126,7 +127,7 @@ const questionsPullProject = [ message: "Choose the project organization", choices: async () => { let client = await sdkForConsole(true); - const { teams } = await paginate(teamsList, { parseOutput: false , sdk: client}, 100, 'teams'); + const { teams } = await paginate(teamsList, { parseOutput: false, sdk: client }, 100, 'teams'); let choices = teams.map((team, idx) => { return { @@ -194,7 +195,7 @@ const questionsPullProject = [ choices: async (answers) => { let response = await projectsList({ parseOutput: false, - queries: [JSON.stringify({ method: 'equal', attribute:'teamId', values: [answers.organization.id] })], + queries: [JSON.stringify({ method: 'equal', attribute: 'teamId', values: [answers.organization.id] })], }) let projects = response["projects"] let choices = projects.map((project, idx) => { @@ -245,7 +246,7 @@ const questionsPullFunction = [ id: runtime['$id'], entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), - commands : getInstallCommand(runtime['$id']) + commands: getInstallCommand(runtime['$id']) }, } }) @@ -280,6 +281,16 @@ const questionsPullCollection = [ ]; const questionsLogin = [ + { + type: "list", + name: "method", + message: "You're already logged in, what you like to do?", + choices: [ + { name: 'Login to a different account', value: 'login' }, + { name: 'Change to a different existed account', value: 'select' } + ], + when: () => globalConfig.getCurrentLogin() !== '' + }, { type: "input", name: "email", @@ -290,6 +301,7 @@ const questionsLogin = [ } return true; }, + when: (answers) => answers.method === 'login' }, { type: "password", @@ -301,8 +313,33 @@ const questionsLogin = [ return "Please enter your password"; } return true; - } + }, + when: (answers) => answers.method === 'login' }, + { + type: "list", + name: "accountId", + message: "Select an account to switch to", + choices() { + const logins = globalConfig.getLogins(); + const current = globalConfig.getCurrentLogin(); + + const data = []; + + const longestEmail = logins.reduce((prev, current) => (prev && prev.email > current.email) ? prev : current).email.length; + + logins.forEach((login) => { + data.push({ + value: login.id, + name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('In use') : ' '.repeat(6)} ${login.endpoint}`, + }); + }) + + return data; + }, + when: (answers) => answers.method === 'select' + }, + ]; const questionsPushResources = [ @@ -494,7 +531,7 @@ const questionsListFactors = [ name: `Recovery code`, value: 'recoveryCode' } - ].filter((ch) => factors[ch.value] === true); + ].filter((ch) => factors[ch.value] === true); return choices; } From d25000374038bcb733871d053c9a9620073c8b99 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 13:43:35 -0400 Subject: [PATCH 06/89] feat(cli): Adding json option to list accounts --- templates/cli/lib/commands/generic.js.twig | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index b237b2565..04069c067 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -143,7 +143,8 @@ const login = new Command("login") login .command('list') .description("List available logged accounts.") - .action(actionRunner(async () => { + .option("-j, --json", "Output in JSON format") + .action(actionRunner(async ({ json }) => { const logins = globalConfig.getLogins(); const current = globalConfig.getCurrentLogin(); @@ -151,11 +152,15 @@ login return { 'Current': login.id === current ? '*' : '', 'ID': login.id, - 'Endpoint': login.endpoint, - 'Email': login.email + 'Email': login.email, + 'Endpoint': login.endpoint }; }))]; + if (json) { + console.log(data); + return; + } drawTable(data); })); From 144e043628f648a7fdf2bf008a437e3941e7c195 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 14:19:16 -0400 Subject: [PATCH 07/89] feat(cli): Interactive multiple user logout --- templates/cli/lib/commands/generic.js.twig | 64 +++++++++++++++++++--- templates/cli/lib/questions.js.twig | 43 +++++++++++++++ 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 04069c067..92598f496 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -5,7 +5,7 @@ const { sdkForConsole, questionGetEndpoint } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); {% if sdk.test != "true" %} -const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions"); +const { questionsLogin, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); const ID = require("../id"); @@ -165,12 +165,8 @@ login })); -const logout = new Command("logout") - .description(commandDescriptions['logout']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }) - .action(actionRunner(async () => { +const singleLogout = async (accountId) => { + try { let client = await sdkForConsole(); await accountDeleteSession({ @@ -179,8 +175,58 @@ const logout = new Command("logout") sdk: client }) - globalConfig.setCookie(""); - success() + globalConfig.removeLogin(accountId); + } catch (e) { + error('Unable to log out, removing locally saved session information') + } + globalConfig.removeLogin(accountId); +} + +const logout = new Command("logout") + .description(commandDescriptions['logout']) + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(async () => { + const logins = globalConfig.getLogins(); + const current = globalConfig.getCurrentLogin(); + + if (current === '') { + return; + } + if (logins.length === 1) { + await singleLogout(current); + success(); + + return; + } + + const answers = await inquirer.prompt(questionsLogout); + const accountIds = []; + + if (answers.method === 'all') { + accountIds.push(...logins.map(login => login.id)); + } + + if (answers.method === 'selected' && answers.accounts) { + accountIds.push(...answers.accounts); + } + + for (let accountId of accountIds) { + globalConfig.setCurrentLogin(accountId); + await singleLogout(accountId); + } + + const leftLogins = globalConfig.getLogins(); + + if (leftLogins.length > 0 && leftLogins.filter(login => login.id === current).length !== 1) { + const accountId = leftLogins[0].id; + globalConfig.setCurrentLogin(accountId); + + success(`Current account is ${accountId}`); + } + + success(); })); {% endif %} diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 7e64252e1..fadd3feb0 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -342,6 +342,48 @@ const questionsLogin = [ ]; +const questionsLogout = [ + { + type: "list", + name: "method", + message: "Which account would you like to logout from?", + choices() { + const logins = globalConfig.getLogins(); + + return [ + { name: 'Account in use' }, + { name: 'Selected accounts', value: 'selected' }, + { name: 'All accounts', value: 'all' } + ]; + }, + when: () => globalConfig.getCurrentLogin() !== '' + }, + { + type: "checkbox", + name: "accounts", + message: "Select accounts to logout from", + when: (answers) => answers.method === 'selected', + validate: (value) => validateRequired('account', value), + choices() { + const logins = globalConfig.getLogins(); + const current = globalConfig.getCurrentLogin(); + + const data = []; + + const longestEmail = logins.reduce((prev, current) => (prev && prev.email > current.email) ? prev : current).email.length; + + logins.forEach((login) => { + data.push({ + value: login.id, + name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('In use') : ' '.repeat(6)} ${login.endpoint}`, + }); + }) + + return data; + } + } +]; + const questionsPushResources = [ { type: "checkbox", @@ -580,6 +622,7 @@ const questionGetEndpoint = [ module.exports = { questionsPullProject, questionsLogin, + questionsLogout, questionsPullFunction, questionsPullCollection, questionsPushResources, From 9a61aefbfbea5de889b09e9984f77e3189979e05 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 14:37:32 -0400 Subject: [PATCH 08/89] fix(cli): Changing migrate to regular function --- templates/cli/lib/commands/generic.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 92598f496..f90739c5f 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -303,7 +303,7 @@ const client = new Command("client") success() })); -const migrate = async () => { +async function migrate() { if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) { return; } From 0cf5edd9486df3e8581152e7e815f35d89a53929 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 14:50:43 -0400 Subject: [PATCH 09/89] fix(cli): Login question --- templates/cli/lib/questions.js.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index fadd3feb0..56244dea0 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -301,7 +301,7 @@ const questionsLogin = [ } return true; }, - when: (answers) => answers.method === 'login' + when: (answers) => answers.method !== 'select' }, { type: "password", @@ -314,7 +314,7 @@ const questionsLogin = [ } return true; }, - when: (answers) => answers.method === 'login' + when: (answers) => answers.method !== 'select' }, { type: "list", From 7c07523ab52b6acc26c4e363a290fb18aa160459 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 14:58:07 -0400 Subject: [PATCH 10/89] fix(cli): Extracting migrate --- templates/cli/index.js.twig | 2 +- templates/cli/lib/commands/generic.js.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index bfa4eaba7..578eb9204 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -28,7 +28,7 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") - .hook('preAction',migrate) + .hook('preAction', (c, a) => migrate()) .on("option:json", () => { cliConfig.json = true; }) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index f90739c5f..92598f496 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -303,7 +303,7 @@ const client = new Command("client") success() })); -async function migrate() { +const migrate = async () => { if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) { return; } From 58df0eeb6b056874da95c0728ede76cd6f823d95 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 15:00:24 -0400 Subject: [PATCH 11/89] fix(cli): Renaming the migrate function name --- templates/cli/index.js.twig | 4 ++-- templates/cli/lib/commands/generic.js.twig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 578eb9204..1d7e0b86a 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -11,7 +11,7 @@ const { version } = require("./package.json"); const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} -const { login, logout, whoami, migrate } = require("./lib/commands/generic"); +const { login, logout, whoami, migrateFromOldVersion } = require("./lib/commands/generic"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} @@ -28,7 +28,7 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") - .hook('preAction', (c, a) => migrate()) + .hook('preAction', (c, a) => migrateFromOldVersion()) .on("option:json", () => { cliConfig.json = true; }) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 92598f496..383d5d2d0 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -303,7 +303,7 @@ const client = new Command("client") success() })); -const migrate = async () => { +const migrateFromOldVersion = async () => { if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) { return; } @@ -331,7 +331,7 @@ module.exports = { whoami, login, logout, - migrate, + migrateFromOldVersion, {% endif %} client }; From 8c7700416f749651dbba974c4a4b83b04b970058 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 15:20:00 -0400 Subject: [PATCH 12/89] fix(cli): Excluding migrate from tests --- templates/cli/index.js.twig | 6 ++++-- templates/cli/lib/commands/generic.js.twig | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 1d7e0b86a..99883c72f 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -11,7 +11,7 @@ const { version } = require("./package.json"); const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} -const { login, logout, whoami, migrateFromOldVersion } = require("./lib/commands/generic"); +const { login, logout, whoami, migrate } = require("./lib/commands/generic"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} @@ -28,7 +28,9 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") - .hook('preAction', (c, a) => migrateFromOldVersion()) +{% if sdk.test != "true" %} + .hook('preAction', migrate) +{% endif %} .on("option:json", () => { cliConfig.json = true; }) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 383d5d2d0..92598f496 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -303,7 +303,7 @@ const client = new Command("client") success() })); -const migrateFromOldVersion = async () => { +const migrate = async () => { if (!globalConfig.has('endpoint') || !globalConfig.has('cookie')) { return; } @@ -331,7 +331,7 @@ module.exports = { whoami, login, logout, - migrateFromOldVersion, + migrate, {% endif %} client }; From c1e5c4128132dc5c8d88adde8650127cc18488c9 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 15:23:45 -0400 Subject: [PATCH 13/89] fix(cli): Adding migrate to tests --- templates/cli/index.js.twig | 6 ++++-- templates/cli/lib/commands/generic.js.twig | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 99883c72f..f7ca52b9b 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,6 +12,10 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} const { login, logout, whoami, migrate } = require("./lib/commands/generic"); +{% else %} +const { migrate } = require("./lib/commands/generic"); +{% endif %} +{% if sdk.test != "true" %} const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} @@ -28,9 +32,7 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") -{% if sdk.test != "true" %} .hook('preAction', migrate) -{% endif %} .on("option:json", () => { cliConfig.json = true; }) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 92598f496..6ba682eed 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -331,7 +331,7 @@ module.exports = { whoami, login, logout, - migrate, {% endif %} + migrate, client }; From ea494b76c3e0f9b911de1bd887c1a3b98246f45d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 16:15:39 -0400 Subject: [PATCH 14/89] fix(cli): Adapting the `client` command and bug when getting key. --- templates/cli/lib/commands/generic.js.twig | 17 ++++++++++------- templates/cli/lib/config.js.twig | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 6ba682eed..4c31749d5 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -4,10 +4,10 @@ const Client = require("../client"); const { sdkForConsole, questionGetEndpoint } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); +const ID = require("../id"); {% if sdk.test != "true" %} const { questionsLogin, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); -const ID = require("../id"); const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; @@ -259,6 +259,7 @@ const client = new Command("client") if (endpoint !== undefined) { try { + const id = ID.unique(); let url = new URL(endpoint); if (url.protocol !== "http:" && url.protocol !== "https:") { throw new Error(); @@ -273,7 +274,8 @@ const client = new Command("client") if (!response.version) { throw new Error(); } - + globalConfig.setCurrentLogin(id); + globalConfig.addLogin(id, {}); globalConfig.setEndpoint(endpoint); } catch (_) { throw new Error("Invalid endpoint or your Appwrite server is not running as expected."); @@ -293,11 +295,12 @@ const client = new Command("client") } if (reset !== undefined) { - globalConfig.setEndpoint(""); - globalConfig.setKey(""); - globalConfig.setCookie(""); - globalConfig.setSelfSigned(""); - localConfig.setProject("", ""); + const logins = globalConfig.getLogins(); + + for (let accountId of logins.map(login => login.id)) { + globalConfig.setCurrentLogin(accountId); + await singleLogout(accountId); + } } success() diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index fb38e40e7..0d377f741 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -460,7 +460,7 @@ class Global extends Config { if (!this.hasFrom(Global.PREFERENCE_KEY)) { return ""; } - return this.get(Global.PREFERENCE_KEY); + return this.getFrom(Global.PREFERENCE_KEY); } setKey(key) { From 094e8dd1f73303fd4d0c24bac70d8347f566420a Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 16:47:45 -0400 Subject: [PATCH 15/89] feat(cli): Non destructive db, starting --- templates/cli/lib/commands/push.js.twig | 72 +++++++++++-------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 8e269cdd4..d16c9b1ca 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -787,29 +787,18 @@ const pushCollection = async ({ all, yes } = {}) => { throw e; } } - - // Create all non-relationship attributes first const attributes = collection.attributes.filter(attribute => attribute.type !== 'relationship'); + const relationshipAttributes = collection.attributes.filter(attribute => attribute.type === 'relationship' && attribute.side === 'parent'); - await Promise.all(attributes.map(attribute => { - return createAttribute(databaseId, collection['$id'], attribute); - })); - - let result = await awaitPools.expectAttributes( - databaseId, - collection['$id'], - attributes.map(attribute => attribute.key) - ); - - if (!result) { - throw new Error("Attribute creation timed out."); + try { + await createAttributes(attributes, 'attributes') + } catch (e) { + throw e; } - success(`Created ${attributes.length} non-relationship attributes`); - log(`Creating indexes ...`) - await Promise.all(collection.indexes.map(async index => { + for (let index of collections.indexes) { await databasesCreateIndex({ databaseId, collectionId: collection['$id'], @@ -819,7 +808,7 @@ const pushCollection = async ({ all, yes } = {}) => { orders: index.orders, parseOutput: false }); - })); + } result = await awaitPools.expectIndexes( databaseId, @@ -831,39 +820,40 @@ const pushCollection = async ({ all, yes } = {}) => { throw new Error("Index creation timed out."); } - success(`Created ${collection.indexes.length} indexes`); + for (let attribute of collection.attributes) { + await createAttribute(databaseId, collection['$id'], attribute); + } - success(`Pushed ${collection.name} ( ${collection['$id']} )`); - } + success(`Created ${collection.indexes.length} indexes`); - // Create the relationship attributes - for (let collection of collections) { - const relationships = collection.attributes.filter(attribute => - attribute.type === 'relationship' && attribute.side === 'parent' - ); - if (relationships.length === 0) { - continue; + try { + await createAttributes(relationshipAttributes, 'relationship attributes') + } catch (e) { + throw e; } - log(`Pushing relationships for collection ${collection.name} ( ${collection['$id']} )`); - await Promise.all(relationships.map(attribute => { - return createAttribute(collection['databaseId'], collection['$id'], attribute); - })); + success(`Pushed ${collection.name} ( ${collection['$id']} )`); + } +} - let result = await awaitPools.expectAttributes( - collection['databaseId'], - collection['$id'], - relationships.map(attribute => attribute.key) - ); +const createAttributes = async (attributes, title) => { + for (let attribute of attributes) { + await createAttribute(databaseId, collection['$id'], attribute); + } - if (!result) { - throw new Error("Attribute creation timed out."); - } + let result = await awaitPools.expectAttributes( + databaseId, + collection['$id'], + collection.attributes.map(attribute => attribute.key) + ); - success(`Created ${relationships.length} relationship attributes`); + if (!result) { + throw new Error(`Attribute creation timed out.`); } + + success(`Created ${attributes.length} ${title}`); } const pushBucket = async ({ all, yes } = {}) => { From 4d14115c82f486f7a76062f4a39f93ba906b5c71 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 23 May 2024 13:31:12 -0400 Subject: [PATCH 16/89] feat(cli): Adding report flag --- templates/cli/index.js.twig | 4 ++++ templates/cli/lib/parser.js.twig | 13 ++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 47725d266..8013a009e 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -28,12 +28,16 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") + .option("--report", "Enable reporting when cli is crashing") .on("option:json", () => { cliConfig.json = true; }) .on("option:verbose", () => { cliConfig.verbose = true; }) + .on("option:report", () => { + cliConfig.report = true; + }) .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(login) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 5dde5aa3e..a756ead41 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -5,7 +5,8 @@ const { description } = require('../package.json'); const cliConfig = { verbose: false, - json: false + json: false, + report: false }; const parse = (data) => { @@ -110,12 +111,18 @@ const drawJSON = (data) => { } const parseError = (err) => { + if(cliConfig.report) { + const githubIssueUrl=`https://github.com/appwrite/appwrite/issues/new?labels=bug&template=bug.yaml&title=%F0%9F%90%9B+Bug+Report%3A+&actual-behavior=Console%20Crash%0A${encodeURIComponent('```\n'+err.stack+'\n```').replaceAll('(','').replaceAll(')','')}` + + log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl}\n`); + } + if(cliConfig.verbose) { console.error(err); + } else { + error(err.message); } - error(err.message); - process.exit(1) } From 25d703290a7070022b409684147a18dc2f5a51ac Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 23 May 2024 14:09:22 -0400 Subject: [PATCH 17/89] refactor(cli): Refactoring sudo and make it globally available --- templates/cli/index.js.twig | 12 ++++++ templates/cli/lib/commands/pull.js.twig | 7 ++-- templates/cli/lib/commands/push.js.twig | 55 ++++++++++--------------- templates/cli/lib/parser.js.twig | 5 ++- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 47725d266..9560d6dd1 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -28,12 +28,24 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") + .option("-f,--force", "Flag to confirm all warnings") + .option("-a,--all", "Flag to push all resources") + .option("--ids [id...]", "Flag to pass list of ids for a giving action") .on("option:json", () => { cliConfig.json = true; }) .on("option:verbose", () => { cliConfig.verbose = true; }) + .on("option:force", () => { + cliConfig.force = true; + }) + .on("option:all", () => { + cliConfig.all = true; + }) + .on("option:ids", function() { + cliConfig.ids = this.opts().ids; + }) .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(login) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 41c2ea9d1..e0e17c7cb 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -14,7 +14,7 @@ const { localConfig } = require("../config"); const ID = require("../id"); const { paginate } = require("../paginate"); const { questionsPullProject, questionsPullFunction, questionsPullCollection } = require("../questions"); -const { success, log, actionRunner, commandDescriptions } = require("../parser"); +const { cliConfig, success, log, actionRunner, commandDescriptions } = require("../parser"); const pull = new Command("pull") .description(commandDescriptions['pull']) @@ -164,12 +164,12 @@ const pullFunction = async () => { success(); } -const pullCollection = async ({ all, databaseId } = {}) => { +const pullCollection = async ({ databaseId } = {}) => { const databaseIds = []; if (databaseId) { databaseIds.push(databaseId); - } else if (all) { + } else if (cliConfig.all) { let allDatabases = await databasesList({ parseOutput: false }) @@ -265,7 +265,6 @@ pull .command("collection") .description("Pulling your {{ spec.title|caseUcfirst }} collections") .option(`--databaseId `, `Database ID`) - .option(`--all`, `Flag to pullialize all databases`) .action(actionRunner(pullCollection)) pull diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 8e269cdd4..e3efe8be8 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -5,7 +5,7 @@ const { localConfig, globalConfig } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics } = require("../questions"); -const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); +const { cliConfig, actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { databasesGet, @@ -232,7 +232,7 @@ const awaitPools = { }, } -const pushResources = async ({ all, yes } = {}) => { +const pushResources = async () => { const actions = { functions: pushFunction, collections: pushCollection, @@ -241,27 +241,28 @@ const pushResources = async ({ all, yes } = {}) => { messages: pushMessagingTopic } - if (all) { - Object.values(actions).forEach(action => action({ all: true, yes })); + if (cliConfig.all) { + Object.values(actions).forEach(action => action()); } else { const answers = await inquirer.prompt(questionsPushResources[0]); + answers.resources.forEach((resource) => { const action = actions[resource]; if (action !== undefined) { - action({ all: true, yes }); + action(); } }) } }; -const pushFunction = async ({ functionId, all, yes, async } = {}) => { +const pushFunction = async ({ functionId, async } = {}) => { let response = {}; const functionIds = []; if (functionId) { functionIds.push(functionId); - } else if (all) { + } else if (cliConfig.all) { const functions = localConfig.getFunctions(); if (functions.length === 0) { throw new Error("No functions found in the current directory."); @@ -299,7 +300,7 @@ const pushFunction = async ({ functionId, all, yes, async } = {}) => { } if (func.variables) { - func.pushVariables = yes; + func.pushVariables = cliConfig.force; try { const { total } = await functionsListVariables({ @@ -645,12 +646,12 @@ const createAttribute = async (databaseId, collectionId, attribute) => { } } -const pushCollection = async ({ all, yes } = {}) => { +const pushCollection = async () => { let response = {}; const collections = []; - if (all) { + if (cliConfig.all) { if (localConfig.getCollections().length === 0) { throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} pull collection` to fetch all your collections."); } @@ -712,7 +713,7 @@ const pushCollection = async ({ all, yes } = {}) => { log(`Collection ${collection.name} ( ${collection['$id']} ) already exists.`); - if (!yes) { + if (!cliConfig.force) { const answers = await inquirer.prompt(questionsPushCollections[1]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${collection.name} ( ${collection['$id']} )`); @@ -866,13 +867,13 @@ const pushCollection = async ({ all, yes } = {}) => { } } -const pushBucket = async ({ all, yes } = {}) => { +const pushBucket = async () => { let response = {}; let bucketIds = []; const configBuckets = localConfig.getBuckets(); - if (all) { + if (cliConfig.all) { if (configBuckets.length === 0) { throw new Error("No buckets found in the current directory. Run `appwrite pull bucket` to fetch all your buckets."); } @@ -901,7 +902,7 @@ const pushBucket = async ({ all, yes } = {}) => { }) log(`Bucket ${bucket.name} ( ${bucket['$id']} ) already exists.`); - if (!yes) { + if (!cliConfig.force) { const answers = await inquirer.prompt(questionsPushBuckets[1]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${bucket.name} ( ${bucket['$id']} )`); @@ -953,13 +954,13 @@ const pushBucket = async ({ all, yes } = {}) => { } } -const pushTeam = async ({ all, yes } = {}) => { +const pushTeam = async () => { let response = {}; let teamIds = []; const configTeams = localConfig.getTeams(); - if (all) { + if (cliConfig.all) { if (configTeams.length === 0) { throw new Error("No teams found in the current directory. Run `appwrite pull team` to fetch all your teams."); } @@ -988,7 +989,7 @@ const pushTeam = async ({ all, yes } = {}) => { }) log(`Team ${team.name} ( ${team['$id']} ) already exists.`); - if (!yes) { + if (!cliConfig.force) { const answers = await inquirer.prompt(questionsPushTeams[1]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${team.name} ( ${team['$id']} )`); @@ -1023,14 +1024,14 @@ const pushTeam = async ({ all, yes } = {}) => { } } -const pushMessagingTopic = async ({ all, yes } = {}) => { +const pushMessagingTopic = async () => { let response = {}; let topicsIds = []; const configTopics = localConfig.getMessagingTopics(); - let overrideExisting = yes; + let overrideExisting = cliConfig.force; - if (all) { + if (cliConfig.all) { if (configTopics.length === 0) { throw new Error("No topics found in the current directory. Run `appwrite pull topics` to pull all your messaging topics."); } @@ -1049,7 +1050,7 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { topics.push(...idTopic); } - if (!yes) { + if (!cliConfig.force) { const answers = await inquirer.prompt(questionsPushMessagingTopics[1]) if (answers.override.toLowerCase() === "yes") { overrideExisting = true; @@ -1102,45 +1103,33 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { const push = new Command("push") .description(commandDescriptions['push']) - .option(`--all`, `Flag to push all resources`) - .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushResources)); push .command("function") .description("Push functions in the current directory.") .option(`--functionId `, `Function ID`) - .option(`--all`, `Flag to push all functions`) - .option(`--yes`, `Flag to confirm all warnings`) .option(`--async`, `Don't wait for functions deployments status`) .action(actionRunner(pushFunction)); push .command("collection") .description("Push collections in the current project.") - .option(`--all`, `Flag to push all collections`) - .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushCollection)); push .command("bucket") .description("Push buckets in the current project.") - .option(`--all`, `Flag to push all buckets`) - .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushBucket)); push .command("team") .description("Push teams in the current project.") - .option(`--all`, `Flag to push all teams`) - .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushTeam)); push .command("topic") .description("Push messaging topics in the current project.") - .option(`--all`, `Flag to deploy all topics`) - .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushMessagingTopic)); module.exports = { diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 5dde5aa3e..ea001ec8e 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -5,7 +5,10 @@ const { description } = require('../package.json'); const cliConfig = { verbose: false, - json: false + json: false, + force: false, + all: false, + ids: [] }; const parse = (data) => { From ed760756ac0f87adc4cc5d40c59b2b04025b6db7 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 23 May 2024 15:27:22 -0400 Subject: [PATCH 18/89] refactor(cli): Parallel DB and Collections creation, attribute in serialize --- templates/cli/lib/commands/push.js.twig | 192 +++++++++--------------- 1 file changed, 68 insertions(+), 124 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index d16c9b1ca..be6048e6f 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -646,8 +646,6 @@ const createAttribute = async (databaseId, collectionId, attribute) => { } const pushCollection = async ({ all, yes } = {}) => { - let response = {}; - const collections = []; if (all) { @@ -666,116 +664,62 @@ const pushCollection = async ({ all, yes } = {}) => { collections.push(collection); }) } + const databases = Array.from(new Set(collections.map(c => c['databaseId']))); - for (let collection of collections) { - log(`Pushing collection ${collection.name} ( ${collection['databaseId']} - ${collection['$id']} )`) - - let databaseId; - - const localDatabase = localConfig.getDatabase(collection.databaseId); + // Parallel db action + await Promise.all(databases.map(async (dbId) => { + const localDatabase = localConfig.getDatabase(dbId); try { const database = await databasesGet({ - databaseId: collection.databaseId, + databaseId: dbId, parseOutput: false, }); - databaseId = database.$id; - - if (database.name !== (localDatabase.name ?? collection.databaseId)) { + if (database.name !== (localDatabase.name ?? dbId)) { await databasesUpdate({ - databaseId: collection.databaseId, - name: localDatabase.name ?? collection.databaseId, + databaseId: dbId, + name: localDatabase.name ?? dbId, parseOutput: false }) - success(`Updated ${localDatabase.name} ( ${collection.databaseId} )`); + success(`Updated ${localDatabase.name} ( ${dbId} ) name`); } } catch (err) { - log(`Database ${collection.databaseId} not found. Creating it now...`); + log(`Database ${dbId} not found. Creating it now...`); - const database = await databasesCreate({ - databaseId: collection.databaseId, - name: localDatabase.name ?? collection.databaseId, + await databasesCreate({ + databaseId: dbId, + name: localDatabase.name ?? dbId, parseOutput: false, }); - - databaseId = database.$id; } + })); + // Parallel collection action + await Promise.all(collections.map(async (collection) => { try { - response = await databasesGetCollection({ - databaseId, + const remoteCollection = await databasesGetCollection({ + databaseId: collection['databaseId'], collectionId: collection['$id'], parseOutput: false, - }) - - log(`Collection ${collection.name} ( ${collection['$id']} ) already exists.`); - - if (!yes) { - const answers = await inquirer.prompt(questionsPushCollections[1]) - if (answers.override.toLowerCase() !== "yes") { - log(`Received "${answers.override}". Skipping ${collection.name} ( ${collection['$id']} )`); - continue; - } - } - - log(`Deleting indexes and attributes ... `); - - const { indexes } = await paginate(databasesListIndexes, { - databaseId, - collectionId: collection['$id'], - parseOutput: false - }, 100, 'indexes'); - - await Promise.all(indexes.map(async index => { - await databasesDeleteIndex({ - databaseId, - collectionId: collection['$id'], - key: index.key, - parseOutput: false - }); - })); - - let result = await awaitPools.wipeIndexes(databaseId, collection['$id']); - if (!result) { - throw new Error("Index deletion timed out."); - } - - const { attributes } = await paginate(databasesListAttributes, { - databaseId, - collectionId: collection['$id'], - parseOutput: false - }, 100, 'attributes'); + }); - await Promise.all(attributes.map(async attribute => { - await databasesDeleteAttribute({ - databaseId, + if (remoteCollection.name !== collection.name) { + await databasesUpdateCollection({ + databaseId: collection['databaseId'], collectionId: collection['$id'], - key: attribute.key, + name: collection.name, parseOutput: false - }); - })); + }) - const deleteAttributesPoolStatus = await awaitPools.wipeAttributes(databaseId, collection['$id']); - if (!deleteAttributesPoolStatus) { - throw new Error("Attribute deletion timed out."); + success(`Updated ${collection.name} ( ${collection['$id']} ) name`); } - - await databasesUpdateCollection({ - databaseId, - collectionId: collection['$id'], - name: collection.name, - documentSecurity: collection.documentSecurity, - permissions: collection['$permissions'], - enabled: collection.enabled, - parseOutput: false - }) } catch (e) { if (e.code == 404) { log(`Collection ${collection.name} does not exist in the project. Creating ... `); - response = await databasesCreateCollection({ - databaseId, + await databasesCreateCollection({ + databaseId: collection['databaseId'], collectionId: collection['$id'], name: collection.name, documentSecurity: collection.documentSecurity, @@ -787,64 +731,64 @@ const pushCollection = async ({ all, yes } = {}) => { throw e; } } - const attributes = collection.attributes.filter(attribute => attribute.type !== 'relationship'); - const relationshipAttributes = collection.attributes.filter(attribute => attribute.type === 'relationship' && attribute.side === 'parent'); + })) + + // Serialize attribute creation + for (let collection of collections) { + log(`Pushing collection ${collection.name} ( ${collection['databaseId']} - ${collection['$id']} ) attributes`) try { - await createAttributes(attributes, 'attributes') + await createAttributes(collection.attributes, collection) } catch (e) { throw e; } - log(`Creating indexes ...`) - - for (let index of collections.indexes) { - await databasesCreateIndex({ - databaseId, - collectionId: collection['$id'], - key: index.key, - type: index.type, - attributes: index.attributes, - orders: index.orders, - parseOutput: false - }); - } - - result = await awaitPools.expectIndexes( - databaseId, - collection['$id'], - collection.indexes.map(attribute => attribute.key) - ); - - if (!result) { - throw new Error("Index creation timed out."); - } - - for (let attribute of collection.attributes) { - await createAttribute(databaseId, collection['$id'], attribute); - } - - success(`Created ${collection.indexes.length} indexes`); - - try { - await createAttributes(relationshipAttributes, 'relationship attributes') + await createIndexes(collection.indexes, collection); } catch (e) { throw e; } - success(`Pushed ${collection.name} ( ${collection['$id']} )`); } } -const createAttributes = async (attributes, title) => { +const createIndexes = async (indexes, collection) => { + log(`Creating indexes ...`) + + for (let index of indexes) { + await databasesCreateIndex({ + databaseId: collection['databaseId'], + collectionId: collection['$id'], + key: index.key, + type: index.type, + attributes: index.attributes, + orders: index.orders, + parseOutput: false + }); + } + + const result = await awaitPools.expectIndexes( + collection['databaseId'], + collection['$id'], + indexes.map(attribute => attribute.key) + ); + + if (!result) { + throw new Error("Index creation timed out."); + } + + success(`Created ${indexes.length} indexes`); +} +const createAttributes = async (attributes, collection) => { for (let attribute of attributes) { - await createAttribute(databaseId, collection['$id'], attribute); + if (attribute.side !== 'child') { + await createAttribute(collection['databaseId'], collection['$id'], attribute); + } } - let result = await awaitPools.expectAttributes( - databaseId, + const result = await awaitPools.expectAttributes( + collection['databaseId'], collection['$id'], collection.attributes.map(attribute => attribute.key) ); @@ -853,7 +797,7 @@ const createAttributes = async (attributes, title) => { throw new Error(`Attribute creation timed out.`); } - success(`Created ${attributes.length} ${title}`); + success(`Created ${attributes.length} attributes`); } const pushBucket = async ({ all, yes } = {}) => { From f9a9c9a27133bd239705e6f4ecde7c3e257badbb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 23 May 2024 17:38:52 -0400 Subject: [PATCH 19/89] feat(cli): Non destructive database and collection update --- templates/cli/lib/commands/push.js.twig | 145 +++++++++++++++++++++++- templates/cli/lib/parser.js.twig | 3 +- templates/cli/lib/questions.js.twig | 7 +- 3 files changed, 145 insertions(+), 10 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index be6048e6f..f139726b5 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1,3 +1,4 @@ +const chalk = require('chalk'); const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); @@ -5,7 +6,7 @@ const { localConfig, globalConfig } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics } = require("../questions"); -const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); +const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { databasesGet, @@ -140,6 +141,39 @@ const awaitPools = { iteration + 1 ); }, + deleteAttributes: async (databaseId, collectionId, attributeKeys, iteration = 1) => { + if (iteration > pollMaxDebounces) { + return false; + } + + let steps = Math.max(1, Math.ceil(attributeKeys.length / STEP_SIZE)); + if (steps > 1 && iteration === 1) { + pollMaxDebounces *= steps; + + log('Found a large number of deleting attributes, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') + } + + const { attributes } = await paginate(databasesListAttributes, { + databaseId, + collectionId, + parseOutput: false + }, 100, 'attributes'); + + const ready = attributeKeys.filter(attribute => attributes.includes(attribute.key)); + + if (ready.length === 0) { + return true; + } + + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); + + return await awaitPools.expectAttributes( + databaseId, + collectionId, + attributeKeys, + iteration + 1 + ); + }, expectAttributes: async (databaseId, collectionId, attributeKeys, iteration = 1) => { if (iteration > pollMaxDebounces) { return false; @@ -645,6 +679,91 @@ const createAttribute = async (databaseId, collectionId, attribute) => { } } +const deleteAttribute = async (collection, attribute) => { + log(`Deleting attribute ${attribute.key} of ${collection.name} ( ${collection['$id']} )`); + + await databasesDeleteAttribute({ + databaseId: collection['databaseId'], + collectionId: collection['$id'], + key: attribute.key, + parseOutput: false + }); +} + +const deepSimilar = (remote, local, collection) => { + if (local === undefined) { + return undefined; + } + + const key = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; + + if (remote.type !== local.type) { + return { key, attribute: remote, reason: `type changed from ${chalk.red(remote.type)} to ${chalk.green(local.type)}` }; + } + + if (remote.array !== local.array) { + return { key, attribute: remote, reason: `array changed from ${chalk.red(remote.array)} to ${chalk.green(local.array)}` }; + } + + if (remote.size !== local.size) { + return { key, attribute: remote, reason: `size changed from ${chalk.red(remote.size)} to ${chalk.green(local.size)}` }; + } + + if (remote.relatedCollectionId !== local.relatedCollectionId) { + return { key, attribute: remote, reason: `relationships collection id changed from ${chalk.red(remote.relatedCollectionId)} to ${chalk.green(local.relatedCollectionId)}` }; + } + + if (remote.twoWay !== local.twoWay) { + return { key, attribute: remote, reason: `relationships twoWay changed from ${chalk.red(remote.twoWay)} to ${chalk.green(local.twoWay)}` }; + } + + if (remote.twoWayKey !== local.twoWayKey) { + return { key, attribute: remote, reason: `relationships twoWayKey changed from ${chalk.red(remote.twoWayKey)} to ${chalk.green(local.twoWayKey)}` }; + } + + return undefined; +} + +const findMatch = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key); + +const updatedList = async (remoteAttributes, localAttributes, collection) => { + const deleting = remoteAttributes.filter((attribute) => !findMatch(attribute, localAttributes)); + const changes = remoteAttributes.map((attribute) => deepSimilar(attribute, findMatch(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); + let changedAttributes = []; + + if (changes.length > 0) { + log('There is a conflict in your collection deployment'); + drawTable(changes.map((change) => { + return { Key: change.key, Reason: change.reason }; + })); + const answers = await inquirer.prompt(questionsPushCollections[1]); + + if (answers.changes.toLowerCase() !== 'yes') { + return []; + } + + changedAttributes = changes.map((change) => change.attribute); + + await Promise.all(changedAttributes.map((changed) => deleteAttribute(collection, changed))); + + remoteAttributes = remoteAttributes.filter((attribute) => !findMatch(attribute, changedAttributes)) + } + + await Promise.all(deleting.map((attribute) => deleteAttribute(collection, attribute))); + + const attributeKeys = [...remoteAttributes.map(attribute => attribute.key), ...deleting.map(attribute => attribute.key)] + + if (attributeKeys.length) { + const deleteAttributesPoolStatus = await awaitPools.deleteAttributes(collection['databaseId'], collection['$id'], attributeKeys); + + if (!deleteAttributesPoolStatus) { + throw new Error("Attribute deletion timed out."); + } + } + + return localAttributes.filter((attribute) => !findMatch(attribute, remoteAttributes)); +} + const pushCollection = async ({ all, yes } = {}) => { const collections = []; @@ -666,7 +785,7 @@ const pushCollection = async ({ all, yes } = {}) => { } const databases = Array.from(new Set(collections.map(c => c['databaseId']))); - // Parallel db action + // Parallel db actions await Promise.all(databases.map(async (dbId) => { const localDatabase = localConfig.getDatabase(dbId); @@ -696,7 +815,7 @@ const pushCollection = async ({ all, yes } = {}) => { } })); - // Parallel collection action + // Parallel collection actions await Promise.all(collections.map(async (collection) => { try { const remoteCollection = await databasesGetCollection({ @@ -715,6 +834,9 @@ const pushCollection = async ({ all, yes } = {}) => { success(`Updated ${collection.name} ( ${collection['$id']} ) name`); } + collection.remoteVersion = remoteCollection; + + collection.isExisted = true; } catch (e) { if (e.code == 404) { log(`Collection ${collection.name} does not exist in the project. Creating ... `); @@ -726,19 +848,30 @@ const pushCollection = async ({ all, yes } = {}) => { permissions: collection['$permissions'], parseOutput: false }) - + collection.isNew = true; } else { throw e; } } })) - // Serialize attribute creation + // Serialize attribute actions for (let collection of collections) { + let attributes = collection.attributes; + + if (collection.isExisted) { + attributes = await updatedList(collection.remoteVersion.attributes, collection.attributes, collection); + + if (Array.isArray(attributes) && attributes.length <= 0) { + log(`No changes has been detected. Skipping ${collection.name} ( ${collection['$id']} )`); + continue; + } + } + log(`Pushing collection ${collection.name} ( ${collection['databaseId']} - ${collection['$id']} ) attributes`) try { - await createAttributes(collection.attributes, collection) + await createAttributes(attributes, collection) } catch (e) { throw e; } diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 5dde5aa3e..cd3b7a72b 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -192,5 +192,6 @@ module.exports = { success, error, commandDescriptions, - cliConfig + cliConfig, + drawTable } diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 67b5fd8ed..1ef5dbf37 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -1,3 +1,4 @@ +const chalk = require("chalk") const { localConfig } = require('./config'); const { projectsList } = require('./commands/projects'); const { teamsList } = require('./commands/teams'); @@ -368,9 +369,9 @@ const questionsPushCollections = [ }, { type: "input", - name: "override", - message: 'Are you sure you want to override this collection? This can lead to loss of data! Type "YES" to confirm.' - }, + name: "changes", + message: `These are the pending destructive changes. To create the new fields, all data in the old ones will be ${chalk.red('deleted')}. Type "YES" to confirm` + } ] const questionsPushBuckets = [ From 349161ff9a30077e47dcd4fb51dcaff18ca5f1bb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 10:47:40 -0400 Subject: [PATCH 20/89] feat(cli): Adding more data to Github issue link --- templates/cli/index.js.twig | 1 + templates/cli/lib/parser.js.twig | 48 ++++++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 8013a009e..7f3909d75 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -37,6 +37,7 @@ program }) .on("option:report", () => { cliConfig.report = true; + cliConfig.reportData = { data: this }; }) .showSuggestionAfterError() {% if sdk.test != "true" %} diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index a756ead41..079f8e0cb 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -2,11 +2,14 @@ const chalk = require('chalk'); const commander = require('commander'); const Table = require('cli-table3'); const { description } = require('../package.json'); +const { globalConfig } = require("./config.js"); +const os = require('os'); const cliConfig = { - verbose: false, - json: false, - report: false + verbose: false, + json: false, + report: false, + reportData: {} }; const parse = (data) => { @@ -111,19 +114,40 @@ const drawJSON = (data) => { } const parseError = (err) => { - if(cliConfig.report) { - const githubIssueUrl=`https://github.com/appwrite/appwrite/issues/new?labels=bug&template=bug.yaml&title=%F0%9F%90%9B+Bug+Report%3A+&actual-behavior=Console%20Crash%0A${encodeURIComponent('```\n'+err.stack+'\n```').replaceAll('(','').replaceAll(')','')}` + if (cliConfig.report) { + (async () => { + let appwriteVersion = 'unknown'; + const isCloud = globalConfig.getEndpoint().includes('cloud.appwrite.io') ? 'Yes' : 'No'; + + try { + const res = await fetch(`${globalConfig.getEndpoint()}/health/version`); + const json = await res.json(); + appwriteVersion = json.version; + } catch { + } - log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl}\n`); - } + const version = '0.16.0'; + const stepsToReproduce = encodeURIComponent(`Running \`appwrite ${cliConfig.reportData.data.args}\` using CLI ${version}`); + const yourEnvironment = encodeURI(`CLI version: ${version}\nOperation System: ${os.type()}\nAppwrite version: ${appwriteVersion}\nIs Cloud: ${isCloud}`) + + const stack = encodeURIComponent('```\n' + err.stack + '\n```').replaceAll('(', '').replaceAll(')', ''); + + const githubIssueUrl = `https://github.com/appwrite/appwrite/issues/new?labels=bug&template=bug.yaml&title=%F0%9F%90%9B+Bug+Report%3A+CLI+error:+${encodeURI(err.message)}&actual-behavior=CLI%20Error:%0A${stack}&steps-to-reproduce=${stepsToReproduce}&environment=${yourEnvironment}`; - if(cliConfig.verbose) { - console.error(err); - } else { - error(err.message); + log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl}\n`); + + process.exit(1); + })() + } + else { + if (cliConfig.verbose) { + console.error(err); + } else { + error(err.message); + } + process.exit(1); } - process.exit(1) } const actionRunner = (fn) => { From 8c7b89460db8075110b6012940d38d6b69710dee Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 11:06:53 -0400 Subject: [PATCH 21/89] refactor(cli): Multiple account login refactoring --- templates/cli/lib/commands/generic.js.twig | 42 +++----------------- templates/cli/lib/questions.js.twig | 46 +++++++++------------- 2 files changed, 23 insertions(+), 65 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 4c31749d5..64744d3a7 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -140,31 +140,6 @@ const login = new Command("login") }) .action(actionRunner(loginCommand)); -login - .command('list') - .description("List available logged accounts.") - .option("-j, --json", "Output in JSON format") - .action(actionRunner(async ({ json }) => { - const logins = globalConfig.getLogins(); - const current = globalConfig.getCurrentLogin(); - - const data = [...logins.map((login => { - return { - 'Current': login.id === current ? '*' : '', - 'ID': login.id, - 'Email': login.email, - 'Endpoint': login.endpoint - }; - }))]; - - if (json) { - console.log(data); - return; - } - drawTable(data); - - })); - const singleLogout = async (accountId) => { try { let client = await sdkForConsole(); @@ -202,19 +177,12 @@ const logout = new Command("logout") } const answers = await inquirer.prompt(questionsLogout); - const accountIds = []; - - if (answers.method === 'all') { - accountIds.push(...logins.map(login => login.id)); - } - - if (answers.method === 'selected' && answers.accounts) { - accountIds.push(...answers.accounts); - } - for (let accountId of accountIds) { - globalConfig.setCurrentLogin(accountId); - await singleLogout(accountId); + if (answers.accounts) { + for (let accountId of answers.accounts) { + globalConfig.setCurrentLogin(accountId); + await singleLogout(accountId); + } } const leftLogins = globalConfig.getLogins(); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 56244dea0..991b12e6b 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -326,16 +326,19 @@ const questionsLogin = [ const data = []; - const longestEmail = logins.reduce((prev, current) => (prev && prev.email > current.email) ? prev : current).email.length; + const longestEmail = logins.reduce((prev, current) => (prev && (prev.email ?? '').length > (current.email ?? '').length) ? prev : current).email.length; logins.forEach((login) => { - data.push({ - value: login.id, - name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('In use') : ' '.repeat(6)} ${login.endpoint}`, - }); + if (login.email) { + data.push({ + current: current === login.id, + value: login.id, + name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('current') : ' '.repeat(6)} ${login.endpoint}`, + }); + } }) - return data; + return data.sort((a, b) => Number(b.current) - Number(a.current)) }, when: (answers) => answers.method === 'select' }, @@ -343,26 +346,10 @@ const questionsLogin = [ ]; const questionsLogout = [ - { - type: "list", - name: "method", - message: "Which account would you like to logout from?", - choices() { - const logins = globalConfig.getLogins(); - - return [ - { name: 'Account in use' }, - { name: 'Selected accounts', value: 'selected' }, - { name: 'All accounts', value: 'all' } - ]; - }, - when: () => globalConfig.getCurrentLogin() !== '' - }, { type: "checkbox", name: "accounts", message: "Select accounts to logout from", - when: (answers) => answers.method === 'selected', validate: (value) => validateRequired('account', value), choices() { const logins = globalConfig.getLogins(); @@ -370,16 +357,19 @@ const questionsLogout = [ const data = []; - const longestEmail = logins.reduce((prev, current) => (prev && prev.email > current.email) ? prev : current).email.length; + const longestEmail = logins.reduce((prev, current) => (prev && (prev.email ?? '').length > (current.email ?? '').length) ? prev : current).email.length; logins.forEach((login) => { - data.push({ - value: login.id, - name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('In use') : ' '.repeat(6)} ${login.endpoint}`, - }); + if (login.email) { + data.push({ + current: current === login.id, + value: login.id, + name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('current') : ' '.repeat(6)} ${login.endpoint}`, + }); + } }) - return data; + return data.sort((a, b) => Number(b.current) - Number(a.current)) } } ]; From 338c14d153b35cd59bbe9f8ac3c3964387f7f1b8 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 11:49:51 -0400 Subject: [PATCH 22/89] feat(cli): Comparing non changeable attributes and showing all changes --- templates/cli/lib/commands/push.js.twig | 84 ++++++++++++++----------- templates/cli/lib/questions.js.twig | 2 +- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index f139726b5..a16bc0a60 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -49,6 +49,8 @@ const POLL_MAX_DEBOUNCE = 30; // Times let pollMaxDebounces = 30; +const changeableKeys = ['status', 'required', 'xdefault', 'elements', 'min', 'max', 'default', 'error']; + const awaitPools = { wipeAttributes: async (databaseId, collectionId, iteration = 1) => { if (iteration > pollMaxDebounces) { @@ -695,63 +697,71 @@ const deepSimilar = (remote, local, collection) => { return undefined; } - const key = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; + const keyName = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; + const action = chalk.cyan('recreating'); + let reason = ''; + for (let key of Object.keys(remote)) { + if (changeableKeys.includes(key)) { + continue; + } - if (remote.type !== local.type) { - return { key, attribute: remote, reason: `type changed from ${chalk.red(remote.type)} to ${chalk.green(local.type)}` }; + if (remote[key] !== local[key]) { + const bol = reason === '' ? '' : '\n'; + reason += `${bol}${key} changed from ${chalk.red(remote[key])} to ${chalk.green(local[key])}`; + } } - if (remote.array !== local.array) { - return { key, attribute: remote, reason: `array changed from ${chalk.red(remote.array)} to ${chalk.green(local.array)}` }; - } + return reason === '' ? undefined : { key: keyName, attribute: remote, reason, action }; +} - if (remote.size !== local.size) { - return { key, attribute: remote, reason: `size changed from ${chalk.red(remote.size)} to ${chalk.green(local.size)}` }; - } +const findMatch = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key); - if (remote.relatedCollectionId !== local.relatedCollectionId) { - return { key, attribute: remote, reason: `relationships collection id changed from ${chalk.red(remote.relatedCollectionId)} to ${chalk.green(local.relatedCollectionId)}` }; - } +const mapChangeAttribute = (attribute, collection, isAdding) => { + return { + key: `${chalk.yellow(attribute.key)} in ${collection.name} (${collection['$id']})`, + attribute, + reason: isAdding ? 'Field doesn\'t exist on the remote server' : 'Field doesn\'t exist in appwrite.json file', + action: isAdding ? chalk.green('adding') : chalk.red('deleting') + }; - if (remote.twoWay !== local.twoWay) { - return { key, attribute: remote, reason: `relationships twoWay changed from ${chalk.red(remote.twoWay)} to ${chalk.green(local.twoWay)}` }; - } +}; - if (remote.twoWayKey !== local.twoWayKey) { - return { key, attribute: remote, reason: `relationships twoWayKey changed from ${chalk.red(remote.twoWayKey)} to ${chalk.green(local.twoWayKey)}` }; - } +const updatedList = async (remoteAttributes, localAttributes, collection) => { + const deleting = remoteAttributes.filter((attribute) => !findMatch(attribute, localAttributes)).map((attr) => mapChangeAttribute(attr, collection, false)); + const adding = localAttributes.filter((attribute) => !findMatch(attribute, remoteAttributes)).map((attr) => mapChangeAttribute(attr, collection, true)); + const conflicts = remoteAttributes.map((attribute) => deepSimilar(attribute, findMatch(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); + let changedAttributes = []; - return undefined; -} + const changing = [...deleting, ...adding, ...conflicts] -const findMatch = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key); + if (changing.length === 0) { + return changedAttributes; + } + log('There are pending changes in your collection deployment'); -const updatedList = async (remoteAttributes, localAttributes, collection) => { - const deleting = remoteAttributes.filter((attribute) => !findMatch(attribute, localAttributes)); - const changes = remoteAttributes.map((attribute) => deepSimilar(attribute, findMatch(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); - let changedAttributes = []; + drawTable(changing.map((change) => { + return { Key: change.key, Action: change.action, Reason: change.reason, }; + })); - if (changes.length > 0) { - log('There is a conflict in your collection deployment'); - drawTable(changes.map((change) => { - return { Key: change.key, Reason: change.reason }; - })); - const answers = await inquirer.prompt(questionsPushCollections[1]); + const answers = await inquirer.prompt(questionsPushCollections[1]); - if (answers.changes.toLowerCase() !== 'yes') { - return []; - } + if (answers.changes.toLowerCase() !== 'yes') { + return changedAttributes; + } - changedAttributes = changes.map((change) => change.attribute); + if (conflicts.length > 0) { + changedAttributes = conflicts.map((change) => change.attribute); await Promise.all(changedAttributes.map((changed) => deleteAttribute(collection, changed))); remoteAttributes = remoteAttributes.filter((attribute) => !findMatch(attribute, changedAttributes)) } - await Promise.all(deleting.map((attribute) => deleteAttribute(collection, attribute))); + const deletingAttributes = deleting.map((change) => change.attribute); + + await Promise.all(deletingAttributes.map((attribute) => deleteAttribute(collection, attribute))); - const attributeKeys = [...remoteAttributes.map(attribute => attribute.key), ...deleting.map(attribute => attribute.key)] + const attributeKeys = [...remoteAttributes.map(attribute => attribute.key), ...deletingAttributes.map(attribute => attribute.key)] if (attributeKeys.length) { const deleteAttributesPoolStatus = await awaitPools.deleteAttributes(collection['databaseId'], collection['$id'], attributeKeys); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 1ef5dbf37..02a8a557a 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -370,7 +370,7 @@ const questionsPushCollections = [ { type: "input", name: "changes", - message: `These are the pending destructive changes. To create the new fields, all data in the old ones will be ${chalk.red('deleted')}. Type "YES" to confirm` + message: `Changes above will cause recreation of an attribute. All existing documents will lose data in those attributes. Type "YES" to confirm` } ] From e5573b60dc892872e11a6f4ee1d6a65da2e7b7a0 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 11:51:22 -0400 Subject: [PATCH 23/89] fix(cli): removing cli version from `steps-to-reproduce` --- templates/cli/lib/parser.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 079f8e0cb..164b8cb21 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -127,7 +127,7 @@ const parseError = (err) => { } const version = '0.16.0'; - const stepsToReproduce = encodeURIComponent(`Running \`appwrite ${cliConfig.reportData.data.args}\` using CLI ${version}`); + const stepsToReproduce = encodeURIComponent(`Running \`appwrite ${cliConfig.reportData.data.args}\``); const yourEnvironment = encodeURI(`CLI version: ${version}\nOperation System: ${os.type()}\nAppwrite version: ${appwriteVersion}\nIs Cloud: ${isCloud}`) const stack = encodeURIComponent('```\n' + err.stack + '\n```').replaceAll('(', '').replaceAll(')', ''); From 738a9f6480a3fb7060827cefaec380e1127bdfba Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 13:35:42 -0400 Subject: [PATCH 24/89] feat(cli): Give `paginate` the option to accept more queries --- templates/cli/lib/paginate.js.twig | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/paginate.js.twig b/templates/cli/lib/paginate.js.twig index 8c57fa731..bb8090507 100644 --- a/templates/cli/lib/paginate.js.twig +++ b/templates/cli/lib/paginate.js.twig @@ -5,11 +5,15 @@ const paginate = async (action, args = {}, limit = 100, wrapper = '') => { while (true) { const offset = pageNumber * limit; - + const additionalQueries = []; + if (args.queries) { + additionalQueries.push(...args.queries); + } // Merge the limit and offset into the args const response = await action({ ...args, queries: [ + ...additionalQueries, JSON.stringify({ method: 'limit', values: [limit] }), JSON.stringify({ method: 'offset', values: [offset] }) ] @@ -48,4 +52,4 @@ const paginate = async (action, args = {}, limit = 100, wrapper = '') => { module.exports = { paginate -}; \ No newline at end of file +}; From 9096fae94facd9499683a2fb0fdc139ab73685e7 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 13:36:55 -0400 Subject: [PATCH 25/89] refactor(cli): renaming to `--id` and adding verification --- templates/cli/index.js.twig | 6 +++--- templates/cli/lib/parser.js.twig | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index c4bf333ef..41dae7ed6 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -30,7 +30,7 @@ program .option("--json", "Output in JSON format") .option("-f,--force", "Flag to confirm all warnings") .option("-a,--all", "Flag to push all resources") - .option("--ids [id...]", "Flag to pass list of ids for a giving action") + .option("--id [id...]", "Flag to pass list of ids for a giving action") .option("--report", "Enable reporting when cli is crashing") .on("option:json", () => { cliConfig.json = true; @@ -48,8 +48,8 @@ program .on("option:all", () => { cliConfig.all = true; }) - .on("option:ids", function() { - cliConfig.ids = this.opts().ids; + .on("option:id", function() { + cliConfig.ids = this.opts().id; }) .showSuggestionAfterError() {% if sdk.test != "true" %} diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 944c50ee3..25cbe87c1 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -154,7 +154,13 @@ const parseError = (err) => { } const actionRunner = (fn) => { - return (...args) => fn(...args).catch(parseError); + return (...args) => { + if (cliConfig.all && (Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0)) { + error(`The '--all' and '--id' flags cannot be used together.`); + process.exit(1); + } + return fn(...args).catch(parseError) + }; } const parseInteger = (value) => { From c82609dfefd4d69e748ed49f1e57c010ba2e9197 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 14:30:40 -0400 Subject: [PATCH 26/89] refactor(cli): Moving --all logic into questions --- templates/cli/lib/commands/pull.js.twig | 29 +-- templates/cli/lib/commands/push.js.twig | 154 ++++++-------- templates/cli/lib/questions.js.twig | 255 +++++++++++++++++++----- 3 files changed, 264 insertions(+), 174 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index e0e17c7cb..e901d4424 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -11,7 +11,7 @@ const { databasesGet, databasesListCollections, databasesList } = require("./dat const { storageListBuckets } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); -const ID = require("../id"); +const ID = require("../id"); const { paginate } = require("../paginate"); const { questionsPullProject, questionsPullFunction, questionsPullCollection } = require("../questions"); const { cliConfig, success, log, actionRunner, commandDescriptions } = require("../parser"); @@ -164,26 +164,12 @@ const pullFunction = async () => { success(); } -const pullCollection = async ({ databaseId } = {}) => { - const databaseIds = []; +const pullCollection = async () => { + const databasesIds = (await inquirer.prompt(questionsPullCollection)).databases; - if (databaseId) { - databaseIds.push(databaseId); - } else if (cliConfig.all) { - let allDatabases = await databasesList({ - parseOutput: false - }) - - databaseIds.push(...allDatabases.databases.map((d) => d.$id)); - } + for (let dbID of databasesIds) { + const databaseId = dbID.value ? dbID.value : dbID; - if (databaseIds.length <= 0) { - let answers = await inquirer.prompt(questionsPullCollection) - if (!answers.databases) process.exit(1) - databaseIds.push(...answers.databases); - } - - for (const databaseId of databaseIds) { const database = await databasesGet({ databaseId, parseOutput: false @@ -198,14 +184,14 @@ const pullCollection = async ({ databaseId } = {}) => { log(`Found ${total} collections`); - collections.forEach(async collection => { + await Promise.all(collections.map(async collection => { log(`Fetching ${collection.name} ...`); localConfig.addCollection({ ...collection, '$createdAt': undefined, '$updatedAt': undefined, }); - }); + })) } success(); @@ -264,7 +250,6 @@ pull pull .command("collection") .description("Pulling your {{ spec.title|caseUcfirst }} collections") - .option(`--databaseId `, `Database ID`) .action(actionRunner(pullCollection)) pull diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index e3efe8be8..01620eb3f 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -4,8 +4,19 @@ const { Command } = require("commander"); const { localConfig, globalConfig } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); -const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics } = require("../questions"); -const { cliConfig, actionRunner, success, log, error, commandDescriptions } = require("../parser"); +const { + questionsPushBuckets, + questionsPushTeams, + questionsPushFunctions, + questionsGetEntrypoint, + questionsPushCollections, + questionsPushResources, + questionsPushMessagingTopics +} = require("../questions"); +const { + cliConfig, actionRunner, + success, log, error, commandDescriptions +} = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { databasesGet, @@ -241,43 +252,29 @@ const pushResources = async () => { messages: pushMessagingTopic } - if (cliConfig.all) { - Object.values(actions).forEach(action => action()); - } else { - const answers = await inquirer.prompt(questionsPushResources[0]); + const answers = await inquirer.prompt(questionsPushResources); - answers.resources.forEach((resource) => { - const action = actions[resource]; - if (action !== undefined) { - action(); + for (const resource of answers.resources) { + const method = resource.value ? resource.value : resource; + const action = actions[method]; + if (action !== undefined) { + try { + await action(); + } catch (e) { + error(e); } - }) + } } }; -const pushFunction = async ({ functionId, async } = {}) => { +const pushFunction = async ({ async } = {}) => { let response = {}; - const functionIds = []; + const functionIds = (await inquirer.prompt(questionsPushFunctions.slice(0, 2))).functions; - if (functionId) { - functionIds.push(functionId); - } else if (cliConfig.all) { - const functions = localConfig.getFunctions(); - if (functions.length === 0) { - throw new Error("No functions found in the current directory."); - } - functionIds.push(...functions.map((func, idx) => { - return func.$id; - })); - } + let functions = functionIds.map((functionId) => { + const id = functionId.value ? functionId.value : functionId; - if (functionIds.length <= 0) { - const answers = await inquirer.prompt(questionsPushFunctions[0]); - functionIds.push(...answers.functions); - } - - let functions = functionIds.map((id) => { const functions = localConfig.getFunctions(); const func = functions.find((f) => f.$id === id); @@ -651,22 +648,19 @@ const pushCollection = async () => { const collections = []; - if (cliConfig.all) { - if (localConfig.getCollections().length === 0) { - throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} pull collection` to fetch all your collections."); - } - collections.push(...localConfig.getCollections()); - } else { - const answers = await inquirer.prompt(questionsPushCollections[0]) - const configCollections = new Map(); - localConfig.getCollections().forEach((c) => { - configCollections.set(`${c['databaseId']}|${c['$id']}`, c); - }); - answers.collections.forEach((a) => { - const collection = configCollections.get(a); - collections.push(collection); - }) - } + const answers = await inquirer.prompt(questionsPushCollections.slice(0, 2)) + + const configCollections = new Map(); + + localConfig.getCollections().forEach((c) => { + configCollections.set(`${c['databaseId']}|${c['$id']}`, c); + }); + + answers.collections.forEach((c) => { + const id = c.value ? c.value : c; + const collection = configCollections.get(id); + collections.push(collection); + }) for (let collection of collections) { log(`Pushing collection ${collection.name} ( ${collection['databaseId']} - ${collection['$id']} )`) @@ -714,7 +708,7 @@ const pushCollection = async () => { log(`Collection ${collection.name} ( ${collection['$id']} ) already exists.`); if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushCollections[1]) + const answers = await inquirer.prompt(questionsPushCollections[2]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${collection.name} ( ${collection['$id']} )`); continue; @@ -870,25 +864,13 @@ const pushCollection = async () => { const pushBucket = async () => { let response = {}; - let bucketIds = []; + const bucketIds = (await inquirer.prompt(questionsPushBuckets.slice(0, 2))).buckets; + const buckets = []; const configBuckets = localConfig.getBuckets(); - if (cliConfig.all) { - if (configBuckets.length === 0) { - throw new Error("No buckets found in the current directory. Run `appwrite pull bucket` to fetch all your buckets."); - } - bucketIds.push(...configBuckets.map((b) => b.$id)); - } - - if (bucketIds.length === 0) { - const answers = await inquirer.prompt(questionsPushBuckets[0]) - bucketIds.push(...answers.buckets); - } - - let buckets = []; - for (const bucketId of bucketIds) { - const idBuckets = configBuckets.filter((b) => b.$id === bucketId); + const id = bucketId.value ? bucketId.value : bucketId; + const idBuckets = configBuckets.filter((b) => b.$id === id); buckets.push(...idBuckets); } @@ -903,7 +885,7 @@ const pushBucket = async () => { log(`Bucket ${bucket.name} ( ${bucket['$id']} ) already exists.`); if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushBuckets[1]) + const answers = await inquirer.prompt(questionsPushBuckets[2]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${bucket.name} ( ${bucket['$id']} )`); continue; @@ -957,25 +939,13 @@ const pushBucket = async () => { const pushTeam = async () => { let response = {}; - let teamIds = []; + const teamIds = (await inquirer.prompt(questionsPushTeams.slice(0, 2))).teams; const configTeams = localConfig.getTeams(); - - if (cliConfig.all) { - if (configTeams.length === 0) { - throw new Error("No teams found in the current directory. Run `appwrite pull team` to fetch all your teams."); - } - teamIds.push(...configTeams.map((t) => t.$id)); - } - - if (teamIds.length === 0) { - const answers = await inquirer.prompt(questionsPushTeams[0]) - teamIds.push(...answers.teams); - } - - let teams = []; + const teams = []; for (const teamId of teamIds) { - const idTeams = configTeams.filter((t) => t.$id === teamId); + const id = teamId.value ? teamId.value : teamId + const idTeams = configTeams.filter((t) => t.$id === id); teams.push(...idTeams); } @@ -990,7 +960,7 @@ const pushTeam = async () => { log(`Team ${team.name} ( ${team['$id']} ) already exists.`); if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushTeams[1]) + const answers = await inquirer.prompt(questionsPushTeams[2]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${team.name} ( ${team['$id']} )`); continue; @@ -1027,31 +997,19 @@ const pushTeam = async () => { const pushMessagingTopic = async () => { let response = {}; - let topicsIds = []; const configTopics = localConfig.getMessagingTopics(); + const topicsIds = (await inquirer.prompt(questionsPushMessagingTopics.slice(0, 2))).topics; let overrideExisting = cliConfig.force; - - if (cliConfig.all) { - if (configTopics.length === 0) { - throw new Error("No topics found in the current directory. Run `appwrite pull topics` to pull all your messaging topics."); - } - topicsIds.push(...configTopics.map((b) => b.$id)); - } - - if (topicsIds.length === 0) { - const answers = await inquirer.prompt(questionsPushMessagingTopics[0]) - topicsIds.push(...answers.topics); - } - - let topics = []; + const topics = []; for (const topicId of topicsIds) { - const idTopic = configTopics.filter((b) => b.$id === topicId); + const id = topicId.value ? topicId.value : topicId + const idTopic = configTopics.filter((b) => b.$id === id); topics.push(...idTopic); } if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushMessagingTopics[1]) + const answers = await inquirer.prompt(questionsPushMessagingTopics[2]) if (answers.override.toLowerCase() === "yes") { overrideExisting = true; } diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 67b5fd8ed..320d854ac 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -6,7 +6,7 @@ const { accountListMfaFactors } = require("./commands/account"); const { sdkForConsole } = require("./sdks"); const { validateRequired } = require("./validations"); const { paginate } = require('./paginate'); - +const { cliConfig } = require('./parser'); const { databasesList } = require('./commands/databases'); const JSONbig = require("json-bigint")({ storeAsString: false }); @@ -126,7 +126,7 @@ const questionsPullProject = [ message: "Choose the project organization", choices: async () => { let client = await sdkForConsole(true); - const { teams } = await paginate(teamsList, { parseOutput: false , sdk: client}, 100, 'teams'); + const { teams } = await paginate(teamsList, { parseOutput: false, sdk: client }, 100, 'teams'); let choices = teams.map((team, idx) => { return { @@ -194,7 +194,7 @@ const questionsPullProject = [ choices: async (answers) => { let response = await projectsList({ parseOutput: false, - queries: [JSON.stringify({ method: 'equal', attribute:'teamId', values: [answers.organization.id] })], + queries: [JSON.stringify({ method: 'equal', attribute: 'teamId', values: [answers.organization.id] })], }) let projects = response["projects"] let choices = projects.map((project, idx) => { @@ -245,7 +245,7 @@ const questionsPullFunction = [ id: runtime['$id'], entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), - commands : getInstallCommand(runtime['$id']) + commands: getInstallCommand(runtime['$id']) }, } }) @@ -256,25 +256,44 @@ const questionsPullFunction = [ const questionsPullCollection = [ { - type: "checkbox", - name: "databases", - message: "From which database would you like to pull collections?", - choices: async () => { - let response = await databasesList({ - parseOutput: false - }) - let databases = response["databases"] + type: 'checkbox', + name: 'setup', + async when(answers) { + let queries = []; + + if (Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + queries = [JSON.stringify({ method: 'equal', attribute: '$id', values: cliConfig.ids })] + } + + const { databases } = await paginate(databasesList, { parseOutput: false, queries }, 100, 'databases'); if (databases.length <= 0) { throw new Error("No databases found. Please create one in project console.") } - let choices = databases.map((database, idx) => { + + answers.setup = databases.map((database, idx) => { return { name: `${database.name} (${database.$id})`, value: database.$id } - }) - return choices; + }); + + return false; + } + }, + { + type: "checkbox", + name: "databases", + message: "From which database would you like to pull collections?", + when: (answers) => { + if (cliConfig.all || Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.databases = answers.setup; + return false; + } + return true; + }, + choices: async (answers) => { + return answers.setup; } } ]; @@ -306,38 +325,78 @@ const questionsLogin = [ ]; const questionsPushResources = [ + { + type: 'checkbox', + name: 'setup', + async when(answers) { + answers.setup = [ + { name: 'Functions', value: 'functions' }, + { name: 'Collections', value: 'collections' }, + { name: 'Buckets', value: 'buckets' }, + { name: 'Teams', value: 'teams' }, + { name: 'Topics', value: 'messages' } + ]; + + return false; + } + }, { type: "checkbox", name: "resources", message: "Which resources would you like to push?", - choices: [ - { name: 'Functions', value: 'functions' }, - { name: 'Collections', value: 'collections' }, - { name: 'Buckets', value: 'buckets' }, - { name: 'Teams', value: 'teams' }, - { name: 'Topics', value: 'messages' } - ] + when: (answers) => { + if (cliConfig.all) { + answers.resources = answers.setup; + return false; + } + + return true; + }, + choices: async (answers) => { + return answers.setup; + } } ] const questionsPushFunctions = [ { - type: "checkbox", - name: "functions", - message: "Which functions would you like to push?", - validate: (value) => validateRequired('function', value), - choices: () => { + type: 'checkbox', + name: 'setup', + async when(answers) { let functions = localConfig.getFunctions(); if (functions.length === 0) { throw new Error("No functions found in the current directory."); } - let choices = functions.map((func, idx) => { + + answers.setup = functions.map((func, idx) => { return { name: `${func.name} (${func.$id})`, value: func.$id } - }) - return choices; + }); + + if (Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.setup = answers.setup.filter((func) => cliConfig.ids.includes(func.value)); + } + + return false; + } + }, + { + type: "checkbox", + name: "functions", + message: "Which functions would you like to push?", + validate: (value) => validateRequired('function', value), + when: (answers) => { + if (cliConfig.all || Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.functions = answers.setup; + return false; + } + + return true; + }, + choices: (answers) => { + return answers.setup; } }, { @@ -349,21 +408,43 @@ const questionsPushFunctions = [ const questionsPushCollections = [ { - type: "checkbox", - name: "collections", - message: "Which collections would you like to push?", - validate: (value) => validateRequired('collection', value), - choices: () => { + type: 'checkbox', + name: 'setup', + async when(answers) { let collections = localConfig.getCollections(); if (collections.length === 0) { throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} pull collection` to fetch all your collections."); } - return collections.map(collection => { + + answers.setup = collections.map((collection, idx) => { return { name: `${collection.name} (${collection['databaseId']} - ${collection['$id']})`, value: `${collection['databaseId']}|${collection['$id']}` } }); + + if (Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.setup = answers.setup.filter((collection) => cliConfig.ids.includes(collection.value)); + } + + return false; + } + }, + { + type: "checkbox", + name: "collections", + message: "Which collections would you like to push?", + validate: (value) => validateRequired('collection', value), + when: (answers) => { + if (cliConfig.all || Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.collections = answers.setup; + return false; + } + + return true; + }, + choices: (answers) => { + return answers.setup; } }, { @@ -375,21 +456,43 @@ const questionsPushCollections = [ const questionsPushBuckets = [ { - type: "checkbox", - name: "buckets", - message: "Which buckets would you like to push?", - validate: (value) => validateRequired('bucket', value), - choices: () => { + type: 'checkbox', + name: 'setup', + async when(answers) { let buckets = localConfig.getBuckets(); if (buckets.length === 0) { throw new Error("No buckets found in the current directory. Run `appwrite pull bucket` to fetch all your buckets."); } - return buckets.map(bucket => { + + answers.setup = buckets.map(bucket => { return { name: `${bucket.name} (${bucket['$id']})`, value: bucket.$id } }); + + if (Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.setup = answers.setup.filter((bucket) => cliConfig.ids.includes(bucket.value)); + } + + return false; + } + }, + { + type: "checkbox", + name: "buckets", + message: "Which buckets would you like to push?", + validate: (value) => validateRequired('bucket', value), + when: (answers) => { + if (cliConfig.all || Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.buckets = answers.setup; + return false; + } + + return true; + }, + choices: (answers) => { + return answers.setup; } }, { @@ -401,20 +504,42 @@ const questionsPushBuckets = [ const questionsPushMessagingTopics = [ { - type: "checkbox", - name: "topics", - message: "Which messaging topic would you like to push?", - choices: () => { + type: 'checkbox', + name: 'setup', + async when(answers) { let topics = localConfig.getMessagingTopics(); if (topics.length === 0) { throw new Error("No topics found in the current directory. Run `appwrite pull messaging` to fetch all your messaging topics."); } - return topics.map(topic => { + + answers.setup = topics.map(topic => { return { name: `${topic.name} (${topic['$id']})`, value: topic.$id } }); + + if (Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.setup = answers.setup.filter((topic) => cliConfig.ids.includes(topic.value)); + } + + return false; + } + }, + { + type: "checkbox", + name: "topics", + message: "Which messaging topic would you like to push?", + when: (answers) => { + if (cliConfig.all || Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.topics = answers.setup; + return false; + } + + return true; + }, + choices: (answers) => { + return answers.setup; } }, { @@ -441,21 +566,43 @@ const questionsGetEntrypoint = [ const questionsPushTeams = [ { - type: "checkbox", - name: "teams", - message: "Which teams would you like to push?", - validate: (value) => validateRequired('team', value), - choices: () => { + type: 'checkbox', + name: 'setup', + async when(answers) { let teams = localConfig.getTeams(); if (teams.length === 0) { throw new Error("No teams found in the current directory. Run `appwrite pull team` to fetch all your teams."); } - return teams.map(team => { + + answers.setup = teams.map(team => { return { name: `${team.name} (${team['$id']})`, value: team.$id } }); + + if (Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.setup = answers.setup.filter((team) => cliConfig.ids.includes(team.value)); + } + + return false; + } + }, + { + type: "checkbox", + name: "teams", + message: "Which teams would you like to push?", + validate: (value) => validateRequired('team', value), + when: (answers) => { + if (cliConfig.all || Array.isArray(cliConfig.ids) && cliConfig.ids.length !== 0) { + answers.teams = answers.setup; + return false; + } + + return true; + }, + choices: (answers) => { + return answers.setup; } }, { @@ -494,7 +641,7 @@ const questionsListFactors = [ name: `Recovery code`, value: 'recoveryCode' } - ].filter((ch) => factors[ch.value] === true); + ].filter((ch) => factors[ch.value] === true); return choices; } From cce2c3abdec427776783cf1ed86edf43b5474648 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 15:08:40 -0400 Subject: [PATCH 27/89] refactor(cli): Moving --force logic into questions --- templates/cli/lib/commands/push.js.twig | 64 ++++++++++-------- templates/cli/lib/questions.js.twig | 88 +++++++++++++++++++------ 2 files changed, 105 insertions(+), 47 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 01620eb3f..8e36ed5ba 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -265,6 +265,8 @@ const pushResources = async () => { } } } + + process.exit(0); }; const pushFunction = async ({ async } = {}) => { @@ -297,8 +299,6 @@ const pushFunction = async ({ async } = {}) => { } if (func.variables) { - func.pushVariables = cliConfig.force; - try { const { total } = await functionsListVariables({ functionId: func['$id'], @@ -310,8 +310,8 @@ const pushFunction = async ({ async } = {}) => { func.pushVariables = true; } else if (total > 0 && !func.pushVariables) { log(`The function ${func.name} has remote variables setup`); - const variableAnswers = await inquirer.prompt(questionsPushFunctions[1]) - func.pushVariables = variableAnswers.override.toLowerCase() === "yes"; + const variableAnswers = await inquirer.prompt(questionsPushFunctions.slice(2, 4)); + func.pushVariables = variableAnswers.setupoverride ? variableAnswers.setupoverride : variableAnswers.override.toLowerCase() === "yes"; } } catch (e) { if (e.code != 404) { @@ -524,6 +524,8 @@ const pushFunction = async ({ async } = {}) => { }) success(`Pushed ${successfullyPushed} functions with ${successfullyDeployed} successful deployments.`); + + process.exit(0); } const createAttribute = async (databaseId, collectionId, attribute) => { @@ -641,6 +643,8 @@ const createAttribute = async (databaseId, collectionId, attribute) => { parseOutput: false }) } + + process.exit(0); } const pushCollection = async () => { @@ -707,12 +711,11 @@ const pushCollection = async () => { log(`Collection ${collection.name} ( ${collection['$id']} ) already exists.`); - if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushCollections[2]) - if (answers.override.toLowerCase() !== "yes") { - log(`Received "${answers.override}". Skipping ${collection.name} ( ${collection['$id']} )`); - continue; - } + const answers = await inquirer.prompt(questionsPushCollections.slice(2, 4)) + const override = answers.setupoverride ? answers.setupoverride : answers.override.toLowerCase() === "yes"; + if (!override) { + log(`Skipping ${collection.name} ( ${collection['$id']} )`); + continue; } log(`Deleting indexes and attributes ... `); @@ -859,6 +862,8 @@ const pushCollection = async () => { success(`Created ${relationships.length} relationship attributes`); } + + process.exit(0); } const pushBucket = async () => { @@ -884,12 +889,11 @@ const pushBucket = async () => { }) log(`Bucket ${bucket.name} ( ${bucket['$id']} ) already exists.`); - if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushBuckets[2]) - if (answers.override.toLowerCase() !== "yes") { - log(`Received "${answers.override}". Skipping ${bucket.name} ( ${bucket['$id']} )`); - continue; - } + const answers = await inquirer.prompt(questionsPushBuckets.slice(2, 4)) + const override = answers.setupoverride ? answers.setupoverride : answers.override.toLowerCase() === "yes"; + if (!override) { + log(`Skipping ${bucket.name} ( ${bucket['$id']} )`); + continue; } log(`Updating bucket ...`) @@ -934,6 +938,8 @@ const pushBucket = async () => { } } } + + process.exit(0); } const pushTeam = async () => { @@ -959,12 +965,11 @@ const pushTeam = async () => { }) log(`Team ${team.name} ( ${team['$id']} ) already exists.`); - if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushTeams[2]) - if (answers.override.toLowerCase() !== "yes") { - log(`Received "${answers.override}". Skipping ${team.name} ( ${team['$id']} )`); - continue; - } + const answers = await inquirer.prompt(questionsPushTeams.slice(2, 4)) + const override = answers.setupoverride ? answers.setupoverride : answers.override.toLowerCase() === "yes"; + if (!override) { + log(`Skipping ${team.name} ( ${team['$id']} )`); + continue; } log(`Updating team ...`) @@ -992,6 +997,8 @@ const pushTeam = async () => { } } } + + process.exit(0); } const pushMessagingTopic = async () => { @@ -999,8 +1006,8 @@ const pushMessagingTopic = async () => { const configTopics = localConfig.getMessagingTopics(); const topicsIds = (await inquirer.prompt(questionsPushMessagingTopics.slice(0, 2))).topics; - let overrideExisting = cliConfig.force; const topics = []; + let overrideExisting = false; for (const topicId of topicsIds) { const id = topicId.value ? topicId.value : topicId @@ -1008,11 +1015,10 @@ const pushMessagingTopic = async () => { topics.push(...idTopic); } - if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushMessagingTopics[2]) - if (answers.override.toLowerCase() === "yes") { - overrideExisting = true; - } + const answers = await inquirer.prompt(questionsPushMessagingTopics.slice(2, 4)) + const override = answers.setupoverride ? answers.setupoverride : answers.override.toLowerCase() === "yes"; + if (override) { + overrideExisting = true } for (let topic of topics) { @@ -1057,6 +1063,8 @@ const pushMessagingTopic = async () => { } } } + + process.exit(0); } const push = new Command("push") diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 320d854ac..95ccdb2d9 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -277,7 +277,6 @@ const questionsPullCollection = [ value: database.$id } }); - return false; } }, @@ -399,9 +398,19 @@ const questionsPushFunctions = [ return answers.setup; } }, + { + type: "input", + name: "setupoverride", + when: (answers) => { + answers.setupoverride = cliConfig.force; + + return false; + } + }, { type: "input", name: "override", + when: () => !cliConfig.force, message: 'Are you sure you want to override this function\'s variables? This can lead to loss of secrets! Type "YES" to confirm.' }, ] @@ -447,11 +456,21 @@ const questionsPushCollections = [ return answers.setup; } }, + { + type: "input", + name: "setupoverride", + when: (answers) => { + answers.setupoverride = cliConfig.force; + + return false; + } + }, { type: "input", name: "override", + when: () => !cliConfig.force, message: 'Are you sure you want to override this collection? This can lead to loss of data! Type "YES" to confirm.' - }, + } ] const questionsPushBuckets = [ @@ -495,11 +514,21 @@ const questionsPushBuckets = [ return answers.setup; } }, + { + type: "input", + name: "setupoverride", + when: (answers) => { + answers.setupoverride = cliConfig.force; + + return false; + } + }, { type: "input", name: "override", + when: () => !cliConfig.force, message: 'Are you sure you want to override this bucket? This can lead to loss of data! Type "YES" to confirm.' - }, + } ] const questionsPushMessagingTopics = [ @@ -544,24 +573,19 @@ const questionsPushMessagingTopics = [ }, { type: "input", - name: "override", - message: 'Would you like to override existing topics? This can lead to loss of data! Type "YES" to confirm.' - } -] + name: "setupoverride", + when: (answers) => { + answers.setupoverride = cliConfig.force; -const questionsGetEntrypoint = [ - { - type: "input", - name: "entrypoint", - message: "Enter the entrypoint", - default: null, - validate(value) { - if (!value) { - return "Please enter your entrypoint"; - } - return true; + return false; } }, + { + type: "input", + name: "override", + when: () => !cliConfig.force, + message: 'Would you like to override existing topics? This can lead to loss of data! Type "YES" to confirm.' + } ] const questionsPushTeams = [ @@ -585,6 +609,7 @@ const questionsPushTeams = [ answers.setup = answers.setup.filter((team) => cliConfig.ids.includes(team.value)); } + return false; } }, @@ -605,13 +630,38 @@ const questionsPushTeams = [ return answers.setup; } }, + { + type: "input", + name: "setupoverride", + when: (answers) => { + answers.setupoverride = cliConfig.force; + + return false; + } + }, { type: "input", name: "override", + when: () => !cliConfig.force, message: 'Are you sure you want to override this team? This can lead to loss of data! Type "YES" to confirm.' - }, + } ]; +const questionsGetEntrypoint = [ + { + type: "input", + name: "entrypoint", + message: "Enter the entrypoint", + default: null, + validate(value) { + if (!value) { + return "Please enter your entrypoint"; + } + return true; + } + }, +] + const questionsListFactors = [ { type: "list", From 32452ffbb3104643ed6cc93fbfb2ba51d4255681 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 27 May 2024 20:21:26 -0400 Subject: [PATCH 28/89] refactor(cli): Review fixing --- templates/cli/lib/commands/generic.js.twig | 4 +--- templates/cli/lib/questions.js.twig | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 64744d3a7..e735bd219 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -134,7 +134,7 @@ const whoami = new Command("whoami") const login = new Command("login") .description(commandDescriptions['login']) - .option(`-sa, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`) + .option(`-sh, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`) .configureHelp({ helpWidth: process.stdout.columns || 80 }) @@ -282,7 +282,6 @@ const migrate = async () => { const endpoint = globalConfig.get('endpoint'); const cookie = globalConfig.get('cookie'); - log("Old Appwrite login settings were detected, migrating..."); const id = ID.unique(); const data = { endpoint, @@ -295,7 +294,6 @@ const migrate = async () => { globalConfig.delete('endpoint'); globalConfig.delete('cookie'); - success(`Account was migrated and it's the current account`); } module.exports = { {% if sdk.test != "true" %} diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 991b12e6b..30fd0008a 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -319,7 +319,7 @@ const questionsLogin = [ { type: "list", name: "accountId", - message: "Select an account to switch to", + message: "Select an account to use", choices() { const logins = globalConfig.getLogins(); const current = globalConfig.getCurrentLogin(); From ffdc1cc4cd3c764efc3aeaa335136c04f6f55bc1 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 27 May 2024 20:34:34 -0400 Subject: [PATCH 29/89] refactor(cli): Review fixing --- templates/cli/index.js.twig | 2 +- templates/cli/lib/parser.js.twig | 21 +++++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 7f3909d75..33b19752e 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -35,7 +35,7 @@ program .on("option:verbose", () => { cliConfig.verbose = true; }) - .on("option:report", () => { + .on("option:report", function() { cliConfig.report = true; cliConfig.reportData = { data: this }; }) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 164b8cb21..f5f92bd1a 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -126,20 +126,25 @@ const parseError = (err) => { } catch { } - const version = '0.16.0'; - const stepsToReproduce = encodeURIComponent(`Running \`appwrite ${cliConfig.reportData.data.args}\``); - const yourEnvironment = encodeURI(`CLI version: ${version}\nOperation System: ${os.type()}\nAppwrite version: ${appwriteVersion}\nIs Cloud: ${isCloud}`) + const version = '{{ sdk.version }}'; + const stepsToReproduce = `Running \`appwrite ${cliConfig.reportData.data.args}\``; + const yourEnvironment = `CLI version: ${version}\nOperation System: ${os.type()}\nAppwrite version: ${appwriteVersion}\nIs Cloud: ${isCloud}`; - const stack = encodeURIComponent('```\n' + err.stack + '\n```').replaceAll('(', '').replaceAll(')', ''); + const stack = '```\n' + err.stack + '\n```'; - const githubIssueUrl = `https://github.com/appwrite/appwrite/issues/new?labels=bug&template=bug.yaml&title=%F0%9F%90%9B+Bug+Report%3A+CLI+error:+${encodeURI(err.message)}&actual-behavior=CLI%20Error:%0A${stack}&steps-to-reproduce=${stepsToReproduce}&environment=${yourEnvironment}`; + const githubIssueUrl = new URL('https://github.com/appwrite/appwrite/issues/new'); + githubIssueUrl.searchParams.append('labels', 'bug'); + githubIssueUrl.searchParams.append('template', 'bug.yaml'); + githubIssueUrl.searchParams.append('title', `🐛 Bug Report: ${err.message}`); + githubIssueUrl.searchParams.append('actual-behavior', `CLI Error:\n${stack}`); + githubIssueUrl.searchParams.append('steps-to-reproduce', stepsToReproduce); + githubIssueUrl.searchParams.append('environment', yourEnvironment); - log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl}\n`); + log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl.href}\n`); process.exit(1); })() - } - else { + } else { if (cliConfig.verbose) { console.error(err); } else { From 60b73a22acb952c41e9f1a1b901c0a70267a6f48 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 09:04:40 -0400 Subject: [PATCH 30/89] refactor(cli): Review fixing --- templates/cli/lib/commands/pull.js.twig | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index e901d4424..4c6a7feba 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -167,8 +167,8 @@ const pullFunction = async () => { const pullCollection = async () => { const databasesIds = (await inquirer.prompt(questionsPullCollection)).databases; - for (let dbID of databasesIds) { - const databaseId = dbID.value ? dbID.value : dbID; + for (let id of databasesIds) { + const databaseId = id.value ? id.value : id; const database = await databasesGet({ databaseId, @@ -184,14 +184,15 @@ const pullCollection = async () => { log(`Found ${total} collections`); - await Promise.all(collections.map(async collection => { + collections.map(async collection => { log(`Fetching ${collection.name} ...`); localConfig.addCollection({ ...collection, '$createdAt': undefined, - '$updatedAt': undefined, + '$updatedAt': undefined }); - })) + }); + } success(); From ea9f2fbc80ecc28ada74ee69f69b4fa77deb6067 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 09:20:48 -0400 Subject: [PATCH 31/89] refactor(cli): Review fixing --- templates/cli/lib/commands/push.js.twig | 36 +++++++++++-------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index a16bc0a60..9790c6f35 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -152,7 +152,7 @@ const awaitPools = { if (steps > 1 && iteration === 1) { pollMaxDebounces *= steps; - log('Found a large number of deleting attributes, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') + log('Found a large number of attributes to be deleted. Increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') } const { attributes } = await paginate(databasesListAttributes, { @@ -727,18 +727,17 @@ const mapChangeAttribute = (attribute, collection, isAdding) => { }; const updatedList = async (remoteAttributes, localAttributes, collection) => { + const deleting = remoteAttributes.filter((attribute) => !findMatch(attribute, localAttributes)).map((attr) => mapChangeAttribute(attr, collection, false)); const adding = localAttributes.filter((attribute) => !findMatch(attribute, remoteAttributes)).map((attr) => mapChangeAttribute(attr, collection, true)); const conflicts = remoteAttributes.map((attribute) => deepSimilar(attribute, findMatch(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); - let changedAttributes = []; + let changedAttributes = []; const changing = [...deleting, ...adding, ...conflicts] - if (changing.length === 0) { return changedAttributes; } log('There are pending changes in your collection deployment'); - drawTable(changing.map((change) => { return { Key: change.key, Action: change.action, Reason: change.reason, }; })); @@ -751,16 +750,12 @@ const updatedList = async (remoteAttributes, localAttributes, collection) => { if (conflicts.length > 0) { changedAttributes = conflicts.map((change) => change.attribute); - await Promise.all(changedAttributes.map((changed) => deleteAttribute(collection, changed))); - remoteAttributes = remoteAttributes.filter((attribute) => !findMatch(attribute, changedAttributes)) } const deletingAttributes = deleting.map((change) => change.attribute); - await Promise.all(deletingAttributes.map((attribute) => deleteAttribute(collection, attribute))); - const attributeKeys = [...remoteAttributes.map(attribute => attribute.key), ...deletingAttributes.map(attribute => attribute.key)] if (attributeKeys.length) { @@ -793,33 +788,33 @@ const pushCollection = async ({ all, yes } = {}) => { collections.push(collection); }) } - const databases = Array.from(new Set(collections.map(c => c['databaseId']))); + const databases = Array.from(new Set(collections.map(collection => collection['databaseId']))); // Parallel db actions - await Promise.all(databases.map(async (dbId) => { - const localDatabase = localConfig.getDatabase(dbId); + await Promise.all(databases.map(async (databaseId) => { + const localDatabase = localConfig.getDatabase(databaseId); try { const database = await databasesGet({ - databaseId: dbId, + databaseId: databaseId, parseOutput: false, }); - if (database.name !== (localDatabase.name ?? dbId)) { + if (database.name !== (localDatabase.name ?? databaseId)) { await databasesUpdate({ - databaseId: dbId, - name: localDatabase.name ?? dbId, + databaseId: databaseId, + name: localDatabase.name ?? databaseId, parseOutput: false }) - success(`Updated ${localDatabase.name} ( ${dbId} ) name`); + success(`Updated ${localDatabase.name} ( ${databaseId} ) name`); } } catch (err) { - log(`Database ${dbId} not found. Creating it now...`); + log(`Database ${databaseId} not found. Creating it now...`); await databasesCreate({ - databaseId: dbId, - name: localDatabase.name ?? dbId, + databaseId: databaseId, + name: localDatabase.name ?? databaseId, parseOutput: false, }); } @@ -858,7 +853,6 @@ const pushCollection = async ({ all, yes } = {}) => { permissions: collection['$permissions'], parseOutput: false }) - collection.isNew = true; } else { throw e; } @@ -914,7 +908,7 @@ const createIndexes = async (indexes, collection) => { const result = await awaitPools.expectIndexes( collection['databaseId'], collection['$id'], - indexes.map(attribute => attribute.key) + indexes.map(index => index.key) ); if (!result) { From bb80ece6ebe323390475f210c876fc504c39df8e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 10:05:26 -0400 Subject: [PATCH 32/89] refactor(cli): Review fixing --- templates/cli/lib/commands/push.js.twig | 46 +++++++++++++++++++------ templates/cli/lib/questions.js.twig | 2 +- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 9790c6f35..8644a1718 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -692,7 +692,15 @@ const deleteAttribute = async (collection, attribute) => { }); } -const deepSimilar = (remote, local, collection) => { +/** + * Check if attribute non-changeable fields has been changed + * If so return the differences as an object. + * @param remote + * @param local + * @param collection + * @returns {undefined|{reason: string, action: *, attribute, key: string}} + */ +const checkAttributeChanges = (remote, local, collection) => { if (local === undefined) { return undefined; } @@ -700,6 +708,7 @@ const deepSimilar = (remote, local, collection) => { const keyName = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; const action = chalk.cyan('recreating'); let reason = ''; + for (let key of Object.keys(remote)) { if (changeableKeys.includes(key)) { continue; @@ -714,23 +723,38 @@ const deepSimilar = (remote, local, collection) => { return reason === '' ? undefined : { key: keyName, attribute: remote, reason, action }; } -const findMatch = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key); +/** + * Check if attributes contain the given attribute + * @param attribute + * @param attributes + * @returns {*} + */ +const attributesContains = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key); + -const mapChangeAttribute = (attribute, collection, isAdding) => { +const generateChangesObject = (attribute, collection, isAdding) => { return { key: `${chalk.yellow(attribute.key)} in ${collection.name} (${collection['$id']})`, - attribute, + attribute: attribute, reason: isAdding ? 'Field doesn\'t exist on the remote server' : 'Field doesn\'t exist in appwrite.json file', action: isAdding ? chalk.green('adding') : chalk.red('deleting') }; }; -const updatedList = async (remoteAttributes, localAttributes, collection) => { +/** + * Filter deleted and recreated attributes, + * return list of attributes to create + * @param remoteAttributes + * @param localAttributes + * @param collection + * @returns {Promise<*|*[]>} + */ +const attributesToCreate = async (remoteAttributes, localAttributes, collection) => { - const deleting = remoteAttributes.filter((attribute) => !findMatch(attribute, localAttributes)).map((attr) => mapChangeAttribute(attr, collection, false)); - const adding = localAttributes.filter((attribute) => !findMatch(attribute, remoteAttributes)).map((attr) => mapChangeAttribute(attr, collection, true)); - const conflicts = remoteAttributes.map((attribute) => deepSimilar(attribute, findMatch(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); + const deleting = remoteAttributes.filter((attribute) => !attributesContains(attribute, localAttributes)).map((attr) => generateChangesObject(attr, collection, false)); + const adding = localAttributes.filter((attribute) => !attributesContains(attribute, remoteAttributes)).map((attr) => generateChangesObject(attr, collection, true)); + const conflicts = remoteAttributes.map((attribute) => checkAttributeChanges(attribute, attributesContains(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); let changedAttributes = []; const changing = [...deleting, ...adding, ...conflicts] @@ -751,7 +775,7 @@ const updatedList = async (remoteAttributes, localAttributes, collection) => { if (conflicts.length > 0) { changedAttributes = conflicts.map((change) => change.attribute); await Promise.all(changedAttributes.map((changed) => deleteAttribute(collection, changed))); - remoteAttributes = remoteAttributes.filter((attribute) => !findMatch(attribute, changedAttributes)) + remoteAttributes = remoteAttributes.filter((attribute) => !attributesContains(attribute, changedAttributes)) } const deletingAttributes = deleting.map((change) => change.attribute); @@ -766,7 +790,7 @@ const updatedList = async (remoteAttributes, localAttributes, collection) => { } } - return localAttributes.filter((attribute) => !findMatch(attribute, remoteAttributes)); + return localAttributes.filter((attribute) => !attributesContains(attribute, remoteAttributes)); } const pushCollection = async ({ all, yes } = {}) => { @@ -864,7 +888,7 @@ const pushCollection = async ({ all, yes } = {}) => { let attributes = collection.attributes; if (collection.isExisted) { - attributes = await updatedList(collection.remoteVersion.attributes, collection.attributes, collection); + attributes = await attributesToCreate(collection.remoteVersion.attributes, collection.attributes, collection); if (Array.isArray(attributes) && attributes.length <= 0) { log(`No changes has been detected. Skipping ${collection.name} ( ${collection['$id']} )`); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 02a8a557a..afa91e663 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -370,7 +370,7 @@ const questionsPushCollections = [ { type: "input", name: "changes", - message: `Changes above will cause recreation of an attribute. All existing documents will lose data in those attributes. Type "YES" to confirm` + message: `Are you sure you want to override this collection? This can lead to loss of data! Type "YES" to confirm.` } ] From a34f5de21211aba1cae7c4b05621e248ca49ce4e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 14:53:59 -0400 Subject: [PATCH 33/89] feat(cli): Added option for twig functions and --- src/SDK/Language.php | 9 +++++++++ src/SDK/SDK.php | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/SDK/Language.php b/src/SDK/Language.php index 0e806b497..496c8ac2b 100644 --- a/src/SDK/Language.php +++ b/src/SDK/Language.php @@ -84,6 +84,15 @@ public function getFilters(): array return []; } + /** + * Language specific functions. + * @return array + */ + public function getFunctions(): array + { + return []; + } + protected function toPascalCase(string $value): string { return \ucfirst($this->toCamelCase($value)); diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index 39d4f4251..e88994b95 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -85,6 +85,13 @@ public function __construct(Language $language, Spec $spec) 'debug' => true ]); + /** + * Add language-specific functions + */ + foreach ($this->language->getFunctions() as $function) { + $this->twig->addFunction($function); + } + /** * Add language specific filters */ From d66b6ae72e6d968e10cd161db6e324553aaf6b7a Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 14:54:24 -0400 Subject: [PATCH 34/89] feat(cli): Added twig `methodHaveConsolePreview` function --- src/SDK/Language/CLI.php | 72 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index e78bcbeb8..340802999 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -2,8 +2,66 @@ namespace Appwrite\SDK\Language; +use Twig\TwigFunction; + class CLI extends Node { + /** + * List of functions to ignore for console preview. + * @var array + */ + private $consoleIgnoreFunctions = [ + 'listidentities', + 'listmfafactors', + 'getprefs', + 'getsession', + 'getattribute', + 'listdocumentlogs', + 'getindex', + 'listcollectionlogs', + 'getcollectionusage', + 'listlogs', + 'listruntimes', + 'getusage', + 'getusage', + 'listvariables', + 'getvariable', + 'listproviderlogs', + 'listsubscriberlogs', + 'getsubscriber', + 'listtopiclogs', + 'getemailtemplate', + 'getsmstemplate', + 'getfiledownload', + 'getfilepreview', + 'getfileview', + 'getusage', + 'listlogs', + 'getprefs', + 'getusage', + 'listlogs', + 'getmembership', + 'listmemberships', + 'listmfafactors', + 'getmfarecoverycodes', + 'getprefs', + 'listtargets', + 'gettarget', + ]; + + /** + * List of SDK services to ignore for console preview. + * @var array + */ + private $consoleIgnoreServices = [ + 'health', + 'migrations', + 'locale', + 'avatars', + 'project', + 'proxy', + 'vcs' + ]; /** * @var array */ @@ -290,4 +348,18 @@ public function getParamExample(array $param): string return $output; } + + /** + * Language specific filters. + * @return array + */ + public function getFunctions(): array + { + return [ + /** Return true if the entered service->method is enabled for a console preview link */ + new TwigFunction('methodHaveConsolePreview', fn($method, $service) => preg_match('/^([Gg]et|[Ll]ist)/', $method) + && !in_array(strtolower($method), $this->consoleIgnoreFunctions) + && !in_array($service, $this->consoleIgnoreServices)), + ]; + } } From 0081b0755f48be8cc25ccb3afd04e486ea5967b7 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 14:54:42 -0400 Subject: [PATCH 35/89] feat(cli): Expanding console flow to list and various get commands --- templates/cli/base/requests/api.twig | 6 +- templates/cli/lib/commands/command.js.twig | 4 +- templates/cli/lib/utils.js.twig | 198 ++++++++++++++++++--- 3 files changed, 181 insertions(+), 27 deletions(-) diff --git a/templates/cli/base/requests/api.twig b/templates/cli/base/requests/api.twig index f1611ad28..479d22ab0 100644 --- a/templates/cli/base/requests/api.twig +++ b/templates/cli/base/requests/api.twig @@ -17,10 +17,10 @@ fs.writeFileSync(destination, response); {%~ endif %} if (parseOutput) { - {%~ if method.name == 'get' and service.name not in ['health','migrations','locale'] %} + {%~ if methodHaveConsolePreview(method.name,service.name) %} if(console) { - showConsoleLink('{{service.name}}', 'get' - {%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%},open {%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%} + showConsoleLink('{{service.name}}', '{{ method.name }}',open + {%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%}{%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%} ); } else { parse(response) diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 13dee1543..d2d16b67c 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -70,7 +70,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%} {%- if method.type == 'location' -%}, destination{%- endif -%} - {%- if method.name == 'get' -%}, console, open{%- endif -%} + {% if methodHaveConsolePreview(method.name,service.name) %}, console, open{%- endif -%} }) => { {%~ endblock %} let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : @@ -95,7 +95,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {% if method.type == 'location' %} .requiredOption(`--destination `, `output file path.`) {% endif %} -{% if method.name == 'get' %} +{% if methodHaveConsolePreview(method.name,service.name) %} .option(`--console`, `Get the resource console url`) .option(`--open`, `Use with '--console' to open the using default browser`) {% endif %} diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 3f12e0d31..356bd13cf 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -18,46 +18,45 @@ function getAllFiles(folder) { return files; } -function showConsoleLink(serviceName, action, open, id = '') { - let resource = ''; - let service = ''; + +function showConsoleLink(serviceName, action, open, ...ids) { + const projectId = localConfig.getProject().projectId; + + const url = new URL(globalConfig.getEndpoint().replace('/v1', '/console')); + url.pathname += `/project-${projectId}`; + action = action.toLowerCase(); switch (serviceName) { case "account": - service = 'account'; + url.pathname = url.pathname.replace(`/project-${projectId}`, ''); + url.pathname += getAccountPath(action); break; case "databases": - resource = 'database'; - service = 'databases'; + url.pathname += getDatabasePath(action, ids); break; case "functions": - resource = 'function'; - service = 'functions'; + url.pathname += getFunctionsPath(action, ids); + break; + case "messaging": + url.pathname += getMessagingPath(action, ids); break; case "projects": - service = `project-${id}`; - id = ''; + url.pathname = url.pathname.replace(`/project-${projectId}`, ''); + url.pathname += getProjectsPath(action, ids); + break; + case "storage": + url.pathname += getBucketsPath(action, ids); break; case "teams": - resource = 'team'; - service = 'auth/teams'; + url.pathname += getTeamsPath(action, ids); break; - case "users": - resource = 'user'; - service = 'auth'; + url.pathname += getUsersPath(action, ids); break; default: return; } - const baseUrl = globalConfig.getEndpoint().replace('/v1', ''); - - const end = action === 'get' ? (id ? `/${resource}-${id}` : `/${resource}`) : ''; - const projectId = localConfig.getProject().projectId; - const middle = resource !== '' ? `/project-${projectId}` : ''; - const url = `${baseUrl}/console${middle}/${service}${end}` - success(url); @@ -67,6 +66,161 @@ function showConsoleLink(serviceName, action, open, id = '') { } } +function getAccountPath(action) { + let path = '/account'; + + if (action === 'listsessions') { + path += '/sessions'; + } + + return path; +} + +function getDatabasePath(action, ids) { + let path = '/databases'; + + + if (['get', 'listcollections', 'getcollection', 'listattributes', 'listdocuments', 'getdocument', 'listindexes', 'getdatabaseusage'].includes(action)) { + path += `/database-${ids[0]}`; + } + + if (action === 'getdatabaseusage') { + path += `/usage`; + } + + if (['getcollection', 'listattributes', 'listdocuments', 'getdocument', 'listindexes'].includes(action)) { + path += `/collection-${ids[1]}`; + } + + if (action === 'listattributes') { + path += '/attributes'; + } + if (action === 'listindexes') { + path += '/indexes'; + } + if (action === 'getdocument') { + path += `/document-${ids[2]}`; + } + + + return path; +} + +function getFunctionsPath(action, ids) { + let path = '/functions'; + + if (action !== 'list') { + path += `/function-${ids[0]}`; + } + + if (action === 'getdeployment') { + path += `/deployment-${ids[1]}` + } + + if (action === 'getexecution' || action === 'listexecution') { + path += `/executions` + } + if (action === 'getfunctionusage') { + path += `/usage` + } + + return path; +} + +function getMessagingPath(action, ids) { + let path = '/messaging'; + + if (['getmessage', 'listmessagelogs'].includes(action)) { + path += `/message-${ids[0]}`; + } + + if (['listproviders', 'getprovider'].includes(action)) { + path += `/providers`; + } + + if (action === 'getprovider') { + path += `/provider-${ids[0]}`; + } + + if (['listtopics', 'gettopic'].includes(action)) { + path += `/topics`; + } + + if (action === 'gettopic') { + path += `/topic-${ids[0]}`; + } + + return path; +} + +function getProjectsPath(action, ids) { + let path = ''; + + if (action !== 'list') { + path += `/project-${ids[0]}`; + } + + if (['listkeys', 'getkey'].includes(action)) { + path += '/overview/keys' + } + + if (['listplatforms', 'getplatform'].includes(action)) { + path += '/overview/platforms' + } + + if (['listwebhooks', 'getwebhook'].includes(action)) { + path += '/settings/webhooks' + } + + if (['getplatform', 'getkey', 'getwebhook'].includes(action)) { + path += ids[1]; + } + + return path; +} + +function getBucketsPath(action, ids) { + let path = '/storage'; + + if (action !== 'listbuckets') { + path += `/bucket-${ids[0]}`; + } + + if (action === 'getbucketusage') { + path += `/usage` + } + + if (action === 'getfile') { + path += `/file-${ids[1]}` + } + + return path; +} + +function getTeamsPath(action, ids) { + let path = '/auth/teams'; + + if (action !== 'list') { + path += `/team-${ids[0]}`; + } + + return path; +} + +function getUsersPath(action, ids) { + let path = '/auth'; + + if (action !== 'list') { + path += `/user-${ids[0]}`; + } + + if (action === 'listsessions') { + path += 'sessions'; + } + + return path; +} + module.exports = { getAllFiles, showConsoleLink From 8b4df13a361de07c05db1649aca25d58dad6dd20 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 09:15:00 -0400 Subject: [PATCH 36/89] fix(cli): Leading slash for project endpoints --- templates/cli/lib/utils.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 356bd13cf..7ec24aabc 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -173,7 +173,7 @@ function getProjectsPath(action, ids) { } if (['getplatform', 'getkey', 'getwebhook'].includes(action)) { - path += ids[1]; + path += `/${ids[1]}`; } return path; From 8e639d14620004af75979fcfe0e01c9cde9e5319 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 12:06:33 -0400 Subject: [PATCH 37/89] fix(cli): Pull and update project available services, auth security settings, and login methods. --- templates/cli/lib/commands/pull.js.twig | 2 + templates/cli/lib/commands/push.js.twig | 63 +++++++++++++++++++++++-- templates/cli/lib/config.js.twig | 49 +++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 578046c4e..2ce948df8 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -22,6 +22,8 @@ const pullProject = async () => { }) localConfig.setProject(response.$id, response.name); + localConfig.setProjectSettings(response); + success(); } catch (e) { throw e; diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 0abe58d9f..76687d69e 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -41,7 +41,17 @@ const { teamsUpdateName, teamsCreate } = require("./teams"); -const { projectsUpdate } = require("./projects"); +const { + projectsUpdate, + projectsUpdateServiceStatus, + projectsUpdateAuthStatus, + projectsUpdateAuthDuration, + projectsUpdateAuthLimit, + projectsUpdateAuthSessionsLimit, + projectsUpdateAuthPasswordDictionary, + projectsUpdateAuthPasswordHistory, + projectsUpdatePersonalDataCheck, +} = require("./projects"); const { checkDeployConditions } = require('../utils'); const STEP_SIZE = 100; // Resources @@ -258,11 +268,56 @@ const pushResources = async ({ all, yes } = {}) => { const pushProject = async () => { try { + const projectId = localConfig.getProject().projectId; + const projectName = localConfig.getProject().projectName; + + log('Updating project name'); + await projectsUpdate({ - projectId: localConfig.getProject().projectId, - name: localConfig.getProject().projectName, + projectId, + name: projectName, parseOutput: false - }) + }); + + const settings = localConfig.getProjectSettings(); + + if (settings.services) { + log('Updating services status'); + for (let [service, status] of Object.entries(settings.services)) { + await projectsUpdateServiceStatus({ + projectId, + service, + status, + parseOutput: false + }); + } + } + + if (settings.auth) { + if (settings.auth.security) { + log('Updating Auth security settings'); + await projectsUpdateAuthDuration({ projectId, duration: settings.auth.security.duration, parseOutput: false }); + await projectsUpdateAuthLimit({ projectId, limit: settings.auth.security.limit, parseOutput: false }); + await projectsUpdateAuthSessionsLimit({ projectId, limit: settings.auth.security.sessionsLimit, parseOutput: false }); + await projectsUpdateAuthPasswordDictionary({ projectId, enabled: settings.auth.security.passwordDictionary, parseOutput: false }); + await projectsUpdateAuthPasswordHistory({ projectId, limit: settings.auth.security.passwordHistory, parseOutput: false }); + await projectsUpdatePersonalDataCheck({ projectId, enabled: settings.auth.security.personalDataCheck, parseOutput: false }); + } + + if (settings.auth.methods) { + log('Updating Auth available login methods'); + + for (let [method, status] of Object.entries(settings.auth.methods)) { + await projectsUpdateAuthStatus({ + projectId, + method, + status, + parseOutput: false + }); + } + } + } + success(); } catch (e) { throw e; diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 37c3899f6..6fe98df7b 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -336,6 +336,55 @@ class Local extends Config { this.set("projectId", projectId); this.set("projectName", projectName); } + + + getProjectSettings() { + if (!this.has("projectSettings")) { + return {}; + } + + return this.get('projectSettings') + } + + setProjectSettings(project) { + const settings = { + services: { + account: project.serviceStatusForAccount, + avatars: project.serviceStatusForAvatars, + databases: project.serviceStatusForDatabases, + locale: project.serviceStatusForLocale, + health: project.serviceStatusForHealth, + storage: project.serviceStatusForStorage, + teams: project.serviceStatusForTeams, + users: project.serviceStatusForUsers, + functions: project.serviceStatusForFunctions, + graphql: project.serviceStatusForGraphql, + messaging: project.serviceStatusForMessaging, + + }, + auth: { + methods: { + jwt: project.authJWT, + phone: project.authPhone, + invites: project.authInvites, + anonymous: project.authAnonymous, + "email-otp": project.authEmailOtp, + "magic-url": project.authUsersAuthMagicURL, + "email-password": project.authEmailPassword + }, + security: { + duration: project.authDuration, + limit: project.authLimit, + sessionsLimit: project.authSessionsLimit, + passwordHistory: project.authPasswordHistory, + passwordDictionary: project.authPasswordDictionary, + personalDataCheck: project.authPersonalDataCheck + } + } + }; + + this.set('projectSettings', settings) + } } class Global extends Config { From 224c3fd5930117ae465a330ae3eaeb6d7e40f524 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 12:13:34 -0400 Subject: [PATCH 38/89] chore(cli): Updating project commands descriptions --- templates/cli/lib/commands/pull.js.twig | 2 +- templates/cli/lib/commands/push.js.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 2ce948df8..ee1f0593f 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -171,7 +171,7 @@ const pull = new Command("pull") pull .command("project") - .description("Pulling your {{ spec.title|caseUcfirst }} project name") + .description("Pulling your {{ spec.title|caseUcfirst }} project name, services and auth settings") .action(actionRunner(pullProject)); pull diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 76687d69e..1bf5afcec 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1186,7 +1186,7 @@ push push .command("project") - .description("Push project name.") + .description("Push project name, services and auth settings") .action(actionRunner(pushProject)); push From 02be0f0f765e94ac9081fcffe7600f3af04ed4ba Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 12:20:17 -0400 Subject: [PATCH 39/89] feat(cli): Updating push to include the project --- templates/cli/lib/commands/push.js.twig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 1bf5afcec..df19a6a75 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -4,7 +4,7 @@ const { Command } = require("commander"); const { localConfig, globalConfig } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); -const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics } = require("../questions"); +const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { @@ -246,6 +246,7 @@ const awaitPools = { const pushResources = async ({ all, yes } = {}) => { const actions = { + project: pushProject, functions: pushFunction, collections: pushCollection, buckets: pushBucket, @@ -254,7 +255,9 @@ const pushResources = async ({ all, yes } = {}) => { } if (all) { - Object.values(actions).forEach(action => action({ all: true, yes })); + for(let action of Object.values(actions)){ + await action({ all: true, yes }); + } } else { const answers = await inquirer.prompt(questionsPushResources[0]); answers.resources.forEach((resource) => { @@ -1182,6 +1185,8 @@ const push = new Command("push") push .command("all") .description("Push all resource.") + .option(`--all`, `Flag to push all functions`) + .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushResources)); push From 9ac316d3a11759bc259dba99396dd9e939152e42 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 12:20:26 -0400 Subject: [PATCH 40/89] feat(cli): Updating push to include the project --- templates/cli/lib/questions.js.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 866a04f2f..e72833c25 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -429,6 +429,7 @@ const questionsPushResources = [ name: "resources", message: "Which resources would you like to push?", choices: [ + { name: 'Project', value: 'project' }, { name: 'Functions', value: 'functions' }, { name: 'Collections', value: 'collections' }, { name: 'Buckets', value: 'buckets' }, From e089e9ca54d91747847a4e1e765705b36312e6a1 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 12:30:59 -0400 Subject: [PATCH 41/89] feat(cli): Pull all resources --- templates/cli/lib/commands/pull.js.twig | 40 ++++++++++++++++++++++++- templates/cli/lib/questions.js.twig | 16 ++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index ee1f0593f..26c359925 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -10,9 +10,34 @@ const { databasesGet, databasesListCollections, databasesList } = require("./dat const { storageListBuckets } = require("./storage"); const { localConfig } = require("../config"); const { paginate } = require("../paginate"); -const { questionsPullCollection, questionsPullFunctions } = require("../questions"); +const { questionsPullCollection, questionsPullFunctions, questionsPullResources } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); +const pullResources = async ({ all, yes } = {}) => { + const actions = { + project: pullProject, + functions: pullFunctions, + collections: pullCollection, + buckets: pullBucket, + teams: pullTeam, + messages: pullMessagingTopic + } + + if (all) { + for (let action of Object.values(actions)) { + await action({ all: true, yes }); + } + } else { + const answers = await inquirer.prompt(questionsPullResources[0]); + answers.resources.forEach((resource) => { + const action = actions[resource]; + if (action !== undefined) { + action({ all: true, yes }); + } + }) + } +}; + const pullProject = async () => { try { let response = await projectsGet({ @@ -47,6 +72,7 @@ const pullFunctions = async ({ all } = {}) => { } else { func['path'] = `functions/${func['$id']}`; localConfig.addFunction(func); + localFunctions.push(func); } const localFunction = localFunctions.find((localFunc) => localFunc['$id'] === func['$id']); @@ -65,6 +91,10 @@ const pullFunctions = async ({ all } = {}) => { parseOutput: false }) + if (!fs.existsSync(localFunction['path'])) { + fs.mkdirSync(localFunction['path'], { recursive: true }); + } + tar.extract({ sync: true, cwd: localFunction['path'], @@ -169,6 +199,14 @@ const pull = new Command("pull") helpWidth: process.stdout.columns || 80 }); +pull + .command("all") + .description("Push all resource.") + .option(`--all`, `Flag to pull all functions`) + .option(`--yes`, `Flag to confirm all warnings`) + .action(actionRunner(pullResources)); + + pull .command("project") .description("Pulling your {{ spec.title|caseUcfirst }} project name, services and auth settings") diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index e72833c25..ad550b557 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -213,6 +213,21 @@ const questionsInitProject = [ when: (answer) => answer.start === 'existing' } ]; +const questionsPullResources = [ + { + type: "checkbox", + name: "resources", + message: "Which resources would you like to pull?", + choices: [ + { name: 'Project', value: 'project' }, + { name: 'Functions', value: 'functions' }, + { name: 'Collections', value: 'collections' }, + { name: 'Buckets', value: 'buckets' }, + { name: 'Teams', value: 'teams' }, + { name: 'Topics', value: 'messages' } + ] + } +] const questionsPullFunctions = [ { @@ -648,6 +663,7 @@ module.exports = { questionsCreateMessagingTopic, questionsPullFunctions, questionsLogin, + questionsPullResources, questionsPullCollection, questionsPushResources, questionsPushFunctions, From d0cc1c2776707b0951cecccd22ab7ee0e5a8d31f Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 16:38:33 -0400 Subject: [PATCH 42/89] feat(cli): Adapting pull to cliConfig --- templates/cli/lib/commands/pull.js.twig | 26 ++++++++++++------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 43fb0259c..9e3e69c91 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -11,7 +11,7 @@ const { storageListBuckets } = require("./storage"); const { localConfig } = require("../config"); const { paginate } = require("../paginate"); const { questionsPullCollection, questionsPullFunctions } = require("../questions"); -const { success, log, actionRunner, commandDescriptions } = require("../parser"); +const { cliConfig, success, log, actionRunner, commandDescriptions } = require("../parser"); const pullProject = async () => { try { @@ -30,10 +30,10 @@ const pullProject = async () => { } } -const pullFunctions = async ({ all } = {}) => { +const pullFunctions = async () => { const localFunctions = localConfig.getFunctions(); - const functions = all + const functions = cliConfig.all ? (await paginate(functionsList, { parseOutput: false }, 100, 'functions')).functions : (await inquirer.prompt(questionsPullFunctions)).functions; @@ -74,22 +74,21 @@ const pullFunctions = async ({ all } = {}) => { fs.rmSync(compressedFileName); success(`Pulled ${func['name']} code and settings`) - } } } const pullCollection = async () => { - const databasesIds = (await inquirer.prompt(questionsPullCollection)).databases; + let databases = cliConfig.ids; - for (let id of databasesIds) { - const databaseId = id.value ? id.value : id; - - if (databaseIds.length <= 0) { - let answers = await inquirer.prompt(questionsPullCollection) - databaseIds.push(...answers.databases); + if (databases.length === 0) { + if (cliConfig.all) { + databases = (await paginate(databasesList, { parseOutput: false }, 100, 'databases')).databases.map(database=>database.$id); + } else{ + databases = (await inquirer.prompt(questionsPullCollection)).databases; + } } - for (const databaseId of databaseIds) { + for (const databaseId of databases) { const database = await databasesGet({ databaseId, parseOutput: false @@ -113,9 +112,9 @@ const pullCollection = async () => { }); }); + success(); } - success(); } const pullBucket = async () => { @@ -172,7 +171,6 @@ pull pull .command("functions") .description(`Pulling your {{ spec.title|caseUcfirst }} functions`) - .option(`--all`, `Flag to pull all functions`) .action(actionRunner(pullFunctions)); pull From 0c1bc455be478cf2a220e9ae94cbaa5a80d28426 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 16:59:13 -0400 Subject: [PATCH 43/89] chore(cli): Adapting pull request --- templates/cli/lib/commands/push.js.twig | 36 ++++++++++++------------- templates/cli/lib/questions.js.twig | 1 - 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index c0f2518e1..6e73a5ead 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -5,9 +5,8 @@ const { Command } = require("commander"); const { localConfig, globalConfig } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); -const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics } = require("../questions"); -const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); -const { cliConfig, actionRunner, success, log, error, commandDescriptions } = require("../parser"); +const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); +const { cliConfig, actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { databasesGet, @@ -29,7 +28,6 @@ const { databasesDeleteAttribute, databasesListAttributes, databasesListIndexes, - databasesDeleteIndex, databasesUpdateCollection } = require("./databases"); const { @@ -376,7 +374,7 @@ const pushFunction = async ({ functionId, async } = {}) => { if (functions.length === 0) { throw new Error("No functions found in the current directory."); } - functionIds.push(...functions.map((func, idx) => { + functionIds.push(...functions.map((func) => { return func.$id; })); } @@ -480,7 +478,7 @@ const pushFunction = async ({ functionId, async } = {}) => { }); } catch (e) { - if (e.code == 404) { + if (Number(e.code) === 404) { functionExists = false; } else { updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); @@ -835,15 +833,19 @@ const attributesToCreate = async (remoteAttributes, localAttributes, collection) if (changing.length === 0) { return changedAttributes; } - log('There are pending changes in your collection deployment'); + + log(!cliConfig.force ? 'There are pending changes in your collection deployment' : 'List of applied changes'); + drawTable(changing.map((change) => { return { Key: change.key, Action: change.action, Reason: change.reason, }; })); - const answers = await inquirer.prompt(questionsPushCollections[1]); + if (!cliConfig.force) { + const answers = await inquirer.prompt(questionsPushCollections[1]); - if (answers.changes.toLowerCase() !== 'yes') { - return changedAttributes; + if (answers.changes.toLowerCase() !== 'yes') { + return changedAttributes; + } } if (conflicts.length > 0) { @@ -888,6 +890,7 @@ const pushCollection = async () => { }) } const databases = Array.from(new Set(collections.map(collection => collection['databaseId']))); + log('Checking for databases and collection changes'); // Parallel db actions await Promise.all(databases.map(async (databaseId) => { @@ -926,7 +929,6 @@ const pushCollection = async () => { databaseId: collection['databaseId'], collectionId: collection['$id'], parseOutput: false, - }); }); if (remoteCollection.name !== collection.name) { @@ -934,7 +936,6 @@ const pushCollection = async () => { databaseId: collection['databaseId'], collectionId: collection['$id'], name: collection.name, - name: collection.name, parseOutput: false }) @@ -944,7 +945,7 @@ const pushCollection = async () => { collection.isExisted = true; } catch (e) { - if (e.code == 404) { + if (Number(e.code) === 404) { log(`Collection ${collection.name} does not exist in the project. Creating ... `); await databasesCreateCollection({ databaseId: collection['databaseId'], @@ -1092,7 +1093,6 @@ const pushBucket = async () => { enabled: bucket.enabled, maximumFileSize: bucket.maximumFileSize, allowedFileExtensions: bucket.allowedFileExtensions, - compression: bucket.compression, encryption: bucket.encryption, antivirus: bucket.antivirus, compression: bucket.compression, @@ -1101,7 +1101,7 @@ const pushBucket = async () => { success(`Pushed ${bucket.name} ( ${bucket['$id']} )`); } catch (e) { - if (e.code == 404) { + if (Number(e.code) === 404) { log(`Bucket ${bucket.name} does not exist in the project. Creating ... `); response = await storageCreateBucket({ @@ -1172,7 +1172,7 @@ const pushTeam = async () => { log(`Updating team ...`) - await teamsUpdate({ + await teamsUpdateName({ teamId: team['$id'], name: team.name, parseOutput: false @@ -1180,7 +1180,7 @@ const pushTeam = async () => { success(`Pushed ${team.name} ( ${team['$id']} )`); } catch (e) { - if (e.code == 404) { + if (Number(e.code) === 404) { log(`Team ${team.name} does not exist in the project. Creating ... `); response = await teamsCreate({ @@ -1257,7 +1257,7 @@ const pushMessagingTopic = async () => { success(`Pushed ${topic.name} ( ${topic['$id']} )`); } catch (e) { - if (e.code == 404) { + if (Number(e.code) === 404) { log(`Topic ${topic.name} does not exist in the project. Creating ... `); response = await messagingCreateTopic({ diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index edb1fca4c..7679c0249 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -7,7 +7,6 @@ const { accountListMfaFactors } = require("./commands/account"); const { sdkForConsole } = require("./sdks"); const { validateRequired } = require("./validations"); const { paginate } = require('./paginate'); -const chalk = require('chalk'); const { databasesList } = require('./commands/databases'); const { checkDeployConditions } = require('./utils'); const JSONbig = require("json-bigint")({ storeAsString: false }); From 705647db218dd52ea0a1903cc03fbb5bde83e336 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 17:35:18 -0400 Subject: [PATCH 44/89] chore(cli): Adding headless login --- templates/cli/lib/commands/generic.js.twig | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index ee1481de8..c96bac56b 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -11,8 +11,8 @@ const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accoun const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; -const loginCommand = async ({ selfHosted }) => { - const answers = await inquirer.prompt(questionsLogin); +const loginCommand = async ({ selfHosted, email, password, endpoint, mfa, code }) => { + const answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); if (answers.method === 'select') { const accountId = answers.accountId; @@ -36,7 +36,7 @@ const loginCommand = async ({ selfHosted }) => { globalConfig.setEndpoint(DEFAULT_ENDPOINT); if (selfHosted) { - const selfHostedAnswers = await inquirer.prompt(questionGetEndpoint); + const selfHostedAnswers = endpoint ? { endpoint } : await inquirer.prompt(questionGetEndpoint); globalConfig.setEndpoint(selfHostedAnswers.endpoint); } @@ -61,7 +61,7 @@ const loginCommand = async ({ selfHosted }) => { }); } catch (error) { if (error.response === 'user_more_factors_required') { - const { factor } = await inquirer.prompt(questionsListFactors); + const { factor } = mfa ? { factor: mfa } : await inquirer.prompt(questionsListFactors); const challenge = await accountCreateMfaChallenge({ factor, @@ -69,7 +69,7 @@ const loginCommand = async ({ selfHosted }) => { sdk: client }); - const { otp } = await inquirer.prompt(questionsMfaChallenge); + const { otp } = code ? { otp: code } : await inquirer.prompt(questionsMfaChallenge); await accountUpdateMfaChallenge({ challengeId: challenge.$id, @@ -135,6 +135,11 @@ const whoami = new Command("whoami") const login = new Command("login") .description(commandDescriptions['login']) .option(`-sh, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`) + .option(`--email [email]`, `User email`) + .option(`--password [password]`, `User password`) + .option(`--endpoint [endpoint]`, `Appwrite endpoint for self hosted instances`) + .option(`--mfa [factor]`, `Multi-factor authentication login factor: totp, email, phone or recoveryCode`) + .option(`--code [code]`, `Multi-factor code`) .configureHelp({ helpWidth: process.stdout.columns || 80 }) From 87e88c3240b2cb8d9ae6453bf09d4ecdcd82bd95 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 29 May 2024 17:56:44 -0400 Subject: [PATCH 45/89] chore(cli): Adapting pull request --- templates/cli/lib/commands/pull.js.twig | 12 ++++++------ templates/cli/lib/commands/push.js.twig | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 97eb6314a..ca825f283 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -29,12 +29,13 @@ const pullResources = async () => { } } else { const answers = await inquirer.prompt(questionsPullResources[0]); - answers.resources.forEach((resource) => { + + for (let resource of answers.resources) { const action = actions[resource]; if (action !== undefined) { - action(); + await action(); } - }) + } } }; @@ -112,10 +113,9 @@ const pullCollection = async () => { if (databases.length === 0) { if (cliConfig.all) { - databases = (await paginate(databasesList, { parseOutput: false }, 100, 'databases')).databases.map(database=>database.$id); - } else{ + databases = (await paginate(databasesList, { parseOutput: false }, 100, 'databases')).databases.map(database => database.$id); + } else { databases = (await inquirer.prompt(questionsPullCollection)).databases; - } } } diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 21ba05502..9b94a6f2b 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -290,18 +290,18 @@ const pushResources = async () => { } if (cliConfig.all) { - for(let action of Object.values(actions)){ + for (let action of Object.values(actions)) { await action(); } } else { const answers = await inquirer.prompt(questionsPushResources[0]); - answers.resources.forEach((resource) => { + for (let resource of answers.resources) { const action = actions[resource]; if (action !== undefined) { - await action(); + await action(); } - }) + } } }; @@ -932,7 +932,6 @@ const pushCollection = async () => { databaseId: collection['databaseId'], collectionId: collection['$id'], parseOutput: false, - }); }); if (remoteCollection.name !== collection.name) { @@ -940,7 +939,7 @@ const pushCollection = async () => { databaseId: collection['databaseId'], collectionId: collection['$id'], name: collection.name, - name: collection.name, + name: collection.name, parseOutput: false }) @@ -949,7 +948,8 @@ const pushCollection = async () => { collection.remoteVersion = remoteCollection; collection.isExisted = true; - } catch (e) { + } catch + (e) { if (Number(e.code) === 404) { log(`Collection ${collection.name} does not exist in the project. Creating ... `); await databasesCreateCollection({ @@ -966,7 +966,7 @@ const pushCollection = async () => { } })) - // Serialize attribute actions +// Serialize attribute actions for (let collection of collections) { let attributes = collection.attributes; From 063ac9e38aa2e47ed84d1e51aa11799f0948cd76 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 30 May 2024 09:01:28 -0400 Subject: [PATCH 46/89] refactor(cli): Texts and removing unnecessary options --- templates/cli/lib/commands/pull.js.twig | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index ca825f283..230880987 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -195,40 +195,38 @@ const pull = new Command("pull") pull .command("all") - .description("Push all resource.") - .option(`--all`, `Flag to pull all functions`) - .option(`--yes`, `Flag to confirm all warnings`) + .description("Pull all resource.") .action(actionRunner(pullResources)); pull .command("project") - .description("Pulling your {{ spec.title|caseUcfirst }} project name, services and auth settings") + .description("Pull your {{ spec.title|caseUcfirst }} project name, services and auth settings") .action(actionRunner(pullProject)); pull .command("functions") - .description(`Pulling your {{ spec.title|caseUcfirst }} functions`) + .description(`Pull your {{ spec.title|caseUcfirst }} functions`) .action(actionRunner(pullFunctions)); pull .command("collections") - .description("Pulling your {{ spec.title|caseUcfirst }} collections") + .description("Pull your {{ spec.title|caseUcfirst }} collections") .action(actionRunner(pullCollection)) pull .command("buckets") - .description("Pulling your {{ spec.title|caseUcfirst }} buckets") + .description("Pull your {{ spec.title|caseUcfirst }} buckets") .action(actionRunner(pullBucket)) pull .command("teams") - .description("Pulling your {{ spec.title|caseUcfirst }} teams") + .description("Pull your {{ spec.title|caseUcfirst }} teams") .action(actionRunner(pullTeam)) pull .command("topics") - .description("Initialise your Appwrite messaging topics") + .description("Pull your {{ spec.title|caseUcfirst }} messaging topics") .action(actionRunner(pullMessagingTopic)) module.exports = { From cc0f8540185dfbf0759fc36f7cbc9bad5a47db6d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 30 May 2024 09:13:20 -0400 Subject: [PATCH 47/89] refactor(cli): Texts and Adding stacktrace --- templates/cli/index.js.twig | 2 +- templates/cli/lib/parser.js.twig | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index ca8a2a1c4..cb95de12c 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -29,7 +29,7 @@ program .version(version, "-v, --version") .option("--verbose", "Show complete error log") .option("--json", "Output in JSON format") - .option("--report", "Enable reporting when cli is crashing") + .option("--report", "Enable reporting in case of CLI errors") .on("option:json", () => { cliConfig.json = true; }) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 616ae26f4..4c38d9746 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -142,6 +142,11 @@ const parseError = (err) => { log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl.href}\n`); + + error('\n Error stacktrace'); + + console.error(err); + process.exit(1); })() } else { From 5cd9f93fc6efb93b63376f9c8e50f504aa46e706 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 30 May 2024 09:24:43 -0400 Subject: [PATCH 48/89] refactor(cli): removing implemented feature --- templates/cli/lib/paginate.js.twig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/templates/cli/lib/paginate.js.twig b/templates/cli/lib/paginate.js.twig index bb8090507..c78814a0e 100644 --- a/templates/cli/lib/paginate.js.twig +++ b/templates/cli/lib/paginate.js.twig @@ -5,15 +5,10 @@ const paginate = async (action, args = {}, limit = 100, wrapper = '') => { while (true) { const offset = pageNumber * limit; - const additionalQueries = []; - if (args.queries) { - additionalQueries.push(...args.queries); - } // Merge the limit and offset into the args const response = await action({ ...args, queries: [ - ...additionalQueries, JSON.stringify({ method: 'limit', values: [limit] }), JSON.stringify({ method: 'offset', values: [offset] }) ] From db6ebfef07fd99c8f988a958a5a25d83e9b26ef8 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 30 May 2024 09:36:23 -0400 Subject: [PATCH 49/89] refactor(cli): refactoring to one function --- templates/cli/lib/commands/pull.js.twig | 3 +- templates/cli/lib/commands/push.js.twig | 2 +- templates/cli/lib/config.js.twig | 63 +++++++++++-------------- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index ee1f0593f..04c92c82d 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -21,8 +21,7 @@ const pullProject = async () => { }) - localConfig.setProject(response.$id, response.name); - localConfig.setProjectSettings(response); + localConfig.setProject(response.$id, response.name, response); success(); } catch (e) { diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 1bf5afcec..d05127172 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -279,7 +279,7 @@ const pushProject = async () => { parseOutput: false }); - const settings = localConfig.getProjectSettings(); + const settings = localConfig.getProject().projectSettings; if (settings.services) { log('Updating services status'); diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 6fe98df7b..13dbd17a2 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -329,62 +329,53 @@ class Local extends Config { return { projectId: this.get("projectId"), projectName: this.get("projectName"), + projectSettings: this.get('projectSettings') }; } - setProject(projectId, projectName) { + setProject(projectId, projectName, projectSettings = {}) { this.set("projectId", projectId); this.set("projectName", projectName); - } - - - getProjectSettings() { - if (!this.has("projectSettings")) { - return {}; - } - - return this.get('projectSettings') - } - setProjectSettings(project) { const settings = { services: { - account: project.serviceStatusForAccount, - avatars: project.serviceStatusForAvatars, - databases: project.serviceStatusForDatabases, - locale: project.serviceStatusForLocale, - health: project.serviceStatusForHealth, - storage: project.serviceStatusForStorage, - teams: project.serviceStatusForTeams, - users: project.serviceStatusForUsers, - functions: project.serviceStatusForFunctions, - graphql: project.serviceStatusForGraphql, - messaging: project.serviceStatusForMessaging, + account: projectSettings.serviceStatusForAccount, + avatars: projectSettings.serviceStatusForAvatars, + databases: projectSettings.serviceStatusForDatabases, + locale: projectSettings.serviceStatusForLocale, + health: projectSettings.serviceStatusForHealth, + storage: projectSettings.serviceStatusForStorage, + teams: projectSettings.serviceStatusForTeams, + users: projectSettings.serviceStatusForUsers, + functions: projectSettings.serviceStatusForFunctions, + graphql: projectSettings.serviceStatusForGraphql, + messaging: projectSettings.serviceStatusForMessaging, }, auth: { methods: { - jwt: project.authJWT, - phone: project.authPhone, - invites: project.authInvites, - anonymous: project.authAnonymous, - "email-otp": project.authEmailOtp, - "magic-url": project.authUsersAuthMagicURL, - "email-password": project.authEmailPassword + jwt: projectSettings.authJWT, + phone: projectSettings.authPhone, + invites: projectSettings.authInvites, + anonymous: projectSettings.authAnonymous, + "email-otp": projectSettings.authEmailOtp, + "magic-url": projectSettings.authUsersAuthMagicURL, + "email-password": projectSettings.authEmailPassword }, security: { - duration: project.authDuration, - limit: project.authLimit, - sessionsLimit: project.authSessionsLimit, - passwordHistory: project.authPasswordHistory, - passwordDictionary: project.authPasswordDictionary, - personalDataCheck: project.authPersonalDataCheck + duration: projectSettings.authDuration, + limit: projectSettings.authLimit, + sessionsLimit: projectSettings.authSessionsLimit, + passwordHistory: projectSettings.authPasswordHistory, + passwordDictionary: projectSettings.authPasswordDictionary, + personalDataCheck: projectSettings.authPersonalDataCheck } } }; this.set('projectSettings', settings) } + } class Global extends Config { From 0135a4f9c577b3056449d2a9006562d329e2627b Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 30 May 2024 13:25:02 -0400 Subject: [PATCH 50/89] refactor(cli): removing unneeded async --- templates/cli/lib/commands/pull.js.twig | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 230880987..46478fd15 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -152,10 +152,7 @@ const pullBucket = async () => { log(`Found ${buckets.length} buckets`); - buckets.forEach(async bucket => { - log(`Fetching ${bucket.name} ...`); - localConfig.addBucket(bucket); - }); + buckets.forEach(bucket => localConfig.addBucket(bucket)); success(); } @@ -165,8 +162,7 @@ const pullTeam = async () => { log(`Found ${teams.length} teams`); - teams.forEach(async team => { - log(`Fetching ${team.name} ...`); + teams.forEach(team => { const { total, $updatedAt, $createdAt, prefs, ...rest } = team; localConfig.addTeam(rest); }); @@ -179,8 +175,7 @@ const pullMessagingTopic = async () => { log(`Found ${topics.length} topics`); - topics.forEach(async topic => { - log(`Pulling ${topic.name} ...`); + topics.forEach(topic => { localConfig.addMessagingTopic(topic); }); From dcea1df36fcdedb1ecab181df882ea6b4cec7783 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 30 May 2024 13:30:24 -0400 Subject: [PATCH 51/89] feat(cli): Skipping unavailable push resources --- templates/cli/lib/commands/push.js.twig | 808 ++++++++++++------------ 1 file changed, 412 insertions(+), 396 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 9b94a6f2b..e1e7c60be 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -279,6 +279,282 @@ const awaitPools = { }, } +const createAttribute = async (databaseId, collectionId, attribute) => { + switch (attribute.type) { + case 'string': + switch (attribute.format) { + case 'email': + return await databasesCreateEmailAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'url': + return await databasesCreateUrlAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'ip': + return await databasesCreateIpAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'enum': + return await databasesCreateEnumAttribute({ + databaseId, + collectionId, + key: attribute.key, + elements: attribute.elements, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + default: + return await databasesCreateStringAttribute({ + databaseId, + collectionId, + key: attribute.key, + size: attribute.size, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + + } + case 'integer': + return await databasesCreateIntegerAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + min: attribute.min, + max: attribute.max, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'double': + return databasesCreateFloatAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + min: attribute.min, + max: attribute.max, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'boolean': + return databasesCreateBooleanAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'datetime': + return databasesCreateDatetimeAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'relationship': + return databasesCreateRelationshipAttribute({ + databaseId, + collectionId, + relatedCollectionId: attribute.relatedCollection, + type: attribute.relationType, + twoWay: attribute.twoWay, + key: attribute.key, + twoWayKey: attribute.twoWayKey, + onDelete: attribute.onDelete, + parseOutput: false + }) + } +} +const deleteAttribute = async (collection, attribute) => { + log(`Deleting attribute ${attribute.key} of ${collection.name} ( ${collection['$id']} )`); + + await databasesDeleteAttribute({ + databaseId: collection['databaseId'], + collectionId: collection['$id'], + key: attribute.key, + parseOutput: false + }); +} + +/** + * Check if attribute non-changeable fields has been changed + * If so return the differences as an object. + * @param remote + * @param local + * @param collection + * @returns {undefined|{reason: string, action: *, attribute, key: string}} + */ +const checkAttributeChanges = (remote, local, collection) => { + if (local === undefined) { + return undefined; + } + + const keyName = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; + const action = chalk.cyan('recreating'); + let reason = ''; + + for (let key of Object.keys(remote)) { + if (changeableKeys.includes(key)) { + continue; + } + + if (remote[key] !== local[key]) { + const bol = reason === '' ? '' : '\n'; + reason += `${bol}${key} changed from ${chalk.red(remote[key])} to ${chalk.green(local[key])}`; + } + } + + return reason === '' ? undefined : { key: keyName, attribute: remote, reason, action }; +} + +/** + * Check if attributes contain the given attribute + * @param attribute + * @param attributes + * @returns {*} + */ +const attributesContains = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key); +const generateChangesObject = (attribute, collection, isAdding) => { + return { + key: `${chalk.yellow(attribute.key)} in ${collection.name} (${collection['$id']})`, + attribute: attribute, + reason: isAdding ? 'Field doesn\'t exist on the remote server' : 'Field doesn\'t exist in appwrite.json file', + action: isAdding ? chalk.green('adding') : chalk.red('deleting') + }; + +}; + +/** + * Filter deleted and recreated attributes, + * return list of attributes to create + * @param remoteAttributes + * @param localAttributes + * @param collection + * @returns {Promise<*|*[]>} + */ +const attributesToCreate = async (remoteAttributes, localAttributes, collection) => { + + const deleting = remoteAttributes.filter((attribute) => !attributesContains(attribute, localAttributes)).map((attr) => generateChangesObject(attr, collection, false)); + const adding = localAttributes.filter((attribute) => !attributesContains(attribute, remoteAttributes)).map((attr) => generateChangesObject(attr, collection, true)); + const conflicts = remoteAttributes.map((attribute) => checkAttributeChanges(attribute, attributesContains(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); + + let changedAttributes = []; + const changing = [...deleting, ...adding, ...conflicts] + if (changing.length === 0) { + return changedAttributes; + } + + log(!cliConfig.force ? 'There are pending changes in your collection deployment' : 'List of applied changes'); + + drawTable(changing.map((change) => { + return { Key: change.key, Action: change.action, Reason: change.reason, }; + })); + + if (!cliConfig.force) { + const answers = await inquirer.prompt(questionsPushCollections[1]); + + if (answers.changes.toLowerCase() !== 'yes') { + return changedAttributes; + } + } + + if (conflicts.length > 0) { + changedAttributes = conflicts.map((change) => change.attribute); + await Promise.all(changedAttributes.map((changed) => deleteAttribute(collection, changed))); + remoteAttributes = remoteAttributes.filter((attribute) => !attributesContains(attribute, changedAttributes)) + } + + const deletingAttributes = deleting.map((change) => change.attribute); + await Promise.all(deletingAttributes.map((attribute) => deleteAttribute(collection, attribute))); + const attributeKeys = [...remoteAttributes.map(attribute => attribute.key), ...deletingAttributes.map(attribute => attribute.key)] + + if (attributeKeys.length) { + const deleteAttributesPoolStatus = await awaitPools.deleteAttributes(collection['databaseId'], collection['$id'], attributeKeys); + + if (!deleteAttributesPoolStatus) { + throw new Error("Attribute deletion timed out."); + } + } + + return localAttributes.filter((attribute) => !attributesContains(attribute, remoteAttributes)); +} +const createIndexes = async (indexes, collection) => { + log(`Creating indexes ...`) + + for (let index of indexes) { + await databasesCreateIndex({ + databaseId: collection['databaseId'], + collectionId: collection['$id'], + key: index.key, + type: index.type, + attributes: index.attributes, + orders: index.orders, + parseOutput: false + }); + } + + const result = await awaitPools.expectIndexes( + collection['databaseId'], + collection['$id'], + indexes.map(index => index.key) + ); + + if (!result) { + throw new Error("Index creation timed out."); + } + + success(`Created ${indexes.length} indexes`); +} +const createAttributes = async (attributes, collection) => { + for (let attribute of attributes) { + if (attribute.side !== 'child') { + await createAttribute(collection['databaseId'], collection['$id'], attribute); + } + } + + const result = await awaitPools.expectAttributes( + collection['databaseId'], + collection['$id'], + collection.attributes.map(attribute => attribute.key) + ); + + if (!result) { + throw new Error(`Attribute creation timed out.`); + } + + success(`Created ${attributes.length} attributes`); +} + const pushResources = async () => { const actions = { project: pushProject, @@ -291,7 +567,7 @@ const pushResources = async () => { if (cliConfig.all) { for (let action of Object.values(actions)) { - await action(); + await action({ returnOnZero: true }); } } else { const answers = await inquirer.prompt(questionsPushResources[0]); @@ -299,13 +575,12 @@ const pushResources = async () => { for (let resource of answers.resources) { const action = actions[resource]; if (action !== undefined) { - await action(); + await action({ returnOnZero: true }); } } } }; - const pushProject = async () => { try { const projectId = localConfig.getProject().projectId; @@ -364,7 +639,7 @@ const pushProject = async () => { } } -const pushFunction = async ({ functionId, async } = {}) => { +const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero: false }) => { let response = {}; const functionIds = []; @@ -375,6 +650,10 @@ const pushFunction = async ({ functionId, async } = {}) => { checkDeployConditions(localConfig); const functions = localConfig.getFunctions(); if (functions.length === 0) { + if (returnOnZero) { + log('No functions found, skipping'); + return; + } throw new Error("No functions found in the current directory."); } functionIds.push(...functions.map((func) => { @@ -506,378 +785,150 @@ const pushFunction = async ({ functionId, async } = {}) => { entrypoint: func.entrypoint, commands: func.commands, vars: JSON.stringify(func.vars), - parseOutput: false - }); - - localConfig.updateFunction(func['$id'], { - "$id": response['$id'], - }); - func["$id"] = response['$id']; - updaterRow.update({ status: 'Created' }); - } catch (e) { - updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); - return; - } - } - - if (func.variables) { - if (!func.pushVariables) { - updaterRow.update({ end: 'Skipping variables' }); - } else { - updaterRow.update({ end: 'Pushing variables' }); - - const { variables } = await paginate(functionsListVariables, { - functionId: func['$id'], - parseOutput: false - }, 100, 'variables'); - - await Promise.all(variables.map(async variable => { - await functionsDeleteVariable({ - functionId: func['$id'], - variableId: variable['$id'], - parseOutput: false - }); - })); - - let result = await awaitPools.wipeVariables(func['$id']); - if (!result) { - updaterRow.fail({ errorMessage: 'Variable deletion timed out' }) - return; - } - - // Push local variables - await Promise.all(Object.keys(func.variables).map(async localVariableKey => { - await functionsCreateVariable({ - functionId: func['$id'], - key: localVariableKey, - value: func.variables[localVariableKey], - parseOutput: false - }); - })); - } - } - - try { - updaterRow.update({ status: 'Pushing' }).replaceSpinner(SPINNER_ARC); - response = await functionsCreateDeployment({ - functionId: func['$id'], - entrypoint: func.entrypoint, - commands: func.commands, - code: func.path, - activate: true, - parseOutput: false - }) - - updaterRow.update({ status: 'Pushed' }); - deploymentCreated = true; - successfullyPushed++; - } catch (e) { - switch (e.code) { - case 'ENOENT': - updaterRow.fail({ errorMessage: 'Not found in the current directory. Skipping...' }) - break; - default: - updaterRow.fail({ errorMessage: e.message ?? 'An unknown error occurred. Please try again.' }) - } - } - - if (deploymentCreated && !async) { - try { - const deploymentId = response['$id']; - updaterRow.update({ status: 'Deploying', end: 'Checking deployment status...' }) - let pollChecks = 0; - - while (true) { - if (pollChecks >= POLL_MAX_DEBOUNCE) { - updaterRow.update({ end: 'Deployment is taking too long. Please check the console for more details.' }) - break; - } - - response = await functionsGetDeployment({ - functionId: func['$id'], - deploymentId: deploymentId, - parseOutput: false - }); - - - const status = response['status']; - if (status === 'ready') { - updaterRow.update({ status: 'Deployed' }); - successfullyDeployed++; - - break; - } else if (status === 'failed') { - failedDeployments.push({ name: func['name'], $id: func['$id'], deployment: response['$id'] }); - updaterRow.fail({ errorMessage: `Failed to deploy` }); - - break; - } else { - updaterRow.update({ status: 'Deploying', end: `Current status: ${status}` }) - } - - pollChecks++; - await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); - } - } catch (e) { - updaterRow.fail({ errorMessage: e.message ?? 'Unknown error occurred. Please try again' }) - } - } - - updaterRow.stopSpinner(); - })); - - Spinner.stop(); - console.log('\n'); - - failedDeployments.forEach((failed) => { - const { name, deployment, $id } = failed; - const failUrl = `${globalConfig.getEndpoint().replace('/v1', '')}/console/project-${localConfig.getProject().projectId}/functions/function-${$id}/deployment-${deployment}`; - - error(`Deployment of ${name} has failed. Check at ${failUrl} for more details\n`); - }) - - success(`Pushed ${successfullyPushed} functions with ${successfullyDeployed} successful deployments.`); -} - -const createAttribute = async (databaseId, collectionId, attribute) => { - switch (attribute.type) { - case 'string': - switch (attribute.format) { - case 'email': - return await databasesCreateEmailAttribute({ - databaseId, - collectionId, - key: attribute.key, - required: attribute.required, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - case 'url': - return await databasesCreateUrlAttribute({ - databaseId, - collectionId, - key: attribute.key, - required: attribute.required, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - case 'ip': - return await databasesCreateIpAttribute({ - databaseId, - collectionId, - key: attribute.key, - required: attribute.required, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - case 'enum': - return await databasesCreateEnumAttribute({ - databaseId, - collectionId, - key: attribute.key, - elements: attribute.elements, - required: attribute.required, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - default: - return await databasesCreateStringAttribute({ - databaseId, - collectionId, - key: attribute.key, - size: attribute.size, - required: attribute.required, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - - } - case 'integer': - return await databasesCreateIntegerAttribute({ - databaseId, - collectionId, - key: attribute.key, - required: attribute.required, - min: attribute.min, - max: attribute.max, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - case 'double': - return databasesCreateFloatAttribute({ - databaseId, - collectionId, - key: attribute.key, - required: attribute.required, - min: attribute.min, - max: attribute.max, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - case 'boolean': - return databasesCreateBooleanAttribute({ - databaseId, - collectionId, - key: attribute.key, - required: attribute.required, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - case 'datetime': - return databasesCreateDatetimeAttribute({ - databaseId, - collectionId, - key: attribute.key, - required: attribute.required, - xdefault: attribute.default, - array: attribute.array, - parseOutput: false - }) - case 'relationship': - return databasesCreateRelationshipAttribute({ - databaseId, - collectionId, - relatedCollectionId: attribute.relatedCollection, - type: attribute.relationType, - twoWay: attribute.twoWay, - key: attribute.key, - twoWayKey: attribute.twoWayKey, - onDelete: attribute.onDelete, - parseOutput: false - }) - } -} + parseOutput: false + }); -const deleteAttribute = async (collection, attribute) => { - log(`Deleting attribute ${attribute.key} of ${collection.name} ( ${collection['$id']} )`); + localConfig.updateFunction(func['$id'], { + "$id": response['$id'], + }); + func["$id"] = response['$id']; + updaterRow.update({ status: 'Created' }); + } catch (e) { + updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); + return; + } + } - await databasesDeleteAttribute({ - databaseId: collection['databaseId'], - collectionId: collection['$id'], - key: attribute.key, - parseOutput: false - }); -} + if (func.variables) { + if (!func.pushVariables) { + updaterRow.update({ end: 'Skipping variables' }); + } else { + updaterRow.update({ end: 'Pushing variables' }); -/** - * Check if attribute non-changeable fields has been changed - * If so return the differences as an object. - * @param remote - * @param local - * @param collection - * @returns {undefined|{reason: string, action: *, attribute, key: string}} - */ -const checkAttributeChanges = (remote, local, collection) => { - if (local === undefined) { - return undefined; - } + const { variables } = await paginate(functionsListVariables, { + functionId: func['$id'], + parseOutput: false + }, 100, 'variables'); - const keyName = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; - const action = chalk.cyan('recreating'); - let reason = ''; + await Promise.all(variables.map(async variable => { + await functionsDeleteVariable({ + functionId: func['$id'], + variableId: variable['$id'], + parseOutput: false + }); + })); - for (let key of Object.keys(remote)) { - if (changeableKeys.includes(key)) { - continue; - } + let result = await awaitPools.wipeVariables(func['$id']); + if (!result) { + updaterRow.fail({ errorMessage: 'Variable deletion timed out' }) + return; + } - if (remote[key] !== local[key]) { - const bol = reason === '' ? '' : '\n'; - reason += `${bol}${key} changed from ${chalk.red(remote[key])} to ${chalk.green(local[key])}`; + // Push local variables + await Promise.all(Object.keys(func.variables).map(async localVariableKey => { + await functionsCreateVariable({ + functionId: func['$id'], + key: localVariableKey, + value: func.variables[localVariableKey], + parseOutput: false + }); + })); + } } - } - - return reason === '' ? undefined : { key: keyName, attribute: remote, reason, action }; -} - -/** - * Check if attributes contain the given attribute - * @param attribute - * @param attributes - * @returns {*} - */ -const attributesContains = (attribute, attributes) => attributes.find((attr) => attr.key === attribute.key); + try { + updaterRow.update({ status: 'Pushing' }).replaceSpinner(SPINNER_ARC); + response = await functionsCreateDeployment({ + functionId: func['$id'], + entrypoint: func.entrypoint, + commands: func.commands, + code: func.path, + activate: true, + parseOutput: false + }) -const generateChangesObject = (attribute, collection, isAdding) => { - return { - key: `${chalk.yellow(attribute.key)} in ${collection.name} (${collection['$id']})`, - attribute: attribute, - reason: isAdding ? 'Field doesn\'t exist on the remote server' : 'Field doesn\'t exist in appwrite.json file', - action: isAdding ? chalk.green('adding') : chalk.red('deleting') - }; + updaterRow.update({ status: 'Pushed' }); + deploymentCreated = true; + successfullyPushed++; + } catch (e) { + switch (e.code) { + case 'ENOENT': + updaterRow.fail({ errorMessage: 'Not found in the current directory. Skipping...' }) + break; + default: + updaterRow.fail({ errorMessage: e.message ?? 'An unknown error occurred. Please try again.' }) + } + } -}; + if (deploymentCreated && !async) { + try { + const deploymentId = response['$id']; + updaterRow.update({ status: 'Deploying', end: 'Checking deployment status...' }) + let pollChecks = 0; -/** - * Filter deleted and recreated attributes, - * return list of attributes to create - * @param remoteAttributes - * @param localAttributes - * @param collection - * @returns {Promise<*|*[]>} - */ -const attributesToCreate = async (remoteAttributes, localAttributes, collection) => { + while (true) { + if (pollChecks >= POLL_MAX_DEBOUNCE) { + updaterRow.update({ end: 'Deployment is taking too long. Please check the console for more details.' }) + break; + } - const deleting = remoteAttributes.filter((attribute) => !attributesContains(attribute, localAttributes)).map((attr) => generateChangesObject(attr, collection, false)); - const adding = localAttributes.filter((attribute) => !attributesContains(attribute, remoteAttributes)).map((attr) => generateChangesObject(attr, collection, true)); - const conflicts = remoteAttributes.map((attribute) => checkAttributeChanges(attribute, attributesContains(attribute, localAttributes), collection)).filter(attribute => attribute !== undefined); + response = await functionsGetDeployment({ + functionId: func['$id'], + deploymentId: deploymentId, + parseOutput: false + }); - let changedAttributes = []; - const changing = [...deleting, ...adding, ...conflicts] - if (changing.length === 0) { - return changedAttributes; - } - log(!cliConfig.force ? 'There are pending changes in your collection deployment' : 'List of applied changes'); + const status = response['status']; + if (status === 'ready') { + updaterRow.update({ status: 'Deployed' }); + successfullyDeployed++; - drawTable(changing.map((change) => { - return { Key: change.key, Action: change.action, Reason: change.reason, }; - })); + break; + } else if (status === 'failed') { + failedDeployments.push({ name: func['name'], $id: func['$id'], deployment: response['$id'] }); + updaterRow.fail({ errorMessage: `Failed to deploy` }); - if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushCollections[1]); + break; + } else { + updaterRow.update({ status: 'Deploying', end: `Current status: ${status}` }) + } - if (answers.changes.toLowerCase() !== 'yes') { - return changedAttributes; + pollChecks++; + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); + } + } catch (e) { + updaterRow.fail({ errorMessage: e.message ?? 'Unknown error occurred. Please try again' }) + } } - } - if (conflicts.length > 0) { - changedAttributes = conflicts.map((change) => change.attribute); - await Promise.all(changedAttributes.map((changed) => deleteAttribute(collection, changed))); - remoteAttributes = remoteAttributes.filter((attribute) => !attributesContains(attribute, changedAttributes)) - } + updaterRow.stopSpinner(); + })); - const deletingAttributes = deleting.map((change) => change.attribute); - await Promise.all(deletingAttributes.map((attribute) => deleteAttribute(collection, attribute))); - const attributeKeys = [...remoteAttributes.map(attribute => attribute.key), ...deletingAttributes.map(attribute => attribute.key)] + Spinner.stop(); + console.log('\n'); - if (attributeKeys.length) { - const deleteAttributesPoolStatus = await awaitPools.deleteAttributes(collection['databaseId'], collection['$id'], attributeKeys); + failedDeployments.forEach((failed) => { + const { name, deployment, $id } = failed; + const failUrl = `${globalConfig.getEndpoint().replace('/v1', '')}/console/project-${localConfig.getProject().projectId}/functions/function-${$id}/deployment-${deployment}`; - if (!deleteAttributesPoolStatus) { - throw new Error("Attribute deletion timed out."); - } - } + error(`Deployment of ${name} has failed. Check at ${failUrl} for more details\n`); + }) - return localAttributes.filter((attribute) => !attributesContains(attribute, remoteAttributes)); + success(`Pushed ${successfullyPushed} functions with ${successfullyDeployed} successful deployments.`); } -const pushCollection = async () => { +const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { const collections = []; if (cliConfig.all) { checkDeployConditions(localConfig); if (localConfig.getCollections().length === 0) { + if (returnOnZero) { + log('No collections found, skipping'); + return; + } + throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} pull collection` to fetch all your collections."); } collections.push(...localConfig.getCollections()); @@ -997,54 +1048,7 @@ const pushCollection = async () => { } } -const createIndexes = async (indexes, collection) => { - log(`Creating indexes ...`) - - for (let index of indexes) { - await databasesCreateIndex({ - databaseId: collection['databaseId'], - collectionId: collection['$id'], - key: index.key, - type: index.type, - attributes: index.attributes, - orders: index.orders, - parseOutput: false - }); - } - - const result = await awaitPools.expectIndexes( - collection['databaseId'], - collection['$id'], - indexes.map(index => index.key) - ); - - if (!result) { - throw new Error("Index creation timed out."); - } - - success(`Created ${indexes.length} indexes`); -} -const createAttributes = async (attributes, collection) => { - for (let attribute of attributes) { - if (attribute.side !== 'child') { - await createAttribute(collection['databaseId'], collection['$id'], attribute); - } - } - - const result = await awaitPools.expectAttributes( - collection['databaseId'], - collection['$id'], - collection.attributes.map(attribute => attribute.key) - ); - - if (!result) { - throw new Error(`Attribute creation timed out.`); - } - - success(`Created ${attributes.length} attributes`); -} - -const pushBucket = async () => { +const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { let response = {}; let bucketIds = []; @@ -1053,6 +1057,10 @@ const pushBucket = async () => { if (cliConfig.all) { checkDeployConditions(localConfig); if (configBuckets.length === 0) { + if (returnOnZero) { + log('No buckets found, skipping'); + return; + } throw new Error("No buckets found in the current directory. Run `appwrite pull bucket` to fetch all your buckets."); } bucketIds.push(...configBuckets.map((b) => b.$id)); @@ -1131,7 +1139,7 @@ const pushBucket = async () => { } } -const pushTeam = async () => { +const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { let response = {}; let teamIds = []; @@ -1140,6 +1148,10 @@ const pushTeam = async () => { if (cliConfig.all) { checkDeployConditions(localConfig); if (configTeams.length === 0) { + if (returnOnZero) { + log('No teams found, skipping'); + return; + } throw new Error("No teams found in the current directory. Run `appwrite pull team` to fetch all your teams."); } teamIds.push(...configTeams.map((t) => t.$id)); @@ -1202,7 +1214,7 @@ const pushTeam = async () => { } } -const pushMessagingTopic = async () => { +const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => { let response = {}; let topicsIds = []; @@ -1212,6 +1224,10 @@ const pushMessagingTopic = async () => { if (cliConfig.all) { checkDeployConditions(localConfig); if (configTopics.length === 0) { + if (returnOnZero) { + log('No topics found, skipping'); + return; + } throw new Error("No topics found in the current directory. Run `appwrite pull topics` to pull all your messaging topics."); } topicsIds.push(...configTopics.map((b) => b.$id)); From 2e8f56071b6a2148de51b15a195e91d65959ef69 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 30 May 2024 17:28:05 -0400 Subject: [PATCH 52/89] feat(cli): Showing deployed function URL --- templates/cli/lib/commands/push.js.twig | 18 +++++++++++++++++- templates/cli/lib/spinner.js.twig | 1 - 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index e1e7c60be..e163ed638 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -7,6 +7,7 @@ const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); const { cliConfig, actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); +const { proxyListRules } = require('./proxy'); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { databasesGet, @@ -881,9 +882,24 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero const status = response['status']; if (status === 'ready') { - updaterRow.update({ status: 'Deployed' }); successfullyDeployed++; + let url = ''; + const res = await proxyListRules({ + parseOutput: false, + queries: [ + JSON.stringify({ method: 'limit', values: [1] }), + JSON.stringify({ method: 'equal', "attribute": "resourceType", "values": ["function"] }), + JSON.stringify({ method: 'equal', "attribute": "resourceId", "values": [func['$id']] }) + ], + }); + + if(Number(res.total) === 1){ + url = res.rules[0].domain; + } + + updaterRow.update({ status: 'Deployed', end: url}); + break; } else if (status === 'failed') { failedDeployments.push({ name: func['name'], $id: func['$id'], deployment: response['$id'] }); diff --git a/templates/cli/lib/spinner.js.twig b/templates/cli/lib/spinner.js.twig index 9bfb81edb..2f5b3ad11 100644 --- a/templates/cli/lib/spinner.js.twig +++ b/templates/cli/lib/spinner.js.twig @@ -47,7 +47,6 @@ class Spinner { } else if (status.toLowerCase().trim() === 'deployed') { start = chalk.green.bold(status); prefix = chalk.green.bold('✓'); - end = ''; } else if (status.toLowerCase().trim() === 'error') { start = chalk.red.bold(status); prefix = chalk.red.bold('✗'); From a2d49e7b821981bb5d563fcee7b4042f64b73889 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 31 May 2024 04:00:15 +0400 Subject: [PATCH 53/89] Update templates/cli/lib/parser.js.twig --- templates/cli/lib/parser.js.twig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 4c38d9746..247465f25 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -143,10 +143,8 @@ const parseError = (err) => { log(`To report this error you can:\n - Create a support ticket in our Discord server https://appwrite.io/discord \n - Create an issue in our Github\n ${githubIssueUrl.href}\n`); - error('\n Error stacktrace'); - + error('\n Stack Trace: \n'); console.error(err); - process.exit(1); })() } else { From a6f08c8cd6c9b2fb0d648c7c82798e5bbea08e42 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Fri, 31 May 2024 04:18:30 +0400 Subject: [PATCH 54/89] Apply suggestions from code review --- templates/cli/lib/commands/push.js.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index d05127172..efd833007 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -271,7 +271,7 @@ const pushProject = async () => { const projectId = localConfig.getProject().projectId; const projectName = localConfig.getProject().projectName; - log('Updating project name'); + log(`Updating project ${projectName} ( ${projectId} )`); await projectsUpdate({ projectId, @@ -282,7 +282,7 @@ const pushProject = async () => { const settings = localConfig.getProject().projectSettings; if (settings.services) { - log('Updating services status'); + log('Updating service statuses'); for (let [service, status] of Object.entries(settings.services)) { await projectsUpdateServiceStatus({ projectId, @@ -295,7 +295,7 @@ const pushProject = async () => { if (settings.auth) { if (settings.auth.security) { - log('Updating Auth security settings'); + log('Updating auth security settings'); await projectsUpdateAuthDuration({ projectId, duration: settings.auth.security.duration, parseOutput: false }); await projectsUpdateAuthLimit({ projectId, limit: settings.auth.security.limit, parseOutput: false }); await projectsUpdateAuthSessionsLimit({ projectId, limit: settings.auth.security.sessionsLimit, parseOutput: false }); @@ -305,7 +305,7 @@ const pushProject = async () => { } if (settings.auth.methods) { - log('Updating Auth available login methods'); + log('Updating auth login methods'); for (let [method, status] of Object.entries(settings.auth.methods)) { await projectsUpdateAuthStatus({ From 68b026b18a0506e6caaacb3b4b2fec7e5005e77b Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 31 May 2024 10:02:54 -0400 Subject: [PATCH 55/89] refactor(cli): Internal client instead of a generic fetch --- templates/cli/lib/parser.js.twig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 4c38d9746..a5f690648 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -4,6 +4,7 @@ const Table = require('cli-table3'); const { description } = require('../package.json'); const { globalConfig } = require("./config.js"); const os = require('os'); +const Client = require("./client"); const cliConfig = { verbose: false, @@ -117,12 +118,13 @@ const parseError = (err) => { if (cliConfig.report) { (async () => { let appwriteVersion = 'unknown'; - const isCloud = globalConfig.getEndpoint().includes('cloud.appwrite.io') ? 'Yes' : 'No'; + const endpoint = globalConfig.getEndpoint(); + const isCloud = endpoint.includes('cloud.appwrite.io') ? 'Yes' : 'No'; try { - const res = await fetch(`${globalConfig.getEndpoint()}/health/version`); - const json = await res.json(); - appwriteVersion = json.version; + const client = new Client().setEndpoint(endpoint); + const res = await client.call('get', '/health/version'); + appwriteVersion = res.version; } catch { } From 7efedaff8a795061bb2ce402a47084f22e481e52 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 31 May 2024 14:07:53 -0400 Subject: [PATCH 56/89] refactor(cli): Adding pagination and reorder questions --- templates/cli/lib/questions.js.twig | 57 ++++++++++++++--------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index b1471b656..01e41021f 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -11,7 +11,7 @@ const { databasesList } = require('./commands/databases'); const { checkDeployConditions } = require('./utils'); const JSONbig = require("json-bigint")({ storeAsString: false }); -const whenOverride = (answers)=> answers.override === undefined ? true : answers.override; +const whenOverride = (answers) => answers.override === undefined ? true : answers.override; const getIgnores = (runtime) => { const languge = runtime.split('-')[0]; @@ -123,6 +123,22 @@ const questionsInitProject = [ return Object.keys(localConfig.getProject()).length !== 0; } }, + { + type: "list", + name: "start", + when: whenOverride, + message: "How would you like to start?", + choices: [ + { + name: "Create a new {{ spec.title|caseUcfirst }} project", + value: "new" + }, + { + name: "Link this directory to an existing {{ spec.title|caseUcfirst }} project", + value: "existing" + } + ] + }, { type: "list", name: "organization", @@ -149,27 +165,6 @@ const questionsInitProject = [ }, when: whenOverride }, - { - type: "list", - name: "start", - when(answers) { - if (answers.override == undefined) { - return true - } - return answers.override; - }, - message: "How would you like to start?", - choices: [ - { - name: "Create a new {{ spec.title|caseUcfirst }} project", - value: "new" - }, - { - name: "Link this directory to an existing {{ spec.title|caseUcfirst }} project", - value: "existing" - } - ] - }, { type: "input", name: "project", @@ -189,12 +184,14 @@ const questionsInitProject = [ name: "project", message: "Choose your {{ spec.title|caseUcfirst }} project.", choices: async (answers) => { - let response = await projectsList({ - parseOutput: false, - queries: [JSON.stringify({ method: 'equal', attribute: 'teamId', values: [answers.organization.id] })], - }) - let projects = response["projects"] - let choices = projects.map((project, idx) => { + const queries = [ + JSON.stringify({ method: 'equal', attribute: 'teamId', values: [answers.organization.id] }), + JSON.stringify({ method: 'orderDesc', attribute: 'Id' }) + ] + + const { projects } = await paginate(projectsList, { parseOutput: false, queries, }, 100, 'projects'); + + let choices = projects.map((project) => { return { name: `${project.name} (${project['$id']})`, value: { @@ -238,7 +235,7 @@ const questionsPullFunctions = [ choices: async () => { const { functions } = await paginate(functionsList, { parseOutput: false }, 100, 'functions'); - if(functions.length === 0){ + if (functions.length === 0) { throw "We couldn't find any functions in your {{ spec.title|caseUcfirst }} project"; } @@ -298,7 +295,7 @@ const questionsCreateFunctionSelectTemplate = (templates) => { name: "template", message: "What template would you like to use?", choices: templates.map((template) => { - const name =`${template[0].toUpperCase()}${template.split('').slice(1).join('')}`.replace(/[-_]/g,' '); + const name = `${template[0].toUpperCase()}${template.split('').slice(1).join('')}`.replace(/[-_]/g, ' '); return { value: template, name } }) From 930685c550e8500ec93f5b670acc4804009a3aa0 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 31 May 2024 14:08:12 -0400 Subject: [PATCH 57/89] feat(cli): headless project setup --- templates/cli/lib/commands/init.js.twig | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 424ed1d6b..ecc0e54cc 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -3,7 +3,7 @@ const path = require("path"); const childProcess = require('child_process'); const { Command } = require("commander"); const inquirer = require("inquirer"); -const { projectsCreate } = require("./projects"); +const { projectsCreate, projectsGet } = require("./projects"); const { storageCreateBucket } = require("./storage"); const { messagingCreateTopic } = require("./messaging"); const { functionsCreate } = require("./functions"); @@ -23,7 +23,7 @@ const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); const { sdkForConsole } = require("../sdks"); -const initProject = async () => { +const initProject = async ({ organizationId, projectId, projectName } = {}) => { let response = {}; try { @@ -40,12 +40,22 @@ const initProject = async () => { log('You must login first') await loginCommand(); } + let answers = {}; - const answers = await inquirer.prompt(questionsInitProject) - if (answers.override === false) { - process.exit(1) + if (organizationId && projectId) { + answers = { + project: { id: projectId, name: projectName }, + organization: { id: organizationId }, + start: 'existing' + } + } else { + answers = await inquirer.prompt(questionsInitProject) + if (answers.override === false) { + process.exit(1) + } } + if (answers.start === 'new') { response = await projectsCreate({ projectId: answers.id, @@ -201,7 +211,6 @@ const initFunction = async () => { } - const copyRecursiveSync = (src, dest) => { let exists = fs.existsSync(src); let stats = exists && fs.statSync(src); @@ -254,6 +263,9 @@ const init = new Command("init") .configureHelp({ helpWidth: process.stdout.columns || 80 }) + .option("--organizationId ", "{{ spec.title|caseUcfirst }} organization ID") + .option("--projectId ", "{{ spec.title|caseUcfirst }} project ID") + .option("--projectName ", "{{ spec.title|caseUcfirst }} project name") .action(actionRunner(initProject)); init From 5bba9cd61f35d8903148a7a496aca9a699fc2bde Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 31 May 2024 14:17:29 -0400 Subject: [PATCH 58/89] feat(cli): creating project if not exist --- templates/cli/lib/commands/init.js.twig | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index ecc0e54cc..1c826dd2d 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -48,6 +48,20 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { organization: { id: organizationId }, start: 'existing' } + + + try { + await projectsGet({ projectId, parseOutput: false }); + } catch (e) { + if (e.code === 404) { + answers.start = 'new'; + answers.id = answers.project.id; + answers.project = answers.project.name; + } else { + throw e; + } + } + } else { answers = await inquirer.prompt(questionsInitProject) if (answers.override === false) { From 3621c3c36a24229f3ef97b3580fb5f18c9563bb6 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 31 May 2024 15:31:05 -0400 Subject: [PATCH 59/89] refactor(cli): reorder questions --- templates/cli/lib/commands/generic.js.twig | 25 ++++---- templates/cli/lib/questions.js.twig | 68 +++++++++++++--------- 2 files changed, 53 insertions(+), 40 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index c96bac56b..cd3332adf 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -6,13 +6,23 @@ const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); const ID = require("../id"); {% if sdk.test != "true" %} -const { questionsLogin, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); +const { questionsLogin, questionLoginWithEndpoint, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; const loginCommand = async ({ selfHosted, email, password, endpoint, mfa, code }) => { - const answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); + const oldCurrent = globalConfig.getCurrentLogin(); + let answers = {}; + let configEndpoint = DEFAULT_ENDPOINT; + + if (selfHosted) { + answers = endpoint && email && password ? { endpoint, email, password } : await inquirer.prompt(questionLoginWithEndpoint); + configEndpoint = answers.endpoint; + } else { + answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); + } + if (answers.method === 'select') { const accountId = answers.accountId; @@ -27,19 +37,12 @@ const loginCommand = async ({ selfHosted, email, password, endpoint, mfa, code } return; } - const oldCurrent = globalConfig.getCurrentLogin(); const id = ID.unique(); - globalConfig.setCurrentLogin(id); globalConfig.addLogin(id, {}); + globalConfig.setCurrentLogin(id); + globalConfig.setEndpoint(configEndpoint); globalConfig.setEmail(answers.email); - globalConfig.setEndpoint(DEFAULT_ENDPOINT); - - if (selfHosted) { - const selfHostedAnswers = endpoint ? { endpoint } : await inquirer.prompt(questionGetEndpoint); - - globalConfig.setEndpoint(selfHostedAnswers.endpoint); - } let client = await sdkForConsole(false); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index b1471b656..98afa1520 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -1,4 +1,5 @@ const chalk = require("chalk"); +const Client = require("./client"); const { localConfig, globalConfig } = require('./config'); const { projectsList } = require('./commands/projects'); const { teamsList } = require('./commands/teams'); @@ -11,7 +12,7 @@ const { databasesList } = require('./commands/databases'); const { checkDeployConditions } = require('./utils'); const JSONbig = require("json-bigint")({ storeAsString: false }); -const whenOverride = (answers)=> answers.override === undefined ? true : answers.override; +const whenOverride = (answers) => answers.override === undefined ? true : answers.override; const getIgnores = (runtime) => { const languge = runtime.split('-')[0]; @@ -238,7 +239,7 @@ const questionsPullFunctions = [ choices: async () => { const { functions } = await paginate(functionsList, { parseOutput: false }, 100, 'functions'); - if(functions.length === 0){ + if (functions.length === 0) { throw "We couldn't find any functions in your {{ spec.title|caseUcfirst }} project"; } @@ -298,7 +299,7 @@ const questionsCreateFunctionSelectTemplate = (templates) => { name: "template", message: "What template would you like to use?", choices: templates.map((template) => { - const name =`${template[0].toUpperCase()}${template.split('').slice(1).join('')}`.replace(/[-_]/g,' '); + const name = `${template[0].toUpperCase()}${template.split('').slice(1).join('')}`.replace(/[-_]/g, ' '); return { value: template, name } }) @@ -474,9 +475,40 @@ const questionsLogin = [ }, when: (answers) => answers.method === 'select' }, - +]; +const questionGetEndpoint = [ + { + type: "input", + name: "endpoint", + message: "Enter the endpoint of your {{ spec.title|caseUcfirst }} server", + default: "http://localhost/v1", + async validate(value) { + if (!value) { + return "Please enter a valid endpoint."; + } + let client = new Client().setEndpoint(value); + try { + let response = await client.call('get', '/health/version'); + if (response.version) { + return true; + } else { + throw new Error(); + } + } catch (error) { + return "Invalid endpoint or your Appwrite server is not running as expected."; + } + } + } ]; +const questionLoginWithEndpoint = [ + questionsLogin[0], + { ...questionGetEndpoint[0], when: (answers) => answers.method !== 'select' }, + questionsLogin[1], + questionsLogin[2], + questionsLogin[3] +] + const questionsLogout = [ { type: "checkbox", @@ -722,30 +754,7 @@ const questionsMfaChallenge = [ } ]; -const questionGetEndpoint = [ - { - type: "input", - name: "endpoint", - message: "Enter the endpoint of your {{ spec.title|caseUcfirst }} server", - default: "http://localhost/v1", - async validate(value) { - if (!value) { - return "Please enter a valid endpoint."; - } - let client = new Client().setEndpoint(value); - try { - let response = await client.call('get', '/health/version'); - if (response.version) { - return true; - } else { - throw new Error(); - } - } catch (error) { - return "Invalid endpoint or your Appwrite server is not running as expected."; - } - } - } -]; + module.exports = { questionsInitProject, @@ -768,5 +777,6 @@ module.exports = { questionsGetEntrypoint, questionsListFactors, questionsMfaChallenge, - questionGetEndpoint + questionGetEndpoint, + questionLoginWithEndpoint }; From d36a6231fceb112ff0ea1e61ffcea0cea95fc8cb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 3 Jun 2024 09:33:52 -0400 Subject: [PATCH 60/89] wip --- templates/cli/lib/commands/init.js.twig | 9 ++++----- templates/cli/lib/commands/push.js.twig | 17 +++++++++-------- templates/cli/lib/config.js.twig | 7 +++++-- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 1c826dd2d..8e976a638 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -23,7 +23,7 @@ const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); const { sdkForConsole } = require("../sdks"); -const initProject = async ({ organizationId, projectId, projectName } = {}) => { +const initProject = async ({ organizationId, projectId } = {}) => { let response = {}; try { @@ -44,7 +44,7 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { if (organizationId && projectId) { answers = { - project: { id: projectId, name: projectName }, + project: { id: projectId, }, organization: { id: organizationId }, start: 'existing' } @@ -78,9 +78,9 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { parseOutput: false }) - localConfig.setProject(response['$id'], response.name); + localConfig.setProject(response['$id']); } else { - localConfig.setProject(answers.project.id, answers.project.name); + localConfig.setProject(answers.project.id); } success(); @@ -279,7 +279,6 @@ const init = new Command("init") }) .option("--organizationId ", "{{ spec.title|caseUcfirst }} organization ID") .option("--projectId ", "{{ spec.title|caseUcfirst }} project ID") - .option("--projectName ", "{{ spec.title|caseUcfirst }} project name") .action(actionRunner(initProject)); init diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index e163ed638..503200a4f 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -589,12 +589,13 @@ const pushProject = async () => { log('Updating project name'); - await projectsUpdate({ - projectId, - name: projectName, - parseOutput: false - }); - + if (projectName) { + await projectsUpdate({ + projectId, + name: projectName, + parseOutput: false + }); + } const settings = localConfig.getProjectSettings(); if (settings.services) { @@ -894,11 +895,11 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero ], }); - if(Number(res.total) === 1){ + if (Number(res.total) === 1) { url = res.rules[0].domain; } - updaterRow.update({ status: 'Deployed', end: url}); + updaterRow.update({ status: 'Deployed', end: url }); break; } else if (status === 'failed') { diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index bcf48dccf..c04d718d7 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -332,9 +332,12 @@ class Local extends Config { }; } - setProject(projectId, projectName) { + setProject(projectId, projectName = '') { this.set("projectId", projectId); - this.set("projectName", projectName); + + if (projectName !== '') { + this.set("projectName", projectName); + } } From 406ef2240473594d28876e8b062bd42120b5267c Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:21:27 -0400 Subject: [PATCH 61/89] feat: Init project only ID --- templates/cli/lib/commands/init.js.twig | 8 ++++---- templates/cli/lib/config.js.twig | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 8e976a638..6df267d9e 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -23,7 +23,7 @@ const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); const { sdkForConsole } = require("../sdks"); -const initProject = async ({ organizationId, projectId } = {}) => { +const initProject = async ({ organizationId, projectId, projectName } = {}) => { let response = {}; try { @@ -42,14 +42,13 @@ const initProject = async ({ organizationId, projectId } = {}) => { } let answers = {}; - if (organizationId && projectId) { + if (organizationId && projectId && projectName) { answers = { - project: { id: projectId, }, + project: { id: projectId, name: projectName}, organization: { id: organizationId }, start: 'existing' } - try { await projectsGet({ projectId, parseOutput: false }); } catch (e) { @@ -279,6 +278,7 @@ const init = new Command("init") }) .option("--organizationId ", "{{ spec.title|caseUcfirst }} organization ID") .option("--projectId ", "{{ spec.title|caseUcfirst }} project ID") + .option("--projectName ", "{{ spec.title|caseUcfirst }} project ID") .action(actionRunner(initProject)); init diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index c04d718d7..9e829d903 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -322,13 +322,13 @@ class Local extends Config { } getProject() { - if (!this.has("projectId") || !this.has("projectName")) { + if (!this.has("projectId")) { return {}; } return { projectId: this.get("projectId"), - projectName: this.get("projectName"), + projectName: this.has("projectName") ? this.get("projectName") : '', }; } From fb8e08a5618ca0d74bcf9522d188a1b73d99a411 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:51:25 -0400 Subject: [PATCH 62/89] feat: Init to local only & database creation --- templates/cli/lib/commands/init.js.twig | 112 +++++++++++------------- templates/cli/lib/questions.js.twig | 29 +++++- 2 files changed, 78 insertions(+), 63 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 6df267d9e..d3dc6eff3 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -44,7 +44,7 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { if (organizationId && projectId && projectName) { answers = { - project: { id: projectId, name: projectName}, + project: { id: projectId, name: projectName }, organization: { id: organizationId }, start: 'existing' } @@ -86,62 +86,59 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { } const initBucket = async () => { - let response = {} const answers = await inquirer.prompt(questionsCreateBucket) - try { - response = await storageCreateBucket({ - bucketId: answers.id, - name: answers.bucket, - fileSecurity: answers.fileSecurity.toLowerCase() === 'yes', - enabled: true, - parseOutput: false - }) - - localConfig.addBucket(response); - success(); - } catch (e) { - error(e.getMessage ?? 'Unknown error occurred. Please try again'); - } + localConfig.addBucket({ + $id: answers.id === 'unique()' ? ID.unique() : answers.id, + name: answers.bucket, + fileSecurity: answers.fileSecurity.toLowerCase() === 'yes', + enabled: true, + }); + success(); }; const initCollection = async () => { - let response = {} const answers = await inquirer.prompt(questionsCreateCollection) + const newDatabase = (answers.method ?? '').toLowerCase() !== 'existing'; - try { - response = await databasesCreateCollection({ - databaseId: answers.database, - collectionId: answers.id, - name: answers.collection, - documentSecurity: answers.documentSecurity.toLowerCase() === 'yes', - enabled: true, - parseOutput: false - }) + if (!newDatabase) { + answers.database_id = answers.database.$id; + answers.database_name = answers.database.name; + } - localConfig.addCollection(response); - success(); - } catch (e) { - error(e.getMessage ?? 'Unknown error occurred. Please try again'); + const databaseId = answers.database_id === 'unique()' ? ID.unique() : answers.database_id; + + if (newDatabase || !localConfig.getDatabase(answers.database_id).$id) { + localConfig.addDatabase({ + $id: databaseId, + name: answers.database_name, + enabled: true + }); } + + localConfig.addCollection({ + $id: answers.id === 'unique()' ? ID.unique() : answers.id, + databaseId: databaseId, + name: answers.collection, + documentSecurity: answers.documentSecurity.toLowerCase() === 'yes', + attributes: [], + indexes: [], + enabled: true, + }); + + success(); }; const initTopic = async () => { - let response = {} const answers = await inquirer.prompt(questionsCreateMessagingTopic) - try { - response = await messagingCreateTopic({ - topicId: answers.id, - name: answers.topic, - parseOutput: false - }) + localConfig.addMessagingTopic({ + $id: answers.id === 'unique()' ? ID.unique() : answers.id, + name: answers.topic, - localConfig.addMessagingTopic(response); - success(); - } catch (e) { - error(e.message ?? 'Unknown error occurred. Please try again'); - } + }); + + success(); }; const initFunction = async () => { @@ -172,14 +169,7 @@ const initFunction = async () => { log(`Installation command for this runtime not found. You will be asked to configure the install command when you first push the function.`); } - let response = await functionsCreate({ - functionId, - name: answers.name, - runtime: answers.runtime.id, - entrypoint: answers.runtime.entrypoint || '', - commands: answers.runtime.commands || '', - parseOutput: false - }) + fs.mkdirSync(functionDir, "777"); fs.mkdirSync(templatesDir, "777"); @@ -252,17 +242,17 @@ const initFunction = async () => { fs.writeFileSync(readmePath, newReadmeFile.join('\n')); let data = { - $id: response['$id'], - name: response.name, - runtime: response.runtime, - execute: response.execute, - events: response.events, - schedule: response.schedule, - timeout: response.timeout, - enabled: response.enabled, - logging: response.logging, - entrypoint: response.entrypoint, - commands: response.commands, + $id: functionId, + name: answers.name, + runtime: answers.runtime.id, + execute: [], + events: [], + schedule: "", + timeout: 15, + enabled: true, + logging: true, + entrypoint: answers.runtime.entrypoint || '', + commands: answers.runtime.commands || '', ignore: answers.runtime.ignore || null, path: `functions/${functionId}`, }; diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 5b4919261..99e7481ff 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -328,6 +328,16 @@ const questionsCreateBucket = [ ]; const questionsCreateCollection = [ + { + type: "list", + name: "method", + message: "What database would you like to use for your collection", + choices: ["New", "Existing"], + when: async () => { + const res = await databasesList({ queries: [], parseOutput: false }); + return res.total !== 0; + } + }, { type: "list", name: "database", @@ -338,7 +348,7 @@ const questionsCreateCollection = [ let choices = databases.map((database, idx) => { return { name: `${database.name} (${database.$id})`, - value: database.$id + value: { $id: database.$id, name: database.name } } }) @@ -347,7 +357,22 @@ const questionsCreateCollection = [ } return choices; - } + }, + when: (answers) => (answers.method ?? '').toLowerCase() === 'existing' + }, + { + type: "input", + name: "database_name", + message: "What would you like to name your database?", + default: "My Awesome Database", + when: (answers) => (answers.method ?? '').toLowerCase() !== 'existing' + }, + { + type: "input", + name: "database_id", + message: "What ID would you like to have for your database?", + default: "unique()", + when: (answers) => (answers.method ?? '').toLowerCase() !== 'existing' }, { type: "input", From 971783018fc02dde7038f71070a0ff0f014a89c9 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 6 Jun 2024 21:56:31 -0400 Subject: [PATCH 63/89] refactor: Improve command line to use `rawArgs` --- templates/cli/lib/parser.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 9bba4f117..a5559201f 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -130,7 +130,7 @@ const parseError = (err) => { } const version = '{{ sdk.version }}'; - const stepsToReproduce = `Running \`appwrite ${cliConfig.reportData.data.args}\``; + const stepsToReproduce = `Running \`appwrite ${cliConfig.reportData.data.args.join(' ')}\``; const yourEnvironment = `CLI version: ${version}\nOperation System: ${os.type()}\nAppwrite version: ${appwriteVersion}\nIs Cloud: ${isCloud}`; const stack = '```\n' + err.stack + '\n```'; From 2fd8f2b4b8ded6158b9a3027d13a820a5603b35e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 6 Jun 2024 22:05:29 -0400 Subject: [PATCH 64/89] feat: Added list search --- templates/cli/index.js.twig | 3 +++ templates/cli/lib/questions.js.twig | 10 +++++----- templates/cli/package.json.twig | 1 + 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 2bad4b50a..6aa486cd7 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -10,6 +10,7 @@ const chalk = require("chalk"); const { version } = require("./package.json"); const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); +const inquirer = require("inquirer"); {% if sdk.test != "true" %} const { login, logout, whoami, migrate } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); @@ -22,6 +23,8 @@ const { migrate } = require("./lib/commands/generic"); const { {{ service.name | caseLower }} } = require("./lib/commands/{{ service.name | caseLower }}"); {% endfor %} +inquirer.registerPrompt('search-list', require('inquirer-search-list')); + program .description(commandDescriptions['main']) .configureHelp({ diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 99e7481ff..7bd8c965b 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -141,7 +141,7 @@ const questionsInitProject = [ ] }, { - type: "list", + type: "search-list", name: "organization", message: "Choose the project organization", choices: async () => { @@ -181,7 +181,7 @@ const questionsInitProject = [ when: (answer) => answer.start === 'new' }, { - type: "list", + type: "search-list", name: "project", message: "Choose your {{ spec.title|caseUcfirst }} project.", choices: async (answers) => { @@ -292,7 +292,7 @@ const questionsCreateFunction = [ const questionsCreateFunctionSelectTemplate = (templates) => { return [ { - type: "list", + type: "search-list", name: "template", message: "What template would you like to use?", choices: templates.map((template) => { @@ -339,7 +339,7 @@ const questionsCreateCollection = [ } }, { - type: "list", + type: "search-list", name: "database", message: "Choose the collection database", choices: async () => { @@ -472,7 +472,7 @@ const questionsLogin = [ when: (answers) => answers.method !== 'select' }, { - type: "list", + type: "search-list", name: "accountId", message: "Select an account to use", choices() { diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 015e494e1..4fde9550f 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -30,6 +30,7 @@ "form-data": "^4.0.0", "json-bigint": "^1.0.0", "inquirer": "^8.2.4", + "inquirer-search-list": "^1.2.6", "tar": "^6.1.11", "ignore": "^5.2.0" }, From 0b817a734e4fbc3003fe6830d3f5a75ba171d9f1 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 6 Jun 2024 22:25:53 -0400 Subject: [PATCH 65/89] feat: Added Register --- templates/cli/index.js.twig | 3 +- templates/cli/lib/commands/generic.js.twig | 47 +++++++++++++++++++-- templates/cli/lib/questions.js.twig | 48 +++++++++++++++++++++- 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 6aa486cd7..9cb26842c 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,7 +12,7 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); const inquirer = require("inquirer"); {% if sdk.test != "true" %} -const { login, logout, whoami, migrate } = require("./lib/commands/generic"); +const { login, register, logout, whoami, migrate } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); @@ -62,6 +62,7 @@ program {% if sdk.test != "true" %} .addCommand(whoami) .addCommand(login) + .addCommand(register) .addCommand(init) .addCommand(pull) .addCommand(push) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index cd3332adf..443c69464 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -6,11 +6,42 @@ const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); const ID = require("../id"); {% if sdk.test != "true" %} -const { questionsLogin, questionLoginWithEndpoint, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); -const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); +const { questionsRegister, questionsRegisterWithEndpoint, questionsLogin, questionLoginWithEndpoint, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); +const { accountCreate, accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; +const registerCommand = async ({ selfHosted, email, password, endpoint, name }) => { + let answers; + let configEndpoint = DEFAULT_ENDPOINT; + + if (selfHosted) { + answers = endpoint && email && password ? { endpoint, email, password } : await inquirer.prompt(questionsRegisterWithEndpoint); + configEndpoint = answers.endpoint; + } else { + answers = email && password ? { email, password } : await inquirer.prompt(questionsRegister); + } + + globalConfig.setEndpoint(configEndpoint); + + let client = await sdkForConsole(false); + + try { + await accountCreate({ + userId: ID.unique(), + email: answers.email, + password: answers.password, + parseOutput: false, + name: answers.name, + sdk: client, + }) + + success(); + } catch (e) { + throw e; + } + +} const loginCommand = async ({ selfHosted, email, password, endpoint, mfa, code }) => { const oldCurrent = globalConfig.getCurrentLogin(); let answers = {}; @@ -18,7 +49,7 @@ const loginCommand = async ({ selfHosted, email, password, endpoint, mfa, code } if (selfHosted) { answers = endpoint && email && password ? { endpoint, email, password } : await inquirer.prompt(questionLoginWithEndpoint); - configEndpoint = answers.endpoint; + configEndpoint = answers.endpoint; } else { answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); } @@ -148,6 +179,15 @@ const login = new Command("login") }) .action(actionRunner(loginCommand)); +const register = new Command("register") + .description(commandDescriptions['login']) + .option(`-sh, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`) + .option(`--name [name]`, `User name`) + .option(`--email [email]`, `User email`) + .option(`--password [password]`, `User password`) + .option(`--endpoint [endpoint]`, `Appwrite endpoint for self hosted instances`) + .action(actionRunner(registerCommand)); + const singleLogout = async (accountId) => { try { let client = await sdkForConsole(); @@ -308,6 +348,7 @@ module.exports = { loginCommand, whoami, login, + register, logout, {% endif %} migrate, diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 7bd8c965b..6d0319789 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -531,6 +531,50 @@ const questionLoginWithEndpoint = [ questionsLogin[3] ] +const questionsRegister = [ + { + type: "input", + name: "name", + message: "Enter your name", + validate(value) { + if (!value) { + return "Please enter your name"; + } + return true; + }, + }, + { + type: "input", + name: "email", + message: "Enter your email", + validate(value) { + if (!value) { + return "Please enter your email"; + } + return true; + }, + }, + { + type: "password", + name: "password", + message: "Enter your password", + mask: "*", + validate(value) { + if (!value) { + return "Please enter your password"; + } + return true; + }, + }, +]; + +const questionsRegisterWithEndpoint = [ + questionGetEndpoint[0], + questionsRegister[0], + questionsRegister[1], + questionsRegister[2] +] + const questionsLogout = [ { type: "checkbox", @@ -800,5 +844,7 @@ module.exports = { questionsListFactors, questionsMfaChallenge, questionGetEndpoint, - questionLoginWithEndpoint + questionLoginWithEndpoint, + questionsRegister, + questionsRegisterWithEndpoint }; From acef728953e12374f1d3adc20df922031bd292d2 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:04:00 -0400 Subject: [PATCH 66/89] feat: Tip for detailed error --- templates/cli/lib/parser.js.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index a5559201f..6541c0d5e 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -151,6 +151,7 @@ const parseError = (err) => { if (cliConfig.verbose) { console.error(err); } else { + log('For detailed error pass the --verbose or --report flag'); error(err.message); } process.exit(1); From 990557a8448105a01122ed1c84c56b1520fefb11 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:13:51 -0400 Subject: [PATCH 67/89] feat: clearer function summary --- templates/cli/lib/commands/push.js.twig | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 503200a4f..b367fc39e 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -932,7 +932,18 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero error(`Deployment of ${name} has failed. Check at ${failUrl} for more details\n`); }) - success(`Pushed ${successfullyPushed} functions with ${successfullyDeployed} successful deployments.`); + let message = chalk.green(`Pushed and deployed ${successfullyPushed} functions`); + + if (!async) { + if (successfullyDeployed < successfullyPushed) { + message = `${chalk.green(`Pushed and deployed ${successfullyPushed} functions.`)} ${chalk.red(`${successfullyPushed - successfullyDeployed} failed to deploy`)}`; + } else { + if (successfullyPushed === 0) { + message = chalk.red(`Error pushing ${functions.length} functions`) + } + } + } + log(message); } const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { From 33a56808c2ab94b589cb5b5d93a2e9b8901ac538 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:23:04 -0400 Subject: [PATCH 68/89] feat: Adding error when enable to open with default browser --- templates/cli/lib/utils.js.twig | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 030689243..cb397c356 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -1,7 +1,7 @@ const fs = require("fs"); const path = require("path"); const { localConfig, globalConfig } = require("./config"); -const { success, log } = require('./parser') +const { success, log, error } = require('./parser') const readline = require('readline'); const cp = require('child_process'); @@ -67,7 +67,13 @@ function showConsoleLink(serviceName, action, open, ...ids) { if (open) { const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open'); - cp.exec(`${start} ${url}`); + + cp.exec(`${start} ${url}`, (err, stdout, stderr) => { + if (err !== null) { + console.log('\n'); + error('Opening in default browser. ' + err) + } + }); } } From 471fc85bccdef9c3659f45483dc7bba0c997114c Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:25:03 -0400 Subject: [PATCH 69/89] feat: Adding endpoint to whoami --- templates/cli/lib/commands/generic.js.twig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 443c69464..71db03ffa 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -154,7 +154,8 @@ const whoami = new Command("whoami") 'ID': account.$id, 'Name': account.name, 'Email': account.email, - 'MFA enabled': account.mfa ? 'Yes' : 'No' + 'MFA enabled': account.mfa ? 'Yes' : 'No', + 'Endpoint': globalConfig.getEndpoint() } ]; if (json) { From f24abb618d0376314c6b92d63ca7dd005c66bc4e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:33:36 -0400 Subject: [PATCH 70/89] feat: Adding self-hosted to project init --- templates/cli/lib/commands/init.js.twig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index d3dc6eff3..61bc37d5a 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -23,7 +23,7 @@ const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); const { sdkForConsole } = require("../sdks"); -const initProject = async ({ organizationId, projectId, projectName } = {}) => { +const initProject = async ({ organizationId, projectId, projectName, selfHosted } = {}) => { let response = {}; try { @@ -38,7 +38,7 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { }); } catch (e) { log('You must login first') - await loginCommand(); + await loginCommand({selfHosted}); } let answers = {}; @@ -269,6 +269,7 @@ const init = new Command("init") .option("--organizationId ", "{{ spec.title|caseUcfirst }} organization ID") .option("--projectId ", "{{ spec.title|caseUcfirst }} project ID") .option("--projectName ", "{{ spec.title|caseUcfirst }} project ID") + .option(`-sh, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances for non-logged in users`) .action(actionRunner(initProject)); init From 85167e9b6eddb8fefc5516421aea1d356154d026 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 08:47:34 -0400 Subject: [PATCH 71/89] feat: Removing unusable accounts --- templates/cli/lib/client.js.twig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index 9dca3ccde..a39af4781 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -4,10 +4,11 @@ const { fetch, FormData, Agent } = require("undici"); const JSONbig = require("json-bigint")({ storeAsString: false }); const {{spec.title | caseUcfirst}}Exception = require("./exception.js"); const { globalConfig } = require("./config.js"); +const {log} = require('./parser'); class Client { CHUNK_SIZE = 5*1024*1024; // 5MB - + constructor() { this.endpoint = '{{spec.endpoint}}'; this.headers = { @@ -144,6 +145,13 @@ class Client { } catch (error) { throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); } + + if(path !== '/account' && json.code === 401 && json.type ==='user_more_factors_required'){ + log('Unusable account found, removing...') + const current = globalConfig.getCurrentLogin(); + globalConfig.setCurrentLogin(''); + globalConfig.removeLogin(current); + } throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); } From 069f70cc799b4fcd2251b1f75e6e71bfbb39cfc4 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:09:38 -0400 Subject: [PATCH 72/89] feat: Ask when deleting, and explanations --- templates/cli/lib/commands/push.js.twig | 11 +++++++++-- templates/cli/lib/questions.js.twig | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index b367fc39e..430d553ad 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -481,7 +481,14 @@ const attributesToCreate = async (remoteAttributes, localAttributes, collection) return { Key: change.key, Action: change.action, Reason: change.reason, }; })); - if (!cliConfig.force) { + if (!cliConfig.force && (deleting.length > 0 || conflicts.length > 0)) { + if (deleting.length > 0) { + log(`Attribute deletion will cause ${chalk.red('loss of data')}`); + } + if (conflicts.length > 0) { + log(`Attribute recreation will cause ${chalk.red('loss of data')}`); + } + const answers = await inquirer.prompt(questionsPushCollections[1]); if (answers.changes.toLowerCase() !== 'yes') { @@ -1045,7 +1052,7 @@ const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { } })) -// Serialize attribute actions + // Serialize attribute actions for (let collection of collections) { let attributes = collection.attributes; diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 6d0319789..f5657522c 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -672,7 +672,7 @@ const questionsPushCollections = [ { type: "input", name: "changes", - message: `Are you sure you want to override this collection? This can lead to loss of data! Type "YES" to confirm.` + message: `Do you want to apply these changes? Type "YES" to confirm.` } ] From 6d8b6f201db1b5888c5bc1219e04eec5a33eb99a Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:20:05 -0400 Subject: [PATCH 73/89] refactor: Make sure `min` and `max` are numbers --- templates/cli/lib/commands/push.js.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 430d553ad..12396c1d8 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -344,8 +344,8 @@ const createAttribute = async (databaseId, collectionId, attribute) => { collectionId, key: attribute.key, required: attribute.required, - min: attribute.min, - max: attribute.max, + min: parseInt(attribute.min.toString()), + max: parseInt(attribute.max.toString()), xdefault: attribute.default, array: attribute.array, parseOutput: false @@ -356,8 +356,8 @@ const createAttribute = async (databaseId, collectionId, attribute) => { collectionId, key: attribute.key, required: attribute.required, - min: attribute.min, - max: attribute.max, + min: parseFloat(attribute.min.toString()), + max: parseFloat(attribute.max.toString()), xdefault: attribute.default, array: attribute.array, parseOutput: false From f64000e1e1c7ba665fa9071f099a8ee3426be9a2 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 7 Jun 2024 16:46:31 -0400 Subject: [PATCH 74/89] refactor: Asking unanswered questions --- templates/cli/lib/commands/init.js.twig | 26 ++++++++++++------------- templates/cli/lib/questions.js.twig | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index d3dc6eff3..66bf21b8a 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -40,14 +40,22 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { log('You must login first') await loginCommand(); } + let answers = {}; - if (organizationId && projectId && projectName) { - answers = { - project: { id: projectId, name: projectName }, - organization: { id: organizationId }, - start: 'existing' + if (!organizationId && !projectId && !projectName) { + answers = await inquirer.prompt(questionsInitProject) + if (answers.override === false) { + process.exit(1) } + } else { + answers.start = 'existing'; + answers.project = {}; + answers.organization = {}; + + answers.organization.id = organizationId ?? (await inquirer.prompt(questionsInitProject[2])).organization; + answers.project.name = projectName ?? (await inquirer.prompt(questionsInitProject[3])).project; + answers.project.id = projectId ?? (await inquirer.prompt(questionsInitProject[4])).id; try { await projectsGet({ projectId, parseOutput: false }); @@ -60,15 +68,8 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { throw e; } } - - } else { - answers = await inquirer.prompt(questionsInitProject) - if (answers.override === false) { - process.exit(1) - } } - if (answers.start === 'new') { response = await projectsCreate({ projectId: answers.id, @@ -170,7 +171,6 @@ const initFunction = async () => { } - fs.mkdirSync(functionDir, "777"); fs.mkdirSync(templatesDir, "777"); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 99e7481ff..3e191a6ec 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -171,14 +171,14 @@ const questionsInitProject = [ name: "project", message: "What would you like to name your project?", default: "My Awesome Project", - when: (answer) => answer.start === 'new' + when: (answer) => answer.start !== 'existing' }, { type: "input", name: "id", message: "What ID would you like to have for your project?", default: "unique()", - when: (answer) => answer.start === 'new' + when: (answer) => answer.start !== 'existing' }, { type: "list", From 034d08e3a0822e046bf786f2c0144348d0c989c9 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 11 Jun 2024 18:00:46 +0530 Subject: [PATCH 75/89] Update templates/cli/lib/questions.js.twig --- templates/cli/lib/questions.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index f5657522c..2f75e36b0 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -672,7 +672,7 @@ const questionsPushCollections = [ { type: "input", name: "changes", - message: `Do you want to apply these changes? Type "YES" to confirm.` + message: `Would you like to apply these changes? Type "YES" to confirm.` } ] From 8420eb058a7e5b671f1ecc415d332d9abd8a215e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:39:36 -0400 Subject: [PATCH 76/89] fix: reviews --- templates/cli/index.js.twig | 3 +- templates/cli/lib/client.js.twig | 4 +- templates/cli/lib/commands/generic.js.twig | 43 +--------------------- 3 files changed, 4 insertions(+), 46 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 9cb26842c..6aa486cd7 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,7 +12,7 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); const inquirer = require("inquirer"); {% if sdk.test != "true" %} -const { login, register, logout, whoami, migrate } = require("./lib/commands/generic"); +const { login, logout, whoami, migrate } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); @@ -62,7 +62,6 @@ program {% if sdk.test != "true" %} .addCommand(whoami) .addCommand(login) - .addCommand(register) .addCommand(init) .addCommand(pull) .addCommand(push) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index a39af4781..a7c6f6503 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -4,7 +4,7 @@ const { fetch, FormData, Agent } = require("undici"); const JSONbig = require("json-bigint")({ storeAsString: false }); const {{spec.title | caseUcfirst}}Exception = require("./exception.js"); const { globalConfig } = require("./config.js"); -const {log} = require('./parser'); +const { log } = require('./parser'); class Client { CHUNK_SIZE = 5*1024*1024; // 5MB @@ -146,7 +146,7 @@ class Client { throw new {{spec.title | caseUcfirst}}Exception(text, response.status, "", text); } - if(path !== '/account' && json.code === 401 && json.type ==='user_more_factors_required'){ + if (path !== '/account' && json.code === 401 && json.type === 'user_more_factors_required') { log('Unusable account found, removing...') const current = globalConfig.getCurrentLogin(); globalConfig.setCurrentLogin(''); diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 71db03ffa..3529197d8 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -6,42 +6,11 @@ const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); const ID = require("../id"); {% if sdk.test != "true" %} -const { questionsRegister, questionsRegisterWithEndpoint, questionsLogin, questionLoginWithEndpoint, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); +const { questionsLogin, questionLoginWithEndpoint, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountCreate, accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; -const registerCommand = async ({ selfHosted, email, password, endpoint, name }) => { - let answers; - let configEndpoint = DEFAULT_ENDPOINT; - - if (selfHosted) { - answers = endpoint && email && password ? { endpoint, email, password } : await inquirer.prompt(questionsRegisterWithEndpoint); - configEndpoint = answers.endpoint; - } else { - answers = email && password ? { email, password } : await inquirer.prompt(questionsRegister); - } - - globalConfig.setEndpoint(configEndpoint); - - let client = await sdkForConsole(false); - - try { - await accountCreate({ - userId: ID.unique(), - email: answers.email, - password: answers.password, - parseOutput: false, - name: answers.name, - sdk: client, - }) - - success(); - } catch (e) { - throw e; - } - -} const loginCommand = async ({ selfHosted, email, password, endpoint, mfa, code }) => { const oldCurrent = globalConfig.getCurrentLogin(); let answers = {}; @@ -180,15 +149,6 @@ const login = new Command("login") }) .action(actionRunner(loginCommand)); -const register = new Command("register") - .description(commandDescriptions['login']) - .option(`-sh, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`) - .option(`--name [name]`, `User name`) - .option(`--email [email]`, `User email`) - .option(`--password [password]`, `User password`) - .option(`--endpoint [endpoint]`, `Appwrite endpoint for self hosted instances`) - .action(actionRunner(registerCommand)); - const singleLogout = async (accountId) => { try { let client = await sdkForConsole(); @@ -349,7 +309,6 @@ module.exports = { loginCommand, whoami, login, - register, logout, {% endif %} migrate, From 4ae99d5017cc329596cae7bac53631a28867a3d5 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:42:27 -0400 Subject: [PATCH 77/89] fix: reviews --- templates/cli/lib/questions.js.twig | 48 +---------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 7632419ef..17a7a204d 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -531,50 +531,6 @@ const questionLoginWithEndpoint = [ questionsLogin[3] ] -const questionsRegister = [ - { - type: "input", - name: "name", - message: "Enter your name", - validate(value) { - if (!value) { - return "Please enter your name"; - } - return true; - }, - }, - { - type: "input", - name: "email", - message: "Enter your email", - validate(value) { - if (!value) { - return "Please enter your email"; - } - return true; - }, - }, - { - type: "password", - name: "password", - message: "Enter your password", - mask: "*", - validate(value) { - if (!value) { - return "Please enter your password"; - } - return true; - }, - }, -]; - -const questionsRegisterWithEndpoint = [ - questionGetEndpoint[0], - questionsRegister[0], - questionsRegister[1], - questionsRegister[2] -] - const questionsLogout = [ { type: "checkbox", @@ -844,7 +800,5 @@ module.exports = { questionsListFactors, questionsMfaChallenge, questionGetEndpoint, - questionLoginWithEndpoint, - questionsRegister, - questionsRegisterWithEndpoint + questionLoginWithEndpoint }; From b5ca8bfacfb8e08cd7dc0ee8da2fd28e3bd757d5 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 08:43:54 -0400 Subject: [PATCH 78/89] fix: reviews --- templates/cli/lib/commands/init.js.twig | 4 ++-- templates/cli/lib/questions.js.twig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 66bf21b8a..3b0b96666 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -103,8 +103,8 @@ const initCollection = async () => { const newDatabase = (answers.method ?? '').toLowerCase() !== 'existing'; if (!newDatabase) { - answers.database_id = answers.database.$id; - answers.database_name = answers.database.name; + answers.databaseId = answers.database.$id; + answers.databaseName = answers.database.name; } const databaseId = answers.database_id === 'unique()' ? ID.unique() : answers.database_id; diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 0054792fa..ceafc6224 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -362,14 +362,14 @@ const questionsCreateCollection = [ }, { type: "input", - name: "database_name", + name: "databaseName", message: "What would you like to name your database?", default: "My Awesome Database", when: (answers) => (answers.method ?? '').toLowerCase() !== 'existing' }, { type: "input", - name: "database_id", + name: "databaseId", message: "What ID would you like to have for your database?", default: "unique()", when: (answers) => (answers.method ?? '').toLowerCase() !== 'existing' From aabddae080e37102ad9e5eddd86483bf651c7691 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:02:45 -0400 Subject: [PATCH 79/89] fix: texts --- templates/cli/lib/sdks.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/sdks.js.twig b/templates/cli/lib/sdks.js.twig index 2e615f706..435375894 100644 --- a/templates/cli/lib/sdks.js.twig +++ b/templates/cli/lib/sdks.js.twig @@ -68,7 +68,7 @@ const sdkForProject = async () => { } if (!project) { - throw new Error("Project is not set. Please run `{{ language.params.executableName }} pull project` to initialize the current directory with an {{ spec.title|caseUcfirst }} project."); + throw new Error("Project is not set. Please run `{{ language.params.executableName }} init` to initialize the current directory with an {{ spec.title|caseUcfirst }} project."); } client From dea75b4aac5afcb656092a778df32978f95fe320 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:31:34 -0400 Subject: [PATCH 80/89] refactor: Removing login from init --- templates/cli/lib/commands/init.js.twig | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index d94ca2509..6ff9c6616 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -18,12 +18,11 @@ const { questionsCreateCollection, questionsInitProject } = require("../questions"); -const { success, log, error, actionRunner, commandDescriptions } = require("../parser"); +const { success, log, error, actionRunner } = require("../parser"); const { accountGet } = require("./account"); -const { loginCommand } = require("./generic"); const { sdkForConsole } = require("../sdks"); -const initProject = async ({ organizationId, projectId, projectName, selfHosted } = {}) => { +const initProject = async ({ organizationId, projectId, projectName } = {}) => { let response = {}; try { @@ -37,8 +36,8 @@ const initProject = async ({ organizationId, projectId, projectName, selfHosted sdk: client }); } catch (e) { - log('You must login first') - await loginCommand({selfHosted}); + error('Error Session not found. Please run `appwrite login` to create a session'); + process.exit(1); } let answers = {}; @@ -269,7 +268,6 @@ const init = new Command("init") .option("--organizationId ", "{{ spec.title|caseUcfirst }} organization ID") .option("--projectId ", "{{ spec.title|caseUcfirst }} project ID") .option("--projectName ", "{{ spec.title|caseUcfirst }} project ID") - .option(`-sh, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances for non-logged in users`) .action(actionRunner(initProject)); init From 5fe0bd270d46c139ac9c60dee7468530b703b239 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 09:37:43 -0400 Subject: [PATCH 81/89] refactor: Removing self-hosted from login --- templates/cli/lib/commands/generic.js.twig | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 3529197d8..e54d1e5fa 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -6,23 +6,16 @@ const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); const ID = require("../id"); {% if sdk.test != "true" %} -const { questionsLogin, questionLoginWithEndpoint, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); -const { accountCreate, accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); +const { questionsLogin, questionsLogout, questionsListFactors, questionsMfaChallenge } = require("../questions"); +const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; -const loginCommand = async ({ selfHosted, email, password, endpoint, mfa, code }) => { +const loginCommand = async ({ email, password, endpoint, mfa, code }) => { const oldCurrent = globalConfig.getCurrentLogin(); - let answers = {}; - let configEndpoint = DEFAULT_ENDPOINT; - - if (selfHosted) { - answers = endpoint && email && password ? { endpoint, email, password } : await inquirer.prompt(questionLoginWithEndpoint); - configEndpoint = answers.endpoint; - } else { - answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); - } + let configEndpoint = endpoint ?? DEFAULT_ENDPOINT; + const answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); if (answers.method === 'select') { const accountId = answers.accountId; @@ -138,7 +131,6 @@ const whoami = new Command("whoami") const login = new Command("login") .description(commandDescriptions['login']) - .option(`-sh, --self-hosted`, `Flag for enabling custom endpoint for self hosted instances`) .option(`--email [email]`, `User email`) .option(`--password [password]`, `User password`) .option(`--endpoint [endpoint]`, `Appwrite endpoint for self hosted instances`) From 9116fc0962b705d2ce249986e849b3b9dd8840e1 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:31:03 -0400 Subject: [PATCH 82/89] feat: Fetching function templates from API --- templates/cli/lib/commands/init.js.twig | 41 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 6ff9c6616..5af0faf3b 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -3,6 +3,7 @@ const path = require("path"); const childProcess = require('child_process'); const { Command } = require("commander"); const inquirer = require("inquirer"); +const { fetch } = require("undici"); const { projectsCreate, projectsGet } = require("./projects"); const { storageCreateBucket } = require("./storage"); const { messagingCreateTopic } = require("./messaging"); @@ -172,10 +173,26 @@ const initFunction = async () => { fs.mkdirSync(functionDir, "777"); fs.mkdirSync(templatesDir, "777"); + const repo = "https://github.com/{{ sdk.gitUserName }}/templates"; + const api = `https://api.github.com/repos/appwrite/templates/contents/${answers.runtime.name}` + const templates = ['Starter']; + let selected = undefined; + + try { + const res = await fetch(api); + templates.push(...(await res.json()).map((template) => template.name)); + + selected = await inquirer.prompt(questionsCreateFunctionSelectTemplate(templates)) + } catch { + // Not a problem will go with directory pulling + log('Loading templates...'); + } + + const sparse = selected ? `${answers.runtime.name}/${selected.template}` : answers.runtime.name; - let gitInitCommands = "git clone --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/templates ."; // depth prevents fetching older commits reducing the amount fetched + let gitInitCommands = `git clone --single-branch --depth 1 --sparse ${repo} .`; // depth prevents fetching older commits reducing the amount fetched - let gitPullCommands = `git sparse-checkout add ${answers.runtime.name}`; + let gitPullCommands = `git sparse-checkout add ${sparse}`; /* Force use CMD as powershell does not support && */ @@ -184,7 +201,6 @@ const initFunction = async () => { gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; } - log('Loading templates...'); /* Execute the child process but do not print any std output */ try { childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: templatesDir }); @@ -201,18 +217,17 @@ const initFunction = async () => { } fs.rmSync(path.join(templatesDir, ".git"), { recursive: true }); - const templates = ['Starter']; - templates.push(...fs.readdirSync(runtimeDir, { withFileTypes: true }) - .filter(item => item.isDirectory() && item.name !== 'starter') - .map(dirent => dirent.name)); - - let selected = { template: 'starter' }; - - if (templates.length > 1) { - selected = await inquirer.prompt(questionsCreateFunctionSelectTemplate(templates)) + if (!selected) { + templates.push(...fs.readdirSync(runtimeDir, { withFileTypes: true }) + .filter(item => item.isDirectory() && item.name !== 'starter') + .map(dirent => dirent.name)); + selected = { template: 'starter' }; + + if (templates.length > 1) { + selected = await inquirer.prompt(questionsCreateFunctionSelectTemplate(templates)) + } } - const copyRecursiveSync = (src, dest) => { let exists = fs.existsSync(src); let stats = exists && fs.statSync(src); From 7ed643e12d5392c3f4fed7471810485412201b71 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 11 Jun 2024 23:21:26 +0530 Subject: [PATCH 83/89] Update templates/cli/lib/commands/init.js.twig --- templates/cli/lib/commands/init.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 5af0faf3b..6fd63bedd 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -174,7 +174,7 @@ const initFunction = async () => { fs.mkdirSync(functionDir, "777"); fs.mkdirSync(templatesDir, "777"); const repo = "https://github.com/{{ sdk.gitUserName }}/templates"; - const api = `https://api.github.com/repos/appwrite/templates/contents/${answers.runtime.name}` + const api = `https://api.github.com/repos/{{ sdk.gitUserName }}/templates/contents/${answers.runtime.name}` const templates = ['Starter']; let selected = undefined; From 0cf288200627235f33446cd8f0cb3550fce9621b Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:28:12 -0400 Subject: [PATCH 84/89] refactor: Removing `--open` option --- templates/cli/base/requests/api.twig | 2 +- templates/cli/lib/commands/command.js.twig | 3 +-- templates/cli/lib/utils.js.twig | 13 +------------ 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/templates/cli/base/requests/api.twig b/templates/cli/base/requests/api.twig index 479d22ab0..8e0c5116d 100644 --- a/templates/cli/base/requests/api.twig +++ b/templates/cli/base/requests/api.twig @@ -19,7 +19,7 @@ if (parseOutput) { {%~ if methodHaveConsolePreview(method.name,service.name) %} if(console) { - showConsoleLink('{{service.name}}', '{{ method.name }}',open + showConsoleLink('{{service.name}}', '{{ method.name }}' {%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%}{%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%} ); } else { diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index d2d16b67c..6fcb90f43 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -70,7 +70,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%} {%- if method.type == 'location' -%}, destination{%- endif -%} - {% if methodHaveConsolePreview(method.name,service.name) %}, console, open{%- endif -%} + {% if methodHaveConsolePreview(method.name,service.name) %}, console{%- endif -%} }) => { {%~ endblock %} let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : @@ -97,7 +97,6 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {% endif %} {% if methodHaveConsolePreview(method.name,service.name) %} .option(`--console`, `Get the resource console url`) - .option(`--open`, `Use with '--console' to open the using default browser`) {% endif %} {% endautoescape %} .action(actionRunner({{ service.name | caseLower }}{{ method.name | caseUcfirst }})) diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index cb397c356..e56382503 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -24,7 +24,7 @@ const checkDeployConditions = (localConfig) => { } } -function showConsoleLink(serviceName, action, open, ...ids) { +function showConsoleLink(serviceName, action, ...ids) { const projectId = localConfig.getProject().projectId; const url = new URL(globalConfig.getEndpoint().replace('/v1', '/console')); @@ -64,17 +64,6 @@ function showConsoleLink(serviceName, action, open, ...ids) { success(url); - - if (open) { - const start = (process.platform == 'darwin' ? 'open' : process.platform == 'win32' ? 'start' : 'xdg-open'); - - cp.exec(`${start} ${url}`, (err, stdout, stderr) => { - if (err !== null) { - console.log('\n'); - error('Opening in default browser. ' + err) - } - }); - } } function getAccountPath(action) { From 309d764c6bda0a85540e0a2d9eaf1301e2c58a60 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 15:53:22 -0400 Subject: [PATCH 85/89] refactor: reviews --- templates/cli/lib/client.js.twig | 11 +++--- templates/cli/lib/commands/generic.js.twig | 46 +++++++++++----------- templates/cli/lib/commands/push.js.twig | 3 +- templates/cli/lib/config.js.twig | 16 ++++---- templates/cli/lib/questions.js.twig | 10 ++--- 5 files changed, 44 insertions(+), 42 deletions(-) diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index a7c6f6503..ee482380a 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -4,7 +4,7 @@ const { fetch, FormData, Agent } = require("undici"); const JSONbig = require("json-bigint")({ storeAsString: false }); const {{spec.title | caseUcfirst}}Exception = require("./exception.js"); const { globalConfig } = require("./config.js"); -const { log } = require('./parser'); +const chalk = require("chalk"); class Client { CHUNK_SIZE = 5*1024*1024; // 5MB @@ -147,10 +147,11 @@ class Client { } if (path !== '/account' && json.code === 401 && json.type === 'user_more_factors_required') { - log('Unusable account found, removing...') - const current = globalConfig.getCurrentLogin(); - globalConfig.setCurrentLogin(''); - globalConfig.removeLogin(current); + console.log(`${chalk.cyan.bold("ℹ Info")} ${chalk.cyan("Unusable account found, removing...")}`); + + const current = globalConfig.getCurrentSession(); + globalConfig.setCurrentSession(''); + globalConfig.removeSession(current); } throw new {{spec.title | caseUcfirst}}Exception(json.message, json.code, json.type, json); } diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index e54d1e5fa..538d2d695 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -12,7 +12,7 @@ const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accou const DEFAULT_ENDPOINT = 'https://cloud.appwrite.io/v1'; const loginCommand = async ({ email, password, endpoint, mfa, code }) => { - const oldCurrent = globalConfig.getCurrentLogin(); + const oldCurrent = globalConfig.getCurrentSession(); let configEndpoint = endpoint ?? DEFAULT_ENDPOINT; const answers = email && password ? { email, password } : await inquirer.prompt(questionsLogin); @@ -24,7 +24,7 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { throw Error('Login ID not found'); } - globalConfig.setCurrentLogin(accountId); + globalConfig.setCurrentSession(accountId); success(`Current account is ${accountId}`); return; @@ -32,8 +32,8 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { const id = ID.unique(); - globalConfig.addLogin(id, {}); - globalConfig.setCurrentLogin(id); + globalConfig.addSession(id, {}); + globalConfig.setCurrentSession(id); globalConfig.setEndpoint(configEndpoint); globalConfig.setEmail(answers.email); @@ -79,8 +79,8 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { parseOutput: false }); } else { - globalConfig.removeLogin(id); - globalConfig.setCurrentLogin(oldCurrent); + globalConfig.removeSession(id); + globalConfig.setCurrentSession(oldCurrent); throw error; } } @@ -141,7 +141,7 @@ const login = new Command("login") }) .action(actionRunner(loginCommand)); -const singleLogout = async (accountId) => { +const deleteSession = async (accountId) => { try { let client = await sdkForConsole(); @@ -151,11 +151,11 @@ const singleLogout = async (accountId) => { sdk: client }) - globalConfig.removeLogin(accountId); + globalConfig.removeSession(accountId); } catch (e) { error('Unable to log out, removing locally saved session information') } - globalConfig.removeLogin(accountId); + globalConfig.removeSession(accountId); } const logout = new Command("logout") @@ -164,14 +164,14 @@ const logout = new Command("logout") helpWidth: process.stdout.columns || 80 }) .action(actionRunner(async () => { - const logins = globalConfig.getLogins(); - const current = globalConfig.getCurrentLogin(); + const logins = globalConfig.getSessions(); + const current = globalConfig.getCurrentSession(); if (current === '') { return; } if (logins.length === 1) { - await singleLogout(current); + await deleteSession(current); success(); return; @@ -181,16 +181,16 @@ const logout = new Command("logout") if (answers.accounts) { for (let accountId of answers.accounts) { - globalConfig.setCurrentLogin(accountId); - await singleLogout(accountId); + globalConfig.setCurrentSession(accountId); + await deleteSession(accountId); } } - const leftLogins = globalConfig.getLogins(); + const leftLogins = globalConfig.getSessions(); if (leftLogins.length > 0 && leftLogins.filter(login => login.id === current).length !== 1) { const accountId = leftLogins[0].id; - globalConfig.setCurrentLogin(accountId); + globalConfig.setCurrentSession(accountId); success(`Current account is ${accountId}`); } @@ -243,8 +243,8 @@ const client = new Command("client") if (!response.version) { throw new Error(); } - globalConfig.setCurrentLogin(id); - globalConfig.addLogin(id, {}); + globalConfig.setCurrentSession(id); + globalConfig.addSession(id, {}); globalConfig.setEndpoint(endpoint); } catch (_) { throw new Error("Invalid endpoint or your Appwrite server is not running as expected."); @@ -264,11 +264,11 @@ const client = new Command("client") } if (reset !== undefined) { - const logins = globalConfig.getLogins(); + const logins = globalConfig.getSessions(); for (let accountId of logins.map(login => login.id)) { - globalConfig.setCurrentLogin(accountId); - await singleLogout(accountId); + globalConfig.setCurrentSession(accountId); + await deleteSession(accountId); } } @@ -290,8 +290,8 @@ const migrate = async () => { email: 'legacy' }; - globalConfig.addLogin(id, data); - globalConfig.setCurrentLogin(id); + globalConfig.addSession(id, data); + globalConfig.setCurrentSession(id); globalConfig.delete('endpoint'); globalConfig.delete('cookie'); diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 67cb7ac6c..ca0ca4cf9 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -593,6 +593,7 @@ const pushProject = async () => { try { const projectId = localConfig.getProject().projectId; const projectName = localConfig.getProject().projectName; + const settings = localConfig.getProject().projectSettings; log(`Updating project ${projectName} ( ${projectId} )`); @@ -602,8 +603,8 @@ const pushProject = async () => { name: projectName, parseOutput: false }); + } - const settings = localConfig.getProject().projectSettings; if (settings.services) { log('Updating service statuses'); diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 0775a01fb..7cfa4eb03 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -407,14 +407,14 @@ class Global extends Config { super(`${homeDir}/${path}`); } - getCurrentLogin() { + getCurrentSession() { if (!this.has(Global.PREFERENCE_CURRENT)) { return ""; } return this.get(Global.PREFERENCE_CURRENT); } - setCurrentLogin(endpoint) { + setCurrentSession(endpoint) { this.set(Global.PREFERENCE_CURRENT, endpoint); } @@ -422,7 +422,7 @@ class Global extends Config { return Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)); } - getLogins() { + getSessions() { const logins = Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)) return logins.map((login) => { @@ -435,11 +435,11 @@ class Global extends Config { }) } - addLogin(login, data) { + addSession(login, data) { this.set(login, data); } - removeLogin(login, data) { + removeSession(login, data) { this.delete(login); } @@ -513,7 +513,7 @@ class Global extends Config { hasFrom(key) { - const current = this.getCurrentLogin(); + const current = this.getCurrentSession(); if (current) { const config = this.get(current); @@ -523,7 +523,7 @@ class Global extends Config { } getFrom(key) { - const current = this.getCurrentLogin(); + const current = this.getCurrentSession(); if (current) { const config = this.get(current); @@ -533,7 +533,7 @@ class Global extends Config { } setTo(key, value) { - const current = this.getCurrentLogin(); + const current = this.getCurrentSession(); if (current) { const config = this.get(current); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index b44dd3238..87ada53c9 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -444,7 +444,7 @@ const questionsLogin = [ { name: 'Login to a different account', value: 'login' }, { name: 'Change to a different existed account', value: 'select' } ], - when: () => globalConfig.getCurrentLogin() !== '' + when: () => globalConfig.getCurrentSession() !== '' }, { type: "input", @@ -476,8 +476,8 @@ const questionsLogin = [ name: "accountId", message: "Select an account to use", choices() { - const logins = globalConfig.getLogins(); - const current = globalConfig.getCurrentLogin(); + const logins = globalConfig.getSessions(); + const current = globalConfig.getCurrentSession(); const data = []; @@ -538,8 +538,8 @@ const questionsLogout = [ message: "Select accounts to logout from", validate: (value) => validateRequired('account', value), choices() { - const logins = globalConfig.getLogins(); - const current = globalConfig.getCurrentLogin(); + const logins = globalConfig.getSessions(); + const current = globalConfig.getCurrentSession(); const data = []; From 4641f61f65fa49874ff5f8595362bb019044d18f Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:02:16 -0400 Subject: [PATCH 86/89] refactor: removing redundant exports and questions. --- templates/cli/lib/commands/generic.js.twig | 2 +- templates/cli/lib/questions.js.twig | 10 ---------- templates/cli/lib/sdks.js.twig | 1 - 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 538d2d695..ebf737d61 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -1,7 +1,7 @@ const inquirer = require("inquirer"); const { Command } = require("commander"); const Client = require("../client"); -const { sdkForConsole, questionGetEndpoint } = require("../sdks"); +const { sdkForConsole } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, error, parse, log, drawTable } = require("../parser"); const ID = require("../id"); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 87ada53c9..367075210 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -523,14 +523,6 @@ const questionGetEndpoint = [ } ]; -const questionLoginWithEndpoint = [ - questionsLogin[0], - { ...questionGetEndpoint[0], when: (answers) => answers.method !== 'select' }, - questionsLogin[1], - questionsLogin[2], - questionsLogin[3] -] - const questionsLogout = [ { type: "checkbox", @@ -799,6 +791,4 @@ module.exports = { questionsGetEntrypoint, questionsListFactors, questionsMfaChallenge, - questionGetEndpoint, - questionLoginWithEndpoint }; diff --git a/templates/cli/lib/sdks.js.twig b/templates/cli/lib/sdks.js.twig index 435375894..2b6e15e13 100644 --- a/templates/cli/lib/sdks.js.twig +++ b/templates/cli/lib/sdks.js.twig @@ -99,5 +99,4 @@ const sdkForProject = async () => { module.exports = { sdkForConsole, sdkForProject, - questionGetEndpoint, }; From 08608671ccedc7e02ce7c35d6c36c2941de0dc2b Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:19:30 -0400 Subject: [PATCH 87/89] refactor: renaming login to sessions --- templates/cli/index.js.twig | 2 +- templates/cli/lib/commands/generic.js.twig | 18 +++++++------- templates/cli/lib/config.js.twig | 20 ++++++++-------- templates/cli/lib/questions.js.twig | 28 +++++++++++----------- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index ba383d947..b3de4e6ef 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,7 +12,7 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); const inquirer = require("inquirer"); {% if sdk.test != "true" %} -const { login, logout, whoami, migrate } = require("./lib/commands/generic"); +const { login, logout, whoami } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index ebf737d61..b7d092733 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -20,8 +20,8 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { if (answers.method === 'select') { const accountId = answers.accountId; - if (!globalConfig.getLoginIds().includes(accountId)) { - throw Error('Login ID not found'); + if (!globalConfig.getSessionIds().includes(accountId)) { + throw Error('Session ID not found'); } globalConfig.setCurrentSession(accountId); @@ -164,13 +164,13 @@ const logout = new Command("logout") helpWidth: process.stdout.columns || 80 }) .action(actionRunner(async () => { - const logins = globalConfig.getSessions(); + const sessions = globalConfig.getSessions(); const current = globalConfig.getCurrentSession(); if (current === '') { return; } - if (logins.length === 1) { + if (sessions.length === 1) { await deleteSession(current); success(); @@ -186,10 +186,10 @@ const logout = new Command("logout") } } - const leftLogins = globalConfig.getSessions(); + const leftSessions = globalConfig.getSessions(); - if (leftLogins.length > 0 && leftLogins.filter(login => login.id === current).length !== 1) { - const accountId = leftLogins[0].id; + if (leftSessions.length > 0 && leftSessions.filter(session => session.id === current).length !== 1) { + const accountId = leftSessions[0].id; globalConfig.setCurrentSession(accountId); success(`Current account is ${accountId}`); @@ -264,9 +264,9 @@ const client = new Command("client") } if (reset !== undefined) { - const logins = globalConfig.getSessions(); + const sessions = globalConfig.getSessions(); - for (let accountId of logins.map(login => login.id)) { + for (let accountId of sessions.map(session => session.id)) { globalConfig.setCurrentSession(accountId); await deleteSession(accountId); } diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 7cfa4eb03..73b654bc7 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -418,29 +418,29 @@ class Global extends Config { this.set(Global.PREFERENCE_CURRENT, endpoint); } - getLoginIds() { + getSessionIds() { return Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)); } getSessions() { - const logins = Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)) + const sessions = Object.keys(this.data).filter((key) => !Global.IGNORE_ATTRIBUTES.includes(key)) - return logins.map((login) => { + return sessions.map((session) => { return { - id: login, - endpoint: this.data[login][Global.PREFERENCE_ENDPOINT], - email: this.data[login][Global.PREFERENCE_EMAIL] + id: session, + endpoint: this.data[session][Global.PREFERENCE_ENDPOINT], + email: this.data[session][Global.PREFERENCE_EMAIL] } }) } - addSession(login, data) { - this.set(login, data); + addSession(session, data) { + this.set(session, data); } - removeSession(login, data) { - this.delete(login); + removeSession(session) { + this.delete(session); } getEmail() { diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 367075210..6798af539 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -476,19 +476,19 @@ const questionsLogin = [ name: "accountId", message: "Select an account to use", choices() { - const logins = globalConfig.getSessions(); + const sessions = globalConfig.getSessions(); const current = globalConfig.getCurrentSession(); const data = []; - const longestEmail = logins.reduce((prev, current) => (prev && (prev.email ?? '').length > (current.email ?? '').length) ? prev : current).email.length; + const longestEmail = sessions.reduce((prev, current) => (prev && (prev.email ?? '').length > (current.email ?? '').length) ? prev : current).email.length; - logins.forEach((login) => { - if (login.email) { + sessions.forEach((session) => { + if (session.email) { data.push({ - current: current === login.id, - value: login.id, - name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('current') : ' '.repeat(6)} ${login.endpoint}`, + current: current === session.id, + value: session.id, + name: `${session.email.padEnd(longestEmail)} ${current === session.id ? chalk.green.bold('current') : ' '.repeat(6)} ${session.endpoint}`, }); } }) @@ -530,19 +530,19 @@ const questionsLogout = [ message: "Select accounts to logout from", validate: (value) => validateRequired('account', value), choices() { - const logins = globalConfig.getSessions(); + const sessions = globalConfig.getSessions(); const current = globalConfig.getCurrentSession(); const data = []; - const longestEmail = logins.reduce((prev, current) => (prev && (prev.email ?? '').length > (current.email ?? '').length) ? prev : current).email.length; + const longestEmail = sessions.reduce((prev, current) => (prev && (prev.email ?? '').length > (current.email ?? '').length) ? prev : current).email.length; - logins.forEach((login) => { - if (login.email) { + sessions.forEach((session) => { + if (session.email) { data.push({ - current: current === login.id, - value: login.id, - name: `${login.email.padEnd(longestEmail)} ${current === login.id ? chalk.green.bold('current') : ' '.repeat(6)} ${login.endpoint}`, + current: current === session.id, + value: session.id, + name: `${session.email.padEnd(longestEmail)} ${current === session.id ? chalk.green.bold('current') : ' '.repeat(6)} ${session.endpoint}`, }); } }) From 0e8c12923a57b496f6bf73bb8f35933e71d4ec67 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:43:17 -0400 Subject: [PATCH 88/89] refactor: reviews --- templates/cli/base/requests/api.twig | 2 +- templates/cli/lib/commands/command.js.twig | 4 ++-- templates/cli/lib/commands/generic.js.twig | 6 +++--- templates/cli/lib/utils.js.twig | 4 +--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/templates/cli/base/requests/api.twig b/templates/cli/base/requests/api.twig index 8e0c5116d..e228765f8 100644 --- a/templates/cli/base/requests/api.twig +++ b/templates/cli/base/requests/api.twig @@ -17,7 +17,7 @@ fs.writeFileSync(destination, response); {%~ endif %} if (parseOutput) { - {%~ if methodHaveConsolePreview(method.name,service.name) %} + {%~ if hasConsolePreview(method.name,service.name) %} if(console) { showConsoleLink('{{service.name}}', '{{ method.name }}' {%- for parameter in method.parameters.path -%}{%- set param = (parameter.name | caseCamel | escapeKeyword) -%}{%- if param ends with 'Id' -%}, {{ param }} {%- endif -%}{%- endfor -%} diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 6fcb90f43..6dcdf7842 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -70,7 +70,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%} {%- if method.type == 'location' -%}, destination{%- endif -%} - {% if methodHaveConsolePreview(method.name,service.name) %}, console{%- endif -%} + {% if hasConsolePreview(method.name,service.name) %}, console{%- endif -%} }) => { {%~ endblock %} let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : @@ -95,7 +95,7 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {% if method.type == 'location' %} .requiredOption(`--destination `, `output file path.`) {% endif %} -{% if methodHaveConsolePreview(method.name,service.name) %} +{% if hasConsolePreview(method.name,service.name) %} .option(`--console`, `Get the resource console url`) {% endif %} {% endautoescape %} diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index b7d092733..752a1aa23 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -186,10 +186,10 @@ const logout = new Command("logout") } } - const leftSessions = globalConfig.getSessions(); + const remainingSessions = globalConfig.getSessions(); - if (leftSessions.length > 0 && leftSessions.filter(session => session.id === current).length !== 1) { - const accountId = leftSessions[0].id; + if (remainingSessions .length > 0 && remainingSessions .filter(session => session.id === current).length !== 1) { + const accountId = remainingSessions [0].id; globalConfig.setCurrentSession(accountId); success(`Current account is ${accountId}`); diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index e56382503..a6681104e 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -1,9 +1,7 @@ const fs = require("fs"); const path = require("path"); const { localConfig, globalConfig } = require("./config"); -const { success, log, error } = require('./parser') -const readline = require('readline'); -const cp = require('child_process'); +const { success } = require('./parser') function getAllFiles(folder) { const files = []; From 169bb863bc8fc29e923eccc65c6f5f1d9ed2cdad Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:49:36 -0400 Subject: [PATCH 89/89] refactor: reviews --- src/SDK/Language/CLI.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 22a1a6d18..ccb53f381 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -357,7 +357,7 @@ public function getFunctions(): array { return [ /** Return true if the entered service->method is enabled for a console preview link */ - new TwigFunction('methodHaveConsolePreview', fn($method, $service) => preg_match('/^([Gg]et|[Ll]ist)/', $method) + new TwigFunction('hasConsolePreview', fn($method, $service) => preg_match('/^([Gg]et|[Ll]ist)/', $method) && !in_array(strtolower($method), $this->consoleIgnoreFunctions) && !in_array($service, $this->consoleIgnoreServices)), ];