From 897a4c2c83e869f8925d36364bb4361af177d79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 13 May 2024 14:44:46 +0000 Subject: [PATCH 001/198] Use ID instead of name for folder name in CLI init function --- templates/cli/lib/commands/init.js.twig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 4e4f1a39f..d8dffd662 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -63,10 +63,10 @@ const initFunction = async () => { }); } - const functionDir = path.join(functionFolder, answers.name); + const functionDir = path.join(functionFolder, answers.id); if (fs.existsSync(functionDir)) { - throw new Error(`( ${answers.name} ) already exists in the current directory. Please choose another name.`); + throw new Error(`( ${answers.id} ) already exists in the current directory. Please choose another name.`); } if (!answers.runtime.entrypoint) { @@ -134,7 +134,7 @@ const initFunction = async () => { fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); - const readmePath = path.join(process.cwd(), 'functions', answers.name, 'README.md'); + const readmePath = path.join(process.cwd(), 'functions', answers.id, 'README.md'); const readmeFile = fs.readFileSync(readmePath).toString(); const newReadmeFile = readmeFile.split('\n'); newReadmeFile[0] = `# ${answers.name}`; @@ -154,7 +154,7 @@ const initFunction = async () => { entrypoint: response.entrypoint, commands: response.commands, ignore: answers.runtime.ignore || null, - path: `functions/${answers.name}`, + path: `functions/${answers.id}`, }; localConfig.addFunction(data); From 5c5dc1949acb23eb385c80c242f96800bb4dbd72 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 13 May 2024 18:44:22 -0400 Subject: [PATCH 002/198] feat: Adding unique id generation --- templates/cli/lib/id.js.twig | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 templates/cli/lib/id.js.twig diff --git a/templates/cli/lib/id.js.twig b/templates/cli/lib/id.js.twig new file mode 100644 index 000000000..b628463da --- /dev/null +++ b/templates/cli/lib/id.js.twig @@ -0,0 +1,30 @@ +class ID { + // Generate an hex ID based on timestamp + // Recreated from https://www.php.net/manual/en/function.uniqid.php + static #hexTimestamp() { + const now = new Date(); + const sec = Math.floor(now.getTime() / 1000); + const msec = now.getMilliseconds(); + + // Convert to hexadecimal + const hexTimestamp = sec.toString(16) + msec.toString(16).padStart(5, '0'); + return hexTimestamp; + } + + static custom(id) { + return id + } + + static unique(padding = 7) { + // Generate a unique ID with padding to have a longer ID + const baseId = ID.#hexTimestamp(); + let randomPadding = ''; + for (let i = 0; i < padding; i++) { + const randomHexDigit = Math.floor(Math.random() * 16).toString(16); + randomPadding += randomHexDigit; + } + return baseId + randomPadding; + } +} + +module.exports = ID; From 7d6b9bf057fbe25f0fa2292c7a5dcc6839fc11a6 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 13 May 2024 18:44:33 -0400 Subject: [PATCH 003/198] feat: Adding unique id generation --- src/SDK/Language/CLI.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 3bd0d77a4..158f5e119 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -152,6 +152,11 @@ public function getFiles(): array 'destination' => 'lib/client.js', 'template' => 'cli/lib/client.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/id.js', + 'template' => 'cli/lib/id.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/utils.js', From ed4089a7f9caee51eca2250d8e1d18296eb29a5d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 13 May 2024 18:45:09 -0400 Subject: [PATCH 004/198] feat: Setting function folder name as unique id or custom --- templates/cli/lib/commands/init.js.twig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index d8dffd662..69fad95de 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -10,6 +10,7 @@ const { databasesGet, databasesListCollections, databasesList } = require("./dat const { storageListBuckets } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); +const ID = require("../id"); const { paginate } = require("../paginate"); const { questionsInitProject, questionsInitFunction, questionsInitCollection } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); @@ -63,10 +64,11 @@ const initFunction = async () => { }); } - const functionDir = path.join(functionFolder, answers.id); + const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; + const functionDir = path.join(functionFolder, functionId); if (fs.existsSync(functionDir)) { - throw new Error(`( ${answers.id} ) already exists in the current directory. Please choose another name.`); + throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); } if (!answers.runtime.entrypoint) { @@ -78,7 +80,7 @@ const initFunction = async () => { } let response = await functionsCreate({ - functionId: answers.id, + functionId, name: answers.name, runtime: answers.runtime.id, entrypoint: answers.runtime.entrypoint || '', @@ -134,7 +136,7 @@ const initFunction = async () => { fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); - const readmePath = path.join(process.cwd(), 'functions', answers.id, 'README.md'); + const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); const readmeFile = fs.readFileSync(readmePath).toString(); const newReadmeFile = readmeFile.split('\n'); newReadmeFile[0] = `# ${answers.name}`; @@ -154,7 +156,7 @@ const initFunction = async () => { entrypoint: response.entrypoint, commands: response.commands, ignore: answers.runtime.ignore || null, - path: `functions/${answers.id}`, + path: `functions/${functionId}`, }; localConfig.addFunction(data); From e88fcaa7b84d56b3e522e91c94c1619133bc7c53 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 15 May 2024 13:52:02 -0400 Subject: [PATCH 005/198] feat: Adding validating for deployment to make choice required. --- templates/cli/lib/questions.js.twig | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index dd79745ed..4efc9ab9e 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -107,6 +107,13 @@ const getInstallCommand = (runtime) => { return undefined; }; +const deployValidate = (singular, items) => { + if (items.length <= 0) { + return `Please select at least one ${singular}`; + } + return true; +} + const questionsInitProject = [ { type: "confirm", @@ -209,12 +216,12 @@ const questionsInitFunction = [ parseOutput: false }) let runtimes = response["runtimes"] - let choices = runtimes.map((runtime, idx) => { + let choices = runtimes.map((runtime, idx) => { return { name: `${runtime.name} (${runtime['$id']})`, - value: { - id: runtime['$id'], - entrypoint: getEntrypoint(runtime['$id']), + value: { + id: runtime['$id'], + entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), commands : getInstallCommand(runtime['$id']) }, @@ -281,6 +288,7 @@ const questionsDeployFunctions = [ type: "checkbox", name: "functions", message: "Which functions would you like to deploy?", + validate: (i) => deployValidate('function', i), choices: () => { let functions = localConfig.getFunctions(); if (functions.length === 0) { @@ -307,6 +315,7 @@ const questionsDeployCollections = [ type: "checkbox", name: "collections", message: "Which collections would you like to deploy?", + validate: (i) => deployValidate('collection', i), choices: () => { let collections = localConfig.getCollections(); if (collections.length === 0) { @@ -332,6 +341,7 @@ const questionsDeployBuckets = [ type: "checkbox", name: "buckets", message: "Which buckets would you like to deploy?", + validate: (i) => deployValidate('bucket', i), choices: () => { let buckets = localConfig.getBuckets(); if (buckets.length === 0) { @@ -372,6 +382,7 @@ const questionsDeployTeams = [ type: "checkbox", name: "teams", message: "Which teams would you like to deploy?", + validate: (i) => deployValidate('team', i), choices: () => { let teams = localConfig.getTeams(); if (teams.length === 0) { @@ -403,7 +414,7 @@ const questionsListFactors = [ sdk: client, parseOutput: false }); - + const choices = [ { name: `TOTP (Time-based One-time Password)`, From 609580862407c721967fe19b40330e301e3105b9 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 15 May 2024 15:55:40 -0400 Subject: [PATCH 006/198] refactor: Renaming init to pull and deploy to push --- src/SDK/Language/CLI.php | 8 +- templates/cli/index.js.twig | 12 +- .../commands/{init.js.twig => pull.js.twig} | 60 +++++----- .../commands/{deploy.js.twig => push.js.twig} | 112 +++++++++--------- templates/cli/lib/parser.js.twig | 6 +- templates/cli/lib/questions.js.twig | 54 ++++----- templates/cli/lib/sdks.js.twig | 2 +- 7 files changed, 127 insertions(+), 127 deletions(-) rename templates/cli/lib/commands/{init.js.twig => pull.js.twig} (85%) rename templates/cli/lib/commands/{deploy.js.twig => push.js.twig} (90%) diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 3bd0d77a4..9aaa63aaa 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -159,13 +159,13 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'lib/commands/init.js', - 'template' => 'cli/lib/commands/init.js.twig', + 'destination' => 'lib/commands/pull.js', + 'template' => 'cli/lib/commands/pull.js.twig', ], [ 'scope' => 'default', - 'destination' => 'lib/commands/deploy.js', - 'template' => 'cli/lib/commands/deploy.js.twig', + 'destination' => 'lib/commands/push.js', + 'template' => 'cli/lib/commands/push.js.twig', ], [ 'scope' => 'service', diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index d4415db72..47725d266 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,8 +12,8 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} const { login, logout } = require("./lib/commands/generic"); -const { init } = require("./lib/commands/init"); -const { deploy } = require("./lib/commands/deploy"); +const { pull } = require("./lib/commands/pull"); +const { push } = require("./lib/commands/push"); {% endif %} {% for service in spec.services %} const { {{ service.name | caseLower }} } = require("./lib/commands/{{ service.name | caseLower }}"); @@ -37,8 +37,8 @@ program .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(login) - .addCommand(init) - .addCommand(deploy) + .addCommand(pull) + .addCommand(push) .addCommand(logout) {% endif %} {% for service in spec.services %} @@ -46,5 +46,5 @@ program {% endfor %} .addCommand(client) .parse(process.argv); - -process.stdout.columns = oldWidth; \ No newline at end of file + +process.stdout.columns = oldWidth; diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/pull.js.twig similarity index 85% rename from templates/cli/lib/commands/init.js.twig rename to templates/cli/lib/commands/pull.js.twig index 4e4f1a39f..1862a3b27 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -11,11 +11,11 @@ const { storageListBuckets } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); const { paginate } = require("../paginate"); -const { questionsInitProject, questionsInitFunction, questionsInitCollection } = require("../questions"); +const { questionsPullProject, questionsPullFunction, questionsPullCollection } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); -const init = new Command("init") - .description(commandDescriptions['init']) +const pull = new Command("pull") + .description(commandDescriptions['pull']) .configureHelp({ helpWidth: process.stdout.columns || 80 }) @@ -23,9 +23,9 @@ const init = new Command("init") command.help(); })); -const initProject = async () => { +const pullProject = async () => { let response = {} - const answers = await inquirer.prompt(questionsInitProject) + const answers = await inquirer.prompt(questionsPullProject) if (!answers.project) process.exit(1) let sdk = await sdkForConsole(); @@ -52,9 +52,9 @@ const initProject = async () => { success(); } -const initFunction = async () => { +const pullFunction = async () => { // TODO: Add CI/CD support (ID, name, runtime) - const answers = await inquirer.prompt(questionsInitFunction) + const answers = await inquirer.prompt(questionsPullFunction) const functionFolder = path.join(process.cwd(), 'functions'); if (!fs.existsSync(functionFolder)) { @@ -70,11 +70,11 @@ const initFunction = async () => { } if (!answers.runtime.entrypoint) { - log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first deploy the function.`); + log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first push the function.`); } if (!answers.runtime.commands) { - log(`Installation command for this runtime not found. You will be asked to configure the install command when you first deploy the function.`); + 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({ @@ -161,7 +161,7 @@ const initFunction = async () => { success(); } -const initCollection = async ({ all, databaseId } = {}) => { +const pullCollection = async ({ all, databaseId } = {}) => { const databaseIds = []; if (databaseId) { @@ -175,7 +175,7 @@ const initCollection = async ({ all, databaseId } = {}) => { } if (databaseIds.length <= 0) { - let answers = await inquirer.prompt(questionsInitCollection) + let answers = await inquirer.prompt(questionsPullCollection) if (!answers.databases) process.exit(1) databaseIds.push(...answers.databases); } @@ -208,7 +208,7 @@ const initCollection = async ({ all, databaseId } = {}) => { success(); } -const initBucket = async () => { +const pullBucket = async () => { const { buckets } = await paginate(storageListBuckets, { parseOutput: false }, 100, 'buckets'); log(`Found ${buckets.length} buckets`); @@ -221,7 +221,7 @@ const initBucket = async () => { success(); } -const initTeam = async () => { +const pullTeam = async () => { const { teams } = await paginate(teamsList, { parseOutput: false }, 100, 'teams'); log(`Found ${teams.length} teams`); @@ -235,33 +235,33 @@ const initTeam = async () => { success(); } -init +pull .command("project") - .description("Initialise your {{ spec.title|caseUcfirst }} project") - .action(actionRunner(initProject)); + .description("Pulling your {{ spec.title|caseUcfirst }} project") + .action(actionRunner(pullProject)); -init +pull .command("function") - .description("Initialise your {{ spec.title|caseUcfirst }} cloud function") - .action(actionRunner(initFunction)) + .description("Pulling your {{ spec.title|caseUcfirst }} cloud function") + .action(actionRunner(pullFunction)) -init +pull .command("collection") - .description("Initialise your {{ spec.title|caseUcfirst }} collections") + .description("Pulling your {{ spec.title|caseUcfirst }} collections") .option(`--databaseId `, `Database ID`) - .option(`--all`, `Flag to initialize all databases`) - .action(actionRunner(initCollection)) + .option(`--all`, `Flag to pullialize all databases`) + .action(actionRunner(pullCollection)) -init +pull .command("bucket") - .description("Initialise your Appwrite buckets") - .action(actionRunner(initBucket)) + .description("Pulling your Appwrite buckets") + .action(actionRunner(pullBucket)) -init +pull .command("team") - .description("Initialise your Appwrite teams") - .action(actionRunner(initTeam)) + .description("Pulling your Appwrite teams") + .action(actionRunner(pullTeam)) module.exports = { - init, + pull, }; diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/push.js.twig similarity index 90% rename from templates/cli/lib/commands/deploy.js.twig rename to templates/cli/lib/commands/push.js.twig index ea30d1ba9..d15053bdb 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -3,7 +3,7 @@ const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); const { paginate } = require('../paginate'); -const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); +const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { @@ -227,8 +227,8 @@ const awaitPools = { }, } -const deploy = new Command("deploy") - .description(commandDescriptions['deploy']) +const push = new Command("push") + .description(commandDescriptions['push']) .configureHelp({ helpWidth: process.stdout.columns || 80 }) @@ -236,7 +236,7 @@ const deploy = new Command("deploy") command.help() })); -const deployFunction = async ({ functionId, all, yes } = {}) => { +const pushFunction = async ({ functionId, all, yes } = {}) => { let response = {}; const functionIds = []; @@ -254,7 +254,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { } if (functionIds.length <= 0) { - const answers = await inquirer.prompt(questionsDeployFunctions[0]); + const answers = await inquirer.prompt(questionsPushFunctions[0]); functionIds.push(...answers.functions); } @@ -270,7 +270,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { }); for (let func of functions) { - log(`Deploying function ${func.name} ( ${func['$id']} )`) + log(`Pushing function ${func.name} ( ${func['$id']} )`) try { response = await functionsGet({ @@ -335,25 +335,25 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }); - let deployVariables = yes; + let pushVariables = yes; if (total === 0) { - deployVariables = true; + pushVariables = true; } else if (total > 0 && !yes) { - const variableAnswers = await inquirer.prompt(questionsDeployFunctions[1]) - deployVariables = variableAnswers.override.toLowerCase() === "yes"; + const variableAnswers = await inquirer.prompt(questionsPushFunctions[1]) + pushVariables = variableAnswers.override.toLowerCase() === "yes"; } - if (!deployVariables) { + if (!pushVariables) { log(`Skipping variables for ${func.name} ( ${func['$id']} )`); } else { - log(`Deploying variables for ${func.name} ( ${func['$id']} )`); + log(`Pushing variables for ${func.name} ( ${func['$id']} )`); const { variables } = await paginate(functionsListVariables, { functionId: func['$id'], parseOutput: false }, 100, 'variables'); - + await Promise.all(variables.map(async variable => { await functionsDeleteVariable({ functionId: func['$id'], @@ -361,13 +361,13 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }); })); - + let result = await awaitPools.wipeVariables(func['$id']); if (!result) { throw new Error("Variable deletion timed out."); } - // Deploy local variables + // Push local variables await Promise.all(Object.keys(func.variables).map(async localVariableKey => { await functionsCreateVariable({ functionId: func['$id'], @@ -396,7 +396,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }) - success(`Deployed ${func.name} ( ${func['$id']} )`); + success(`Pushed ${func.name} ( ${func['$id']} )`); } catch (e) { switch (e.code) { @@ -409,7 +409,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { } } - success(`Deployed ${functions.length} functions`); + success(`Pushed ${functions.length} functions`); } const createAttribute = async (databaseId, collectionId, attribute) => { @@ -529,18 +529,18 @@ const createAttribute = async (databaseId, collectionId, attribute) => { } } -const deployCollection = async ({ all, yes } = {}) => { +const pushCollection = async ({ all, yes } = {}) => { let response = {}; const collections = []; if (all) { if (localConfig.getCollections().length === 0) { - throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} init collection` to fetch all your collections."); + 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(questionsDeployCollections[0]) + const answers = await inquirer.prompt(questionsPushCollections[0]) const configCollections = new Map(); localConfig.getCollections().forEach((c) => { configCollections.set(`${c['databaseId']}|${c['$id']}`, c); @@ -552,7 +552,7 @@ const deployCollection = async ({ all, yes } = {}) => { } for (let collection of collections) { - log(`Deploying collection ${collection.name} ( ${collection['databaseId']} - ${collection['$id']} )`) + log(`Pushing collection ${collection.name} ( ${collection['databaseId']} - ${collection['$id']} )`) let databaseId; @@ -597,7 +597,7 @@ const deployCollection = async ({ all, yes } = {}) => { log(`Collection ${collection.name} ( ${collection['$id']} ) already exists.`); if (!yes) { - const answers = await inquirer.prompt(questionsDeployCollections[1]) + const answers = await inquirer.prompt(questionsPushCollections[1]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${collection.name} ( ${collection['$id']} )`); continue; @@ -717,7 +717,7 @@ const deployCollection = async ({ all, yes } = {}) => { success(`Created ${collection.indexes.length} indexes`); - success(`Deployed ${collection.name} ( ${collection['$id']} )`); + success(`Pushed ${collection.name} ( ${collection['$id']} )`); } // Create the relationship attributes @@ -730,7 +730,7 @@ const deployCollection = async ({ all, yes } = {}) => { continue; } - log(`Deploying relationships for collection ${collection.name} ( ${collection['$id']} )`); + log(`Pushing relationships for collection ${collection.name} ( ${collection['$id']} )`); await Promise.all(relationships.map(attribute => { return createAttribute(collection['databaseId'], collection['$id'], attribute); @@ -750,7 +750,7 @@ const deployCollection = async ({ all, yes } = {}) => { } } -const deployBucket = async ({ all, yes } = {}) => { +const pushBucket = async ({ all, yes } = {}) => { let response = {}; let bucketIds = []; @@ -758,13 +758,13 @@ const deployBucket = async ({ all, yes } = {}) => { if (all) { if (configBuckets.length === 0) { - throw new Error("No buckets found in the current directory. Run `appwrite init bucket` to fetch all your buckets."); + 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(questionsDeployBuckets[0]) + const answers = await inquirer.prompt(questionsPushBuckets[0]) bucketIds.push(...answers.buckets); } @@ -776,7 +776,7 @@ const deployBucket = async ({ all, yes } = {}) => { } for (let bucket of buckets) { - log(`Deploying bucket ${bucket.name} ( ${bucket['$id']} )`) + log(`Pushing bucket ${bucket.name} ( ${bucket['$id']} )`) try { response = await storageGetBucket({ @@ -786,7 +786,7 @@ const deployBucket = async ({ all, yes } = {}) => { log(`Bucket ${bucket.name} ( ${bucket['$id']} ) already exists.`); if (!yes) { - const answers = await inquirer.prompt(questionsDeployBuckets[1]) + const answers = await inquirer.prompt(questionsPushBuckets[1]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${bucket.name} ( ${bucket['$id']} )`); continue; @@ -810,7 +810,7 @@ const deployBucket = async ({ all, yes } = {}) => { parseOutput: false }); - success(`Deployed ${bucket.name} ( ${bucket['$id']} )`); + success(`Pushed ${bucket.name} ( ${bucket['$id']} )`); } catch (e) { if (e.code == 404) { log(`Bucket ${bucket.name} does not exist in the project. Creating ... `); @@ -829,7 +829,7 @@ const deployBucket = async ({ all, yes } = {}) => { parseOutput: false }) - success(`Deployed ${bucket.name} ( ${bucket['$id']} )`); + success(`Pushed ${bucket.name} ( ${bucket['$id']} )`); } else { throw e; } @@ -837,7 +837,7 @@ const deployBucket = async ({ all, yes } = {}) => { } } -const deployTeam = async ({ all, yes } = {}) => { +const pushTeam = async ({ all, yes } = {}) => { let response = {}; let teamIds = []; @@ -845,13 +845,13 @@ const deployTeam = async ({ all, yes } = {}) => { if (all) { if (configTeams.length === 0) { - throw new Error("No teams found in the current directory. Run `appwrite init team` to fetch all your teams."); + 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(questionsDeployTeams[0]) + const answers = await inquirer.prompt(questionsPushTeams[0]) teamIds.push(...answers.teams); } @@ -863,7 +863,7 @@ const deployTeam = async ({ all, yes } = {}) => { } for (let team of teams) { - log(`Deploying team ${team.name} ( ${team['$id']} )`) + log(`Pushing team ${team.name} ( ${team['$id']} )`) try { response = await teamsGet({ @@ -873,7 +873,7 @@ const deployTeam = async ({ all, yes } = {}) => { log(`Team ${team.name} ( ${team['$id']} ) already exists.`); if (!yes) { - const answers = await inquirer.prompt(questionsDeployTeams[1]) + const answers = await inquirer.prompt(questionsPushTeams[1]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${team.name} ( ${team['$id']} )`); continue; @@ -888,7 +888,7 @@ const deployTeam = async ({ all, yes } = {}) => { parseOutput: false }); - success(`Deployed ${team.name} ( ${team['$id']} )`); + success(`Pushed ${team.name} ( ${team['$id']} )`); } catch (e) { if (e.code == 404) { log(`Team ${team.name} does not exist in the project. Creating ... `); @@ -899,7 +899,7 @@ const deployTeam = async ({ all, yes } = {}) => { parseOutput: false }) - success(`Deployed ${team.name} ( ${team['$id']} )`); + success(`Pushed ${team.name} ( ${team['$id']} )`); } else { throw e; } @@ -907,35 +907,35 @@ const deployTeam = async ({ all, yes } = {}) => { } } -deploy +push .command("function") - .description("Deploy functions in the current directory.") + .description("Push functions in the current directory.") .option(`--functionId `, `Function ID`) - .option(`--all`, `Flag to deploy all functions`) + .option(`--all`, `Flag to push all functions`) .option(`--yes`, `Flag to confirm all warnings`) - .action(actionRunner(deployFunction)); + .action(actionRunner(pushFunction)); -deploy +push .command("collection") - .description("Deploy collections in the current project.") - .option(`--all`, `Flag to deploy all collections`) + .description("Push collections in the current project.") + .option(`--all`, `Flag to push all collections`) .option(`--yes`, `Flag to confirm all warnings`) - .action(actionRunner(deployCollection)); + .action(actionRunner(pushCollection)); -deploy +push .command("bucket") - .description("Deploy buckets in the current project.") - .option(`--all`, `Flag to deploy all buckets`) + .description("Push buckets in the current project.") + .option(`--all`, `Flag to push all buckets`) .option(`--yes`, `Flag to confirm all warnings`) - .action(actionRunner(deployBucket)); + .action(actionRunner(pushBucket)); -deploy +push .command("team") - .description("Deploy teams in the current project.") - .option(`--all`, `Flag to deploy all teams`) + .description("Push teams in the current project.") + .option(`--all`, `Flag to push all teams`) .option(`--yes`, `Flag to confirm all warnings`) - .action(actionRunner(deployTeam)); + .action(actionRunner(pushTeam)); module.exports = { - deploy -} \ No newline at end of file + push +} diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 60b3e9c4d..5dde5aa3e 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -156,10 +156,10 @@ const commandDescriptions = { "graphql": `The graphql command allows you to query and mutate any resource type on your Appwrite server.`, "avatars": `The avatars command aims to help you complete everyday tasks related to your app image, icons, and avatars.`, "databases": `The databases command allows you to create structured collections of documents, query and filter lists of documents.`, - "deploy": `The deploy command provides a convenient wrapper for deploying your functions and collections.`, + "push": `The push command provides a convenient wrapper for pushing your functions, collections, buckets, teams and messaging.`, "functions": `The functions command allows you view, create and manage your Cloud Functions.`, "health": `The health command allows you to both validate and monitor your {{ spec.title|caseUcfirst }} server's health.`, - "init": `The init command helps you initialize your {{ spec.title|caseUcfirst }} project, functions and collections`, + "pull": `The pull command helps you pull your {{ spec.title|caseUcfirst }} project, functions, collections, buckets, teams and messaging`, "locale": `The locale command allows you to customize your app based on your users' location.`, "projects": `The projects command allows you to view, create and manage your {{ spec.title|caseUcfirst }} projects.`, "storage": `The storage command allows you to manage your project files.`, @@ -193,4 +193,4 @@ module.exports = { error, commandDescriptions, cliConfig -} \ No newline at end of file +} diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index dd79745ed..84ab73a24 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -107,7 +107,7 @@ const getInstallCommand = (runtime) => { return undefined; }; -const questionsInitProject = [ +const questionsPullProject = [ { type: "confirm", name: "override", @@ -187,7 +187,7 @@ const questionsInitProject = [ } ]; -const questionsInitFunction = [ +const questionsPullFunction = [ { type: "input", name: "name", @@ -209,12 +209,12 @@ const questionsInitFunction = [ parseOutput: false }) let runtimes = response["runtimes"] - let choices = runtimes.map((runtime, idx) => { + let choices = runtimes.map((runtime, idx) => { return { name: `${runtime.name} (${runtime['$id']})`, - value: { - id: runtime['$id'], - entrypoint: getEntrypoint(runtime['$id']), + value: { + id: runtime['$id'], + entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), commands : getInstallCommand(runtime['$id']) }, @@ -225,11 +225,11 @@ const questionsInitFunction = [ } ]; -const questionsInitCollection = [ +const questionsPullCollection = [ { type: "checkbox", name: "databases", - message: "From which database would you like to init collections?", + message: "From which database would you like to pull collections?", choices: async () => { let response = await databasesList({ parseOutput: false @@ -276,11 +276,11 @@ const questionsLogin = [ }, ]; -const questionsDeployFunctions = [ +const questionsPushFunctions = [ { type: "checkbox", name: "functions", - message: "Which functions would you like to deploy?", + message: "Which functions would you like to push?", choices: () => { let functions = localConfig.getFunctions(); if (functions.length === 0) { @@ -302,15 +302,15 @@ const questionsDeployFunctions = [ }, ] -const questionsDeployCollections = [ +const questionsPushCollections = [ { type: "checkbox", name: "collections", - message: "Which collections would you like to deploy?", + message: "Which collections would you like to push?", choices: () => { let collections = localConfig.getCollections(); if (collections.length === 0) { - throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} init collection` to fetch all your collections."); + 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 => { return { @@ -327,15 +327,15 @@ const questionsDeployCollections = [ }, ] -const questionsDeployBuckets = [ +const questionsPushBuckets = [ { type: "checkbox", name: "buckets", - message: "Which buckets would you like to deploy?", + message: "Which buckets would you like to push?", choices: () => { let buckets = localConfig.getBuckets(); if (buckets.length === 0) { - throw new Error("No buckets found in the current directory. Run `appwrite init bucket` to fetch all your buckets."); + throw new Error("No buckets found in the current directory. Run `appwrite pull bucket` to fetch all your buckets."); } return buckets.map(bucket => { return { @@ -367,15 +367,15 @@ const questionsGetEntrypoint = [ }, ] -const questionsDeployTeams = [ +const questionsPushTeams = [ { type: "checkbox", name: "teams", - message: "Which teams would you like to deploy?", + message: "Which teams would you like to push?", choices: () => { let teams = localConfig.getTeams(); if (teams.length === 0) { - throw new Error("No teams found in the current directory. Run `appwrite init team` to fetch all your teams."); + throw new Error("No teams found in the current directory. Run `appwrite pull team` to fetch all your teams."); } return teams.map(team => { return { @@ -403,7 +403,7 @@ const questionsListFactors = [ sdk: client, parseOutput: false }); - + const choices = [ { name: `TOTP (Time-based One-time Password)`, @@ -443,14 +443,14 @@ const questionsMfaChallenge = [ ]; module.exports = { - questionsInitProject, + questionsPullProject, questionsLogin, - questionsInitFunction, - questionsInitCollection, - questionsDeployFunctions, - questionsDeployCollections, - questionsDeployBuckets, - questionsDeployTeams, + questionsPullFunction, + questionsPullCollection, + questionsPushFunctions, + questionsPushCollections, + questionsPushBuckets, + questionsPushTeams, questionsGetEntrypoint, questionsListFactors, questionsMfaChallenge diff --git a/templates/cli/lib/sdks.js.twig b/templates/cli/lib/sdks.js.twig index 49b161f11..e3f5f38dc 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 }} init project` to initialize the current directory with an {{ spec.title|caseUcfirst }} 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."); } client From 329acc3202286ee5d6db38ddaa63795f207a5f67 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 15 May 2024 17:01:50 -0400 Subject: [PATCH 007/198] feat: Deploy all resources or single resource at once. --- templates/cli/lib/commands/deploy.js.twig | 42 ++++++++++++++++------- templates/cli/lib/questions.js.twig | 26 +++++++++++--- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index ea30d1ba9..12f22d36b 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -3,7 +3,7 @@ const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); const { paginate } = require('../paginate'); -const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); +const { questionsDeployResources, questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { @@ -227,14 +227,27 @@ const awaitPools = { }, } -const deploy = new Command("deploy") - .description(commandDescriptions['deploy']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }) - .action(actionRunner(async (_options, command) => { - command.help() - })); +const deployResources = async ({ all, yes } = {}) => { + const actions = { + functions: deployFunction, + collections: deployCollection, + buckets: deployBucket, + teams: deployTeam, + messages: new Function() + } + + if (all) { + Object.values(actions).forEach(action => action({ all: true, yes })); + } else { + const answers = await inquirer.prompt(questionsDeployResources[0]); + answers.resources.forEach((resource) => { + const action = actions[resource]; + if (action !== undefined) { + action({ all: true, yes }); + } + }) + } +}; const deployFunction = async ({ functionId, all, yes } = {}) => { let response = {}; @@ -353,7 +366,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { functionId: func['$id'], parseOutput: false }, 100, 'variables'); - + await Promise.all(variables.map(async variable => { await functionsDeleteVariable({ functionId: func['$id'], @@ -361,7 +374,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }); })); - + let result = await awaitPools.wipeVariables(func['$id']); if (!result) { throw new Error("Variable deletion timed out."); @@ -906,6 +919,11 @@ const deployTeam = async ({ all, yes } = {}) => { } } } +const deploy = new Command("deploy") + .description(commandDescriptions['deploy']) + .option(`--all`, `Flag to deploy all resources`) + .option(`--yes`, `Flag to confirm all warnings`) + .action(actionRunner(deployResources)); deploy .command("function") @@ -938,4 +956,4 @@ deploy module.exports = { deploy -} \ No newline at end of file +} diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index dd79745ed..57296539b 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -209,12 +209,12 @@ const questionsInitFunction = [ parseOutput: false }) let runtimes = response["runtimes"] - let choices = runtimes.map((runtime, idx) => { + let choices = runtimes.map((runtime, idx) => { return { name: `${runtime.name} (${runtime['$id']})`, - value: { - id: runtime['$id'], - entrypoint: getEntrypoint(runtime['$id']), + value: { + id: runtime['$id'], + entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), commands : getInstallCommand(runtime['$id']) }, @@ -276,6 +276,21 @@ const questionsLogin = [ }, ]; +const questionsDeployResources = [ + { + type: "checkbox", + name: "resources", + message: "Which resources would you like to deploy?", + choices: [ + { name: 'Functions', value: 'functions' }, + { name: 'Collections', value: 'collections' }, + { name: 'Buckets', value: 'buckets' }, + { name: 'Teams', value: 'teams' }, + { name: 'Messages', value: 'messages' } + ] + } +] + const questionsDeployFunctions = [ { type: "checkbox", @@ -403,7 +418,7 @@ const questionsListFactors = [ sdk: client, parseOutput: false }); - + const choices = [ { name: `TOTP (Time-based One-time Password)`, @@ -447,6 +462,7 @@ module.exports = { questionsLogin, questionsInitFunction, questionsInitCollection, + questionsDeployResources, questionsDeployFunctions, questionsDeployCollections, questionsDeployBuckets, From 5b6063140e4dcef1d8226d085721a208347e88cb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 16 May 2024 17:07:05 -0400 Subject: [PATCH 008/198] feat(cli): Adding messaging init & deploy --- templates/cli/lib/commands/deploy.js.twig | 90 ++++++++++++++++++++++- templates/cli/lib/commands/init.js.twig | 19 +++++ templates/cli/lib/config.js.twig | 39 ++++++++++ templates/cli/lib/questions.js.twig | 36 +++++++-- 4 files changed, 175 insertions(+), 9 deletions(-) diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index ea30d1ba9..358170dc3 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -3,7 +3,7 @@ const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); const { paginate } = require('../paginate'); -const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); +const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsDeployMessagingTopics, questionsConfirmDeployCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { @@ -32,6 +32,9 @@ const { const { storageGetBucket, storageUpdateBucket, storageCreateBucket } = require("./storage"); +const { + messagingGetTopic, messagingUpdateTopic, messagingCreateTopic +} = require("./messaging"); const { teamsGet, teamsUpdate, @@ -353,7 +356,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { functionId: func['$id'], parseOutput: false }, 100, 'variables'); - + await Promise.all(variables.map(async variable => { await functionsDeleteVariable({ functionId: func['$id'], @@ -361,7 +364,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }); })); - + let result = await awaitPools.wipeVariables(func['$id']); if (!result) { throw new Error("Variable deletion timed out."); @@ -907,6 +910,78 @@ const deployTeam = async ({ all, yes } = {}) => { } } +const deployMessagingTopic = async ({ all, yes } = {}) => { + let response = {}; + + let topicsIds = []; + const configTopics = localConfig.getMessagingTopics(); + + if (all) { + if (configTopics.length === 0) { + throw new Error("No topics found in the current directory. Run `appwrite init topics` to fetch all your messaging topics."); + } + topicsIds.push(...configTopics.map((b) => b.$id)); + } + + if (topicsIds.length === 0) { + const answers = await inquirer.prompt(questionsDeployMessagingTopics[0]) + topicsIds.push(...answers.topics); + } + + let topics = []; + + for (const topicId of topicsIds) { + const idTopic = configTopics.filter((b) => b.$id === topicId); + topics.push(...idTopic); + } + + for (let topic of topics) { + log(`Deploying topic ${topic.name} ( ${topic['$id']} )`) + + try { + response = await messagingGetTopic({ + topicId: topic['$id'], + parseOutput: false + }) + log(`Topic ${topic.name} ( ${topic['$id']} ) already exists.`); + + if (!yes) { + const answers = await inquirer.prompt(questionsDeployMessagingTopics[1]) + if (answers.override.toLowerCase() !== "yes") { + log(`Received "${answers.override}". Skipping ${topic.name} ( ${topic['$id']} )`); + continue; + } + } + + log(`Updating Topic ...`) + + await messagingUpdateTopic({ + topicId: topic['$id'], + name: topic.name, + subscribe: topic.subscribe, + parseOutput: false + }); + + success(`Deployed ${topic.name} ( ${topic['$id']} )`); + } catch (e) { + if (e.code == 404) { + log(`Topic ${topic.name} does not exist in the project. Creating ... `); + + response = await messagingCreateTopic({ + topicId: topic['$id'], + name: topic.name, + subscribe: topic.subscribe, + parseOutput: false + }) + + success(`Deployed ${topic.name} ( ${topic['$id']} )`); + } else { + throw e; + } + } + } +} + deploy .command("function") .description("Deploy functions in the current directory.") @@ -936,6 +1011,13 @@ deploy .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(deployTeam)); +deploy + .command("topic") + .description("Deploy messaging topics in the current project.") + .option(`--all`, `Flag to deploy all topics`) + .option(`--yes`, `Flag to confirm all warnings`) + .action(actionRunner(deployMessagingTopic)); + module.exports = { deploy -} \ No newline at end of file +} diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 69fad95de..48bfd1d60 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 { messagingCreateTopic, messagingListTopics } = require("./messaging"); const { teamsCreate, teamsList } = require("./teams"); const { projectsCreate } = require("./projects"); const { functionsCreate } = require("./functions"); @@ -237,6 +238,19 @@ const initTeam = async () => { success(); } +const initMessagingTopic = async () => { + const { topics } = await paginate(messagingListTopics, { parseOutput: false }, 100, 'topics'); + + log(`Found ${topics.length} topics`); + + topics.forEach(async topic => { + log(`Fetching ${topic.name} ...`); + localConfig.addMessagingTopic(topic); + }); + + success(); +} + init .command("project") .description("Initialise your {{ spec.title|caseUcfirst }} project") @@ -264,6 +278,11 @@ init .description("Initialise your Appwrite teams") .action(actionRunner(initTeam)) +init + .command("topic") + .description("Initialise your Appwrite messaging topics") + .action(actionRunner(initMessagingTopic)) + module.exports = { init, }; diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 916865f44..cafa89f79 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -204,6 +204,45 @@ class Local extends Config { this.set("buckets", buckets); } + getMessagingTopics() { + if (!this.has("topics")) { + return []; + } + return this.get("topics"); + } + + getMessagingTopic($id) { + if (!this.has("topics")) { + return {}; + } + + let topic = this.get("topics"); + for (let i = 0; i < topic.length; i++) { + if (topic[i]['$id'] == $id) { + return topic[i]; + } + } + + return {}; + } + + addMessagingTopic(props) { + if (!this.has("topics")) { + this.set("topics", []); + } + + let topics = this.get("topics"); + for (let i = 0; i < topics.length; i++) { + if (topics[i]['$id'] == props['$id']) { + topics[i] = props; + this.set("topics", topics); + return; + } + } + topics.push(props); + this.set("topics", topics); + } + getDatabases() { if (!this.has("databases")) { return []; diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index dd79745ed..f5e7e302d 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -209,12 +209,12 @@ const questionsInitFunction = [ parseOutput: false }) let runtimes = response["runtimes"] - let choices = runtimes.map((runtime, idx) => { + let choices = runtimes.map((runtime, idx) => { return { name: `${runtime.name} (${runtime['$id']})`, - value: { - id: runtime['$id'], - entrypoint: getEntrypoint(runtime['$id']), + value: { + id: runtime['$id'], + entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), commands : getInstallCommand(runtime['$id']) }, @@ -352,6 +352,31 @@ const questionsDeployBuckets = [ }, ] +const questionsDeployMessagingTopics = [ + { + type: "checkbox", + name: "topics", + message: "Which messaging topic would you like to deploy?", + choices: () => { + let topics = localConfig.getMessagingTopics(); + if (topics.length === 0) { + throw new Error("No topics found in the current directory. Run `appwrite init messaging` to fetch all your messaging topics."); + } + return topics.map(topic => { + return { + name: `${topic.name} (${topic['$id']})`, + value: topic.$id + } + }); + } + }, + { + type: "input", + name: "override", + message: 'Are you sure you want to override this topic? This can lead to loss of data! Type "YES" to confirm.' + } +] + const questionsGetEntrypoint = [ { type: "input", @@ -403,7 +428,7 @@ const questionsListFactors = [ sdk: client, parseOutput: false }); - + const choices = [ { name: `TOTP (Time-based One-time Password)`, @@ -450,6 +475,7 @@ module.exports = { questionsDeployFunctions, questionsDeployCollections, questionsDeployBuckets, + questionsDeployMessagingTopics, questionsDeployTeams, questionsGetEntrypoint, questionsListFactors, From a61cb67640170c81d87d409e52b6111b709b8230 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 16 May 2024 17:35:31 -0400 Subject: [PATCH 009/198] feat(cli): Adding organization select before selecting the project. --- templates/cli/lib/questions.js.twig | 45 ++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index dd79745ed..ba8a1f7f7 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -1,5 +1,6 @@ const { localConfig } = require('./config'); const { projectsList } = require('./commands/projects'); +const { teamsList } = require('./commands/teams'); const { functionsListRuntimes } = require('./commands/functions'); const { accountListMfaFactors } = require("./commands/account"); const { sdkForConsole } = require("./sdks"); @@ -117,6 +118,35 @@ const questionsInitProject = [ return Object.keys(localConfig.getProject()).length !== 0; } }, + { + type: "list", + name: "organization", + message: "Choose the project Organization", + choices: async () => { + let client = await sdkForConsole(true); + + let response = await teamsList({ + parseOutput: false, + sdk: client + }) + let teams = response["teams"] + let choices = teams.map((team, idx) => { + return { + name: `${team.name} (${team['$id']})`, + value: { + name: team.name, + id: team['$id'] + } + } + }) + + if (choices.length == 0) { + throw new Error("No organizations found. Please create a new organization.") + } + + return choices; + } + }, { type: "list", name: "start", @@ -163,9 +193,10 @@ const questionsInitProject = [ when(answers) { return answers.start == "existing"; }, - choices: async () => { + choices: async (answers) => { let response = await projectsList({ - parseOutput: false + parseOutput: false, + queries: [JSON.stringify({ method: 'equal', attribute:'teamId', values: [answers.organization.id] })], }) let projects = response["projects"] let choices = projects.map((project, idx) => { @@ -209,12 +240,12 @@ const questionsInitFunction = [ parseOutput: false }) let runtimes = response["runtimes"] - let choices = runtimes.map((runtime, idx) => { + let choices = runtimes.map((runtime, idx) => { return { name: `${runtime.name} (${runtime['$id']})`, - value: { - id: runtime['$id'], - entrypoint: getEntrypoint(runtime['$id']), + value: { + id: runtime['$id'], + entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), commands : getInstallCommand(runtime['$id']) }, @@ -403,7 +434,7 @@ const questionsListFactors = [ sdk: client, parseOutput: false }); - + const choices = [ { name: `TOTP (Time-based One-time Password)`, From 5ad093cee7ee2f64c37eefd50b4b260718b8435e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 17 May 2024 09:27:44 -0400 Subject: [PATCH 010/198] Beginning async function deployment --- templates/cli/lib/commands/deploy.js.twig | 21 ++++++++++++++++----- templates/cli/package.json.twig | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index ea30d1ba9..bed9aa90a 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -1,3 +1,4 @@ +const _progress = require('cli-progress'); const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); @@ -269,8 +270,17 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { return func; }); + const updatesBar = new _progress.MultiBar({ + format: ` ${success('{status}')} | {function}({id})`, + hideCursor: true, + clearOnComplete: false, + stopOnComplete: true, + noTTYOutput: true + }); + log('Deploying functions') for (let func of functions) { - log(`Deploying function ${func.name} ( ${func['$id']} )`) + const a = updatesBar.create(4, 0, { status: '', function: func.name, id: func['$id'] }) + {#log(`Deploying function ${func.name} ( ${func['$id']} )`)#} try { response = await functionsGet({ @@ -281,7 +291,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { if (response.runtime !== func.runtime) { throw new Error(`Runtime missmatch! (local=${func.runtime},remote=${response.runtime}) Please delete remote function or update your appwrite.json`); } - + a.increment({status:'Updating'}) response = await functionsUpdate({ functionId: func['$id'], name: func.name, @@ -298,6 +308,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { }); } catch (e) { if (e.code == 404) { + a.increment({status:'Creating'}) log(`Function ${func.name} ( ${func['$id']} ) does not exist in the project. Creating ... `); response = await functionsCreate({ functionId: func.$id || 'unique()', @@ -353,7 +364,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { functionId: func['$id'], parseOutput: false }, 100, 'variables'); - + await Promise.all(variables.map(async variable => { await functionsDeleteVariable({ functionId: func['$id'], @@ -361,7 +372,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }); })); - + let result = await awaitPools.wipeVariables(func['$id']); if (!result) { throw new Error("Variable deletion timed out."); @@ -938,4 +949,4 @@ deploy module.exports = { deploy -} \ No newline at end of file +} diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 1392ff294..015e494e1 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -24,6 +24,7 @@ "dependencies": { "undici": "^5.28.2", "chalk": "4.1.2", + "cli-progress": "^3.12.0", "cli-table3": "^0.6.2", "commander": "^9.2.0", "form-data": "^4.0.0", From 3cf8f7b8bf317295a14020412528edbbf880fb8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 17 May 2024 15:45:46 +0200 Subject: [PATCH 011/198] 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 ba8a1f7f7..70da5d9d6 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -121,7 +121,7 @@ const questionsInitProject = [ { type: "list", name: "organization", - message: "Choose the project Organization", + message: "Choose the project organization", choices: async () => { let client = await sdkForConsole(true); From fbb08ad4491ff804da74af44b3513925095191b4 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 17 May 2024 09:55:56 -0400 Subject: [PATCH 012/198] refactoring(cli): extract and generalizing validations --- src/SDK/Language/CLI.php | 5 +++++ templates/cli/lib/questions.js.twig | 15 +++++---------- templates/cli/lib/validations.js.twig | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 templates/cli/lib/validations.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 3bd0d77a4..00495f42d 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -127,6 +127,11 @@ public function getFiles(): array 'destination' => 'lib/questions.js', 'template' => 'cli/lib/questions.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/validations.js', + 'template' => 'cli/lib/validations.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/parser.js', diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 4efc9ab9e..f20669b5c 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -3,6 +3,7 @@ const { projectsList } = require('./commands/projects'); const { functionsListRuntimes } = require('./commands/functions'); const { accountListMfaFactors } = require("./commands/account"); const { sdkForConsole } = require("./sdks"); +const { validateRequired } = require("./validations"); const { databasesList } = require('./commands/databases'); const JSONbig = require("json-bigint")({ storeAsString: false }); @@ -107,12 +108,6 @@ const getInstallCommand = (runtime) => { return undefined; }; -const deployValidate = (singular, items) => { - if (items.length <= 0) { - return `Please select at least one ${singular}`; - } - return true; -} const questionsInitProject = [ { @@ -288,7 +283,7 @@ const questionsDeployFunctions = [ type: "checkbox", name: "functions", message: "Which functions would you like to deploy?", - validate: (i) => deployValidate('function', i), + validate: (value) => validateRequired('function', value), choices: () => { let functions = localConfig.getFunctions(); if (functions.length === 0) { @@ -315,7 +310,7 @@ const questionsDeployCollections = [ type: "checkbox", name: "collections", message: "Which collections would you like to deploy?", - validate: (i) => deployValidate('collection', i), + validate: (value) => validateRequired('collection', value), choices: () => { let collections = localConfig.getCollections(); if (collections.length === 0) { @@ -341,7 +336,7 @@ const questionsDeployBuckets = [ type: "checkbox", name: "buckets", message: "Which buckets would you like to deploy?", - validate: (i) => deployValidate('bucket', i), + validate: (value) => validateRequired('bucket', value), choices: () => { let buckets = localConfig.getBuckets(); if (buckets.length === 0) { @@ -382,7 +377,7 @@ const questionsDeployTeams = [ type: "checkbox", name: "teams", message: "Which teams would you like to deploy?", - validate: (i) => deployValidate('team', i), + validate: (value) => validateRequired('team', value), choices: () => { let teams = localConfig.getTeams(); if (teams.length === 0) { diff --git a/templates/cli/lib/validations.js.twig b/templates/cli/lib/validations.js.twig new file mode 100644 index 000000000..bfae5ba37 --- /dev/null +++ b/templates/cli/lib/validations.js.twig @@ -0,0 +1,17 @@ +const validateRequired = (resource, value) => { + if (Array.isArray(value)) { + if (value.length <= 0) { + return `Please select at least one ${resource}`; + } + } else { + if (value === undefined || value === null || value === 0 || (typeof value === "string" && value.trim() === '')) { + return `${resource} is required`; + } + } + + return true; +} + +module.exports = { + validateRequired +} From 38ddf484bb34dedd726558a73dd7da0665b64eaa Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 17 May 2024 10:33:18 -0400 Subject: [PATCH 013/198] refactoring(cli): adding pagination and const the variable --- templates/cli/lib/questions.js.twig | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 70da5d9d6..d918ddcc6 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -4,6 +4,7 @@ const { teamsList } = require('./commands/teams'); const { functionsListRuntimes } = require('./commands/functions'); const { accountListMfaFactors } = require("./commands/account"); const { sdkForConsole } = require("./sdks"); +const { paginate } = require('./paginate'); const { databasesList } = require('./commands/databases'); const JSONbig = require("json-bigint")({ storeAsString: false }); @@ -124,12 +125,8 @@ const questionsInitProject = [ message: "Choose the project organization", choices: async () => { let client = await sdkForConsole(true); + const { teams } = await paginate(teamsList, { parseOutput: false , sdk: client}, 100, 'teams'); - let response = await teamsList({ - parseOutput: false, - sdk: client - }) - let teams = response["teams"] let choices = teams.map((team, idx) => { return { name: `${team.name} (${team['$id']})`, From 6dc761ba9dd91e08743caeb614eb1a7f256f35bf Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 17 May 2024 13:36:26 -0400 Subject: [PATCH 014/198] feat(cli): Deploy function in parallelism. --- src/SDK/Language/CLI.php | 5 ++ templates/cli/base/params.twig | 8 +-- templates/cli/base/requests/file.twig | 10 +-- templates/cli/lib/commands/deploy.js.twig | 78 +++++++++++++++++------ templates/cli/lib/formatters.js.twig | 35 ++++++++++ templates/cli/lib/utils.js.twig | 22 ++++++- 6 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 templates/cli/lib/formatters.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index b531f81f9..0ef9109ce 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -132,6 +132,11 @@ public function getFiles(): array 'destination' => 'lib/validations.js', 'template' => 'cli/lib/validations.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/formatters.js', + 'template' => 'cli/lib/formatters.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/parser.js', diff --git a/templates/cli/base/params.twig b/templates/cli/base/params.twig index e36ae2f9c..87ca01341 100644 --- a/templates/cli/base/params.twig +++ b/templates/cli/base/params.twig @@ -11,19 +11,17 @@ if (!fs.lstatSync(folderPath).isDirectory()) { throw new Error('The path is not a directory.'); } - + const ignorer = ignore(); const func = localConfig.getFunction(functionId); if (func.ignore) { ignorer.add(func.ignore); - log('Ignoring files using configuration from appwrite.json'); } else if (fs.existsSync(pathLib.join({{ parameter.name | caseCamel | escapeKeyword }}, '.gitignore'))) { ignorer.add(fs.readFileSync(pathLib.join({{ parameter.name | caseCamel | escapeKeyword }}, '.gitignore')).toString()); - log('Ignoring files in .gitignore'); } - + const files = getAllFiles({{ parameter.name | caseCamel | escapeKeyword }}).map((file) => pathLib.relative({{ parameter.name | caseCamel | escapeKeyword }}, file)).filter((file) => !ignorer.ignores(file)); await tar @@ -81,4 +79,4 @@ payload['key'] = globalConfig.getKey(); const queryParams = new URLSearchParams(payload); apiPath = `${globalConfig.getEndpoint()}${apiPath}?${queryParams.toString()}`; -{% endif %} \ No newline at end of file +{% endif %} diff --git a/templates/cli/base/requests/file.twig b/templates/cli/base/requests/file.twig index 6765eed3c..64b07d891 100644 --- a/templates/cli/base/requests/file.twig +++ b/templates/cli/base/requests/file.twig @@ -1,7 +1,7 @@ {% for parameter in method.parameters.all %} {% if parameter.type == 'file' %} const size = {{ parameter.name | caseCamel | escapeKeyword }}.size; - + const apiHeaders = { {% for parameter in method.parameters.header %} '{{ parameter.name }}': ${{ parameter.name | caseCamel | escapeKeyword }}, @@ -45,7 +45,7 @@ } let uploadableChunkTrimmed; - + if(currentPosition + 1 >= client.CHUNK_SIZE) { uploadableChunkTrimmed = uploadableChunk; } else { @@ -99,12 +99,12 @@ } {% if method.packaging %} - fs.unlinkSync(filePath); + await fs.unlink(filePath,()=>{}); {% endif %} {% if method.type == 'location' %} fs.writeFileSync(destination, response); {% endif %} - + if (parseOutput) { parse(response) success() @@ -112,4 +112,4 @@ return response; {% endif %} -{% endfor %} \ No newline at end of file +{% endfor %} diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index bed9aa90a..6d224fe15 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -3,6 +3,8 @@ const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); +const { loaderInterval, clearLoaderInterval, } = require('../utils'); +const { formatterFunction } = require('../formatters'); const { paginate } = require('../paginate'); const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); @@ -271,27 +273,38 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { }); const updatesBar = new _progress.MultiBar({ - format: ` ${success('{status}')} | {function}({id})`, + format: formatterFunction, hideCursor: true, clearOnComplete: false, stopOnComplete: true, noTTYOutput: true }); - log('Deploying functions') - for (let func of functions) { - const a = updatesBar.create(4, 0, { status: '', function: func.name, id: func['$id'] }) - {#log(`Deploying function ${func.name} ( ${func['$id']} )`)#} + + log('Deploying functions\n'); + + let successfullyDeployed = 0; + + await Promise.all(functions.map(async (func) => { + const ignore = func.ignore ? 'appwrite.json' : '.gitignore'; + let functionExists = false; + + const bar = updatesBar.create(100, 0, { status: 'Deploying', function: func.name, id: func['$id'], ignore }) + bar.update({ status: 'Getting' }); + const updatingInterval = loaderInterval(bar, 8, 1, 80); try { response = await functionsGet({ functionId: func['$id'], parseOutput: false, }); - + functionExists = true; if (response.runtime !== func.runtime) { - throw new Error(`Runtime missmatch! (local=${func.runtime},remote=${response.runtime}) Please delete remote function or update your appwrite.json`); + bar.update({ status: 'Error', errorMessage: `Runtime missmatch! (local=${func.runtime},remote=${response.runtime}) Please delete remote function or update your appwrite.json` }) + clearLoaderInterval(bar, updatingInterval); + return; } - a.increment({status:'Updating'}) + bar.update({ status: 'Updating' }); + response = await functionsUpdate({ functionId: func['$id'], name: func.name, @@ -307,9 +320,23 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }); } catch (e) { + if (e.code == 404) { - a.increment({status:'Creating'}) - log(`Function ${func.name} ( ${func['$id']} ) does not exist in the project. Creating ... `); + functionExists = false; + } else { + clearLoaderInterval(bar, updatingInterval) + bar.update({ status: 'Error', errorMessage: e.message ?? 'General error occurs please try again' }); + return; + } + } + + clearLoaderInterval(bar, updatingInterval) + + if (!functionExists) { + bar.update({ status: 'Creating' }) + const creatingInterval = loaderInterval(bar, 8, 1, 80); + + try { response = await functionsCreate({ functionId: func.$id || 'unique()', name: func.name, @@ -329,15 +356,20 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { localConfig.updateFunction(func['$id'], { "$id": response['$id'], }); - func["$id"] = response['$id']; - log(`Function ${func.name} created.`); - } else { - throw e; + bar.update({ status: 'Created' }); + + clearLoaderInterval(bar, creatingInterval); + } catch (e) { + clearLoaderInterval(bar, creatingInterval); + + bar.update({ status: 'Error', errorMessage: e.message ?? 'General error occurs please try again' }); + return; } } if (func.variables) { + // TODO: // Delete existing variables const { total } = await functionsListVariables({ @@ -375,7 +407,8 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { let result = await awaitPools.wipeVariables(func['$id']); if (!result) { - throw new Error("Variable deletion timed out."); + bar.update({ status: 'Error', errorMessage: 'Variable deletion timed out' }) + return; } // Deploy local variables @@ -396,6 +429,8 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { func.entrypoint = answers.entrypoint; localConfig.updateFunction(func['$id'], func); } + bar.update({ status: 'Deploying' }) + const deployingInterval = loaderInterval(bar, 8, 1, 80); try { response = await functionsCreateDeployment({ @@ -407,20 +442,23 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }) - success(`Deployed ${func.name} ( ${func['$id']} )`); + bar.update({ status: 'Deployed' }) + successfullyDeployed++; } catch (e) { switch (e.code) { case 'ENOENT': - error(`Function ${func.name} ( ${func['$id']} ) not found in the current directory. Skipping ...`); + bar.update({ status: 'Error', errorMessage: 'Not found in the current directory. Skipping...' }) break; default: - throw e; + bar.update({ status: 'Error', errorMessage: e.message ?? 'General error occurs please try again' }) } } - } + clearLoaderInterval(bar, deployingInterval); + })) - success(`Deployed ${functions.length} functions`); + updatesBar.stop() + success(`Deployed ${successfullyDeployed} functions`); } const createAttribute = async (databaseId, collectionId, attribute) => { diff --git a/templates/cli/lib/formatters.js.twig b/templates/cli/lib/formatters.js.twig new file mode 100644 index 000000000..36dffa263 --- /dev/null +++ b/templates/cli/lib/formatters.js.twig @@ -0,0 +1,35 @@ +const chalk = require('chalk'); +const dots = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]; + +const formatterFunction = (options, params, payload) => { + const status = payload.status.padEnd(12); + const middle = `${payload.function} (${payload.id})`.padEnd(40); + + let start = chalk.blue(status); + let end = chalk.yellow(`Ignoring using: ${payload.ignore}`); + let progress = '⌛'; + + if (status.toLowerCase().trim() === 'deployed') { + start = chalk.green.bold(status); + progress = chalk.green.bold('✓'.padEnd(2)) + } else if (status.toLowerCase().trim() === 'error') { + start = chalk.red.bold(status); + progress = chalk.red.bold('✗'.padEnd(2)) + end = chalk.red(payload.errorMessage); + } + + if (payload.progress && Number.isInteger(payload.progress)) { + progress = dots[payload.progress - 1].padEnd(2); + } + + return formatter(start, middle, end, progress); +} + +const formatter = (start, middle, end, progress, separator = '•') => { + return `${progress}${start} ${separator} ${middle} ${separator} ${end}`; + +} + +module.exports = { + formatterFunction +} diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index cb7d06ce3..8a108da6b 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -1,9 +1,23 @@ const fs = require("fs"); const path = require("path"); +function loaderInterval(bar, max, start, timeout) { + let a = start; + + return setInterval(() => { + if (a === max) a = start; + bar.update({ progress: a++ }) + }, timeout); +} + +function clearLoaderInterval(bar, interval) { + clearInterval(interval); + bar.update({ progress: '⏳' }) +} + 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)); @@ -15,5 +29,7 @@ function getAllFiles(folder) { } module.exports = { - getAllFiles -}; \ No newline at end of file + getAllFiles, + loaderInterval, + clearLoaderInterval +}; From 32f9de3a45426c4459d8ebb64da69dcc3e77a82e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Sun, 19 May 2024 10:12:38 -0400 Subject: [PATCH 015/198] wip: Deploy function in parallelism. --- src/SDK/Language/CLI.php | 5 +++++ templates/cli/lib/commands/deploy.js.twig | 9 +-------- templates/cli/lib/updateTable.js.twig | 23 +++++++++++++++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 templates/cli/lib/updateTable.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 0ef9109ce..e19be19f3 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -137,6 +137,11 @@ public function getFiles(): array 'destination' => 'lib/formatters.js', 'template' => 'cli/lib/formatters.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/updateTable.js', + 'template' => 'cli/lib/updateTable.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/parser.js', diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index 6d224fe15..706011ba6 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -1,4 +1,3 @@ -const _progress = require('cli-progress'); const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); @@ -272,13 +271,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { return func; }); - const updatesBar = new _progress.MultiBar({ - format: formatterFunction, - hideCursor: true, - clearOnComplete: false, - stopOnComplete: true, - noTTYOutput: true - }); + log('Deploying functions\n'); diff --git a/templates/cli/lib/updateTable.js.twig b/templates/cli/lib/updateTable.js.twig new file mode 100644 index 000000000..cd3908d31 --- /dev/null +++ b/templates/cli/lib/updateTable.js.twig @@ -0,0 +1,23 @@ +const progress = require('cli-progress'); + +class UpdateTable { + static init(format, clearOnComplete = true, hideCursor = true) { + this.updatesBar = new progress.MultiBar({ + format, + hideCursor, + clearOnComplete, + stopOnComplete: true, + noTTYOutput: true + }); + } + + constructor(value, end, endTitle = '', type = '', maxValue = 1, currentValue = 0) { + this.bar = UpdateTable.updatesBar.create(1, 0, { status: 'Deploying', function: func.name, id: func['$id'], ignore }) + + } + +} + +module.exports = { + UpdateTable +} From 209c3fe92733ec1c234521412b30f62cc3f5d4ed Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Sun, 19 May 2024 11:45:33 -0400 Subject: [PATCH 016/198] feat(cli): Async deploy with status updates --- src/SDK/Language/CLI.php | 9 +-- templates/cli/lib/commands/deploy.js.twig | 52 +++++------- templates/cli/lib/formatters.js.twig | 35 -------- templates/cli/lib/updateTable.js.twig | 23 ------ templates/cli/lib/updater.js.twig | 98 +++++++++++++++++++++++ 5 files changed, 121 insertions(+), 96 deletions(-) delete mode 100644 templates/cli/lib/formatters.js.twig delete mode 100644 templates/cli/lib/updateTable.js.twig create mode 100644 templates/cli/lib/updater.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index e19be19f3..030a0f35c 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -134,13 +134,8 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'lib/formatters.js', - 'template' => 'cli/lib/formatters.js.twig', - ], - [ - 'scope' => 'default', - 'destination' => 'lib/updateTable.js', - 'template' => 'cli/lib/updateTable.js.twig', + 'destination' => 'lib/updater.js', + 'template' => 'cli/lib/updater.js.twig', ], [ 'scope' => 'default', diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index 706011ba6..5e4711b21 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -3,7 +3,7 @@ const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); const { loaderInterval, clearLoaderInterval, } = require('../utils'); -const { formatterFunction } = require('../formatters'); +const { Updater, SPINNER_ARC, SPINNER_DOTS } = require('../updater'); const { paginate } = require('../paginate'); const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); @@ -271,19 +271,18 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { return func; }); + log('Pushing functions\n'); - - log('Deploying functions\n'); - + Updater.start(false,) let successfullyDeployed = 0; await Promise.all(functions.map(async (func) => { const ignore = func.ignore ? 'appwrite.json' : '.gitignore'; let functionExists = false; - const bar = updatesBar.create(100, 0, { status: 'Deploying', function: func.name, id: func['$id'], ignore }) - bar.update({ status: 'Getting' }); - const updatingInterval = loaderInterval(bar, 8, 1, 80); + const updaterRow = new Updater({ status: '', resource: func.name, id: func['$id'], end: `Ignoring using: ${ignore}` }); + + updaterRow.update({ status: 'Getting' }).startSpinner(SPINNER_DOTS); try { response = await functionsGet({ @@ -292,11 +291,11 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { }); functionExists = true; if (response.runtime !== func.runtime) { - bar.update({ status: 'Error', errorMessage: `Runtime missmatch! (local=${func.runtime},remote=${response.runtime}) Please delete remote function or update your appwrite.json` }) - clearLoaderInterval(bar, updatingInterval); + updaterRow.fail({ errorMessage: `Runtime mismatch! (local=${func.runtime},remote=${response.runtime}) Please delete remote function or update your appwrite.json` }) return; } - bar.update({ status: 'Updating' }); + + updaterRow.update({ status: 'Updating' }).replaceSpinner(SPINNER_ARC); response = await functionsUpdate({ functionId: func['$id'], @@ -317,17 +316,13 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { if (e.code == 404) { functionExists = false; } else { - clearLoaderInterval(bar, updatingInterval) - bar.update({ status: 'Error', errorMessage: e.message ?? 'General error occurs please try again' }); + updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); return; } } - clearLoaderInterval(bar, updatingInterval) - if (!functionExists) { - bar.update({ status: 'Creating' }) - const creatingInterval = loaderInterval(bar, 8, 1, 80); + updaterRow.update({ status: 'Creating' }).replaceSpinner(SPINNER_DOTS); try { response = await functionsCreate({ @@ -350,13 +345,9 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { "$id": response['$id'], }); func["$id"] = response['$id']; - bar.update({ status: 'Created' }); - - clearLoaderInterval(bar, creatingInterval); + updaterRow.update({ status: 'Created' }); } catch (e) { - clearLoaderInterval(bar, creatingInterval); - - bar.update({ status: 'Error', errorMessage: e.message ?? 'General error occurs please try again' }); + updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }); return; } } @@ -400,7 +391,7 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { let result = await awaitPools.wipeVariables(func['$id']); if (!result) { - bar.update({ status: 'Error', errorMessage: 'Variable deletion timed out' }) + updaterRow.fail({ errorMessage: 'Variable deletion timed out' }) return; } @@ -422,10 +413,9 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { func.entrypoint = answers.entrypoint; localConfig.updateFunction(func['$id'], func); } - bar.update({ status: 'Deploying' }) - const deployingInterval = loaderInterval(bar, 8, 1, 80); try { + updaterRow.update({ status: 'Pushing' }).replaceSpinner(SPINNER_ARC); response = await functionsCreateDeployment({ functionId: func['$id'], entrypoint: func.entrypoint, @@ -435,23 +425,23 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }) - bar.update({ status: 'Deployed' }) + updaterRow.update({ status: 'Pushed' }) successfullyDeployed++; } catch (e) { switch (e.code) { case 'ENOENT': - bar.update({ status: 'Error', errorMessage: 'Not found in the current directory. Skipping...' }) + updaterRow.fail({ errorMessage: 'Not found in the current directory. Skipping...' }) break; default: - bar.update({ status: 'Error', errorMessage: e.message ?? 'General error occurs please try again' }) + updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }) } } - clearLoaderInterval(bar, deployingInterval); + updaterRow.stopSpinner(); })) + Updater.stop(); - updatesBar.stop() - success(`Deployed ${successfullyDeployed} functions`); + success(`Pushed ${successfullyDeployed} functions`); } const createAttribute = async (databaseId, collectionId, attribute) => { diff --git a/templates/cli/lib/formatters.js.twig b/templates/cli/lib/formatters.js.twig deleted file mode 100644 index 36dffa263..000000000 --- a/templates/cli/lib/formatters.js.twig +++ /dev/null @@ -1,35 +0,0 @@ -const chalk = require('chalk'); -const dots = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"]; - -const formatterFunction = (options, params, payload) => { - const status = payload.status.padEnd(12); - const middle = `${payload.function} (${payload.id})`.padEnd(40); - - let start = chalk.blue(status); - let end = chalk.yellow(`Ignoring using: ${payload.ignore}`); - let progress = '⌛'; - - if (status.toLowerCase().trim() === 'deployed') { - start = chalk.green.bold(status); - progress = chalk.green.bold('✓'.padEnd(2)) - } else if (status.toLowerCase().trim() === 'error') { - start = chalk.red.bold(status); - progress = chalk.red.bold('✗'.padEnd(2)) - end = chalk.red(payload.errorMessage); - } - - if (payload.progress && Number.isInteger(payload.progress)) { - progress = dots[payload.progress - 1].padEnd(2); - } - - return formatter(start, middle, end, progress); -} - -const formatter = (start, middle, end, progress, separator = '•') => { - return `${progress}${start} ${separator} ${middle} ${separator} ${end}`; - -} - -module.exports = { - formatterFunction -} diff --git a/templates/cli/lib/updateTable.js.twig b/templates/cli/lib/updateTable.js.twig deleted file mode 100644 index cd3908d31..000000000 --- a/templates/cli/lib/updateTable.js.twig +++ /dev/null @@ -1,23 +0,0 @@ -const progress = require('cli-progress'); - -class UpdateTable { - static init(format, clearOnComplete = true, hideCursor = true) { - this.updatesBar = new progress.MultiBar({ - format, - hideCursor, - clearOnComplete, - stopOnComplete: true, - noTTYOutput: true - }); - } - - constructor(value, end, endTitle = '', type = '', maxValue = 1, currentValue = 0) { - this.bar = UpdateTable.updatesBar.create(1, 0, { status: 'Deploying', function: func.name, id: func['$id'], ignore }) - - } - -} - -module.exports = { - UpdateTable -} diff --git a/templates/cli/lib/updater.js.twig b/templates/cli/lib/updater.js.twig new file mode 100644 index 000000000..abac082a7 --- /dev/null +++ b/templates/cli/lib/updater.js.twig @@ -0,0 +1,98 @@ +const progress = require('cli-progress'); +const chalk = require('chalk'); + +const SPINNER_ARC = 'arc'; +const SPINNER_DOTS = 'dots'; + +const spinners = { + [SPINNER_ARC]: { + "interval": 100, + "frames": ["◜", "◠", "◝", "◞", "◡", "◟"] + }, + [SPINNER_DOTS]: { + "interval": 80, + "frames": ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + } +} + +class Updater { + static start(clearOnComplete = true, hideCursor = true) { + Updater.updatesBar = new progress.MultiBar({ + format: this.#formatter, + hideCursor, + clearOnComplete, + stopOnComplete: true, + noTTYOutput: true + }); + } + + static stop() { + Updater.updatesBar.stop(); + } + + static #formatter(options, params, payload) { + const status = payload.status.padEnd(12); + const middle = `${payload.resource} (${payload.id})`.padEnd(40); + + let prefix = chalk.cyan(payload.prefix ?? '⧗'); + let start = chalk.cyan(status); + let end = chalk.yellow(payload.end); + + if (status.toLowerCase().trim() === 'pushed') { + 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('✗'); + end = chalk.red(payload.errorMessage); + } + + return Updater.#line(prefix, start, middle, end); + } + + static #line(prefix, start, middle, end, separator = '•') { + return `${prefix} ${start} ${separator} ${middle} ${separator} ${end}`; + + } + + constructor(payload, total = 100, startValue = 0) { + this.bar = Updater.updatesBar.create(total, startValue, payload) + } + + update(payload) { + this.bar.update(payload); + return this; + } + + fail(payload) { + this.stopSpinner(); + this.update({ status: 'Error', ...payload }); + } + + startSpinner(name) { + let spinnerFrame = 1; + const spinner = spinners[name] ?? spinners['dots']; + + this.spinnerInterval = setInterval(() => { + if (spinnerFrame === spinner.frames.length) spinnerFrame = 1; + this.bar.update({ prefix: spinner.frames[spinnerFrame++] }); + }, spinner.interval); + } + + stopSpinner() { + clearInterval(this.spinnerInterval); + } + + replaceSpinner(name) { + this.stopSpinner(); + this.startSpinner(name); + } +} + + +module.exports = { + Updater, + SPINNER_ARC, + SPINNER_DOTS +} From 9318d4806aa3285edcad00fd8ec66d4847a567f5 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Sun, 19 May 2024 11:46:52 -0400 Subject: [PATCH 017/198] chore(cli): Cleaning added functions --- templates/cli/lib/commands/deploy.js.twig | 4 ++-- templates/cli/lib/utils.js.twig | 20 ++------------------ 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index 5e4711b21..ba1ac43be 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -2,7 +2,6 @@ const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); -const { loaderInterval, clearLoaderInterval, } = require('../utils'); const { Updater, SPINNER_ARC, SPINNER_DOTS } = require('../updater'); const { paginate } = require('../paginate'); const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); @@ -438,7 +437,8 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { } } updaterRow.stopSpinner(); - })) + })); + Updater.stop(); success(`Pushed ${successfullyDeployed} functions`); diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 8a108da6b..289b1fa6e 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -1,23 +1,9 @@ const fs = require("fs"); const path = require("path"); -function loaderInterval(bar, max, start, timeout) { - let a = start; - - return setInterval(() => { - if (a === max) a = start; - bar.update({ progress: a++ }) - }, timeout); -} - -function clearLoaderInterval(bar, interval) { - clearInterval(interval); - bar.update({ progress: '⏳' }) -} - 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)); @@ -29,7 +15,5 @@ function getAllFiles(folder) { } module.exports = { - getAllFiles, - loaderInterval, - clearLoaderInterval + getAllFiles }; From 901e0457e33b182d6956a7ad657dfb16d74dfb7b Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Sun, 19 May 2024 12:30:46 -0400 Subject: [PATCH 018/198] feat(cli): Validation function possible path before deploying --- templates/cli/lib/commands/deploy.js.twig | 70 ++++++++++++++--------- templates/cli/lib/updater.js.twig | 2 +- 2 files changed, 43 insertions(+), 29 deletions(-) diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index ba1ac43be..fc97aa379 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -270,6 +270,45 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { return func; }); + log('Validating functions'); + // Validation is done BEFORE deploying so the deployment process can be run in async with progress update + for (let i = 0; i < functions.length; i++) { + const func = functions[i]; + + if (!func.entrypoint) { + log(`Function ${func.name} does not have an endpoint`); + const answers = await inquirer.prompt(questionsGetEntrypoint) + func.entrypoint = answers.entrypoint; + localConfig.updateFunction(func['$id'], func); + } + + if (func.variables) { + func.deployVariables = yes; + + try { + const { total } = await functionsListVariables({ + functionId: func['$id'], + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false + }); + + if (total === 0) { + func.deployVariables = true; + } else if (total > 0 && !func.deployVariables) { + log(`The function ${func.name} has remote variables setup`); + const variableAnswers = await inquirer.prompt(questionsDeployFunctions[1]) + func.deployVariables = variableAnswers.override.toLowerCase() === "yes"; + } + } catch (e) { + if (e.code != 404) { + throw e.message; + } + } + } + } + + + log('All functions are validated'); log('Pushing functions\n'); Updater.start(false,) @@ -352,28 +391,10 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { } if (func.variables) { - // TODO: - // Delete existing variables - - const { total } = await functionsListVariables({ - functionId: func['$id'], - queries: [JSON.stringify({ method: 'limit', values: [1] })], - parseOutput: false - }); - - let deployVariables = yes; - - if (total === 0) { - deployVariables = true; - } else if (total > 0 && !yes) { - const variableAnswers = await inquirer.prompt(questionsDeployFunctions[1]) - deployVariables = variableAnswers.override.toLowerCase() === "yes"; - } - - if (!deployVariables) { - log(`Skipping variables for ${func.name} ( ${func['$id']} )`); + if (!func.deployVariables) { + updaterRow.update({ end: 'Skipping variables' }); } else { - log(`Deploying variables for ${func.name} ( ${func['$id']} )`); + updaterRow.update({ end: 'Pushing variables' }); const { variables } = await paginate(functionsListVariables, { functionId: func['$id'], @@ -406,13 +427,6 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { } } - // Create tag - if (!func.entrypoint) { - const answers = await inquirer.prompt(questionsGetEntrypoint) - func.entrypoint = answers.entrypoint; - localConfig.updateFunction(func['$id'], func); - } - try { updaterRow.update({ status: 'Pushing' }).replaceSpinner(SPINNER_ARC); response = await functionsCreateDeployment({ diff --git a/templates/cli/lib/updater.js.twig b/templates/cli/lib/updater.js.twig index abac082a7..9e871ea02 100644 --- a/templates/cli/lib/updater.js.twig +++ b/templates/cli/lib/updater.js.twig @@ -52,7 +52,7 @@ class Updater { } static #line(prefix, start, middle, end, separator = '•') { - return `${prefix} ${start} ${separator} ${middle} ${separator} ${end}`; + return `${prefix} ${start} ${separator} ${middle} ${!end ? '' : separator} ${end}`; } From d8e22d71032dc12f1881e3159d363af4b4dc9ecf Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Sun, 19 May 2024 13:14:19 -0400 Subject: [PATCH 019/198] feat(cli): Updating function deployment status --- templates/cli/lib/commands/deploy.js.twig | 55 ++++++++++++++++++++--- templates/cli/lib/updater.js.twig | 6 +++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index fc97aa379..3966fd105 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -6,7 +6,7 @@ const { Updater, SPINNER_ARC, SPINNER_DOTS } = require('../updater'); const { paginate } = require('../paginate'); const { questionsDeployBuckets, questionsDeployTeams, questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollections, questionsConfirmDeployCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); -const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); +const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsGetDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { databasesGet, databasesCreate, @@ -41,6 +41,7 @@ const { const STEP_SIZE = 100; // Resources const POOL_DEBOUNCE = 2000; // Milliseconds +const POOL_MAX_DEBOUNCE = 30; // Times let poolMaxDebounces = 30; @@ -312,11 +313,13 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { log('Pushing functions\n'); Updater.start(false,) + let successfullyPushed = 0; let successfullyDeployed = 0; await Promise.all(functions.map(async (func) => { const ignore = func.ignore ? 'appwrite.json' : '.gitignore'; let functionExists = false; + let deploymentCreated = false; const updaterRow = new Updater({ status: '', resource: func.name, id: func['$id'], end: `Ignoring using: ${ignore}` }); @@ -438,9 +441,9 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { parseOutput: false }) - updaterRow.update({ status: 'Pushed' }) - successfullyDeployed++; - + updaterRow.update({ status: 'Pushed' }); + deploymentCreated = true; + successfullyPushed++; } catch (e) { switch (e.code) { case 'ENOENT': @@ -450,12 +453,54 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }) } } + + if (deploymentCreated) { + try { + const deploymentId = response['$id']; + updaterRow.update({ status: 'Deploying', end: 'Checking deployment status...' }) + let pool_checks = 0; + + while (true) { + if (pool_checks >= POOL_MAX_DEBOUNCE) { + updaterRow.update({ end: 'Deployment takes too long; check the console' }) + 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') { + updaterRow.fail({ errorMessage: 'failed to deploy' }); + + break; + } else { + updaterRow.update({ status: 'Deploying', end: `current status: ${status}` }) + } + + pool_checks++; + await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); + } + } catch (e) { + updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }) + } + } + updaterRow.stopSpinner(); })); Updater.stop(); - success(`Pushed ${successfullyDeployed} functions`); + success(`Pushed ${successfullyPushed} functions with ${successfullyDeployed} successfully deployed`); } const createAttribute = async (databaseId, collectionId, attribute) => { diff --git a/templates/cli/lib/updater.js.twig b/templates/cli/lib/updater.js.twig index 9e871ea02..d732061c5 100644 --- a/templates/cli/lib/updater.js.twig +++ b/templates/cli/lib/updater.js.twig @@ -39,6 +39,12 @@ class Updater { let end = chalk.yellow(payload.end); if (status.toLowerCase().trim() === 'pushed') { + start = chalk.greenBright.bold(status); + prefix = chalk.greenBright.bold('✓'); + end = ''; + } else if (status.toLowerCase().trim() === 'deploying') { + start = chalk.cyanBright.bold(status); + } else if (status.toLowerCase().trim() === 'deployed') { start = chalk.green.bold(status); prefix = chalk.green.bold('✓'); end = ''; From fdd6325b8da99699cddf5356523b4f21af9e142a Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 07:13:05 -0400 Subject: [PATCH 020/198] Adjusting to init & push commands --- templates/cli/lib/commands/pull.js.twig | 8 ++++---- templates/cli/lib/commands/push.js.twig | 22 +++++++++++----------- templates/cli/lib/questions.js.twig | 8 ++++---- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 8085c1e06..41c2ea9d1 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -238,13 +238,13 @@ const pullTeam = async () => { success(); } -const initMessagingTopic = async () => { +const pullMessagingTopic = async () => { const { topics } = await paginate(messagingListTopics, { parseOutput: false }, 100, 'topics'); log(`Found ${topics.length} topics`); topics.forEach(async topic => { - log(`Fetching ${topic.name} ...`); + log(`Pulling ${topic.name} ...`); localConfig.addMessagingTopic(topic); }); @@ -278,10 +278,10 @@ pull .description("Pulling your Appwrite teams") .action(actionRunner(pullTeam)) -init +pull .command("topic") .description("Initialise your Appwrite messaging topics") - .action(actionRunner(initMessagingTopic)) + .action(actionRunner(pullMessagingTopic)) module.exports = { pull, diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 3312e29cf..fac6e706f 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -3,7 +3,7 @@ const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); const { paginate } = require('../paginate'); -const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsDeployMessagingTopics } = require("../questions"); +const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections, questionsPushMessagingTopics } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { @@ -910,7 +910,7 @@ const pushTeam = async ({ all, yes } = {}) => { } } -const deployMessagingTopic = async ({ all, yes } = {}) => { +const pushMessagingTopic = async ({ all, yes } = {}) => { let response = {}; let topicsIds = []; @@ -918,13 +918,13 @@ const deployMessagingTopic = async ({ all, yes } = {}) => { if (all) { if (configTopics.length === 0) { - throw new Error("No topics found in the current directory. Run `appwrite init topics` to fetch all your messaging topics."); + 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(questionsDeployMessagingTopics[0]) + const answers = await inquirer.prompt(questionsPushMessagingTopics[0]) topicsIds.push(...answers.topics); } @@ -936,7 +936,7 @@ const deployMessagingTopic = async ({ all, yes } = {}) => { } for (let topic of topics) { - log(`Deploying topic ${topic.name} ( ${topic['$id']} )`) + log(`Pushing topic ${topic.name} ( ${topic['$id']} )`) try { response = await messagingGetTopic({ @@ -946,7 +946,7 @@ const deployMessagingTopic = async ({ all, yes } = {}) => { log(`Topic ${topic.name} ( ${topic['$id']} ) already exists.`); if (!yes) { - const answers = await inquirer.prompt(questionsDeployMessagingTopics[1]) + const answers = await inquirer.prompt(questionsPushMessagingTopics[1]) if (answers.override.toLowerCase() !== "yes") { log(`Received "${answers.override}". Skipping ${topic.name} ( ${topic['$id']} )`); continue; @@ -962,7 +962,7 @@ const deployMessagingTopic = async ({ all, yes } = {}) => { parseOutput: false }); - success(`Deployed ${topic.name} ( ${topic['$id']} )`); + success(`Pushed ${topic.name} ( ${topic['$id']} )`); } catch (e) { if (e.code == 404) { log(`Topic ${topic.name} does not exist in the project. Creating ... `); @@ -974,7 +974,7 @@ const deployMessagingTopic = async ({ all, yes } = {}) => { parseOutput: false }) - success(`Deployed ${topic.name} ( ${topic['$id']} )`); + success(`Pushed ${topic.name} ( ${topic['$id']} )`); } else { throw e; } @@ -1011,12 +1011,12 @@ push .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushTeam)); -deploy +push .command("topic") - .description("Deploy messaging topics in the current project.") + .description("Push messaging topics in the current project.") .option(`--all`, `Flag to deploy all topics`) .option(`--yes`, `Flag to confirm all warnings`) - .action(actionRunner(deployMessagingTopic)); + .action(actionRunner(pushMessagingTopic)); module.exports = { push diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 2c5cc9bbc..71314d0c1 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -356,15 +356,15 @@ const questionsPushBuckets = [ }, ] -const questionsDeployMessagingTopics = [ +const questionsPushMessagingTopics = [ { type: "checkbox", name: "topics", - message: "Which messaging topic would you like to deploy?", + message: "Which messaging topic would you like to push?", choices: () => { let topics = localConfig.getMessagingTopics(); if (topics.length === 0) { - throw new Error("No topics found in the current directory. Run `appwrite init messaging` to fetch all your messaging topics."); + throw new Error("No topics found in the current directory. Run `appwrite pull messaging` to fetch all your messaging topics."); } return topics.map(topic => { return { @@ -480,7 +480,7 @@ module.exports = { questionsPushFunctions, questionsPushCollections, questionsPushBuckets, - questionsDeployMessagingTopics, + questionsPushMessagingTopics, questionsPushTeams, questionsGetEntrypoint, questionsListFactors, From e44eec06225ee0b1d889a9df35c7e4bba8c64ba3 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 07:18:31 -0400 Subject: [PATCH 021/198] feat(cli): Ask once for topics override --- templates/cli/lib/commands/push.js.twig | 17 +++++++++++------ templates/cli/lib/questions.js.twig | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index fac6e706f..e58fa9e5e 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -915,6 +915,7 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { let topicsIds = []; const configTopics = localConfig.getMessagingTopics(); + let overrideExisting = yes; if (all) { if (configTopics.length === 0) { @@ -935,6 +936,13 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { topics.push(...idTopic); } + if (!yes) { + const answers = await inquirer.prompt(questionsPushMessagingTopics[1]) + if (answers.override.toLowerCase() === "yes") { + overrideExisting = true; + } + } + for (let topic of topics) { log(`Pushing topic ${topic.name} ( ${topic['$id']} )`) @@ -945,12 +953,9 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { }) log(`Topic ${topic.name} ( ${topic['$id']} ) already exists.`); - if (!yes) { - const answers = await inquirer.prompt(questionsPushMessagingTopics[1]) - if (answers.override.toLowerCase() !== "yes") { - log(`Received "${answers.override}". Skipping ${topic.name} ( ${topic['$id']} )`); - continue; - } + if (!overrideExisting) { + log(`Skipping ${topic.name} ( ${topic['$id']} )`); + continue; } log(`Updating Topic ...`) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 71314d0c1..24f149d3e 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -377,7 +377,7 @@ const questionsPushMessagingTopics = [ { type: "input", name: "override", - message: 'Are you sure you want to override this topic? This can lead to loss of data! Type "YES" to confirm.' + message: 'What you like to override existing topics? This can lead to loss of data! Type "YES" to confirm.' } ] From cda1847178902b85d69dbe3a2630a959ac6ac956 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 07:26:21 -0400 Subject: [PATCH 022/198] merging from cli-g2 --- templates/cli/lib/commands/push.js.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 6c97e5e46..34aa0f383 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -3,7 +3,7 @@ const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); const { paginate } = require('../paginate'); -const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections } = require("../questions"); +const { questionsPushResources, questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment, functionsListVariables, functionsDeleteVariable, functionsCreateVariable } = require('./functions'); const { @@ -239,7 +239,7 @@ const pushResources = async ({ all, yes } = {}) => { if (all) { Object.values(actions).forEach(action => action({ all: true, yes })); } else { - const answers = await inquirer.prompt(questionsDeployResources[0]); + const answers = await inquirer.prompt(questionsPushResources[0]); answers.resources.forEach((resource) => { const action = actions[resource]; if (action !== undefined) { From 5633df0923d71a369eebcbd9092a05ce48b94e7c Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 07:32:31 -0400 Subject: [PATCH 023/198] feat(cli): adding messaging to push all --- templates/cli/lib/commands/push.js.twig | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 2c5539b6c..32d5570eb 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -230,19 +230,13 @@ const awaitPools = { }, } -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)); - const pushResources = async ({ all, yes } = {}) => { const actions = { functions: pushFunction, collections: pushCollection, buckets: pushBucket, teams: pushTeam, - messages: new Function() + messages: pushMessagingTopic } if (all) { @@ -1006,6 +1000,11 @@ 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") From 3e9d58c033fd0692a82518d722116e252ee12207 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 07:44:46 -0400 Subject: [PATCH 024/198] refactoring(cli): deploy to push --- templates/cli/lib/commands/push.js.twig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index ac30c989a..049aaa079 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -255,7 +255,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { } if (functionIds.length <= 0) { - const answers = await inquirer.prompt(questionsDeployFunctions[0]); + const answers = await inquirer.prompt(questionsPushFunctions[0]); functionIds.push(...answers.functions); } @@ -271,7 +271,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { }); log('Validating functions'); - // Validation is done BEFORE deploying so the deployment process can be run in async with progress update + // Validation is done BEFORE pushing so the deployment process can be run in async with progress update for (let i = 0; i < functions.length; i++) { const func = functions[i]; @@ -283,7 +283,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { } if (func.variables) { - func.deployVariables = yes; + func.pushVariables = yes; try { const { total } = await functionsListVariables({ @@ -293,11 +293,11 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { }); if (total === 0) { - func.deployVariables = true; - } else if (total > 0 && !func.deployVariables) { + func.pushVariables = true; + } else if (total > 0 && !func.pushVariables) { log(`The function ${func.name} has remote variables setup`); - const variableAnswers = await inquirer.prompt(questionsDeployFunctions[1]) - func.deployVariables = variableAnswers.override.toLowerCase() === "yes"; + const variableAnswers = await inquirer.prompt(questionsPushFunctions[1]) + func.pushVariables = variableAnswers.override.toLowerCase() === "yes"; } } catch (e) { if (e.code != 404) { @@ -312,7 +312,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { log('Pushing functions\n'); Updater.start(false,) - let successfullyDeployed = 0; + let successfullyPushed = 0; await Promise.all(functions.map(async (func) => { const ignore = func.ignore ? 'appwrite.json' : '.gitignore'; @@ -391,7 +391,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { } if (func.variables) { - if (!func.deployVariables) { + if (!func.pushVariables) { updaterRow.update({ end: 'Skipping variables' }); } else { updaterRow.update({ end: 'Pushing variables' }); @@ -415,7 +415,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { return; } - // Deploy local variables + // Push local variables await Promise.all(Object.keys(func.variables).map(async localVariableKey => { await functionsCreateVariable({ functionId: func['$id'], @@ -439,7 +439,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { }) updaterRow.update({ status: 'Pushed' }) - successfullyDeployed++; + successfullyPushed++; } catch (e) { switch (e.code) { @@ -455,7 +455,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { Updater.stop(); - success(`Pushed ${successfullyDeployed} functions`); + success(`Pushed ${successfullyPushed} functions`); } const createAttribute = async (databaseId, collectionId, attribute) => { From 3868a8073c4dcfeea419b37afe079cc5b38511c4 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 07:52:12 -0400 Subject: [PATCH 025/198] refactoring(cli): conventions and messages --- templates/cli/lib/commands/deploy.js.twig | 62 +++++++++++------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/templates/cli/lib/commands/deploy.js.twig b/templates/cli/lib/commands/deploy.js.twig index 3966fd105..846c4fea6 100644 --- a/templates/cli/lib/commands/deploy.js.twig +++ b/templates/cli/lib/commands/deploy.js.twig @@ -40,14 +40,14 @@ const { } = require("./teams"); const STEP_SIZE = 100; // Resources -const POOL_DEBOUNCE = 2000; // Milliseconds -const POOL_MAX_DEBOUNCE = 30; // Times +const POLL_DEBOUNCE = 2000; // Milliseconds +const POLL_MAX_DEBOUNCE = 30; // Times -let poolMaxDebounces = 30; +let pollMaxDebounces = 30; const awaitPools = { wipeAttributes: async (databaseId, collectionId, iteration = 1) => { - if (iteration > poolMaxDebounces) { + if (iteration > pollMaxDebounces) { return false; } @@ -64,12 +64,12 @@ const awaitPools = { let steps = Math.max(1, Math.ceil(total / STEP_SIZE)); if (steps > 1 && iteration === 1) { - poolMaxDebounces *= steps; + pollMaxDebounces *= steps; - log('Found a large number of attributes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + log('Found a large number of attributes, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') } - await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); return await awaitPools.wipeAttributes( databaseId, @@ -78,7 +78,7 @@ const awaitPools = { ); }, wipeIndexes: async (databaseId, collectionId, iteration = 1) => { - if (iteration > poolMaxDebounces) { + if (iteration > pollMaxDebounces) { return false; } @@ -95,12 +95,12 @@ const awaitPools = { let steps = Math.max(1, Math.ceil(total / STEP_SIZE)); if (steps > 1 && iteration === 1) { - poolMaxDebounces *= steps; + pollMaxDebounces *= steps; - log('Found a large number of indexes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + log('Found a large number of indexes, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') } - await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); return await awaitPools.wipeIndexes( databaseId, @@ -109,7 +109,7 @@ const awaitPools = { ); }, wipeVariables: async (functionId, iteration = 1) => { - if (iteration > poolMaxDebounces) { + if (iteration > pollMaxDebounces) { return false; } @@ -125,12 +125,12 @@ const awaitPools = { let steps = Math.max(1, Math.ceil(total / STEP_SIZE)); if (steps > 1 && iteration === 1) { - poolMaxDebounces *= steps; + pollMaxDebounces *= steps; - log('Found a large number of variables, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + log('Found a large number of variables, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') } - await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); return await awaitPools.wipeVariables( functionId, @@ -138,15 +138,15 @@ const awaitPools = { ); }, expectAttributes: async (databaseId, collectionId, attributeKeys, iteration = 1) => { - if (iteration > poolMaxDebounces) { + if (iteration > pollMaxDebounces) { return false; } let steps = Math.max(1, Math.ceil(attributeKeys.length / STEP_SIZE)); if (steps > 1 && iteration === 1) { - poolMaxDebounces *= steps; + pollMaxDebounces *= steps; - log('Creating a large number of attributes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + log('Creating a large number of attributes, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') } const { attributes } = await paginate(databasesListAttributes, { @@ -173,7 +173,7 @@ const awaitPools = { return true; } - await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); return await awaitPools.expectAttributes( databaseId, @@ -183,15 +183,15 @@ const awaitPools = { ); }, expectIndexes: async (databaseId, collectionId, indexKeys, iteration = 1) => { - if (iteration > poolMaxDebounces) { + if (iteration > pollMaxDebounces) { return false; } let steps = Math.max(1, Math.ceil(indexKeys.length / STEP_SIZE)); if (steps > 1 && iteration === 1) { - poolMaxDebounces *= steps; + pollMaxDebounces *= steps; - log('Creating a large number of indexes, increasing timeout to ' + (poolMaxDebounces * POOL_DEBOUNCE / 1000 / 60) + ' minutes') + log('Creating a large number of indexes, increasing timeout to ' + (pollMaxDebounces * POLL_DEBOUNCE / 1000 / 60) + ' minutes') } const { indexes } = await paginate(databasesListIndexes, { @@ -218,7 +218,7 @@ const awaitPools = { return true; } - await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); return await awaitPools.expectIndexes( databaseId, @@ -458,11 +458,11 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { try { const deploymentId = response['$id']; updaterRow.update({ status: 'Deploying', end: 'Checking deployment status...' }) - let pool_checks = 0; + let pollChecks = 0; while (true) { - if (pool_checks >= POOL_MAX_DEBOUNCE) { - updaterRow.update({ end: 'Deployment takes too long; check the console' }) + if (pollChecks >= POLL_MAX_DEBOUNCE) { + updaterRow.update({ end: 'Deployment is taking too long. Please check the console for more details.' }) break; } @@ -480,18 +480,18 @@ const deployFunction = async ({ functionId, all, yes } = {}) => { break; } else if (status === 'failed') { - updaterRow.fail({ errorMessage: 'failed to deploy' }); + updaterRow.fail({ errorMessage: 'Failed to deploy.' }); break; } else { - updaterRow.update({ status: 'Deploying', end: `current status: ${status}` }) + updaterRow.update({ status: 'Deploying', end: `Current status: ${status}` }) } - pool_checks++; - await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); + pollChecks++; + await new Promise(resolve => setTimeout(resolve, POLL_DEBOUNCE)); } } catch (e) { - updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }) + updaterRow.fail({ errorMessage: e.message ?? 'Unknown error occurred. Please try again' }) } } From 1fb5d09a60b19f1342f817a384e95e1800b0bbd3 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 08:02:25 -0400 Subject: [PATCH 026/198] refactoring(cli): conventions and messages --- templates/cli/lib/commands/push.js.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 14545e7a6..2585840de 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -312,9 +312,9 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { log('All functions are validated'); log('Pushing functions\n'); - Updater.start(false,) - let successfullyPushed = 0; + Updater.start(false); let successfullyPushed = 0; + let successfullyDeployed = 0; await Promise.all(functions.map(async (func) => { const ignore = func.ignore ? 'appwrite.json' : '.gitignore'; From 2029e8a90d49920aa8abdf1ccfa5909e6a3073fe Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 09:33:20 -0400 Subject: [PATCH 027/198] feat(cli): Split project creation and pulling --- src/SDK/Language/CLI.php | 5 +++ templates/cli/index.js.twig | 2 + templates/cli/lib/commands/create.js.twig | 41 +++++++++++++++++++ templates/cli/lib/commands/pull.js.twig | 25 +----------- templates/cli/lib/parser.js.twig | 1 + templates/cli/lib/questions.js.twig | 50 +++++++---------------- 6 files changed, 65 insertions(+), 59 deletions(-) create mode 100644 templates/cli/lib/commands/create.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index adc4ec214..9ee58a8b5 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -167,6 +167,11 @@ public function getFiles(): array 'destination' => 'lib/utils.js', 'template' => 'cli/lib/utils.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/commands/create.js', + 'template' => 'cli/lib/commands/create.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/commands/pull.js', diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 47725d266..9e5e9d714 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,6 +12,7 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} const { login, logout } = require("./lib/commands/generic"); +const { create } = require("./lib/commands/create"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} @@ -37,6 +38,7 @@ program .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(login) + .addCommand(create) .addCommand(pull) .addCommand(push) .addCommand(logout) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig new file mode 100644 index 000000000..339a21ace --- /dev/null +++ b/templates/cli/lib/commands/create.js.twig @@ -0,0 +1,41 @@ +const { Command } = require("commander"); +const inquirer = require("inquirer"); +const { projectsCreate } = require("./projects"); +const { sdkForConsole } = require("../sdks"); +const { localConfig } = require("../config"); +const { questionsCreateProject } = require("../questions"); +const { success, actionRunner, commandDescriptions } = require("../parser"); + +const create = new Command("create") + .description(commandDescriptions['create']) + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(async (_options, command) => { + command.help(); + })); + +const createProject = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateProject) + if (!answers.project || !answers.organization) process.exit(1) + + response = await projectsCreate({ + projectId: answers.id, + name: answers.project, + teamId: answers.organization.id, + parseOutput: false + }) + + localConfig.setProject(response['$id'], response.name); + success(); +} + +create + .command("project") + .description("Create a new {{ spec.title|caseUcfirst }} project") + .action(actionRunner(createProject)); + +module.exports = { + create, +}; diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 2c1bad6c9..a4e99161d 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -10,7 +10,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 { success, log, actionRunner, commandDescriptions } = require("../parser"); @@ -25,31 +25,10 @@ const pull = new Command("pull") })); const pullProject = async () => { - let response = {} const answers = await inquirer.prompt(questionsPullProject) if (!answers.project) process.exit(1) - let sdk = await sdkForConsole(); - if (answers.start === "new") { - response = await teamsCreate({ - teamId: 'unique()', - name: answers.project, - sdk, - parseOutput: false - }) - - let teamId = response['$id']; - response = await projectsCreate({ - projectId: answers.id, - name: answers.project, - teamId, - parseOutput: false - }) - - localConfig.setProject(response['$id'], response.name); - } else { - localConfig.setProject(answers.project.id, answers.project.name); - } + localConfig.setProject(answers.project.id, answers.project.name); success(); } diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 5dde5aa3e..cf94b3a8b 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -156,6 +156,7 @@ const commandDescriptions = { "graphql": `The graphql command allows you to query and mutate any resource type on your Appwrite server.`, "avatars": `The avatars command aims to help you complete everyday tasks related to your app image, icons, and avatars.`, "databases": `The databases command allows you to create structured collections of documents, query and filter lists of documents.`, + "create": `The create command provides a convenient wrapper for creating projects functions, collections, buckets, teams and messaging.`, "push": `The push command provides a convenient wrapper for pushing your functions, collections, buckets, teams and messaging.`, "functions": `The functions command allows you view, create and manage your Cloud Functions.`, "health": `The health command allows you to both validate and monitor your {{ spec.title|caseUcfirst }} server's health.`, diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 11b203dff..31fec7192 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -110,7 +110,7 @@ const getInstallCommand = (runtime) => { return undefined; }; -const questionsPullProject = [ +const questionsProject = [ { type: "confirm", name: "override", @@ -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 { @@ -145,56 +145,33 @@ const questionsPullProject = [ return choices; } }, - { - 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", - }, - ], - }, +]; + +const questionsCreateProject = [ + ...questionsProject, { type: "input", name: "project", message: "What would you like to name your project?", - default: "My Awesome Project", - when(answers) { - return answers.start == "new"; - }, + default: "My Awesome Project" }, { type: "input", name: "id", message: "What ID would you like to have for your project?", - default: "unique()", - when(answers) { - return answers.start == "new"; - }, - }, + default: "unique()" + } +]; +const questionsPullProject = [ + ...questionsProject, { type: "list", name: "project", message: "Choose your {{ spec.title|caseUcfirst }} project.", - when(answers) { - return answers.start == "existing"; - }, 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) => { @@ -476,6 +453,7 @@ const questionsMfaChallenge = [ ]; module.exports = { + questionsCreateProject, questionsPullProject, questionsLogin, questionsPullFunction, From 75a01c83c9ef127473a4ea2d7bb08e05c2c372dc Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 10:48:55 -0400 Subject: [PATCH 028/198] feat(cli): Interactive bucket creation --- templates/cli/lib/commands/create.js.twig | 59 ++++++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig index 339a21ace..a764ac988 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/create.js.twig @@ -1,10 +1,11 @@ const { Command } = require("commander"); const inquirer = require("inquirer"); const { projectsCreate } = require("./projects"); +const { storageCreateBucket } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); -const { questionsCreateProject } = require("../questions"); -const { success, actionRunner, commandDescriptions } = require("../parser"); +const { questionsCreateProject, questionsCreateBucket } = require("../questions"); +const { success, error, actionRunner, commandDescriptions } = require("../parser"); const create = new Command("create") .description(commandDescriptions['create']) @@ -31,11 +32,65 @@ const createProject = async () => { success(); } + +const createBucket = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateBucket) + if (!answers.bucket || !answers.id || !answers.fileSecurity) process.exit(1) + + 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'); + } +}; + +const createFunction = async () => { + +}; + +const createCollection = async () => { + +}; + +const createTopic = async () => { + +}; + create .command("project") .description("Create a new {{ spec.title|caseUcfirst }} project") .action(actionRunner(createProject)); +create + .command("function") + .description("Create a new {{ spec.title|caseUcfirst }} function") + .action(actionRunner(createFunction)); + +create + .command("bucket") + .description("Create a new {{ spec.title|caseUcfirst }} bucket") + .action(actionRunner(createBucket)); + +create + .command("collection") + .description("Create a new {{ spec.title|caseUcfirst }} collection") + .action(actionRunner(createCollection)); + +create + .command("topic") + .description("Create a new {{ spec.title|caseUcfirst }} topic") + .action(actionRunner(createTopic)); + module.exports = { create, }; From 05df5dd61c031e3210ea879a7a65cdb09ea8dfb6 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 10:55:23 -0400 Subject: [PATCH 029/198] feat(cli): Interactive messaging-topic creation --- templates/cli/lib/commands/create.js.twig | 29 +++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig index a764ac988..b48be56c5 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/create.js.twig @@ -2,9 +2,10 @@ const { Command } = require("commander"); const inquirer = require("inquirer"); const { projectsCreate } = require("./projects"); const { storageCreateBucket } = require("./storage"); +const { messagingCreateTopic } = require("./messaging"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); -const { questionsCreateProject, questionsCreateBucket } = require("../questions"); +const { questionsCreateProject, questionsCreateBucket, questionsCreateMessagingTopic } = require("../questions"); const { success, error, actionRunner, commandDescriptions } = require("../parser"); const create = new Command("create") @@ -32,7 +33,6 @@ const createProject = async () => { success(); } - const createBucket = async () => { let response = {} const answers = await inquirer.prompt(questionsCreateBucket) @@ -54,18 +54,37 @@ const createBucket = async () => { } }; -const createFunction = async () => { +const createCollection = async () => { }; -const createCollection = async () => { +const createTopic = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateMessagingTopic) + if (!answers.topic || !answers.id) process.exit(1) + + try { + response = await messagingCreateTopic({ + topicId: answers.id, + name: answers.topic, + parseOutput: false + }) + {#localConfig.addMessagingTopic(response);#} + success(); + } catch (e) { + error(e.getMessage ?? 'Unknown error occurred. Please try again'); + } }; -const createTopic = async () => { +const createFunction = async () => { }; + + + + create .command("project") .description("Create a new {{ spec.title|caseUcfirst }} project") From 01d7bc586244205f7f8ef6721b87cccfb35dd342 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 11:01:32 -0400 Subject: [PATCH 030/198] feat(cli): Interactive collection creation --- templates/cli/lib/commands/create.js.twig | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig index b48be56c5..aabebf280 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/create.js.twig @@ -3,9 +3,10 @@ const inquirer = require("inquirer"); const { projectsCreate } = require("./projects"); const { storageCreateBucket } = require("./storage"); const { messagingCreateTopic } = require("./messaging"); +const { databasesCreateCollection } = require("./databases"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); -const { questionsCreateProject, questionsCreateBucket, questionsCreateMessagingTopic } = require("../questions"); +const { questionsCreateProject, questionsCreateBucket, questionsCreateMessagingTopic, questionsCreateCollection } = require("../questions"); const { success, error, actionRunner, commandDescriptions } = require("../parser"); const create = new Command("create") @@ -55,7 +56,25 @@ const createBucket = async () => { }; const createCollection = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateCollection) + if (!answers.database || !answers.collection || !answers.id || !answers.documentSecurity) process.exit(1) + + try { + response = await databasesCreateCollection({ + databaseId: answers.database, + collectionId: answers.id, + name: answers.collection, + documentSecurity: answers.documentSecurity.toLowerCase() === 'yes', + enabled: true, + parseOutput: false + }) + localConfig.addCollection(response); + success(); + } catch (e) { + error(e.getMessage ?? 'Unknown error occurred. Please try again'); + } }; const createTopic = async () => { @@ -70,7 +89,7 @@ const createTopic = async () => { parseOutput: false }) - {#localConfig.addMessagingTopic(response);#} + {# localConfig.addMessagingTopic(response); #} success(); } catch (e) { error(e.getMessage ?? 'Unknown error occurred. Please try again'); From e2e80e6f233e8928cc62128ff2a9b5df0be97518 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 11:03:04 -0400 Subject: [PATCH 031/198] feat(cli): Interactive questions --- templates/cli/lib/questions.js.twig | 87 ++++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 31fec7192..a52c11555 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -162,6 +162,86 @@ const questionsCreateProject = [ default: "unique()" } ]; + +const questionsCreateBucket = [ + { + type: "input", + name: "bucket", + message: "What would you like to name your bucket?", + default: "My Awesome Bucket" + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your bucket?", + default: "unique()" + }, + { + type: "list", + name: "fileSecurity", + message: "Enable File-Security configuring permissions for individual file", + choices: ["No", "Yes"] + } +]; + +const questionsCreateCollection = [ + { + type: "list", + name: "database", + message: "Choose the collection database", + choices: async () => { + const { databases } = await paginate(databasesList, { parseOutput: false }, 100, 'databases'); + + let choices = databases.map((database, idx) => { + return { + name: `${database.name} (${database.$id})`, + value: database.$id + } + }) + + if (choices.length === 0) { + throw new Error("No databases found. Please create one in project console.") + } + + return choices; + } + }, + { + type: "input", + name: "collection", + message: "What would you like to name your collection?", + default: "My Awesome Collection" + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your collection?", + default: "unique()" + }, + { + type: "list", + name: "documentSecurity", + message: "Enable Document-Security for configuring permissions for individual documents", + choices: ["No", "Yes"] + } +]; + +const questionsCreateMessagingTopic = [ + { + type: "input", + name: "topic", + message: "What would you like to name your messaging topic?", + default: "My Awesome Topic" + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your messaging topic?", + default: "unique()" + } +]; + + const questionsPullProject = [ ...questionsProject, { @@ -222,7 +302,7 @@ const questionsPullFunction = [ id: runtime['$id'], entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), - commands : getInstallCommand(runtime['$id']) + commands: getInstallCommand(runtime['$id']) }, } }) @@ -431,7 +511,7 @@ const questionsListFactors = [ name: `Recovery code`, value: 'recoveryCode' } - ].filter((ch) => factors[ch.value] === true); + ].filter((ch) => factors[ch.value] === true); return choices; } @@ -454,6 +534,9 @@ const questionsMfaChallenge = [ module.exports = { questionsCreateProject, + questionsCreateBucket, + questionsCreateCollection, + questionsCreateMessagingTopic, questionsPullProject, questionsLogin, questionsPullFunction, From d4b2c3319756c05949d5451e598c99238c725b25 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 11:06:27 -0400 Subject: [PATCH 032/198] refactor(cli): Function `pull` to `create` --- templates/cli/lib/commands/create.js.twig | 118 ++++++++++++++++++++- templates/cli/lib/commands/pull.js.twig | 121 +--------------------- templates/cli/lib/questions.js.twig | 78 +++++++------- 3 files changed, 156 insertions(+), 161 deletions(-) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig index aabebf280..9f96d9275 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/create.js.twig @@ -1,12 +1,22 @@ +const fs = require("fs"); +const path = require("path"); +const childProcess = require('child_process'); const { Command } = require("commander"); const inquirer = require("inquirer"); const { projectsCreate } = require("./projects"); const { storageCreateBucket } = require("./storage"); const { messagingCreateTopic } = require("./messaging"); +const { functionsCreate } = require("./functions"); const { databasesCreateCollection } = require("./databases"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); -const { questionsCreateProject, questionsCreateBucket, questionsCreateMessagingTopic, questionsCreateCollection } = require("../questions"); +const { + questionsCreateProject, + questionsCreateFunction, + questionsCreateBucket, + questionsCreateMessagingTopic, + questionsCreateCollection +} = require("../questions"); const { success, error, actionRunner, commandDescriptions } = require("../parser"); const create = new Command("create") @@ -97,10 +107,114 @@ const createTopic = async () => { }; const createFunction = async () => { + // TODO: Add CI/CD support (ID, name, runtime) + const answers = await inquirer.prompt(questionsCreateFunction) + const functionFolder = path.join(process.cwd(), 'functions'); + + if (!fs.existsSync(functionFolder)) { + fs.mkdirSync(functionFolder, { + recursive: true + }); + } -}; + const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; + const functionDir = path.join(functionFolder, functionId); + + if (fs.existsSync(functionDir)) { + throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); + } + + if (!answers.runtime.entrypoint) { + log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first push the function.`); + } + + if (!answers.runtime.commands) { + 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"); + + let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched + + let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; + + /* Force use CMD as powershell does not support && */ + if (process.platform === 'win32') { + gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; + gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; + } + /* Execute the child process but do not print any std output */ + try { + childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: functionDir }); + childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: functionDir }); + } catch (error) { + /* Specialised errors with recommended actions to take */ + if (error.message.includes('error: unknown option')) { + throw new Error(`${error.message} \n\nSuggestion: Try updating your git to the latest version, then trying to run this command again.`) + } else if (error.message.includes('is not recognized as an internal or external command,') || error.message.includes('command not found')) { + throw new Error(`${error.message} \n\nSuggestion: It appears that git is not installed, try installing git then trying to run this command again.`) + } else { + throw error; + } + } + fs.rmSync(path.join(functionDir, ".git"), { recursive: true }); + const copyRecursiveSync = (src, dest) => { + let exists = fs.existsSync(src); + let stats = exists && fs.statSync(src); + let isDirectory = exists && stats.isDirectory(); + if (isDirectory) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest); + } + + fs.readdirSync(src).forEach(function (childItemName) { + copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); + }); + } else { + fs.copyFileSync(src, dest); + } + }; + copyRecursiveSync(path.join(functionDir, answers.runtime.id), functionDir); + + fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); + + const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); + const readmeFile = fs.readFileSync(readmePath).toString(); + const newReadmeFile = readmeFile.split('\n'); + newReadmeFile[0] = `# ${answers.name}`; + newReadmeFile.splice(1, 2); + 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, + ignore: answers.runtime.ignore || null, + path: `functions/${functionId}`, + }; + + localConfig.addFunction(data); + success(); +} diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index a4e99161d..84a865b80 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -1,18 +1,14 @@ -const fs = require("fs"); -const path = require("path"); -const childProcess = require('child_process'); const { Command } = require("commander"); const inquirer = require("inquirer"); const { teamsCreate, teamsList } = require("./teams"); const { projectsCreate } = require("./projects"); -const { functionsCreate } = require("./functions"); const { databasesGet, databasesListCollections, databasesList } = require("./databases"); const { storageListBuckets } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); const ID = require("../id"); const { paginate } = require("../paginate"); -const { questionsPullProject, questionsPullFunction, questionsPullCollection } = require("../questions"); +const { questionsPullProject, questionsPullCollection } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); const pull = new Command("pull") @@ -32,116 +28,6 @@ const pullProject = async () => { success(); } -const pullFunction = async () => { - // TODO: Add CI/CD support (ID, name, runtime) - const answers = await inquirer.prompt(questionsPullFunction) - const functionFolder = path.join(process.cwd(), 'functions'); - - if (!fs.existsSync(functionFolder)) { - fs.mkdirSync(functionFolder, { - recursive: true - }); - } - - const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; - const functionDir = path.join(functionFolder, functionId); - - if (fs.existsSync(functionDir)) { - throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); - } - - if (!answers.runtime.entrypoint) { - log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first push the function.`); - } - - if (!answers.runtime.commands) { - 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"); - - let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched - - let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; - - /* Force use CMD as powershell does not support && */ - if (process.platform === 'win32') { - gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; - gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; - } - - /* Execute the child process but do not print any std output */ - try { - childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: functionDir }); - childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: functionDir }); - } catch (error) { - /* Specialised errors with recommended actions to take */ - if (error.message.includes('error: unknown option')) { - throw new Error(`${error.message} \n\nSuggestion: Try updating your git to the latest version, then trying to run this command again.`) - } else if (error.message.includes('is not recognized as an internal or external command,') || error.message.includes('command not found')) { - throw new Error(`${error.message} \n\nSuggestion: It appears that git is not installed, try installing git then trying to run this command again.`) - } else { - throw error; - } - } - - fs.rmSync(path.join(functionDir, ".git"), { recursive: true }); - const copyRecursiveSync = (src, dest) => { - let exists = fs.existsSync(src); - let stats = exists && fs.statSync(src); - let isDirectory = exists && stats.isDirectory(); - if (isDirectory) { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest); - } - - fs.readdirSync(src).forEach(function (childItemName) { - copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); - }); - } else { - fs.copyFileSync(src, dest); - } - }; - copyRecursiveSync(path.join(functionDir, answers.runtime.id), functionDir); - - fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); - - const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); - const readmeFile = fs.readFileSync(readmePath).toString(); - const newReadmeFile = readmeFile.split('\n'); - newReadmeFile[0] = `# ${answers.name}`; - newReadmeFile.splice(1, 2); - 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, - ignore: answers.runtime.ignore || null, - path: `functions/${functionId}`, - }; - - localConfig.addFunction(data); - success(); -} - const pullCollection = async ({ all, databaseId } = {}) => { const databaseIds = []; @@ -221,11 +107,6 @@ pull .description("Pulling your {{ spec.title|caseUcfirst }} project") .action(actionRunner(pullProject)); -pull - .command("function") - .description("Pulling your {{ spec.title|caseUcfirst }} cloud function") - .action(actionRunner(pullFunction)) - pull .command("collection") .description("Pulling your {{ spec.title|caseUcfirst }} collections") diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index a52c11555..b8f8724aa 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -163,6 +163,44 @@ const questionsCreateProject = [ } ]; +const questionsCreateFunction = [ + { + type: "input", + name: "name", + message: "What would you like to name your function?", + default: "My Awesome Function" + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your function?", + default: "unique()" + }, + { + type: "list", + name: "runtime", + message: "What runtime would you like to use?", + choices: async () => { + let response = await functionsListRuntimes({ + parseOutput: false + }) + let runtimes = response["runtimes"] + let choices = runtimes.map((runtime, idx) => { + return { + name: `${runtime.name} (${runtime['$id']})`, + value: { + id: runtime['$id'], + entrypoint: getEntrypoint(runtime['$id']), + ignore: getIgnores(runtime['$id']), + commands: getInstallCommand(runtime['$id']) + }, + } + }) + return choices; + } + } +]; + const questionsCreateBucket = [ { type: "input", @@ -273,44 +311,6 @@ const questionsPullProject = [ } ]; -const questionsPullFunction = [ - { - type: "input", - name: "name", - message: "What would you like to name your function?", - default: "My Awesome Function" - }, - { - type: "input", - name: "id", - message: "What ID would you like to have for your function?", - default: "unique()" - }, - { - type: "list", - name: "runtime", - message: "What runtime would you like to use?", - choices: async () => { - let response = await functionsListRuntimes({ - parseOutput: false - }) - let runtimes = response["runtimes"] - let choices = runtimes.map((runtime, idx) => { - return { - name: `${runtime.name} (${runtime['$id']})`, - value: { - id: runtime['$id'], - entrypoint: getEntrypoint(runtime['$id']), - ignore: getIgnores(runtime['$id']), - commands: getInstallCommand(runtime['$id']) - }, - } - }) - return choices; - } - } -]; - const questionsPullCollection = [ { type: "checkbox", @@ -534,12 +534,12 @@ const questionsMfaChallenge = [ module.exports = { questionsCreateProject, + questionsCreateFunction, questionsCreateBucket, questionsCreateCollection, questionsCreateMessagingTopic, questionsPullProject, questionsLogin, - questionsPullFunction, questionsPullCollection, questionsPushFunctions, questionsPushCollections, From c90817e038ba808981b4c388a93ba6f10f82c10d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 11:07:29 -0400 Subject: [PATCH 033/198] chore(cli): Marking as todo --- templates/cli/lib/commands/create.js.twig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig index 9f96d9275..418879b14 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/create.js.twig @@ -99,7 +99,8 @@ const createTopic = async () => { parseOutput: false }) - {# localConfig.addMessagingTopic(response); #} + // TODO: after https://github.com/appwrite/sdk-generator/pull/839 + // localConfig.addMessagingTopic(response); success(); } catch (e) { error(e.getMessage ?? 'Unknown error occurred. Please try again'); From 3a47c6625cdc737b320514dc0973ff2f4c513758 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 13:17:07 -0400 Subject: [PATCH 034/198] feat(cli): Pulling function code and definition --- templates/cli/base/params.twig | 16 +++-- templates/cli/base/requests/api.twig | 8 ++- templates/cli/lib/commands/command.js.twig | 5 +- templates/cli/lib/commands/pull.js.twig | 76 ++++++++++++++++++++-- templates/cli/lib/questions.js.twig | 29 ++++++++- 5 files changed, 117 insertions(+), 17 deletions(-) diff --git a/templates/cli/base/params.twig b/templates/cli/base/params.twig index e36ae2f9c..d0a8b685e 100644 --- a/templates/cli/base/params.twig +++ b/templates/cli/base/params.twig @@ -11,7 +11,7 @@ if (!fs.lstatSync(folderPath).isDirectory()) { throw new Error('The path is not a directory.'); } - + const ignorer = ignore(); const func = localConfig.getFunction(functionId); @@ -23,7 +23,7 @@ ignorer.add(fs.readFileSync(pathLib.join({{ parameter.name | caseCamel | escapeKeyword }}, '.gitignore')).toString()); log('Ignoring files in .gitignore'); } - + const files = getAllFiles({{ parameter.name | caseCamel | escapeKeyword }}).map((file) => pathLib.relative({{ parameter.name | caseCamel | escapeKeyword }}, file)).filter((file) => !ignorer.ignores(file)); await tar @@ -77,8 +77,10 @@ {% endif %} {% endfor %} {% if method.type == 'location' %} - payload['project'] = localConfig.getProject().projectId - payload['key'] = globalConfig.getKey(); - const queryParams = new URLSearchParams(payload); - apiPath = `${globalConfig.getEndpoint()}${apiPath}?${queryParams.toString()}`; -{% endif %} \ No newline at end of file + if (!overrideForCli) { + payload['project'] = localConfig.getProject().projectId + payload['key'] = globalConfig.getKey(); + const queryParams = new URLSearchParams(payload); + apiPath = `${globalConfig.getEndpoint()}${apiPath}?${queryParams.toString()}`; + } +{% endif %} diff --git a/templates/cli/base/requests/api.twig b/templates/cli/base/requests/api.twig index c8e97c064..608e10980 100644 --- a/templates/cli/base/requests/api.twig +++ b/templates/cli/base/requests/api.twig @@ -10,6 +10,10 @@ }, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); {% if method.type == 'location' %} + if (overrideForCli) { + response = Buffer.from(response); + } + fs.writeFileSync(destination, response); {% endif %} @@ -17,5 +21,5 @@ parse(response) success() } - - return response; \ No newline at end of file + + return response; diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 3607fb3d3..7ad257951 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -45,6 +45,7 @@ const {{ service.name | caseLower }} = new Command("{{ service.name | caseLower {% for parameter in method.parameters.all %} * @property {{ "{" }}{{ parameter | typeName }}{{ "}" }} {{ parameter.name | caseCamel | escapeKeyword }} {{ parameter.description | replace({'`':'\''}) | replace({'\n':' '}) | replace({'\n \n':' '}) }} {% endfor %} + * @property {boolean} overrideForCli * @property {boolean} parseOutput * @property {libClient | undefined} sdk {% if 'multipart/form-data' in method.consumes %} @@ -58,7 +59,7 @@ const {{ service.name | caseLower }} = new Command("{{ service.name | caseLower /** * @param {{ "{" }}{{ service.name | caseUcfirst }}{{ method.name | caseUcfirst }}RequestParams{{ "}" }} params */ -const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %}parseOutput = true, sdk = undefined{% if 'multipart/form-data' in method.consumes %}, onProgress = () => {}{% endif %}{% if method.type == 'location' %}, destination{% endif %}}) => { +const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %}parseOutput = true, overrideForCli = false, sdk = undefined{% if 'multipart/form-data' in method.consumes %}, onProgress = () => {}{% endif %}{% if method.type == 'location' %}, destination{% endif %}}) => { let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : sdk; let apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; {{ include ('cli/base/params.twig') }} @@ -90,4 +91,4 @@ module.exports = { {{ service.name | caseLower }}{{ method.name | caseUcfirst }}{% if not loop.last %},{% endif %} {% endfor %} -}; \ No newline at end of file +}; diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 84a865b80..457981372 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -1,14 +1,17 @@ +const fs = require("fs"); +const tar = require("tar"); const { Command } = require("commander"); const inquirer = require("inquirer"); const { teamsCreate, teamsList } = require("./teams"); const { projectsCreate } = require("./projects"); +const { functionsList, functionsDownloadDeployment } = require("./functions"); const { databasesGet, databasesListCollections, databasesList } = require("./databases"); const { storageListBuckets } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); const ID = require("../id"); const { paginate } = require("../paginate"); -const { questionsPullProject, questionsPullCollection } = require("../questions"); +const { questionsPullProject, questionsPullCollection, questionsPullFunctions } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); const pull = new Command("pull") @@ -28,6 +31,64 @@ const pullProject = async () => { success(); } +const pullFunctions = async ({ all, yes } = {}) => { + let functions = []; + let questions = questionsPullFunctions; + + const localFunctions = localConfig.getFunctions(); + + if (all) { + questions = yes ? [] : questionsPullFunctions[1]; + functions = (await paginate(functionsList, { parseOutput: false }, 100, 'functions')).functions; + } + + const answers = await inquirer.prompt(questions); + + const overridingLocalChanges = yes ?? answers.override.toLowerCase() === "yes"; + const selectedFunctions = functions.length === 0 ? answers.functions : functions; + + for (let func of selectedFunctions) { + const functionExistLocally = localFunctions.find((localFunc) => localFunc['$id'] === func['$id']) !== undefined; + + if (!overridingLocalChanges && functionExistLocally) { + log(`Skipping locally found implementation of ${func['name']}`) + continue; + } + if (functionExistLocally) { + localConfig.updateFunction(func['$id'], func); + } else { + func['path'] = `functions/${func['$id']}`; + localConfig.addFunction(func); + } + + const localFunction = localFunctions.find((localFunc) => localFunc['$id'] === func['$id']); + + if (localFunction['deployment'] === '') { + continue + } + + const compressedFileName = `${+new Date()}.tar.gz` + + await functionsDownloadDeployment({ + functionId: func['$id'], + deploymentId: func['deployment'], + destination: compressedFileName, + overrideForCli: true, + parseOutput: false + }) + + tar.extract({ + sync: true, + cwd: localFunction['path'], + file: compressedFileName, + strict: false, + }); + + fs.rmSync(compressedFileName); + success(`Pulled ${func['name']} code and definition`) + } +} + const pullCollection = async ({ all, databaseId } = {}) => { const databaseIds = []; @@ -104,14 +165,21 @@ const pullTeam = async () => { pull .command("project") - .description("Pulling your {{ spec.title|caseUcfirst }} project") + .description("Pulling your Appwrite project") .action(actionRunner(pullProject)); +pull + .command("function") + .description(`Pulling your Appwrite functions`) + .option(`--yes`, `Flag to confirm all warnings`) + .option(`--all`, `Flag to pull all functions`) + .action(actionRunner(pullFunctions)); + pull .command("collection") - .description("Pulling your {{ spec.title|caseUcfirst }} collections") + .description("Pulling your Appwrite collections") .option(`--databaseId `, `Database ID`) - .option(`--all`, `Flag to pullialize all databases`) + .option(`--all`, `Flag to pull all databases`) .action(actionRunner(pullCollection)) pull diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index b8f8724aa..491d0bac5 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -1,12 +1,12 @@ const { localConfig } = require('./config'); const { projectsList } = require('./commands/projects'); const { teamsList } = require('./commands/teams'); -const { functionsListRuntimes } = require('./commands/functions'); +const { functionsListRuntimes, functionsList } = require('./commands/functions'); 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 JSONbig = require("json-bigint")({ storeAsString: false }); @@ -311,6 +311,30 @@ const questionsPullProject = [ } ]; + +const questionsPullFunctions = [ + { + type: "checkbox", + name: "functions", + message: "Which functions would you like to pull?", + choices: async () => { + const { functions } = await paginate(functionsList, { parseOutput: false }, 100, 'functions'); + + return functions.map(func => { + return { + name: `${func.name} (${func.$id})`, + value: func + } + }); + } + }, + { + type: "input", + name: "override", + message: `Do you want to override local functions code and definition? ${chalk.red('all local changes will lost!')} Type "YES" to confirm.` + } +]; + const questionsPullCollection = [ { type: "checkbox", @@ -539,6 +563,7 @@ module.exports = { questionsCreateCollection, questionsCreateMessagingTopic, questionsPullProject, + questionsPullFunctions, questionsLogin, questionsPullCollection, questionsPushFunctions, From c0766e49f632397050a1e9ee16af1d0e2d40a4ac Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 13:19:58 -0400 Subject: [PATCH 035/198] fix(cli): replacing ID import --- templates/cli/lib/commands/create.js.twig | 1 + templates/cli/lib/commands/pull.js.twig | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig index 418879b14..bd3c86af9 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/create.js.twig @@ -9,6 +9,7 @@ const { messagingCreateTopic } = require("./messaging"); const { functionsCreate } = require("./functions"); const { databasesCreateCollection } = require("./databases"); const { sdkForConsole } = require("../sdks"); +const ID = require("../id"); const { localConfig } = require("../config"); const { questionsCreateProject, diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 84a865b80..95698a39e 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -6,7 +6,6 @@ const { databasesGet, databasesListCollections, databasesList } = require("./dat const { storageListBuckets } = require("./storage"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); -const ID = require("../id"); const { paginate } = require("../paginate"); const { questionsPullProject, questionsPullCollection } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); From fdd0014845771763380a062b14c160f23cb6bf68 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 13:26:37 -0400 Subject: [PATCH 036/198] refactor(cli): Messaging to Topics --- 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 7aa071288..508cbb553 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -287,7 +287,7 @@ const questionsPushResources = [ { name: 'Collections', value: 'collections' }, { name: 'Buckets', value: 'buckets' }, { name: 'Teams', value: 'teams' }, - { name: 'Messages', value: 'messages' } + { name: 'Topics', value: 'messages' } ] } ] From 836e79c4d532f3c5aeca57d3feec944975cbd453 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 13:32:24 -0400 Subject: [PATCH 037/198] refactor(cli): Updater -> Spinner, and refactoring --- src/SDK/Language/CLI.php | 4 ++-- templates/cli/lib/commands/push.js.twig | 11 +++++------ .../cli/lib/{updater.js.twig => spinner.js.twig} | 12 ++++++------ 3 files changed, 13 insertions(+), 14 deletions(-) rename templates/cli/lib/{updater.js.twig => spinner.js.twig} (90%) diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index b79dcd183..4974c7cde 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -134,8 +134,8 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'lib/updater.js', - 'template' => 'cli/lib/updater.js.twig', + 'destination' => 'lib/spinner.js', + 'template' => 'cli/lib/spinner.js.twig', ], [ 'scope' => 'default', diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 049aaa079..f2db9188e 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -2,7 +2,7 @@ const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const { localConfig } = require("../config"); -const { Updater, SPINNER_ARC, SPINNER_DOTS } = require('../updater'); +const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); @@ -272,8 +272,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { log('Validating functions'); // Validation is done BEFORE pushing so the deployment process can be run in async with progress update - for (let i = 0; i < functions.length; i++) { - const func = functions[i]; + for (let func of functions) { if (!func.entrypoint) { log(`Function ${func.name} does not have an endpoint`); @@ -311,14 +310,14 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { log('All functions are validated'); log('Pushing functions\n'); - Updater.start(false,) + Spinner.start(false); let successfullyPushed = 0; await Promise.all(functions.map(async (func) => { const ignore = func.ignore ? 'appwrite.json' : '.gitignore'; let functionExists = false; - const updaterRow = new Updater({ status: '', resource: func.name, id: func['$id'], end: `Ignoring using: ${ignore}` }); + const updaterRow = new Spinner({ status: '', resource: func.name, id: func['$id'], end: `Ignoring using: ${ignore}` }); updaterRow.update({ status: 'Getting' }).startSpinner(SPINNER_DOTS); @@ -453,7 +452,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { updaterRow.stopSpinner(); })); - Updater.stop(); + Spinner.stop(); success(`Pushed ${successfullyPushed} functions`); } diff --git a/templates/cli/lib/updater.js.twig b/templates/cli/lib/spinner.js.twig similarity index 90% rename from templates/cli/lib/updater.js.twig rename to templates/cli/lib/spinner.js.twig index 9e871ea02..bc7e9d7ac 100644 --- a/templates/cli/lib/updater.js.twig +++ b/templates/cli/lib/spinner.js.twig @@ -15,9 +15,9 @@ const spinners = { } } -class Updater { +class Spinner { static start(clearOnComplete = true, hideCursor = true) { - Updater.updatesBar = new progress.MultiBar({ + Spinner.updatesBar = new progress.MultiBar({ format: this.#formatter, hideCursor, clearOnComplete, @@ -27,7 +27,7 @@ class Updater { } static stop() { - Updater.updatesBar.stop(); + Spinner.updatesBar.stop(); } static #formatter(options, params, payload) { @@ -48,7 +48,7 @@ class Updater { end = chalk.red(payload.errorMessage); } - return Updater.#line(prefix, start, middle, end); + return Spinner.#line(prefix, start, middle, end); } static #line(prefix, start, middle, end, separator = '•') { @@ -57,7 +57,7 @@ class Updater { } constructor(payload, total = 100, startValue = 0) { - this.bar = Updater.updatesBar.create(total, startValue, payload) + this.bar = Spinner.updatesBar.create(total, startValue, payload) } update(payload) { @@ -92,7 +92,7 @@ class Updater { module.exports = { - Updater, + Spinner, SPINNER_ARC, SPINNER_DOTS } From bd4c05439df471768b3008001f7be6b675408336 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 13:56:51 -0400 Subject: [PATCH 038/198] feat(cli): Adding link to failed deployments --- templates/cli/lib/commands/push.js.twig | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index e960cc3be..ef812965a 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1,7 +1,7 @@ const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); -const { localConfig } = require("../config"); +const { localConfig, globalConfig } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsGetEntrypoint, questionsPushCollections, questionsConfirmPushCollections } = require("../questions"); @@ -314,6 +314,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { Spinner.start(false); let successfullyPushed = 0; let successfullyDeployed = 0; + const failedDeployments = []; await Promise.all(functions.map(async (func) => { const ignore = func.ignore ? 'appwrite.json' : '.gitignore'; @@ -479,7 +480,8 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { break; } else if (status === 'failed') { - updaterRow.fail({ errorMessage: 'Failed to deploy.' }); + failedDeployments.push({ name: func['name'], $id: func['$id'], deployment: response['$id'] }); + updaterRow.fail({ errorMessage: `Failed to deploy` }); break; } else { @@ -498,6 +500,14 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { })); 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} successfully deployed`); } From 7cb45fe6fbf5958423b3c8f18fb7ddd4879938f6 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 14:50:31 -0400 Subject: [PATCH 039/198] refactoring(cli): refactoring --- templates/cli/lib/config.js.twig | 2 +- templates/cli/lib/questions.js.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index cafa89f79..37c3899f6 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -233,7 +233,7 @@ class Local extends Config { let topics = this.get("topics"); for (let i = 0; i < topics.length; i++) { - if (topics[i]['$id'] == props['$id']) { + if (topics[i]['$id'] === props['$id']) { topics[i] = props; this.set("topics", topics); return; diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 508cbb553..b7831fc1b 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -392,7 +392,7 @@ const questionsPushMessagingTopics = [ { type: "input", name: "override", - message: 'What you like to override existing topics? This can lead to loss of data! Type "YES" to confirm.' + message: 'Would you like to override existing topics? This can lead to loss of data! Type "YES" to confirm.' } ] From b33054e86711a1ccf7ab8a66ead1761d58178d4b Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 14:53:42 -0400 Subject: [PATCH 040/198] feat(cli): Ignore function deployment status --- templates/cli/lib/commands/push.js.twig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index ef812965a..b2f3967d9 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -238,7 +238,7 @@ const push = new Command("push") command.help() })); -const pushFunction = async ({ functionId, all, yes } = {}) => { +const pushFunction = async ({ functionId, all, yes, async } = {}) => { let response = {}; const functionIds = []; @@ -454,7 +454,7 @@ const pushFunction = async ({ functionId, all, yes } = {}) => { } } - if (deploymentCreated) { + if (deploymentCreated && !async) { try { const deploymentId = response['$id']; updaterRow.update({ status: 'Deploying', end: 'Checking deployment status...' }) @@ -1013,6 +1013,7 @@ push .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 From 0a0a67100d78f0b17828d3669ec2b536751aa386 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 20 May 2024 14:59:49 -0400 Subject: [PATCH 041/198] feat(cli): Interactive topics creation --- templates/cli/lib/commands/create.js.twig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig index bd3c86af9..8a3cbf11d 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/create.js.twig @@ -100,8 +100,7 @@ const createTopic = async () => { parseOutput: false }) - // TODO: after https://github.com/appwrite/sdk-generator/pull/839 - // localConfig.addMessagingTopic(response); + localConfig.addMessagingTopic(response); success(); } catch (e) { error(e.getMessage ?? 'Unknown error occurred. Please try again'); From 951598131ba841d2bbf675f84c1d219108b51479 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Mon, 20 May 2024 23:53:38 +0400 Subject: [PATCH 042/198] Update templates/cli/lib/commands/push.js.twig --- templates/cli/lib/commands/push.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 32d5570eb..373baeccc 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -992,7 +992,7 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { parseOutput: false }) - success(`Pushed ${topic.name} ( ${topic['$id']} )`); + success(`Created ${topic.name} ( ${topic['$id']} )`); } else { throw e; } From 23523be37c22f1d067d8a9810a5d0e1f1bf81841 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 21 May 2024 01:09:39 +0400 Subject: [PATCH 043/198] Update templates/cli/lib/commands/push.js.twig --- templates/cli/lib/commands/push.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index b2f3967d9..5cbf861c5 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -509,7 +509,7 @@ const pushFunction = async ({ functionId, all, yes, async } = {}) => { error(`Deployment of ${name} has failed. Check at ${failUrl} for more details\n`); }) - success(`Pushed ${successfullyPushed} functions with ${successfullyDeployed} successfully deployed`); + success(`Pushed ${successfullyPushed} functions with ${successfullyDeployed} successful deployments.`); } const createAttribute = async (databaseId, collectionId, attribute) => { From 6d9896803b24f4c2c7b3337cff599ff9a9d344ca Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 21 May 2024 01:36:24 +0400 Subject: [PATCH 044/198] Update templates/cli/lib/commands/push.js.twig --- templates/cli/lib/commands/push.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 5cbf861c5..ac1606654 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -450,7 +450,7 @@ const pushFunction = async ({ functionId, all, yes, async } = {}) => { updaterRow.fail({ errorMessage: 'Not found in the current directory. Skipping...' }) break; default: - updaterRow.fail({ errorMessage: e.message ?? 'General error occurs please try again' }) + updaterRow.fail({ errorMessage: e.message ?? 'An unknown error occurred. Please try again.' }) } } From 60c2bd5172b0618b8e504603e7f09b4d260f5a26 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 21 May 2024 08:55:29 -0400 Subject: [PATCH 045/198] refactor(cli): twig tags --- templates/cli/base/requests/api.twig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/templates/cli/base/requests/api.twig b/templates/cli/base/requests/api.twig index 608e10980..aee246c41 100644 --- a/templates/cli/base/requests/api.twig +++ b/templates/cli/base/requests/api.twig @@ -9,14 +9,13 @@ {% endfor %} }, payload{% if method.type == 'location' %}, 'arraybuffer'{% endif %}); -{% if method.type == 'location' %} + {%~ if method.type == 'location' %} if (overrideForCli) { response = Buffer.from(response); } fs.writeFileSync(destination, response); - -{% endif %} + {%~ endif %} if (parseOutput) { parse(response) success() From 4dd832f2d1e2d67382626967fb839b22e07a70fb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 21 May 2024 09:19:07 -0400 Subject: [PATCH 046/198] refactor(cli): twig multiple lines --- templates/cli/lib/commands/command.js.twig | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 7ad257951..7751e1804 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -59,8 +59,21 @@ const {{ service.name | caseLower }} = new Command("{{ service.name | caseLower /** * @param {{ "{" }}{{ service.name | caseUcfirst }}{{ method.name | caseUcfirst }}RequestParams{{ "}" }} params */ -const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ {% for parameter in method.parameters.all %}{{ parameter.name | caseCamel | escapeKeyword }}, {% endfor %}parseOutput = true, overrideForCli = false, sdk = undefined{% if 'multipart/form-data' in method.consumes %}, onProgress = () => {}{% endif %}{% if method.type == 'location' %}, destination{% endif %}}) => { - let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : sdk; +{%~ block decleration -%} +const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({ + {%- for parameter in method.parameters.all -%} + {{ parameter.name | caseCamel | escapeKeyword }}, + {%- endfor -%} + + {%- block baseParams -%}parseOutput = true, overrideForCli = false, sdk = undefined {%- endblock -%} + + {%- if 'multipart/form-data' in method.consumes -%},onProgress = () => {}{%- endif -%} + + {%- if method.type == 'location' -%}, destination{%- endif -%} +}) => { +{%~ endblock %} + let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} : + sdk; let apiPath = '{{ method.path }}'{% for parameter in method.parameters.path %}.replace('{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}', {{ parameter.name | caseCamel | escapeKeyword }}){% endfor %}; {{ include ('cli/base/params.twig') }} {% if 'multipart/form-data' in method.consumes %} 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 047/198] 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 46a1b5e43429c6cc04e51b634957ffc046ff31d1 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 21 May 2024 11:55:36 -0400 Subject: [PATCH 048/198] feat(cli): Adding console whoami command --- templates/cli/index.js.twig | 3 +- templates/cli/lib/commands/generic.js.twig | 36 +++++++++++++++++++++- templates/cli/lib/parser.js.twig | 2 ++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 47725d266..2126485c6 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 } = require("./lib/commands/generic"); +const { login, logout, whoami } = require("./lib/commands/generic"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} @@ -36,6 +36,7 @@ program }) .showSuggestionAfterError() {% if sdk.test != "true" %} + .addCommand(whoami) .addCommand(login) .addCommand(pull) .addCommand(push) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index f66a36ea3..14b311e59 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -3,11 +3,44 @@ const { Command } = require("commander"); const Client = require("../client"); const { sdkForConsole } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); -const { actionRunner, success, parseBool, commandDescriptions, log, parse } = require("../parser"); +const { actionRunner, success, parseBool, commandDescriptions, log, parse, drawTable } = require("../parser"); {% if sdk.test != "true" %} const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); +const whoami = new Command("whoami") + .description(commandDescriptions['whoami']) + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(async () => { + let client = await sdkForConsole(false); + + let account; + + try { + account = await accountGet({ + sdk: client, + parseOutput: false + }); + } catch(error) { + throw error; + } + + success("Signed in"); + + const data = [ + { + 'ID': account.$id, + 'Name': account.name, + 'Email': account.email, + 'MFA enabled': account.mfa + } + ]; + + drawTable(data) + })); + const login = new Command("login") .description(commandDescriptions['login']) .configureHelp({ @@ -159,6 +192,7 @@ const client = new Command("client") module.exports = { {% if sdk.test != "true" %} + whoami, login, logout, {% endif %} diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 5dde5aa3e..cf7cfbffa 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -168,6 +168,7 @@ const commandDescriptions = { "client": `The client command allows you to configure your CLI`, "login": `The login command allows you to authenticate and manage a user account.`, "logout": `The logout command allows you to logout of your {{ spec.title|caseUcfirst }} account.`, + "whoami": `The whoami command gives a basic account information about the logged in user.`, "console" : `The console command allows gives you access to the APIs used by the Appwrite console.`, "assistant": `The assistant command allows you to interact with the Appwrite Assistant AI`, "messaging": `The messaging command allows you to send messages.`, @@ -184,6 +185,7 @@ const commandDescriptions = { } module.exports = { + drawTable, parse, actionRunner, parseInteger, From 151f59b2f110b2e40c95df239421152afe057423 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 21 May 2024 12:04:00 -0400 Subject: [PATCH 049/198] feat(cli): Console whoami better error handling --- templates/cli/lib/commands/generic.js.twig | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 14b311e59..ee87252e3 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 } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); -const { actionRunner, success, parseBool, commandDescriptions, log, parse, drawTable } = require("../parser"); +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"); @@ -14,6 +14,11 @@ const whoami = new Command("whoami") helpWidth: process.stdout.columns || 80 }) .action(actionRunner(async () => { + if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { + error("No user is signed in"); + return; + } + let client = await sdkForConsole(false); let account; @@ -23,8 +28,9 @@ const whoami = new Command("whoami") sdk: client, parseOutput: false }); - } catch(error) { - throw error; + } catch (error) { + error("No user is signed in"); + return; } success("Signed in"); @@ -67,7 +73,7 @@ const login = new Command("login") sdk: client, parseOutput: false }); - } catch(error) { + } catch (error) { if (error.response === 'user_more_factors_required') { const { factor } = await inquirer.prompt(questionsListFactors); @@ -191,10 +197,10 @@ const client = new Command("client") })); module.exports = { -{% if sdk.test != "true" %} + {% if sdk.test != "true" %} whoami, login, logout, -{% endif %} + {% endif %} client }; 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 050/198] 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 2d8fe79e61cea4f20fec75c61a936d51c99613d1 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 21 May 2024 16:28:58 -0400 Subject: [PATCH 051/198] feat(cli): Init project forces login --- src/SDK/Language/CLI.php | 4 +- templates/cli/index.js.twig | 4 +- templates/cli/lib/commands/generic.js.twig | 91 ++++++++++--------- .../commands/{create.js.twig => init.js.twig} | 35 +++++-- templates/cli/lib/parser.js.twig | 3 +- templates/cli/lib/questions.js.twig | 14 ++- 6 files changed, 88 insertions(+), 63 deletions(-) rename templates/cli/lib/commands/{create.js.twig => init.js.twig} (51%) diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index e78bcbeb8..9f91cccf3 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -174,8 +174,8 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'lib/commands/create.js', - 'template' => 'cli/lib/commands/create.js.twig', + 'destination' => 'lib/commands/init.js', + 'template' => 'cli/lib/commands/init.js.twig', ], [ 'scope' => 'default', diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 9e5e9d714..87dd70298 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"); {% if sdk.test != "true" %} const { login, logout } = require("./lib/commands/generic"); -const { create } = require("./lib/commands/create"); +const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} @@ -38,7 +38,7 @@ program .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(login) - .addCommand(create) + .addCommand(init) .addCommand(pull) .addCommand(push) .addCommand(logout) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index f66a36ea3..ce052b681 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -8,62 +8,64 @@ const { actionRunner, success, parseBool, commandDescriptions, log, parse } = re const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); -const login = new Command("login") - .description(commandDescriptions['login']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 +const loginCommand = async () => { + const answers = await inquirer.prompt(questionsLogin) + + let client = await sdkForConsole(false); + + await accountCreateEmailPasswordSession({ + email: answers.email, + password: answers.password, + parseOutput: false, + sdk: client }) - .action(actionRunner(async () => { - const answers = await inquirer.prompt(questionsLogin) - let client = await sdkForConsole(false); + client.setCookie(globalConfig.getCookie()); - await accountCreateEmailPasswordSession({ - email: answers.email, - password: answers.password, - parseOutput: false, - sdk: client - }) + let account; + + try { + account = await accountGet({ + sdk: client, + parseOutput: false + }); + } catch(error) { + if (error.response === 'user_more_factors_required') { + const { factor } = await inquirer.prompt(questionsListFactors); - client.setCookie(globalConfig.getCookie()); + const challenge = await accountCreateMfaChallenge({ + factor, + parseOutput: false, + sdk: client + }); - let account; + const { otp } = await inquirer.prompt(questionsMfaChallenge); + + await accountUpdateMfaChallenge({ + challengeId: challenge.$id, + otp, + parseOutput: false, + sdk: client + }); - 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; - } + } else { + throw error; } + } - success("Signed in as user with ID: " + account.$id); - })); + success("Signed in as user with ID: " + account.$id); +}; + +const login = new Command("login") + .description(commandDescriptions['login']) + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(loginCommand)); const logout = new Command("logout") .description(commandDescriptions['logout']) @@ -159,6 +161,7 @@ const client = new Command("client") module.exports = { {% if sdk.test != "true" %} + loginCommand, login, logout, {% endif %} diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/init.js.twig similarity index 51% rename from templates/cli/lib/commands/create.js.twig rename to templates/cli/lib/commands/init.js.twig index 339a21ace..e163c0363 100644 --- a/templates/cli/lib/commands/create.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -2,12 +2,14 @@ const { Command } = require("commander"); const inquirer = require("inquirer"); const { projectsCreate } = require("./projects"); const { sdkForConsole } = require("../sdks"); -const { localConfig } = require("../config"); +const { localConfig, globalConfig } = require("../config"); const { questionsCreateProject } = require("../questions"); -const { success, actionRunner, commandDescriptions } = require("../parser"); +const { success, error, actionRunner, commandDescriptions } = require("../parser"); +const { accountGet } = require("./account"); +const { loginCommand } = require("./generic"); -const create = new Command("create") - .description(commandDescriptions['create']) +const init = new Command("init") + .description(commandDescriptions['init']) .configureHelp({ helpWidth: process.stdout.columns || 80 }) @@ -15,8 +17,21 @@ const create = new Command("create") command.help(); })); -const createProject = async () => { - let response = {} +const initProject = async () => { + let response = {}; + + try { + if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { + throw ''; + } + await accountGet({ + parseOutput: false + }); + } catch (e) { + error('You must login first') + await loginCommand() + } + const answers = await inquirer.prompt(questionsCreateProject) if (!answers.project || !answers.organization) process.exit(1) @@ -31,11 +46,11 @@ const createProject = async () => { success(); } -create +init .command("project") - .description("Create a new {{ spec.title|caseUcfirst }} project") - .action(actionRunner(createProject)); + .description("Init and create a new {{ spec.title|caseUcfirst }} project") + .action(actionRunner(initProject)); module.exports = { - create, + init, }; diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index cf94b3a8b..cc380e789 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -156,7 +156,8 @@ const commandDescriptions = { "graphql": `The graphql command allows you to query and mutate any resource type on your Appwrite server.`, "avatars": `The avatars command aims to help you complete everyday tasks related to your app image, icons, and avatars.`, "databases": `The databases command allows you to create structured collections of documents, query and filter lists of documents.`, - "create": `The create command provides a convenient wrapper for creating projects functions, collections, buckets, teams and messaging.`, + "init": `The init command provides a convenient wrapper for creating and initializing project in Appwrite.`, + "create": `The create command provides a convenient wrapper for creating functions, collections, buckets, teams and messaging.`, "push": `The push command provides a convenient wrapper for pushing your functions, collections, buckets, teams and messaging.`, "functions": `The functions command allows you view, create and manage your Cloud Functions.`, "health": `The health command allows you to both validate and monitor your {{ spec.title|caseUcfirst }} server's health.`, diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index a2c66965b..ae0744dd1 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -10,6 +10,8 @@ const { paginate } = require('./paginate'); const { databasesList } = require('./commands/databases'); const JSONbig = require("json-bigint")({ storeAsString: false }); +const whenOverride = (answers)=> answers.override === undefined ? true : answers.override; + const getIgnores = (runtime) => { const languge = runtime.split('-')[0]; @@ -143,7 +145,8 @@ const questionsProject = [ } return choices; - } + }, + when: whenOverride }, ]; @@ -153,13 +156,15 @@ const questionsCreateProject = [ type: "input", name: "project", message: "What would you like to name your project?", - default: "My Awesome Project" + default: "My Awesome Project", + when: whenOverride }, { type: "input", name: "id", message: "What ID would you like to have for your project?", - default: "unique()" + default: "unique()", + when: whenOverride } ]; const questionsPullProject = [ @@ -189,7 +194,8 @@ const questionsPullProject = [ } return choices; - } + }, + when: whenOverride } ]; From abef847d882d9f6a57b59e84c7d00fb04b1fbc9d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 08:29:33 -0400 Subject: [PATCH 052/198] refactor(cli): Refactor message text --- 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 830039785..197d81edd 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -331,7 +331,7 @@ const questionsPullFunctions = [ { type: "input", name: "override", - message: `Do you want to override local functions code and definition? ${chalk.red('all local changes will lost!')} Type "YES" to confirm.` + message: `Are you sure you want to override local functions code and definition? ${chalk.red('All local changes will be lost!')} Type "YES" to confirm.` } ]; From 634679a06a6a9a974dcb3961f9db955cd67a921e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 08:49:50 -0400 Subject: [PATCH 053/198] chore(cli): rebasing head --- src/SDK/Language/CLI.php | 5 + templates/cli/index.js.twig | 2 + templates/cli/lib/commands/create.js.twig | 224 ++++++++++++++++++++++ templates/cli/lib/commands/init.js.twig | 210 +------------------- templates/cli/lib/questions.js.twig | 32 +--- 5 files changed, 233 insertions(+), 240 deletions(-) create mode 100644 templates/cli/lib/commands/create.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 9f91cccf3..5b36eea70 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -177,6 +177,11 @@ public function getFiles(): array 'destination' => 'lib/commands/init.js', 'template' => 'cli/lib/commands/init.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/commands/create.js', + 'template' => 'cli/lib/commands/create.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/commands/pull.js', diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 87dd70298..3798dedc6 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,6 +12,7 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} const { login, logout } = require("./lib/commands/generic"); +const { create } = require("./lib/commands/create"); const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); @@ -38,6 +39,7 @@ program .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(login) + .addCommand(create) .addCommand(init) .addCommand(pull) .addCommand(push) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig new file mode 100644 index 000000000..852116822 --- /dev/null +++ b/templates/cli/lib/commands/create.js.twig @@ -0,0 +1,224 @@ +const fs = require("fs"); +const path = require("path"); +const childProcess = require('child_process'); +const { Command } = require("commander"); +const inquirer = require("inquirer"); +const { storageCreateBucket } = require("./storage"); +const { messagingCreateTopic } = require("./messaging"); +const { functionsCreate } = require("./functions"); +const { databasesCreateCollection } = require("./databases"); +const ID = require("../id"); +const { localConfig } = require("../config"); +const { + questionsCreateFunction, + questionsCreateBucket, + questionsCreateMessagingTopic, + questionsCreateCollection +} = require("../questions"); +const { success, error, actionRunner, commandDescriptions } = require("../parser"); + +const create = new Command("create") + .description(commandDescriptions['create']) + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(async (_options, command) => { + command.help(); + })); + + +const createBucket = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateBucket) + if (!answers.bucket || !answers.id || !answers.fileSecurity) process.exit(1) + + 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'); + } +}; + +const createCollection = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateCollection) + if (!answers.database || !answers.collection || !answers.id || !answers.documentSecurity) process.exit(1) + + try { + response = await databasesCreateCollection({ + databaseId: answers.database, + collectionId: answers.id, + name: answers.collection, + documentSecurity: answers.documentSecurity.toLowerCase() === 'yes', + enabled: true, + parseOutput: false + }) + + localConfig.addCollection(response); + success(); + } catch (e) { + error(e.getMessage ?? 'Unknown error occurred. Please try again'); + } +}; + +const createTopic = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateMessagingTopic) + if (!answers.topic || !answers.id) process.exit(1) + + try { + response = await messagingCreateTopic({ + topicId: answers.id, + name: answers.topic, + parseOutput: false + }) + + localConfig.addMessagingTopic(response); + success(); + } catch (e) { + error(e.getMessage ?? 'Unknown error occurred. Please try again'); + } +}; + +const createFunction = async () => { + // TODO: Add CI/CD support (ID, name, runtime) + const answers = await inquirer.prompt(questionsCreateFunction) + const functionFolder = path.join(process.cwd(), 'functions'); + + if (!fs.existsSync(functionFolder)) { + fs.mkdirSync(functionFolder, { + recursive: true + }); + } + + const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; + const functionDir = path.join(functionFolder, functionId); + + if (fs.existsSync(functionDir)) { + throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); + } + + if (!answers.runtime.entrypoint) { + log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first push the function.`); + } + + if (!answers.runtime.commands) { + 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"); + + let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched + + let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; + + /* Force use CMD as powershell does not support && */ + if (process.platform === 'win32') { + gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; + gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; + } + + /* Execute the child process but do not print any std output */ + try { + childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: functionDir }); + childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: functionDir }); + } catch (error) { + /* Specialised errors with recommended actions to take */ + if (error.message.includes('error: unknown option')) { + throw new Error(`${error.message} \n\nSuggestion: Try updating your git to the latest version, then trying to run this command again.`) + } else if (error.message.includes('is not recognized as an internal or external command,') || error.message.includes('command not found')) { + throw new Error(`${error.message} \n\nSuggestion: It appears that git is not installed, try installing git then trying to run this command again.`) + } else { + throw error; + } + } + + fs.rmSync(path.join(functionDir, ".git"), { recursive: true }); + const copyRecursiveSync = (src, dest) => { + let exists = fs.existsSync(src); + let stats = exists && fs.statSync(src); + let isDirectory = exists && stats.isDirectory(); + if (isDirectory) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest); + } + + fs.readdirSync(src).forEach(function (childItemName) { + copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); + }); + } else { + fs.copyFileSync(src, dest); + } + }; + copyRecursiveSync(path.join(functionDir, answers.runtime.id), functionDir); + + fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); + + const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); + const readmeFile = fs.readFileSync(readmePath).toString(); + const newReadmeFile = readmeFile.split('\n'); + newReadmeFile[0] = `# ${answers.name}`; + newReadmeFile.splice(1, 2); + 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, + ignore: answers.runtime.ignore || null, + path: `functions/${functionId}`, + }; + + localConfig.addFunction(data); + success(); +} + +create + .command("function") + .description("Create a new {{ spec.title|caseUcfirst }} function") + .action(actionRunner(createFunction)); + +create + .command("bucket") + .description("Create a new {{ spec.title|caseUcfirst }} bucket") + .action(actionRunner(createBucket)); + +create + .command("collection") + .description("Create a new {{ spec.title|caseUcfirst }} collection") + .action(actionRunner(createCollection)); + +create + .command("topic") + .description("Create a new {{ spec.title|caseUcfirst }} topic") + .action(actionRunner(createTopic)); + +module.exports = { + create, +}; diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 523cdee53..a55ee0f36 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -1,23 +1,8 @@ -const fs = require("fs"); -const path = require("path"); -const childProcess = require('child_process'); const { Command } = require("commander"); const inquirer = require("inquirer"); const { projectsCreate } = require("./projects"); -const { storageCreateBucket } = require("./storage"); -const { messagingCreateTopic } = require("./messaging"); -const { functionsCreate } = require("./functions"); -const { databasesCreateCollection } = require("./databases"); -const { sdkForConsole } = require("../sdks"); -const ID = require("../id"); const { localConfig, globalConfig } = require("../config"); -const { - questionsCreateProject, - questionsCreateFunction, - questionsCreateBucket, - questionsCreateMessagingTopic, - questionsCreateCollection -} = require("../questions"); +const { questionsCreateProject, } = require("../questions"); const { success, error, actionRunner, commandDescriptions } = require("../parser"); const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); @@ -60,205 +45,12 @@ const initProject = async () => { success(); } -const createBucket = async () => { - let response = {} - const answers = await inquirer.prompt(questionsCreateBucket) - if (!answers.bucket || !answers.id || !answers.fileSecurity) process.exit(1) - - 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'); - } -}; - -const createCollection = async () => { - let response = {} - const answers = await inquirer.prompt(questionsCreateCollection) - if (!answers.database || !answers.collection || !answers.id || !answers.documentSecurity) process.exit(1) - - try { - response = await databasesCreateCollection({ - databaseId: answers.database, - collectionId: answers.id, - name: answers.collection, - documentSecurity: answers.documentSecurity.toLowerCase() === 'yes', - enabled: true, - parseOutput: false - }) - - localConfig.addCollection(response); - success(); - } catch (e) { - error(e.getMessage ?? 'Unknown error occurred. Please try again'); - } -}; - -const createTopic = async () => { - let response = {} - const answers = await inquirer.prompt(questionsCreateMessagingTopic) - if (!answers.topic || !answers.id) process.exit(1) - - try { - response = await messagingCreateTopic({ - topicId: answers.id, - name: answers.topic, - parseOutput: false - }) - - localConfig.addMessagingTopic(response); - success(); - } catch (e) { - error(e.getMessage ?? 'Unknown error occurred. Please try again'); - } -}; - -const createFunction = async () => { - // TODO: Add CI/CD support (ID, name, runtime) - const answers = await inquirer.prompt(questionsCreateFunction) - const functionFolder = path.join(process.cwd(), 'functions'); - - if (!fs.existsSync(functionFolder)) { - fs.mkdirSync(functionFolder, { - recursive: true - }); - } - - const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; - const functionDir = path.join(functionFolder, functionId); - - if (fs.existsSync(functionDir)) { - throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); - } - - if (!answers.runtime.entrypoint) { - log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first push the function.`); - } - - if (!answers.runtime.commands) { - 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"); - - let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched - - let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; - - /* Force use CMD as powershell does not support && */ - if (process.platform === 'win32') { - gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; - gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; - } - - /* Execute the child process but do not print any std output */ - try { - childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: functionDir }); - childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: functionDir }); - } catch (error) { - /* Specialised errors with recommended actions to take */ - if (error.message.includes('error: unknown option')) { - throw new Error(`${error.message} \n\nSuggestion: Try updating your git to the latest version, then trying to run this command again.`) - } else if (error.message.includes('is not recognized as an internal or external command,') || error.message.includes('command not found')) { - throw new Error(`${error.message} \n\nSuggestion: It appears that git is not installed, try installing git then trying to run this command again.`) - } else { - throw error; - } - } - - fs.rmSync(path.join(functionDir, ".git"), { recursive: true }); - const copyRecursiveSync = (src, dest) => { - let exists = fs.existsSync(src); - let stats = exists && fs.statSync(src); - let isDirectory = exists && stats.isDirectory(); - if (isDirectory) { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest); - } - - fs.readdirSync(src).forEach(function (childItemName) { - copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); - }); - } else { - fs.copyFileSync(src, dest); - } - }; - copyRecursiveSync(path.join(functionDir, answers.runtime.id), functionDir); - - fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); - - const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); - const readmeFile = fs.readFileSync(readmePath).toString(); - const newReadmeFile = readmeFile.split('\n'); - newReadmeFile[0] = `# ${answers.name}`; - newReadmeFile.splice(1, 2); - 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, - ignore: answers.runtime.ignore || null, - path: `functions/${functionId}`, - }; - - localConfig.addFunction(data); - success(); -} - - init .command("project") .description("Init and create a new {{ spec.title|caseUcfirst }} project") .action(actionRunner(initProject)); -create - .command("function") - .description("Create a new {{ spec.title|caseUcfirst }} function") - .action(actionRunner(createFunction)); - -create - .command("bucket") - .description("Create a new {{ spec.title|caseUcfirst }} bucket") - .action(actionRunner(createBucket)); - -create - .command("collection") - .description("Create a new {{ spec.title|caseUcfirst }} collection") - .action(actionRunner(createCollection)); - -create - .command("topic") - .description("Create a new {{ spec.title|caseUcfirst }} topic") - .action(actionRunner(createTopic)); - module.exports = { init, }; diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 63f3a93ed..f407c382b 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -167,6 +167,7 @@ const questionsCreateProject = [ when: whenOverride } ]; + const questionsPullProject = [ ...questionsProject, { @@ -316,37 +317,6 @@ const questionsCreateMessagingTopic = [ ]; -const questionsPullProject = [ - ...questionsProject, - { - type: "list", - 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) => { - return { - name: `${project.name} (${project['$id']})`, - value: { - name: project.name, - id: project['$id'] - } - } - }) - - if (choices.length == 0) { - throw new Error("No projects found. Please create a new project.") - } - - return choices; - } - } -]; - const questionsPullCollection = [ { type: "checkbox", From 283a0631d8182dd35ddf784d079bf1926590470d Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 08:59:46 -0400 Subject: [PATCH 054/198] chore(cli): rebasing head --- templates/cli/lib/questions.js.twig | 32 ----------------------------- 1 file changed, 32 deletions(-) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 30c9594c8..17b070598 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -339,38 +339,6 @@ const questionsCreateMessagingTopic = [ } ]; - -const questionsPullProject = [ - ...questionsProject, - { - type: "list", - 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) => { - return { - name: `${project.name} (${project['$id']})`, - value: { - name: project.name, - id: project['$id'] - } - } - }) - - if (choices.length == 0) { - throw new Error("No projects found. Please create a new project.") - } - - return choices; - } - } -]; - const questionsPullCollection = [ { type: "checkbox", From 8c3e359c0b63bc8798c56206ce60f8dd93f0aec4 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 09:04:52 -0400 Subject: [PATCH 055/198] refactor(cli): texts refactor --- templates/cli/lib/commands/generic.js.twig | 2 +- templates/cli/lib/parser.js.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index ee87252e3..84ae0a4f3 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -15,7 +15,7 @@ const whoami = new Command("whoami") }) .action(actionRunner(async () => { if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { - error("No user is signed in"); + error("No user is signed in. Run appwrite login. ..... "); return; } diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index cf7cfbffa..dce756c9d 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -168,7 +168,7 @@ const commandDescriptions = { "client": `The client command allows you to configure your CLI`, "login": `The login command allows you to authenticate and manage a user account.`, "logout": `The logout command allows you to logout of your {{ spec.title|caseUcfirst }} account.`, - "whoami": `The whoami command gives a basic account information about the logged in user.`, + "whoami": `The whoami command gives information about the currently logged in user.`, "console" : `The console command allows gives you access to the APIs used by the Appwrite console.`, "assistant": `The assistant command allows you to interact with the Appwrite Assistant AI`, "messaging": `The messaging command allows you to send messages.`, From f8926736a4318a6a355ecadd61749bbcbbc6db19 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 10:00:50 -0400 Subject: [PATCH 056/198] refactor(cli): Omitting the `project` sub-command --- templates/cli/lib/commands/init.js.twig | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index e163c0363..c42f7aff0 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -8,15 +8,6 @@ const { success, error, actionRunner, commandDescriptions } = require("../parser const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); -const init = new Command("init") - .description(commandDescriptions['init']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }) - .action(actionRunner(async (_options, command) => { - command.help(); - })); - const initProject = async () => { let response = {}; @@ -46,9 +37,11 @@ const initProject = async () => { success(); } -init - .command("project") - .description("Init and create a new {{ spec.title|caseUcfirst }} project") +const init = new Command("init") + .description('Init and create a new {{ spec.title|caseUcfirst }} project') + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) .action(actionRunner(initProject)); module.exports = { 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 057/198] 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 848e222d7a6b1e6e2b9c5f8d9994161d0d32ada0 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 12:20:48 -0400 Subject: [PATCH 058/198] refactor(cli): Changing error to info --- templates/cli/lib/commands/init.js.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index c42f7aff0..5af7a4c61 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -4,7 +4,7 @@ const { projectsCreate } = require("./projects"); const { sdkForConsole } = require("../sdks"); const { localConfig, globalConfig } = require("../config"); const { questionsCreateProject } = require("../questions"); -const { success, error, actionRunner, commandDescriptions } = require("../parser"); +const { success, log, actionRunner, commandDescriptions } = require("../parser"); const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); @@ -19,7 +19,7 @@ const initProject = async () => { parseOutput: false }); } catch (e) { - error('You must login first') + log('You must login first') await loginCommand() } From ee6612512112e9d6da4c872c8f0f7bf95a8affc9 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 22 May 2024 12:30:49 -0400 Subject: [PATCH 059/198] refactor(cli): Review refactoring and adding option for json output --- templates/cli/lib/commands/generic.js.twig | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 84ae0a4f3..a8d6f91b5 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -10,12 +10,10 @@ const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accoun const whoami = new Command("whoami") .description(commandDescriptions['whoami']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }) - .action(actionRunner(async () => { + .option("-j, --json", "Output in JSON format") + .action(actionRunner(async ({ json }) => { if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { - error("No user is signed in. Run appwrite login. ..... "); + error("No user is signed in. To sign in, run: appwrite login "); return; } @@ -29,20 +27,23 @@ const whoami = new Command("whoami") parseOutput: false }); } catch (error) { - error("No user is signed in"); + error("No user is signed in. To sign in, run: appwrite login"); return; } - success("Signed in"); - const data = [ { 'ID': account.$id, 'Name': account.name, 'Email': account.email, - 'MFA enabled': account.mfa + 'MFA enabled': account.mfa ? 'Yes' : 'No' } ]; + if (json) { + console.log(data); + + return; + } drawTable(data) })); 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 060/198] 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 061/198] 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 062/198] 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 063/198] 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 064/198] 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 065/198] 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 066/198] 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 067/198] 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 068/198] 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 069/198] 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 070/198] 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 071/198] 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 a48c3e28496c35406f848172f38d0d7ff7771a4f Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 23 May 2024 12:11:20 -0400 Subject: [PATCH 072/198] refactor(cli): Grouping init functionality Project init will do both, create and choose an existing one. --- src/SDK/Language/CLI.php | 5 - templates/cli/index.js.twig | 2 - templates/cli/lib/commands/create.js.twig | 224 --------------------- templates/cli/lib/commands/init.js.twig | 234 +++++++++++++++++++++- templates/cli/lib/parser.js.twig | 3 +- templates/cli/lib/questions.js.twig | 42 ++-- 6 files changed, 251 insertions(+), 259 deletions(-) delete mode 100644 templates/cli/lib/commands/create.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 5b36eea70..9f91cccf3 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -177,11 +177,6 @@ public function getFiles(): array 'destination' => 'lib/commands/init.js', 'template' => 'cli/lib/commands/init.js.twig', ], - [ - 'scope' => 'default', - 'destination' => 'lib/commands/create.js', - 'template' => 'cli/lib/commands/create.js.twig', - ], [ 'scope' => 'default', 'destination' => 'lib/commands/pull.js', diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 3798dedc6..87dd70298 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,7 +12,6 @@ const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} const { login, logout } = require("./lib/commands/generic"); -const { create } = require("./lib/commands/create"); const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); @@ -39,7 +38,6 @@ program .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(login) - .addCommand(create) .addCommand(init) .addCommand(pull) .addCommand(push) diff --git a/templates/cli/lib/commands/create.js.twig b/templates/cli/lib/commands/create.js.twig deleted file mode 100644 index 852116822..000000000 --- a/templates/cli/lib/commands/create.js.twig +++ /dev/null @@ -1,224 +0,0 @@ -const fs = require("fs"); -const path = require("path"); -const childProcess = require('child_process'); -const { Command } = require("commander"); -const inquirer = require("inquirer"); -const { storageCreateBucket } = require("./storage"); -const { messagingCreateTopic } = require("./messaging"); -const { functionsCreate } = require("./functions"); -const { databasesCreateCollection } = require("./databases"); -const ID = require("../id"); -const { localConfig } = require("../config"); -const { - questionsCreateFunction, - questionsCreateBucket, - questionsCreateMessagingTopic, - questionsCreateCollection -} = require("../questions"); -const { success, error, actionRunner, commandDescriptions } = require("../parser"); - -const create = new Command("create") - .description(commandDescriptions['create']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }) - .action(actionRunner(async (_options, command) => { - command.help(); - })); - - -const createBucket = async () => { - let response = {} - const answers = await inquirer.prompt(questionsCreateBucket) - if (!answers.bucket || !answers.id || !answers.fileSecurity) process.exit(1) - - 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'); - } -}; - -const createCollection = async () => { - let response = {} - const answers = await inquirer.prompt(questionsCreateCollection) - if (!answers.database || !answers.collection || !answers.id || !answers.documentSecurity) process.exit(1) - - try { - response = await databasesCreateCollection({ - databaseId: answers.database, - collectionId: answers.id, - name: answers.collection, - documentSecurity: answers.documentSecurity.toLowerCase() === 'yes', - enabled: true, - parseOutput: false - }) - - localConfig.addCollection(response); - success(); - } catch (e) { - error(e.getMessage ?? 'Unknown error occurred. Please try again'); - } -}; - -const createTopic = async () => { - let response = {} - const answers = await inquirer.prompt(questionsCreateMessagingTopic) - if (!answers.topic || !answers.id) process.exit(1) - - try { - response = await messagingCreateTopic({ - topicId: answers.id, - name: answers.topic, - parseOutput: false - }) - - localConfig.addMessagingTopic(response); - success(); - } catch (e) { - error(e.getMessage ?? 'Unknown error occurred. Please try again'); - } -}; - -const createFunction = async () => { - // TODO: Add CI/CD support (ID, name, runtime) - const answers = await inquirer.prompt(questionsCreateFunction) - const functionFolder = path.join(process.cwd(), 'functions'); - - if (!fs.existsSync(functionFolder)) { - fs.mkdirSync(functionFolder, { - recursive: true - }); - } - - const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; - const functionDir = path.join(functionFolder, functionId); - - if (fs.existsSync(functionDir)) { - throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); - } - - if (!answers.runtime.entrypoint) { - log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first push the function.`); - } - - if (!answers.runtime.commands) { - 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"); - - let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched - - let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; - - /* Force use CMD as powershell does not support && */ - if (process.platform === 'win32') { - gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; - gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; - } - - /* Execute the child process but do not print any std output */ - try { - childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: functionDir }); - childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: functionDir }); - } catch (error) { - /* Specialised errors with recommended actions to take */ - if (error.message.includes('error: unknown option')) { - throw new Error(`${error.message} \n\nSuggestion: Try updating your git to the latest version, then trying to run this command again.`) - } else if (error.message.includes('is not recognized as an internal or external command,') || error.message.includes('command not found')) { - throw new Error(`${error.message} \n\nSuggestion: It appears that git is not installed, try installing git then trying to run this command again.`) - } else { - throw error; - } - } - - fs.rmSync(path.join(functionDir, ".git"), { recursive: true }); - const copyRecursiveSync = (src, dest) => { - let exists = fs.existsSync(src); - let stats = exists && fs.statSync(src); - let isDirectory = exists && stats.isDirectory(); - if (isDirectory) { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest); - } - - fs.readdirSync(src).forEach(function (childItemName) { - copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); - }); - } else { - fs.copyFileSync(src, dest); - } - }; - copyRecursiveSync(path.join(functionDir, answers.runtime.id), functionDir); - - fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); - - const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); - const readmeFile = fs.readFileSync(readmePath).toString(); - const newReadmeFile = readmeFile.split('\n'); - newReadmeFile[0] = `# ${answers.name}`; - newReadmeFile.splice(1, 2); - 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, - ignore: answers.runtime.ignore || null, - path: `functions/${functionId}`, - }; - - localConfig.addFunction(data); - success(); -} - -create - .command("function") - .description("Create a new {{ spec.title|caseUcfirst }} function") - .action(actionRunner(createFunction)); - -create - .command("bucket") - .description("Create a new {{ spec.title|caseUcfirst }} bucket") - .action(actionRunner(createBucket)); - -create - .command("collection") - .description("Create a new {{ spec.title|caseUcfirst }} collection") - .action(actionRunner(createCollection)); - -create - .command("topic") - .description("Create a new {{ spec.title|caseUcfirst }} topic") - .action(actionRunner(createTopic)); - -module.exports = { - create, -}; diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 5af7a4c61..5cc9e8723 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -1,10 +1,23 @@ +const fs = require("fs"); +const path = require("path"); +const childProcess = require('child_process'); const { Command } = require("commander"); const inquirer = require("inquirer"); const { projectsCreate } = require("./projects"); -const { sdkForConsole } = require("../sdks"); +const { storageCreateBucket } = require("./storage"); +const { messagingCreateTopic } = require("./messaging"); +const { functionsCreate } = require("./functions"); +const { databasesCreateCollection } = require("./databases"); +const ID = require("../id"); const { localConfig, globalConfig } = require("../config"); -const { questionsCreateProject } = require("../questions"); -const { success, log, actionRunner, commandDescriptions } = require("../parser"); +const { + questionsCreateFunction, + questionsCreateBucket, + questionsCreateMessagingTopic, + questionsCreateCollection, + questionsInitProject +} = require("../questions"); +const { success, log, error, actionRunner, commandDescriptions } = require("../parser"); const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); @@ -23,27 +36,226 @@ const initProject = async () => { await loginCommand() } - const answers = await inquirer.prompt(questionsCreateProject) - if (!answers.project || !answers.organization) process.exit(1) + const answers = await inquirer.prompt(questionsInitProject) + if (!answers.project || !answers.organization) { + process.exit(1) + } + + if (answers.start === 'new') { + response = await projectsCreate({ + projectId: answers.id, + name: answers.project, + teamId: answers.organization.id, + parseOutput: false + }) + + localConfig.setProject(response['$id'], response.name); + } else { + localConfig.setProject(answers.project.id, answers.project.name); + } + + success(); +} + +const initBucket = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateBucket) + if (!answers.bucket || !answers.id || !answers.fileSecurity) process.exit(1) + + 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'); + } +}; + +const initCollection = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateCollection) + if (!answers.database || !answers.collection || !answers.id || !answers.documentSecurity) process.exit(1) + + try { + response = await databasesCreateCollection({ + databaseId: answers.database, + collectionId: answers.id, + name: answers.collection, + documentSecurity: answers.documentSecurity.toLowerCase() === 'yes', + enabled: true, + parseOutput: false + }) + + localConfig.addCollection(response); + success(); + } catch (e) { + error(e.getMessage ?? 'Unknown error occurred. Please try again'); + } +}; + +const initTopic = async () => { + let response = {} + const answers = await inquirer.prompt(questionsCreateMessagingTopic) + if (!answers.topic || !answers.id) process.exit(1) + + try { + response = await messagingCreateTopic({ + topicId: answers.id, + name: answers.topic, + parseOutput: false + }) + + localConfig.addMessagingTopic(response); + success(); + } catch (e) { + error(e.getMessage ?? 'Unknown error occurred. Please try again'); + } +}; + +const initFunction = async () => { + // TODO: Add CI/CD support (ID, name, runtime) + const answers = await inquirer.prompt(questionsCreateFunction) + const functionFolder = path.join(process.cwd(), 'functions'); + + if (!fs.existsSync(functionFolder)) { + fs.mkdirSync(functionFolder, { + recursive: true + }); + } + + const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; + const functionDir = path.join(functionFolder, functionId); + + if (fs.existsSync(functionDir)) { + throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); + } - response = await projectsCreate({ - projectId: answers.id, - name: answers.project, - teamId: answers.organization.id, + if (!answers.runtime.entrypoint) { + log(`Entrypoint for this runtime not found. You will be asked to configure entrypoint when you first push the function.`); + } + + if (!answers.runtime.commands) { + 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 }) - localConfig.setProject(response['$id'], response.name); + fs.mkdirSync(functionDir, "777"); + + let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched + + let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; + + /* Force use CMD as powershell does not support && */ + if (process.platform === 'win32') { + gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; + gitPullCommands = 'cmd /c "' + gitPullCommands + '"'; + } + + /* Execute the child process but do not print any std output */ + try { + childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: functionDir }); + childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: functionDir }); + } catch (error) { + /* Specialised errors with recommended actions to take */ + if (error.message.includes('error: unknown option')) { + throw new Error(`${error.message} \n\nSuggestion: Try updating your git to the latest version, then trying to run this command again.`) + } else if (error.message.includes('is not recognized as an internal or external command,') || error.message.includes('command not found')) { + throw new Error(`${error.message} \n\nSuggestion: It appears that git is not installed, try installing git then trying to run this command again.`) + } else { + throw error; + } + } + + fs.rmSync(path.join(functionDir, ".git"), { recursive: true }); + const copyRecursiveSync = (src, dest) => { + let exists = fs.existsSync(src); + let stats = exists && fs.statSync(src); + let isDirectory = exists && stats.isDirectory(); + if (isDirectory) { + if (!fs.existsSync(dest)) { + fs.mkdirSync(dest); + } + + fs.readdirSync(src).forEach(function (childItemName) { + copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName)); + }); + } else { + fs.copyFileSync(src, dest); + } + }; + copyRecursiveSync(path.join(functionDir, answers.runtime.id), functionDir); + + fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); + + const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); + const readmeFile = fs.readFileSync(readmePath).toString(); + const newReadmeFile = readmeFile.split('\n'); + newReadmeFile[0] = `# ${answers.name}`; + newReadmeFile.splice(1, 2); + 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, + ignore: answers.runtime.ignore || null, + path: `functions/${functionId}`, + }; + + localConfig.addFunction(data); success(); } const init = new Command("init") - .description('Init and create a new {{ spec.title|caseUcfirst }} project') + .description('Init an {{ spec.title|caseUcfirst }} project') .configureHelp({ helpWidth: process.stdout.columns || 80 }) .action(actionRunner(initProject)); +init + .command("function") + .description("Init a new {{ spec.title|caseUcfirst }} function") + .action(actionRunner(initFunction)); + +init + .command("bucket") + .description("Init a new {{ spec.title|caseUcfirst }} bucket") + .action(actionRunner(initBucket)); + +init + .command("collection") + .description("Init a new {{ spec.title|caseUcfirst }} collection") + .action(actionRunner(initCollection)); + +init + .command("topic") + .description("Init a new {{ spec.title|caseUcfirst }} topic") + .action(actionRunner(initTopic)); + module.exports = { init, }; diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index cc380e789..7b6e1b22f 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -156,8 +156,7 @@ const commandDescriptions = { "graphql": `The graphql command allows you to query and mutate any resource type on your Appwrite server.`, "avatars": `The avatars command aims to help you complete everyday tasks related to your app image, icons, and avatars.`, "databases": `The databases command allows you to create structured collections of documents, query and filter lists of documents.`, - "init": `The init command provides a convenient wrapper for creating and initializing project in Appwrite.`, - "create": `The create command provides a convenient wrapper for creating functions, collections, buckets, teams and messaging.`, + "init": `The init command provides a convenient wrapper for creating and initializing project, functions, collections, buckets, teams and messaging in Appwrite.`, "push": `The push command provides a convenient wrapper for pushing your functions, collections, buckets, teams and messaging.`, "functions": `The functions command allows you view, create and manage your Cloud Functions.`, "health": `The health command allows you to both validate and monitor your {{ spec.title|caseUcfirst }} server's health.`, diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 17b070598..96806b2c3 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -112,7 +112,7 @@ const getInstallCommand = (runtime) => { return undefined; }; -const questionsProject = [ +const questionsInitProject = [ { type: "confirm", name: "override", @@ -148,28 +148,41 @@ const questionsProject = [ }, when: whenOverride }, -]; - -const questionsCreateProject = [ - ...questionsProject, + { + 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", message: "What would you like to name your project?", default: "My Awesome Project", - when: whenOverride + when: (answer) => answer.start === 'new' }, { type: "input", name: "id", message: "What ID would you like to have for your project?", default: "unique()", - when: whenOverride - } -]; - -const questionsPullProject = [ - ...questionsProject, + when: (answer) => answer.start === 'new' + }, { type: "list", name: "project", @@ -196,7 +209,7 @@ const questionsPullProject = [ return choices; }, - when: whenOverride + when: (answer) => answer.start === 'existing' } ]; @@ -601,12 +614,11 @@ const questionsMfaChallenge = [ ]; module.exports = { - questionsCreateProject, + questionsInitProject, questionsCreateFunction, questionsCreateBucket, questionsCreateCollection, questionsCreateMessagingTopic, - questionsPullProject, questionsPullFunctions, questionsLogin, questionsPullCollection, From 2bb8da35644e60c0aa2bead063f947e8589fd16e Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 23 May 2024 12:19:14 -0400 Subject: [PATCH 073/198] refactor(cli): Project pull to update project name --- templates/cli/lib/commands/pull.js.twig | 27 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index dd79410ba..885393967 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -2,16 +2,15 @@ const fs = require("fs"); const tar = require("tar"); const { Command } = require("commander"); const inquirer = require("inquirer"); -const { messagingCreateTopic, messagingListTopics } = require("./messaging"); -const { teamsCreate, teamsList } = require("./teams"); -const { projectsCreate } = require("./projects"); +const { messagingListTopics } = require("./messaging"); +const { teamsList } = require("./teams"); +const { projectsList } = require("./projects"); const { functionsList, functionsDownloadDeployment } = require("./functions"); const { databasesGet, databasesListCollections, databasesList } = require("./databases"); const { storageListBuckets } = require("./storage"); -const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); const { paginate } = require("../paginate"); -const { questionsPullProject, questionsPullCollection, questionsPullFunctions } = require("../questions"); +const { questionsPullCollection, questionsPullFunctions } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); const pull = new Command("pull") @@ -24,11 +23,19 @@ const pull = new Command("pull") })); const pullProject = async () => { - const answers = await inquirer.prompt(questionsPullProject) - if (!answers.project) process.exit(1) + try { + let response = await projectsList({ + parseOutput: false, + queries: [JSON.stringify({ method: 'equal', attribute: '$id', values: [localConfig.getProject().projectId] })] - localConfig.setProject(answers.project.id, answers.project.name); - success(); + }) + if(response.total === 1){ + localConfig.setProject(response.projects[0].$id, response.projects[0].name); + } + success(); + } catch (e) { + throw e; + } } const pullFunctions = async ({ all, yes } = {}) => { @@ -178,7 +185,7 @@ const pullMessagingTopic = async () => { pull .command("project") - .description("Pulling your Appwrite project") + .description("Pulling your Appwrite project name") .action(actionRunner(pullProject)); pull From cd505f33f36957ad4999f29339d19c000b9d45ff Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Thu, 23 May 2024 12:25:37 -0400 Subject: [PATCH 074/198] refactor(cli): Project push to update remote project name --- templates/cli/lib/commands/push.js.twig | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 8e269cdd4..c9b5b099e 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -41,6 +41,7 @@ const { teamsUpdate, teamsCreate } = require("./teams"); +const { projectsUpdate } = require("./projects"); const STEP_SIZE = 100; // Resources const POLL_DEBOUNCE = 2000; // Milliseconds @@ -254,6 +255,19 @@ const pushResources = async ({ all, yes } = {}) => { } }; +const pushProject = async () => { + try { + await projectsUpdate({ + projectId: localConfig.getProject().projectId, + name: localConfig.getProject().projectName, + parseOutput: false + }) + success(); + } catch (e) { + throw e; + } +} + const pushFunction = async ({ functionId, all, yes, async } = {}) => { let response = {}; @@ -1106,6 +1120,11 @@ const push = new Command("push") .option(`--yes`, `Flag to confirm all warnings`) .action(actionRunner(pushResources)); +push + .command("project") + .description("Push project name.") + .action(actionRunner(pushProject)); + push .command("function") .description("Push functions in the current directory.") 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 075/198] 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 076/198] 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 077/198] 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 078/198] 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 079/198] 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 080/198] 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 081/198] 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 082/198] 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 bce40e8ba28fda8f6231de71dba89a6189b63c42 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 12:38:49 -0400 Subject: [PATCH 083/198] feat(cli): Pulling function without questions, and review refactoring --- templates/cli/index.js.twig | 1 + templates/cli/lib/commands/init.js.twig | 15 ++++---- templates/cli/lib/commands/pull.js.twig | 50 +++++++++---------------- templates/cli/lib/questions.js.twig | 11 +++--- 4 files changed, 33 insertions(+), 44 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 220286bd3..02bacb99e 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -12,6 +12,7 @@ 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 { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { push } = require("./lib/commands/push"); {% endif %} diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 5cc9e8723..e0711ee03 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -20,6 +20,7 @@ const { const { success, log, error, actionRunner, commandDescriptions } = require("../parser"); const { accountGet } = require("./account"); const { loginCommand } = require("./generic"); +const { sdkForConsole } = require("../sdks"); const initProject = async () => { let response = {}; @@ -28,16 +29,19 @@ const initProject = async () => { if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { throw ''; } + const client = await sdkForConsole(); + await accountGet({ - parseOutput: false + parseOutput: false, + sdk: client }); } catch (e) { log('You must login first') - await loginCommand() + await loginCommand(); } const answers = await inquirer.prompt(questionsInitProject) - if (!answers.project || !answers.organization) { + if (answers.override === false) { process.exit(1) } @@ -60,7 +64,6 @@ const initProject = async () => { const initBucket = async () => { let response = {} const answers = await inquirer.prompt(questionsCreateBucket) - if (!answers.bucket || !answers.id || !answers.fileSecurity) process.exit(1) try { response = await storageCreateBucket({ @@ -81,7 +84,6 @@ const initBucket = async () => { const initCollection = async () => { let response = {} const answers = await inquirer.prompt(questionsCreateCollection) - if (!answers.database || !answers.collection || !answers.id || !answers.documentSecurity) process.exit(1) try { response = await databasesCreateCollection({ @@ -103,7 +105,6 @@ const initCollection = async () => { const initTopic = async () => { let response = {} const answers = await inquirer.prompt(questionsCreateMessagingTopic) - if (!answers.topic || !answers.id) process.exit(1) try { response = await messagingCreateTopic({ @@ -115,7 +116,7 @@ const initTopic = async () => { localConfig.addMessagingTopic(response); success(); } catch (e) { - error(e.getMessage ?? 'Unknown error occurred. Please try again'); + error(e.message ?? 'Unknown error occurred. Please try again'); } }; diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 885393967..45e517a42 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -4,7 +4,7 @@ const { Command } = require("commander"); const inquirer = require("inquirer"); const { messagingListTopics } = require("./messaging"); const { teamsList } = require("./teams"); -const { projectsList } = require("./projects"); +const { projectsGet } = require("./projects"); const { functionsList, functionsDownloadDeployment } = require("./functions"); const { databasesGet, databasesListCollections, databasesList } = require("./databases"); const { storageListBuckets } = require("./storage"); @@ -24,43 +24,31 @@ const pull = new Command("pull") const pullProject = async () => { try { - let response = await projectsList({ + let response = await projectsGet({ parseOutput: false, - queries: [JSON.stringify({ method: 'equal', attribute: '$id', values: [localConfig.getProject().projectId] })] + projectId: localConfig.getProject().projectId }) - if(response.total === 1){ - localConfig.setProject(response.projects[0].$id, response.projects[0].name); - } + + localConfig.setProject(response.$id, response.name); success(); } catch (e) { throw e; } } -const pullFunctions = async ({ all, yes } = {}) => { - let functions = []; - let questions = questionsPullFunctions; - +const pullFunctions = async ({ all } = {}) => { const localFunctions = localConfig.getFunctions(); - if (all) { - questions = yes ? [] : questionsPullFunctions[1]; - functions = (await paginate(functionsList, { parseOutput: false }, 100, 'functions')).functions; - } - - const answers = await inquirer.prompt(questions); + const functions = all + ? (await paginate(functionsList, { parseOutput: false }, 100, 'functions')).functions + : (await inquirer.prompt(questionsPullFunctions)).functions; - const overridingLocalChanges = yes ?? answers.override.toLowerCase() === "yes"; - const selectedFunctions = functions.length === 0 ? answers.functions : functions; + log(`Pulling ${functions.length} functions`); - for (let func of selectedFunctions) { + for (let func of functions) { const functionExistLocally = localFunctions.find((localFunc) => localFunc['$id'] === func['$id']) !== undefined; - if (!overridingLocalChanges && functionExistLocally) { - log(`Skipping locally found implementation of ${func['name']}`) - continue; - } if (functionExistLocally) { localConfig.updateFunction(func['$id'], func); } else { @@ -74,7 +62,7 @@ const pullFunctions = async ({ all, yes } = {}) => { continue } - const compressedFileName = `${+new Date()}.tar.gz` + const compressedFileName = `${func['$id']}-${+new Date()}.tar.gz` await functionsDownloadDeployment({ functionId: func['$id'], @@ -92,7 +80,7 @@ const pullFunctions = async ({ all, yes } = {}) => { }); fs.rmSync(compressedFileName); - success(`Pulled ${func['name']} code and definition`) + success(`Pulled ${func['name']} code and settings`) } } @@ -111,7 +99,6 @@ const pullCollection = async ({ all, databaseId } = {}) => { if (databaseIds.length <= 0) { let answers = await inquirer.prompt(questionsPullCollection) - if (!answers.databases) process.exit(1) databaseIds.push(...answers.databases); } @@ -185,31 +172,30 @@ const pullMessagingTopic = async () => { pull .command("project") - .description("Pulling your Appwrite project name") + .description("Pulling your {{ spec.title|caseUcfirst }} project name") .action(actionRunner(pullProject)); pull .command("function") - .description(`Pulling your Appwrite functions`) - .option(`--yes`, `Flag to confirm all warnings`) + .description(`Pulling your {{ spec.title|caseUcfirst }} functions`) .option(`--all`, `Flag to pull all functions`) .action(actionRunner(pullFunctions)); pull .command("collection") - .description("Pulling your Appwrite collections") + .description("Pulling your {{ spec.title|caseUcfirst }} collections") .option(`--databaseId `, `Database ID`) .option(`--all`, `Flag to pull all databases`) .action(actionRunner(pullCollection)) pull .command("bucket") - .description("Pulling your Appwrite buckets") + .description("Pulling your {{ spec.title|caseUcfirst }} buckets") .action(actionRunner(pullBucket)) pull .command("team") - .description("Pulling your Appwrite teams") + .description("Pulling your {{ spec.title|caseUcfirst }} teams") .action(actionRunner(pullTeam)) pull diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 96806b2c3..2f93e5956 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -218,9 +218,14 @@ const questionsPullFunctions = [ type: "checkbox", name: "functions", message: "Which functions would you like to pull?", + validate: (value) => validateRequired('function', value), choices: async () => { const { functions } = await paginate(functionsList, { parseOutput: false }, 100, 'functions'); + if(functions.length === 0){ + throw "We couldn't find any functions in your {{ spec.title|caseUcfirst }} project"; + } + return functions.map(func => { return { name: `${func.name} (${func.$id})`, @@ -228,11 +233,6 @@ const questionsPullFunctions = [ } }); } - }, - { - type: "input", - name: "override", - message: `Are you sure you want to override local functions code and definition? ${chalk.red('All local changes will be lost!')} Type "YES" to confirm.` } ]; @@ -357,6 +357,7 @@ const questionsPullCollection = [ type: "checkbox", name: "databases", message: "From which database would you like to pull collections?", + validate: (value) => validateRequired('collection', value), choices: async () => { let response = await databasesList({ parseOutput: false 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 084/198] 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 085/198] 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 086/198] 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 087/198] 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 051891b422887df5f0f4e7c3fdf22d87ebeda903 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 15:13:39 -0400 Subject: [PATCH 088/198] chore(cli): Allowing the use of `deploy` like `push` --- templates/cli/lib/commands/push.js.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index c9b5b099e..d6fe7efe4 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1115,6 +1115,7 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { } const push = new Command("push") + .alias('deploy') .description(commandDescriptions['push']) .option(`--all`, `Flag to push all resources`) .option(`--yes`, `Flag to confirm all warnings`) From 768c4ed33753de9bae080e474d017688c0b19763 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 15:24:45 -0400 Subject: [PATCH 089/198] refactor(cli): Changing commands from single to plural when applicable --- templates/cli/lib/commands/pull.js.twig | 25 +++++++++++-------------- templates/cli/lib/commands/push.js.twig | 16 +++++++++------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 45e517a42..578046c4e 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -13,15 +13,6 @@ const { paginate } = require("../paginate"); const { questionsPullCollection, questionsPullFunctions } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); -const pull = new Command("pull") - .description(commandDescriptions['pull']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }) - .action(actionRunner(async (_options, command) => { - command.help(); - })); - const pullProject = async () => { try { let response = await projectsGet({ @@ -170,36 +161,42 @@ const pullMessagingTopic = async () => { success(); } +const pull = new Command("pull") + .description(commandDescriptions['pull']) + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }); + pull .command("project") .description("Pulling your {{ spec.title|caseUcfirst }} project name") .action(actionRunner(pullProject)); pull - .command("function") + .command("functions") .description(`Pulling your {{ spec.title|caseUcfirst }} functions`) .option(`--all`, `Flag to pull all functions`) .action(actionRunner(pullFunctions)); pull - .command("collection") + .command("collections") .description("Pulling your {{ spec.title|caseUcfirst }} collections") .option(`--databaseId `, `Database ID`) .option(`--all`, `Flag to pull all databases`) .action(actionRunner(pullCollection)) pull - .command("bucket") + .command("buckets") .description("Pulling your {{ spec.title|caseUcfirst }} buckets") .action(actionRunner(pullBucket)) pull - .command("team") + .command("teams") .description("Pulling your {{ spec.title|caseUcfirst }} teams") .action(actionRunner(pullTeam)) pull - .command("topic") + .command("topics") .description("Initialise your Appwrite messaging topics") .action(actionRunner(pullMessagingTopic)) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index d6fe7efe4..f94eb935c 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1117,8 +1117,10 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { const push = new Command("push") .alias('deploy') .description(commandDescriptions['push']) - .option(`--all`, `Flag to push all resources`) - .option(`--yes`, `Flag to confirm all warnings`) + +push + .command("all") + .description("Push all resource.") .action(actionRunner(pushResources)); push @@ -1127,7 +1129,7 @@ push .action(actionRunner(pushProject)); push - .command("function") + .command("functions") .description("Push functions in the current directory.") .option(`--functionId `, `Function ID`) .option(`--all`, `Flag to push all functions`) @@ -1136,28 +1138,28 @@ push .action(actionRunner(pushFunction)); push - .command("collection") + .command("collections") .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") + .command("buckets") .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") + .command("teams") .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") + .command("topics") .description("Push messaging topics in the current project.") .option(`--all`, `Flag to deploy all topics`) .option(`--yes`, `Flag to confirm all warnings`) From ca5c806affc2caa661df92a6ed8720b776a0e6c3 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 24 May 2024 16:43:14 -0400 Subject: [PATCH 090/198] feat(cli): Creating function from all available templates --- templates/cli/lib/commands/init.js.twig | 33 +++++++++++++++++++------ templates/cli/lib/questions.js.twig | 19 ++++++++++++++ 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index e0711ee03..4712f6ea6 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -12,6 +12,7 @@ const ID = require("../id"); const { localConfig, globalConfig } = require("../config"); const { questionsCreateFunction, + questionsCreateFunctionSelectTemplate, questionsCreateBucket, questionsCreateMessagingTopic, questionsCreateCollection, @@ -133,6 +134,8 @@ const initFunction = async () => { const functionId = answers.id === 'unique()' ? ID.unique() : answers.id; const functionDir = path.join(functionFolder, functionId); + const templatesDir = path.join(functionFolder, `${functionId}-templates`); + const runtimeDir = path.join(templatesDir, answers.runtime.name); if (fs.existsSync(functionDir)) { throw new Error(`( ${functionId} ) already exists in the current directory. Please choose another name.`); @@ -156,10 +159,12 @@ const initFunction = async () => { }) fs.mkdirSync(functionDir, "777"); + fs.mkdirSync(templatesDir, "777"); - let gitInitCommands = "git clone -b v3 --single-branch --depth 1 --sparse https://github.com/{{ sdk.gitUserName }}/functions-starter ."; // depth prevents fetching older commits reducing the amount fetched + 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 gitPullCommands = `git sparse-checkout add ${answers.runtime.name}`; - let gitPullCommands = `git sparse-checkout add ${answers.runtime.id}`; /* Force use CMD as powershell does not support && */ if (process.platform === 'win32') { @@ -167,10 +172,11 @@ 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: functionDir }); - childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: functionDir }); + childProcess.execSync(gitInitCommands, { stdio: 'pipe', cwd: templatesDir }); + childProcess.execSync(gitPullCommands, { stdio: 'pipe', cwd: templatesDir }); } catch (error) { /* Specialised errors with recommended actions to take */ if (error.message.includes('error: unknown option')) { @@ -182,7 +188,20 @@ const initFunction = async () => { } } - fs.rmSync(path.join(functionDir, ".git"), { recursive: true }); + fs.rmSync(path.join(templatesDir, ".git"), { recursive: true }); + const templates = ['Starter']; + templates.push(...fs.readdirSync(runtimeDir, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory() && dirent.name !== 'starter') + .map(dirent => dirent.name)); + + let 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); @@ -199,9 +218,9 @@ const initFunction = async () => { fs.copyFileSync(src, dest); } }; - copyRecursiveSync(path.join(functionDir, answers.runtime.id), functionDir); + copyRecursiveSync(path.join(runtimeDir, selected.template), functionDir); - fs.rmSync(`${functionDir}/${answers.runtime.id}`, { recursive: true, force: true }); + fs.rmSync(templatesDir, { recursive: true, force: true }); const readmePath = path.join(process.cwd(), 'functions', functionId, 'README.md'); const readmeFile = fs.readFileSync(readmePath).toString(); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 2f93e5956..28522ba9a 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -263,6 +263,7 @@ const questionsCreateFunction = [ name: `${runtime.name} (${runtime['$id']})`, value: { id: runtime['$id'], + name: runtime['$id'].split('-')[0], entrypoint: getEntrypoint(runtime['$id']), ignore: getIgnores(runtime['$id']), commands: getInstallCommand(runtime['$id']) @@ -274,6 +275,23 @@ const questionsCreateFunction = [ } ]; +const questionsCreateFunctionSelectTemplate = (templates) => { + return [ + { + type: "list", + 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,' '); + + return { value: template, name } + }) + } + ]; +}; + + + const questionsCreateBucket = [ { type: "input", @@ -617,6 +635,7 @@ const questionsMfaChallenge = [ module.exports = { questionsInitProject, questionsCreateFunction, + questionsCreateFunctionSelectTemplate, questionsCreateBucket, questionsCreateCollection, questionsCreateMessagingTopic, From 284bc48c8a172a872b63044a1cbe7f7d0eedaf8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 May 2024 13:41:33 +0000 Subject: [PATCH 091/198] Improve CLI aliases --- templates/cli/index.js.twig | 4 +-- templates/cli/lib/commands/generic.js.twig | 21 +++++++------ templates/cli/lib/commands/pull.js.twig | 9 ++++-- templates/cli/lib/commands/push.js.twig | 34 +++++++++++++--------- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 2126485c6..1466a5b54 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -26,8 +26,8 @@ program sortSubcommands: true }) .version(version, "-v, --version") - .option("--verbose", "Show complete error log") - .option("--json", "Output in JSON format") + .option("-V, --verbose", "Show complete error log") + .option("-j, --json", "Output in JSON format") .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 a8d6f91b5..96dec830f 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -3,15 +3,14 @@ const { Command } = require("commander"); const Client = require("../client"); const { sdkForConsole } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); -const { actionRunner, success, parseBool, commandDescriptions, error, parse, drawTable } = require("../parser"); +const { actionRunner, success, parseBool, commandDescriptions, error, parse, drawTable, cliConfig } = require("../parser"); {% if sdk.test != "true" %} const { questionsLogin, questionsListFactors, questionsMfaChallenge } = require("../questions"); const { accountUpdateMfaChallenge, accountCreateMfaChallenge, accountGet, accountCreateEmailPasswordSession, accountDeleteSession } = require("./account"); const whoami = new Command("whoami") .description(commandDescriptions['whoami']) - .option("-j, --json", "Output in JSON format") - .action(actionRunner(async ({ json }) => { + .action(actionRunner(async () => { if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { error("No user is signed in. To sign in, run: appwrite login "); return; @@ -39,9 +38,9 @@ const whoami = new Command("whoami") 'MFA enabled': account.mfa ? 'Yes' : 'No' } ]; - if (json) { - console.log(data); + if(cliConfig.json) { + console.log(data); return; } @@ -129,12 +128,12 @@ const client = new Command("client") .configureHelp({ helpWidth: process.stdout.columns || 80 }) - .option("--selfSigned ", "Configure the CLI to use a self-signed certificate ( true or false )", parseBool) - .option("--endpoint ", "Set your Appwrite server endpoint") - .option("--projectId ", "Set your Appwrite project ID") - .option("--key ", "Set your Appwrite server's API key") - .option("--debug", "Print CLI debug information") - .option("--reset", "Reset the CLI configuration") + .option("-ss, --selfSigned ", "Configure the CLI to use a self-signed certificate ( true or false )", parseBool) + .option("-e, --endpoint ", "Set your Appwrite server endpoint") + .option("-p, --projectId ", "Set your Appwrite project ID") + .option("-k, --key ", "Set your Appwrite server's API key") + .option("-d, --debug", "Print CLI debug information") + .option("-r, --reset", "Reset the CLI configuration") .action(actionRunner(async ({ selfSigned, endpoint, projectId, key, debug, reset }, command) => { if (selfSigned == undefined && endpoint == undefined && projectId == undefined && key == undefined && debug == undefined && reset == undefined) { command.help() diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 41c2ea9d1..22bdc3ae9 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -258,28 +258,33 @@ pull pull .command("function") + .alias("functions") .description("Pulling your {{ spec.title|caseUcfirst }} cloud function") .action(actionRunner(pullFunction)) pull .command("collection") + .command("collections") .description("Pulling your {{ spec.title|caseUcfirst }} collections") - .option(`--databaseId `, `Database ID`) - .option(`--all`, `Flag to pullialize all databases`) + .option(`-d, --databaseId `, `Database ID`) + .option(`-a, --all`, `Flag to pullialize all databases`) .action(actionRunner(pullCollection)) pull .command("bucket") + .command("buckets") .description("Pulling your Appwrite buckets") .action(actionRunner(pullBucket)) pull .command("team") + .command("teams") .description("Pulling your Appwrite teams") .action(actionRunner(pullTeam)) pull .command("topic") + .command("topics") .description("Initialise your Appwrite messaging topics") .action(actionRunner(pullMessagingTopic)) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 8e269cdd4..4eb801e12 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1101,46 +1101,52 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { } const push = new Command("push") + .alias("deploy") .description(commandDescriptions['push']) - .option(`--all`, `Flag to push all resources`) - .option(`--yes`, `Flag to confirm all warnings`) + .option(`-a, --all`, `Flag to push all resources`) + .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushResources)); push .command("function") + .alias("functions") .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`) + .option(`-f, --functionId `, `Function ID`) + .option(`-a, --all`, `Flag to push all functions`) + .option(`-y, --yes`, `Flag to confirm all warnings`) + .option(`-A, --async`, `Don't wait for functions deployments status`) .action(actionRunner(pushFunction)); push .command("collection") + .alias("collections") .description("Push collections in the current project.") - .option(`--all`, `Flag to push all collections`) - .option(`--yes`, `Flag to confirm all warnings`) + .option(`-a, --all`, `Flag to push all collections`) + .option(`-a, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushCollection)); push .command("bucket") + .alias("buckets") .description("Push buckets in the current project.") - .option(`--all`, `Flag to push all buckets`) - .option(`--yes`, `Flag to confirm all warnings`) + .option(`-a, --all`, `Flag to push all buckets`) + .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushBucket)); push .command("team") + .alias("teams") .description("Push teams in the current project.") - .option(`--all`, `Flag to push all teams`) - .option(`--yes`, `Flag to confirm all warnings`) + .option(`-a, --all`, `Flag to push all teams`) + .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushTeam)); push .command("topic") + .alias("topics") .description("Push messaging topics in the current project.") - .option(`--all`, `Flag to deploy all topics`) - .option(`--yes`, `Flag to confirm all warnings`) + .option(`-a, --all`, `Flag to deploy all topics`) + .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushMessagingTopic)); module.exports = { From 64726b2b81d7ce9518ae0d5b6c4615b73e7b58b1 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Mon, 27 May 2024 20:19:52 -0400 Subject: [PATCH 092/198] refactor(cli): Review fixing --- 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 4712f6ea6..424ed1d6b 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -191,7 +191,7 @@ const initFunction = async () => { fs.rmSync(path.join(templatesDir, ".git"), { recursive: true }); const templates = ['Starter']; templates.push(...fs.readdirSync(runtimeDir, { withFileTypes: true }) - .filter(dirent => dirent.isDirectory() && dirent.name !== 'starter') + .filter(item => item.isDirectory() && item.name !== 'starter') .map(dirent => dirent.name)); let selected = { template: 'starter' }; 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 093/198] 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 094/198] 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 0ea07f11fa564e148ba089ea7bbc461ba699d6c0 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 08:58:16 -0400 Subject: [PATCH 095/198] feat(cli): Beta CLI releases --- .github/workflows/cli-beta-publish.yaml | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/cli-beta-publish.yaml diff --git a/.github/workflows/cli-beta-publish.yaml b/.github/workflows/cli-beta-publish.yaml new file mode 100644 index 000000000..13373df64 --- /dev/null +++ b/.github/workflows/cli-beta-publish.yaml @@ -0,0 +1,47 @@ +name: CLI beta Publish +on: + pull_request: + paths: + - 'templates/cli/**.twig' + - 'src/SDK/Language/CLI.php' + +env: + PACKAGE_NAME: "${{ vars.PACKAGE_NAME }}@0.16.0${{ github.event.pull_request.head.sha }}" + +jobs: + publish: + environment: cli-testing + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v3 + - name: Setup Composer dependencies + run: docker run --rm --volume "$(pwd)":/app composer install --ignore-platform-reqs + - name: Generate SDKS + run: docker run --rm -v "$(pwd)":/app -w /app php:8.1-cli php example.php + - name: Fix permission + run: sudo chown -R 1001:1001 examples + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 'latest' + registry-url: 'https://registry.npmjs.org' + - name: Setup + working-directory: ./examples/cli/ + run: npm install + - name: Set version + working-directory: ./examples/cli/ + run: | + sed -i "s#appwrite-cli#${{ vars.PACKAGE_NAME }}#g" package.json + sed -i "s#0.16.0#0.16.0${{ github.event.pull_request.head.sha }}#g" package.json + - name: Publish + working-directory: examples/cli/ + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Instruction + run: | + echo "Install it by running npm install ${{ env.PACKAGE_NAME }}" + echo "Run it using npx ${{ env.PACKAGE_NAME }}" 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 096/198] 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 097/198] 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 098/198] 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 84b4c666eff850c153705254d9e3a853edc23f17 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 28 May 2024 10:39:00 -0400 Subject: [PATCH 099/198] refactor(cli): Review fixing --- example.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example.php b/example.php index 760657ee3..bcfdc2c5f 100644 --- a/example.php +++ b/example.php @@ -186,7 +186,7 @@ function getSSLPage($url) { ->setTwitter('appwrite_io') ->setDiscord('564160730845151244', 'https://appwrite.io/discord') ->setDefaultHeaders([ - 'X-Appwrite-Response-Format' => '0.15.0', + 'X-Appwrite-Response-Format' => '1.5.0', ]) ; @@ -393,7 +393,7 @@ function getSSLPage($url) { ; $sdk->generate(__DIR__ . '/examples/apple'); - + // DotNet $sdk = new SDK(new DotNet(), new Swagger2($spec)); @@ -442,7 +442,7 @@ function getSSLPage($url) { // Android $sdk = new SDK(new Android(), new Swagger2($spec)); - + $sdk ->setName('Android') ->setNamespace('io appwrite') @@ -466,7 +466,7 @@ function getSSLPage($url) { // Kotlin $sdk = new SDK(new Kotlin(), new Swagger2($spec)); - + $sdk ->setName('Kotlin') ->setNamespace('io appwrite') 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 100/198] 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 101/198] 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 102/198] 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 353d2a64d9d599d2e6d82f6515884fe19e6a0385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 29 May 2024 13:01:04 +0000 Subject: [PATCH 103/198] selection, configuration and settings of CLI local development --- src/SDK/Language/CLI.php | 5 + templates/cli/index.js.twig | 2 + templates/cli/lib/commands/run.js.twig | 121 +++++++++++++++++++++++++ templates/cli/lib/parser.js.twig | 1 + templates/cli/lib/questions.js.twig | 74 ++++++++++++++- templates/cli/lib/utils.js.twig | 40 +++++++- 6 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 templates/cli/lib/commands/run.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 4974c7cde..eb26e0c1f 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -182,6 +182,11 @@ public function getFiles(): array 'destination' => 'lib/commands/push.js', 'template' => 'cli/lib/commands/push.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/commands/run.js', + 'template' => 'cli/lib/commands/run.js.twig', + ], [ 'scope' => 'service', 'destination' => '/lib/commands/{{service.name | caseDash}}.js', diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 2126485c6..7b245d46a 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -13,6 +13,7 @@ const { client } = require("./lib/commands/generic"); {% if sdk.test != "true" %} const { login, logout, whoami } = require("./lib/commands/generic"); const { pull } = require("./lib/commands/pull"); +const { run } = require("./lib/commands/run"); const { push } = require("./lib/commands/push"); {% endif %} {% for service in spec.services %} @@ -40,6 +41,7 @@ program .addCommand(login) .addCommand(pull) .addCommand(push) + .addCommand(run) .addCommand(logout) {% endif %} {% for service in spec.services %} diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig new file mode 100644 index 000000000..a1184c844 --- /dev/null +++ b/templates/cli/lib/commands/run.js.twig @@ -0,0 +1,121 @@ +const inquirer = require("inquirer"); +const { Command } = require("commander"); +const { localConfig, globalConfig } = require("../config"); +const { paginate } = require('../paginate'); +const { questionsRunFunctions } = require("../questions"); +const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); +const { systemHasCommand, isPortTaken } = require('../utils'); + +const systemTools = { + 'node': { + commands: [ + { command: "node", docs: "https://nodejs.org/en/download/package-manager" }, + { command: "npm", docs: "https://nodejs.org/en/download/package-manager" }, + ], + dependencyFiles: [ "package.json", "package-lock.json" ] + }, + // TODO: Add all runtime needs +}; + +const runFunction = async ({ port, engine, functionId } = {}) => { + // Selection + if(!functionId) { + const answers = await inquirer.prompt(questionsRunFunctions[0]); + functionId = answers.function; + } + + const functions = localConfig.getFunctions(); + const func = functions.find((f) => f.$id === functionId); + if (!func) { + throw new Error("Function '" + functionId + "' not found.") + } + + // Configuration: Port + if(port) { + port = +port; + } + + if(isNaN(port)) { + port = null; + } + + if(port) { + const taken = await isPortTaken(port); + + if(taken) { + log(`Port ${port} is already used.`); + port = null; + } + } + + if(!port) { + const answers = await inquirer.prompt(questionsRunFunctions[1]); + port = answers.port; + } + + // Configuration: Engine + if(engine !== "system" && engine !== "docker") { + engine = null; + } + + if(!engine) { + const answers = await inquirer.prompt(questionsRunFunctions[2]); + engine = answers.engine; + } + + if(engine === 'docker') { + log('💡 Hint: Using system is faster, but using Docker simulates the production environment precisely.'); + + if(!systemHasCommand('docker')) { + return error("Please install Docker first: https://docs.docker.com/engine/install/"); + } + } else if(engine === 'system') { + log('💡 Hint: Docker simulates the production environment precisely, but using system is faster'); + + const runtimeName = func.runtime.split('-')[0]; + const tool = systemTools[runtimeName]; + + for(const command of tool.commands) { + if(!systemHasCommand(command.command)) { + return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`); + } + } + } + + // Settings + const settings = { + runtime: func.runtime, + entrypoint: func.entrypoint, + path: func.path, + commands: func.commands, + }; + log("Local function configuration:"); + drawTable([settings]); + log('If you wish to change local settings, update appwrite.json file and rerun the command. To deploy the function, run: appwrite push function'); + + childProcess.execSync('where ' + command, { stdio: 'pipe' }) + +} + +const run = new Command("run") + .alias("dev") + .description(commandDescriptions['run']) + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(async (_options, command) => { + command.help(); + })); + +run + .command("function") + .alias("functions") + .description("Run functions in the current directory.") + .option(`--functionId `, `Function ID`) + .option(`--port `, `Local port`) + .option(`--engine `, `Local engine, "system" or "docker"`) + .action(actionRunner(runFunction)); + +module.exports = { + run +} diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index dce756c9d..7eaf93170 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -157,6 +157,7 @@ const commandDescriptions = { "avatars": `The avatars command aims to help you complete everyday tasks related to your app image, icons, and avatars.`, "databases": `The databases command allows you to create structured collections of documents, query and filter lists of documents.`, "push": `The push command provides a convenient wrapper for pushing your functions, collections, buckets, teams and messaging.`, + "run": `The dev command allows you to run project locally to allow easy development and quick debugging.`, "functions": `The functions command allows you view, create and manage your Cloud Functions.`, "health": `The health command allows you to both validate and monitor your {{ spec.title|caseUcfirst }} server's health.`, "pull": `The pull command helps you pull your {{ spec.title|caseUcfirst }} project, functions, collections, buckets, teams and messaging`, diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 67b5fd8ed..694e34dd5 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -6,6 +6,7 @@ const { accountListMfaFactors } = require("./commands/account"); const { sdkForConsole } = require("./sdks"); const { validateRequired } = require("./validations"); const { paginate } = require('./paginate'); +const { isPortTaken } = require('./utils'); const { databasesList } = require('./commands/databases'); const JSONbig = require("json-bigint")({ storeAsString: false }); @@ -515,6 +516,76 @@ const questionsMfaChallenge = [ } ]; +const questionsRunFunctions = [ + { + type: "list", + name: "function", + message: "Which function would you like to develop locally?", + validate: (value) => validateRequired('function', value), + choices: () => { + let functions = localConfig.getFunctions(); + if (functions.length === 0) { + throw new Error("No functions found in the current directory."); + } + let choices = functions.map((func, idx) => { + return { + name: `${func.name} (${func.$id})`, + value: func.$id + } + }) + return choices; + } + }, + { + type: "number", + name: "port", + message: 'Which port would you like function to listen on?', + default: async () => { + let port = 3000; + while(port < 3100) { + const taken = await isPortTaken(port); + if(!taken) { + return port; + } + + port++; + } + + return 3000; + }, + validate: function(value) { + const done = this.async(); + + (async () => { + const taken = await isPortTaken(value); + + if(taken) { + throw Error(`Port ${value} is taken. Pick another one.`); + } + })().then(() => { + done(null, true); + }).catch((err) => { + done(err.message); + }); + }, + }, + { + type: "list", + name: "engine", + message: "Which engine would you like to use?", + choices: [ + { + name: "Docker", + value: "docker", + }, + { + name: "System", + value: "system", + }, + ], + }, +]; + module.exports = { questionsPullProject, questionsLogin, @@ -528,5 +599,6 @@ module.exports = { questionsPushTeams, questionsGetEntrypoint, questionsListFactors, - questionsMfaChallenge + questionsMfaChallenge, + questionsRunFunctions }; diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index 289b1fa6e..48af40325 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -1,5 +1,7 @@ const fs = require("fs"); const path = require("path"); +const net = require("net"); +const childProcess = require('child_process'); function getAllFiles(folder) { const files = []; @@ -14,6 +16,42 @@ function getAllFiles(folder) { return files; } +async function isPortTaken(port) { + const taken = await new Promise((res, rej) => { + const tester = net.createServer() + .once('error', function (err) { + if (err.code != 'EADDRINUSE') return rej(err) + res(true) + }) + .once('listening', function() { + tester.once('close', function() { res(false) }) + .close() + }) + .listen(port); + }); + + return taken; +} + +function systemHasCommand(command) { + const isUsingWindows = process.platform == 'win32' + + try { + if(isUsingWindows) { + childProcess.execSync('where ' + command, { stdio: 'pipe' }) + } else { + childProcess.execSync(`[[ $(${command} --version) ]] || { exit 1; } && echo "OK"`, { stdio: 'pipe', shell: '/bin/bash' }); + } + } catch (error) { + console.log(error); + return false; + } + + return true; +} + module.exports = { - getAllFiles + getAllFiles, + isPortTaken, + systemHasCommand }; 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 104/198] 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 105/198] 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 106/198] 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 107/198] 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 108/198] 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 109/198] 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 110/198] 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 111/198] 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 112/198] 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 113/198] 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 0575002daf56cafa2b18ce9b34982c557d7133b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 30 May 2024 11:05:05 +0000 Subject: [PATCH 114/198] More progress on local development --- templates/cli/base/params.twig | 2 + templates/cli/lib/commands/run.js.twig | 159 ++++++++++++++++++++++++- templates/cli/package.json.twig | 3 +- 3 files changed, 159 insertions(+), 5 deletions(-) diff --git a/templates/cli/base/params.twig b/templates/cli/base/params.twig index 87ca01341..a369232b0 100644 --- a/templates/cli/base/params.twig +++ b/templates/cli/base/params.twig @@ -16,6 +16,8 @@ const func = localConfig.getFunction(functionId); + ignore.add('.appwrite'); + if (func.ignore) { ignorer.add(func.ignore); } else if (fs.existsSync(pathLib.join({{ parameter.name | caseCamel | escapeKeyword }}, '.gitignore'))) { diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index a1184c844..367deafd3 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -1,13 +1,21 @@ +const childProcess = require('child_process'); +const chokidar = require('chokidar'); const inquirer = require("inquirer"); +const path = require("path"); const { Command } = require("commander"); const { localConfig, globalConfig } = require("../config"); const { paginate } = require('../paginate'); const { questionsRunFunctions } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); const { systemHasCommand, isPortTaken } = require('../utils'); +const { info } = require('console'); + +const activeDockerIds = {}; const systemTools = { 'node': { + isCompiled: false, + startCommand: "node src/server.js", commands: [ { command: "node", docs: "https://nodejs.org/en/download/package-manager" }, { command: "npm", docs: "https://nodejs.org/en/download/package-manager" }, @@ -17,6 +25,101 @@ const systemTools = { // TODO: Add all runtime needs }; +async function dockerStop(id) { + delete activeDockerIds[id]; + const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { + stdio: 'pipe', + }); + + await new Promise((res) => { stopProcess.on('close', res) }); +} + +async function dockerPull(func) { + log('Pulling Docker image of function runtime ...'); + + const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); + const imageName = `openruntimes/${runtimeName}:v3-${runtimeVersion}`; + + const pullProcess = childProcess.spawn('docker', ['pull', imageName], { + stdio: 'pipe', + pwd: path.join(process.cwd(), func.path) + }); + + pullProcess.stderr.on('data', (data) => { + process.stderr.write(`\n${data}$ `); + }); + + await new Promise((res) => { pullProcess.on('close', res) }); +} + +async function dockerBuild(func) { + log('Building function using Docker engine ...'); + + const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); + const imageName = `openruntimes/${runtimeName}:v3-${runtimeVersion}`; + + const functionDir = path.join(process.cwd(), func.path); + + const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`; + const params = ['run', '--rm', '--name', id, '-i', '-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`, '-v', `${functionDir}/:/mnt/code:rw`, imageName, 'sh', '-c', ` helpers/build.sh "${func.commands}"`]; + + const buildProcess = childProcess.spawn('docker', params, { + stdio: 'pipe', + pwd: functionDir + }); + + buildProcess.stdout.on('data', (data) => { + process.stdout.write(`\n${data}`); + }); + + buildProcess.stderr.on('data', (data) => { + process.stderr.write(`\n${data}`); + }); + + activeDockerIds[id] = true; + + await new Promise((res) => { buildProcess.on('close', res) }); + + delete activeDockerIds[id]; +} + +async function dockerStart(func, port) { + log('Starting function using Docker engine ...'); + + log("Permissions, events, CRON and timeouts dont apply when running locally."); + + log('💡 Hint: Function automatically restarts when you edit your code.'); + + success(`Visit http://localhost:${port}/ to execute your function.`); + + const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); + const imageName = `openruntimes/${runtimeName}:v3-${runtimeVersion}`; + + const tool = systemTools[runtimeName]; + + const functionDir = path.join(process.cwd(), func.path); + + const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`; + const params = ['run', '--rm', '--name', id, '-i', '-e', 'OPEN_RUNTIMES_SECRET=', '-p', `${port}:3000`, '-v', `${functionDir}/:/mnt/code:rw`, imageName, 'sh', '-c', ` helpers/start.sh "${tool.startCommand}"`]; + + const execProcess = childProcess.spawn('docker', params, { + stdio: 'pipe', + pwd: functionDir + }); + + // TODO: Find a way to see context.log + + execProcess.stdout.on('data', (data) => { + process.stdout.write(`\n${data}`); + }); + + execProcess.stderr.on('data', (data) => { + process.stderr.write(`\n${data}`); + }); + + activeDockerIds[id] = true; +} + const runFunction = async ({ port, engine, functionId } = {}) => { // Selection if(!functionId) { @@ -30,6 +133,9 @@ const runFunction = async ({ port, engine, functionId } = {}) => { throw new Error("Function '" + functionId + "' not found.") } + const runtimeName = func.runtime.split('-')[0]; + const tool = systemTools[runtimeName]; + // Configuration: Port if(port) { port = +port; @@ -72,9 +178,6 @@ const runFunction = async ({ port, engine, functionId } = {}) => { } else if(engine === 'system') { log('💡 Hint: Docker simulates the production environment precisely, but using system is faster'); - const runtimeName = func.runtime.split('-')[0]; - const tool = systemTools[runtimeName]; - for(const command of tool.commands) { if(!systemHasCommand(command.command)) { return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`); @@ -93,8 +196,56 @@ const runFunction = async ({ port, engine, functionId } = {}) => { drawTable([settings]); log('If you wish to change local settings, update appwrite.json file and rerun the command. To deploy the function, run: appwrite push function'); - childProcess.execSync('where ' + command, { stdio: 'pipe' }) + process.on('SIGINT', () => { + for(const id in activeDockerIds) { + dockerStop(id); + } + + process.exit(); + }); + + if(engine === "docker") { + await dockerPull(func); + await dockerBuild(func); + await dockerStart(func, port); + + let watcherRunning = false; + + chokidar.watch('.', { + cwd: path.join(process.cwd(), func.path), + ignoreInitial: true, + ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite' ] + }).on('all', async (event, path) => { + if(watcherRunning) { + info("File change detected but ignored, because live reload is already being ran."); + return; + } + watcherRunning = true; + + log('Detected a change in ' + path); + + try { + log('Stopping the function ...'); + + for(const id in activeDockerIds) { + await dockerStop(id); + } + + if(tool.isCompiled || tool.dependencyFiles.includes(path)) { + await dockerBuild(func); + await dockerStart(func, port); + } else { + // TODO: Update code.tar.gz with latest changes + await dockerStart(func, port); + } + } catch(err) { + console.error(err); + } finally { + watcherRunning = false; + } + }); + } } const run = new Command("run") diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 015e494e1..09d6ca0ba 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -31,7 +31,8 @@ "json-bigint": "^1.0.0", "inquirer": "^8.2.4", "tar": "^6.1.11", - "ignore": "^5.2.0" + "ignore": "^5.2.0", + "chokidar": "^3.6.0" }, "devDependencies": { "pkg": "5.8.1" 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 115/198] 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 116/198] 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 117/198] 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 118/198] 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 119/198] 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 120/198] 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 121/198] 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 122/198] 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 123/198] 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 5804bf70b1b0a43f17dbc9aa5e1c602bc6368db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 31 May 2024 10:48:16 +0000 Subject: [PATCH 124/198] Update hot swap logic --- templates/cli/base/params.twig | 2 +- templates/cli/lib/commands/run.js.twig | 87 +++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 11 deletions(-) diff --git a/templates/cli/base/params.twig b/templates/cli/base/params.twig index a369232b0..88ca44da0 100644 --- a/templates/cli/base/params.twig +++ b/templates/cli/base/params.twig @@ -16,7 +16,7 @@ const func = localConfig.getFunction(functionId); - ignore.add('.appwrite'); + ignorer.add('.appwrite'); if (func.ignore) { ignorer.add(func.ignore); diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index 367deafd3..ea2b4c16d 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -1,3 +1,6 @@ +const ignore = require("ignore"); +const tar = require("tar"); +const fs = require("fs"); const childProcess = require('child_process'); const chokidar = require('chokidar'); const inquirer = require("inquirer"); @@ -7,11 +10,12 @@ const { localConfig, globalConfig } = require("../config"); const { paginate } = require('../paginate'); const { questionsRunFunctions } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); -const { systemHasCommand, isPortTaken } = require('../utils'); +const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils'); const { info } = require('console'); const activeDockerIds = {}; +const openRuntimesVersion = 'v3'; const systemTools = { 'node': { isCompiled: false, @@ -35,10 +39,11 @@ async function dockerStop(id) { } async function dockerPull(func) { + return; // TODO: Remove log('Pulling Docker image of function runtime ...'); const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); - const imageName = `openruntimes/${runtimeName}:v3-${runtimeVersion}`; + const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; const pullProcess = childProcess.spawn('docker', ['pull', imageName], { stdio: 'pipe', @@ -56,7 +61,7 @@ async function dockerBuild(func) { log('Building function using Docker engine ...'); const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); - const imageName = `openruntimes/${runtimeName}:v3-${runtimeVersion}`; + const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; const functionDir = path.join(process.cwd(), func.path); @@ -93,14 +98,14 @@ async function dockerStart(func, port) { success(`Visit http://localhost:${port}/ to execute your function.`); const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); - const imageName = `openruntimes/${runtimeName}:v3-${runtimeVersion}`; + const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; const tool = systemTools[runtimeName]; const functionDir = path.join(process.cwd(), func.path); const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`; - const params = ['run', '--rm', '--name', id, '-i', '-e', 'OPEN_RUNTIMES_SECRET=', '-p', `${port}:3000`, '-v', `${functionDir}/:/mnt/code:rw`, imageName, 'sh', '-c', ` helpers/start.sh "${tool.startCommand}"`]; + const params = ['run', '--rm', '--name', id, '-i', '-e', 'OPEN_RUNTIMES_SECRET=', '-p', `${port}:3000`, '-v', `${functionDir}/.appwrite/logs:/mnt/logs:rw`, '-v', `${functionDir}/:/mnt/code:rw`, imageName, 'sh', '-c', ` helpers/start.sh "${tool.startCommand}"`]; const execProcess = childProcess.spawn('docker', params, { stdio: 'pipe', @@ -211,11 +216,21 @@ const runFunction = async ({ port, engine, functionId } = {}) => { let watcherRunning = false; + childProcess.execSync(`sudo mkdir -p ${path.join(process.cwd(), func.path, '.appwrite/logs')}`, { + pwd: path.join(process.cwd(), func.path) + }); + chokidar.watch('.appwrite/logs', { + cwd: path.join(process.cwd(), func.path), + ignoreInitial: true, + }).on('all', async (event, filePath) => { + console.log(fs.readFileSync(path.join(process.cwd(), func.path, filePath)).toString()); + }); + chokidar.watch('.', { cwd: path.join(process.cwd(), func.path), ignoreInitial: true, - ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite' ] - }).on('all', async (event, path) => { + ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite', '.appwrite/', '.appwrite/*', '.appwrite/**', '.appwrite/*.*', '.appwrite/**/*.*' ] + }).on('all', async (event, filePath) => { if(watcherRunning) { info("File change detected but ignored, because live reload is already being ran."); return; @@ -223,7 +238,7 @@ const runFunction = async ({ port, engine, functionId } = {}) => { watcherRunning = true; - log('Detected a change in ' + path); + log('Detected a change in ' + filePath); try { log('Stopping the function ...'); @@ -232,11 +247,63 @@ const runFunction = async ({ port, engine, functionId } = {}) => { await dockerStop(id); } - if(tool.isCompiled || tool.dependencyFiles.includes(path)) { + if(tool.isCompiled || tool.dependencyFiles.includes(filePath)) { await dockerBuild(func); await dockerStart(func, port); } else { - // TODO: Update code.tar.gz with latest changes + // TODO: Some try-catch approach, to rebuild if fails + log('Hot swapping function files ...'); + + const functionPath = path.join(process.cwd(), func.path); + const hotSwapPath = path.join(functionPath, '.appwrite/hot-swap'); + const buildPath = path.join(functionPath, 'code.tar.gz') + + // TODO: Using Node code, no sudo + childProcess.execSync(`sudo mkdir -p ${hotSwapPath} && sudo chmod 777 ${buildPath} ${hotSwapPath} && sudo tar -zxf ${buildPath} -C ${hotSwapPath}`, { + pwd: path.join(process.cwd(), func.path) + }); + + const ignorer = ignore(); + ignorer.add('.appwrite'); + + if (func.ignore) { + ignorer.add(func.ignore); + } + + // TODO: Better approach + const filesToCopy = getAllFiles(functionPath).map((file) => path.relative(functionPath, file)).filter((file) => !ignorer.ignores(file)); + + const copyCommands = []; + for(const f of filesToCopy) { + const filePath = path.join(hotSwapPath, f); + copyCommands.push(`sudo rm -rf ${filePath}`); + + const fileDir = path.dirname(filePath); + copyCommands.push(`sudo mkdir -p ${fileDir}`); + + const sourcePath = path.join(functionPath, f); + copyCommands.push(`sudo cp ${sourcePath} ${filePath}`); + } + childProcess.execSync(copyCommands.join(" && "), { + pwd: path.join(process.cwd(), func.path) + }); + + console.log(path.join(process.cwd(), func.path, '.appwrite')); + /* + childProcess.execSync("sudo chmod -R 777 .", { + pwd: path.join(process.cwd(), func.path, '.appwrite') + }); + */ + + childProcess.execSync(`sudo tar -C ${hotSwapPath} --exclude code.tar.gz -zcf ${buildPath} .`, { + pwd: hotSwapPath + }); + + // TODO: Using Node code + childProcess.execSync(`sudo rm -rf ${hotSwapPath}`, { + pwd: path.join(process.cwd(), func.path) + }); + await dockerStart(func, port); } } catch(err) { 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 125/198] 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 126/198] 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 127/198] 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 128/198] 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 129/198] 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 130/198] 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 4ca540239de42abeacc4b68dcf18f665d7cc837a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 3 Jun 2024 13:43:13 +0000 Subject: [PATCH 131/198] Finish run function Docker implementation --- templates/cli/lib/commands/run.js.twig | 283 ++++++++++++++++++------- templates/cli/lib/questions.js.twig | 8 +- templates/cli/package.json.twig | 3 +- 3 files changed, 215 insertions(+), 79 deletions(-) diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index ea2b4c16d..042b12cc3 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -1,3 +1,5 @@ +const Tail = require('tail').Tail; +const EventEmitter = require('node:events'); const ignore = require("ignore"); const tar = require("tar"); const fs = require("fs"); @@ -8,10 +10,10 @@ const path = require("path"); const { Command } = require("commander"); const { localConfig, globalConfig } = require("../config"); const { paginate } = require('../paginate'); +const { functionsListVariables } = require('./functions'); const { questionsRunFunctions } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils'); -const { info } = require('console'); const activeDockerIds = {}; @@ -29,6 +31,33 @@ const systemTools = { // TODO: Add all runtime needs }; +// TODO: Logs dont work + +const Queue = { + files: [], + locked: false, + events: new EventEmitter(), + push(file) { + if(!this.files.includes(file)) { + this.files.push(file); + } + + if(!this.locked) { + this.events.emit('reload', { files: this.files }); + } + }, + lock() { + this.files = []; + this.locked = true; + }, + unlock() { + this.locked = false; + if(this.files.length > 0) { + this.events.emit('reload', { files: this.files }); + } + } +}; + async function dockerStop(id) { delete activeDockerIds[id]; const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { @@ -57,7 +86,7 @@ async function dockerPull(func) { await new Promise((res) => { pullProcess.on('close', res) }); } -async function dockerBuild(func) { +async function dockerBuild(func, variables) { log('Building function using Docker engine ...'); const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); @@ -66,7 +95,20 @@ async function dockerBuild(func) { const functionDir = path.join(process.cwd(), func.path); const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`; - const params = ['run', '--rm', '--name', id, '-i', '-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`, '-v', `${functionDir}/:/mnt/code:rw`, imageName, 'sh', '-c', ` helpers/build.sh "${func.commands}"`]; + + const params = [ 'run' ]; + params.push('--name', id); + params.push('-v', `${functionDir}/:/mnt/code:rw`); + params.push('-e', 'APPWRITE_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_SECRET='); + params.push('-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`); + + for(const v of variables) { + params.push('-e', `${v.key}=${v.value}`); + } + + params.push(imageName, 'sh', '-c', `helpers/build.sh "${func.commands}"`); const buildProcess = childProcess.spawn('docker', params, { stdio: 'pipe', @@ -81,14 +123,37 @@ async function dockerBuild(func) { process.stderr.write(`\n${data}`); }); - activeDockerIds[id] = true; - await new Promise((res) => { buildProcess.on('close', res) }); + const copyPath = path.join(process.cwd(), func.path, '.appwrite', 'build.tar.gz'); + const copyDir = path.dirname(copyPath); + if (!fs.existsSync(copyDir)) { + fs.mkdirSync(copyDir, { recursive: true }); + } + + const copyProcess = childProcess.spawn('docker', ['cp', `${id}:/mnt/code/code.tar.gz`, copyPath], { + stdio: 'pipe', + pwd: functionDir + }); + + await new Promise((res) => { copyProcess.on('close', res) }); + + const cleanupProcess = childProcess.spawn('docker', ['rm', '--force', id], { + stdio: 'pipe', + pwd: functionDir + }); + + await new Promise((res) => { cleanupProcess.on('close', res) }); + delete activeDockerIds[id]; + + const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); + if (fs.existsSync(tempPath)) { + fs.rmSync(tempPath, { force: true }); + } } -async function dockerStart(func, port) { +async function dockerStart(func, variables, port) { log('Starting function using Docker engine ...'); log("Permissions, events, CRON and timeouts dont apply when running locally."); @@ -105,24 +170,51 @@ async function dockerStart(func, port) { const functionDir = path.join(process.cwd(), func.path); const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`; - const params = ['run', '--rm', '--name', id, '-i', '-e', 'OPEN_RUNTIMES_SECRET=', '-p', `${port}:3000`, '-v', `${functionDir}/.appwrite/logs:/mnt/logs:rw`, '-v', `${functionDir}/:/mnt/code:rw`, imageName, 'sh', '-c', ` helpers/start.sh "${tool.startCommand}"`]; - const execProcess = childProcess.spawn('docker', params, { + const params = [ 'run' ]; + params.push('--rm'); + params.push('-d'); + params.push('--name', id); + params.push('-p', `${port}:3000`); + params.push('-e', 'APPWRITE_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_SECRET='); + + for(const v of variables) { + params.push('-e', `${v.key}=${v.value}`); + } + + params.push('-v', `${functionDir}/.appwrite/logs.txt:/mnt/logs/dev_logs.log:rw`); + params.push('-v', `${functionDir}/.appwrite/errors.txt:/mnt/logs/dev_errors.log:rw`); + params.push('-v', `${functionDir}/.appwrite/build.tar.gz:/mnt/code/code.tar.gz:ro`); + params.push(imageName, 'sh', '-c', `helpers/start.sh "${tool.startCommand}"`); + + childProcess.spawn('docker', params, { stdio: 'pipe', pwd: functionDir }); - // TODO: Find a way to see context.log + activeDockerIds[id] = true; +} - execProcess.stdout.on('data', (data) => { - process.stdout.write(`\n${data}`); - }); - - execProcess.stderr.on('data', (data) => { - process.stderr.write(`\n${data}`); - }); +async function dockerCleanup() { + const ids = Object.keys(activeDockerIds); + for await (const id of ids) { + await dockerStop(id); + } - activeDockerIds[id] = true; + const functions = localConfig.getFunctions(); + for(const func of functions) { + const appwritePath = path.join(process.cwd(), func.path, '.appwrite'); + if (fs.existsSync(appwritePath)) { + fs.rmSync(appwritePath, { recursive: true, force: true }); + } + + const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); + if (fs.existsSync(tempPath)) { + fs.rmSync(tempPath, { force: true }); + } + } } const runFunction = async ({ port, engine, functionId } = {}) => { @@ -197,48 +289,80 @@ const runFunction = async ({ port, engine, functionId } = {}) => { path: func.path, commands: func.commands, }; + log("Local function configuration:"); drawTable([settings]); log('If you wish to change local settings, update appwrite.json file and rerun the command. To deploy the function, run: appwrite push function'); - process.on('SIGINT', () => { - for(const id in activeDockerIds) { - dockerStop(id); - } + await dockerCleanup(); + process.on('SIGINT', async () => { + log('Cleaning up ...'); + await dockerCleanup(); + success(); process.exit(); }); if(engine === "docker") { - await dockerPull(func); - await dockerBuild(func); - await dockerStart(func, port); + const logsPath = path.join(process.cwd(), func.path, '.appwrite/logs.txt'); + const errorsPath = path.join(process.cwd(), func.path, '.appwrite/errors.txt'); + + if(!fs.existsSync(path.dirname(logsPath))) { + fs.mkdirSync(path.dirname(logsPath), { recursive: true }); + } + + if (!fs.existsSync(logsPath)) { + fs.writeFileSync(logsPath, ''); + } - let watcherRunning = false; + if (!fs.existsSync(errorsPath)) { + fs.writeFileSync(errorsPath, ''); + } - childProcess.execSync(`sudo mkdir -p ${path.join(process.cwd(), func.path, '.appwrite/logs')}`, { - pwd: path.join(process.cwd(), func.path) + let variables = []; + if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { + // TODO: Flag to disable + error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set."); + } else { + const { variables: remoteVariables } = await paginate(functionsListVariables, { + functionId: func['$id'], + parseOutput: false + }, 100, 'variables'); + + remoteVariables.forEach((v) => { + variables.push({ + key: v.key, + value: v.value + }); + }); + } + + await dockerPull(func); + await dockerBuild(func, variables); + await dockerStart(func, variables, port); + + new Tail(logsPath).on("line", function(data) { + console.log(data); }); - chokidar.watch('.appwrite/logs', { - cwd: path.join(process.cwd(), func.path), - ignoreInitial: true, - }).on('all', async (event, filePath) => { - console.log(fs.readFileSync(path.join(process.cwd(), func.path, filePath)).toString()); + new Tail(errorsPath).on("line", function(data) { + console.log(data); }); chokidar.watch('.', { cwd: path.join(process.cwd(), func.path), ignoreInitial: true, ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite', '.appwrite/', '.appwrite/*', '.appwrite/**', '.appwrite/*.*', '.appwrite/**/*.*' ] - }).on('all', async (event, filePath) => { - if(watcherRunning) { - info("File change detected but ignored, because live reload is already being ran."); - return; - } + }).on('all', async (_event, filePath) => { + Queue.push(filePath); + }); - watcherRunning = true; + Queue.events.on('reload', async ({ files }) => { + Queue.lock(); - log('Detected a change in ' + filePath); + log('Live-reloading due to file changes: '); + for(const file of files) { + log(`- ${file}`); + } try { log('Stopping the function ...'); @@ -246,70 +370,73 @@ const runFunction = async ({ port, engine, functionId } = {}) => { for(const id in activeDockerIds) { await dockerStop(id); } - - if(tool.isCompiled || tool.dependencyFiles.includes(filePath)) { - await dockerBuild(func); - await dockerStart(func, port); + + const dependencyFile = files.find((filePath) => tool.dependencyFiles.includes(filePath)); + if(tool.isCompiled || dependencyFile) { + log(`Rebuilding the function due to cange in ${dependencyFile} ...`); + await dockerBuild(func, variables); + await dockerStart(func, variables, port); } else { - // TODO: Some try-catch approach, to rebuild if fails - log('Hot swapping function files ...'); + log('Hot-swapping function files ...'); const functionPath = path.join(process.cwd(), func.path); const hotSwapPath = path.join(functionPath, '.appwrite/hot-swap'); - const buildPath = path.join(functionPath, 'code.tar.gz') + const buildPath = path.join(functionPath, '.appwrite/build.tar.gz'); + + // Prepare temp folder + if (!fs.existsSync(hotSwapPath)) { + fs.mkdirSync(hotSwapPath, { recursive: true }); + } else { + fs.rmSync(hotSwapPath, { recursive: true, force: true }); + fs.mkdirSync(hotSwapPath, { recursive: true }); + } - // TODO: Using Node code, no sudo - childProcess.execSync(`sudo mkdir -p ${hotSwapPath} && sudo chmod 777 ${buildPath} ${hotSwapPath} && sudo tar -zxf ${buildPath} -C ${hotSwapPath}`, { - pwd: path.join(process.cwd(), func.path) - }); + await tar + .extract({ + gzip: true, + sync: true, + cwd: hotSwapPath, + file: buildPath + }); const ignorer = ignore(); ignorer.add('.appwrite'); - if (func.ignore) { ignorer.add(func.ignore); } - // TODO: Better approach const filesToCopy = getAllFiles(functionPath).map((file) => path.relative(functionPath, file)).filter((file) => !ignorer.ignores(file)); - - const copyCommands = []; for(const f of filesToCopy) { const filePath = path.join(hotSwapPath, f); - copyCommands.push(`sudo rm -rf ${filePath}`); + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { force: true }); + } const fileDir = path.dirname(filePath); - copyCommands.push(`sudo mkdir -p ${fileDir}`); + if (!fs.existsSync(fileDir)) { + fs.mkdirSync(fileDir, { recursive: true }); + } const sourcePath = path.join(functionPath, f); - copyCommands.push(`sudo cp ${sourcePath} ${filePath}`); + fs.copyFileSync(sourcePath, filePath); } - childProcess.execSync(copyCommands.join(" && "), { - pwd: path.join(process.cwd(), func.path) - }); - - console.log(path.join(process.cwd(), func.path, '.appwrite')); - /* - childProcess.execSync("sudo chmod -R 777 .", { - pwd: path.join(process.cwd(), func.path, '.appwrite') - }); - */ - - childProcess.execSync(`sudo tar -C ${hotSwapPath} --exclude code.tar.gz -zcf ${buildPath} .`, { - pwd: hotSwapPath - }); + + await tar + .create({ + gzip: true, + sync: true, + cwd: hotSwapPath, + file: buildPath + }, ['.']); - // TODO: Using Node code - childProcess.execSync(`sudo rm -rf ${hotSwapPath}`, { - pwd: path.join(process.cwd(), func.path) - }); + fs.rmSync(hotSwapPath, { recursive: true, force: true }); - await dockerStart(func, port); + await dockerStart(func, variables, port); } } catch(err) { console.error(err); } finally { - watcherRunning = false; + Queue.unlock(); } }); } @@ -332,6 +459,8 @@ run .option(`--functionId `, `Function ID`) .option(`--port `, `Local port`) .option(`--engine `, `Local engine, "system" or "docker"`) + // TODO: Option to disable auto reloading + // TODO: Option to enable write operations to function folder .action(actionRunner(runFunction)); module.exports = { diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 694e34dd5..17b77bcd6 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -537,7 +537,7 @@ const questionsRunFunctions = [ } }, { - type: "number", + type: "input", name: "port", message: 'Which port would you like function to listen on?', default: async () => { @@ -557,6 +557,12 @@ const questionsRunFunctions = [ const done = this.async(); (async () => { + if (typeof value !== 'number' && isNaN(+value)) { + throw Error(`You need to provide a number.`); + } + + value = +value; + const taken = await isPortTaken(value); if(taken) { diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 09d6ca0ba..c683d0710 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -32,7 +32,8 @@ "inquirer": "^8.2.4", "tar": "^6.1.11", "ignore": "^5.2.0", - "chokidar": "^3.6.0" + "chokidar": "^3.6.0", + "tail": "^2.2.6" }, "devDependencies": { "pkg": "5.8.1" From 93ea80a89f21668c86279d6620927cdc65404e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 3 Jun 2024 16:31:31 +0000 Subject: [PATCH 132/198] Add tags to dev command --- templates/cli/lib/commands/run.js.twig | 53 +++++++++++++------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index 042b12cc3..68e1ee04d 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -31,8 +31,6 @@ const systemTools = { // TODO: Add all runtime needs }; -// TODO: Logs dont work - const Queue = { files: [], locked: false, @@ -217,7 +215,7 @@ async function dockerCleanup() { } } -const runFunction = async ({ port, engine, functionId } = {}) => { +const runFunction = async ({ port, engine, functionId, noVariables, noReload } = {}) => { // Selection if(!functionId) { const answers = await inquirer.prompt(questionsRunFunctions[0]); @@ -320,21 +318,22 @@ const runFunction = async ({ port, engine, functionId } = {}) => { } let variables = []; - if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { - // TODO: Flag to disable - error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set."); - } else { - const { variables: remoteVariables } = await paginate(functionsListVariables, { - functionId: func['$id'], - parseOutput: false - }, 100, 'variables'); - - remoteVariables.forEach((v) => { - variables.push({ - key: v.key, - value: v.value + if(!noVariables) { + if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { + error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set."); + } else { + const { variables: remoteVariables } = await paginate(functionsListVariables, { + functionId: func['$id'], + parseOutput: false + }, 100, 'variables'); + + remoteVariables.forEach((v) => { + variables.push({ + key: v.key, + value: v.value + }); }); - }); + } } await dockerPull(func); @@ -348,13 +347,15 @@ const runFunction = async ({ port, engine, functionId } = {}) => { console.log(data); }); - chokidar.watch('.', { - cwd: path.join(process.cwd(), func.path), - ignoreInitial: true, - ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite', '.appwrite/', '.appwrite/*', '.appwrite/**', '.appwrite/*.*', '.appwrite/**/*.*' ] - }).on('all', async (_event, filePath) => { - Queue.push(filePath); - }); + if(!noReload) { + chokidar.watch('.', { + cwd: path.join(process.cwd(), func.path), + ignoreInitial: true, + ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite', '.appwrite/', '.appwrite/*', '.appwrite/**', '.appwrite/*.*', '.appwrite/**/*.*' ] + }).on('all', async (_event, filePath) => { + Queue.push(filePath); + }); + } Queue.events.on('reload', async ({ files }) => { Queue.lock(); @@ -459,8 +460,8 @@ run .option(`--functionId `, `Function ID`) .option(`--port `, `Local port`) .option(`--engine `, `Local engine, "system" or "docker"`) - // TODO: Option to disable auto reloading - // TODO: Option to enable write operations to function folder + .option(`--noVariables`, `Prevent pulling variables from function settings`) + .option(`--noReload`, `Prevent live reloading of server when changes are made to function files`) .action(actionRunner(runFunction)); module.exports = { From 73e3c407828f9280fa6a5761a8616a75c3dc5b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 5 Jun 2024 13:31:06 +0000 Subject: [PATCH 133/198] Add env var and headers support to local development --- templates/cli/lib/commands/run.js.twig | 131 ++++++++++++++++++++----- templates/cli/lib/config.js.twig | 3 +- templates/cli/lib/questions.js.twig | 6 +- 3 files changed, 110 insertions(+), 30 deletions(-) diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index 68e1ee04d..ccf609327 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -3,6 +3,7 @@ const EventEmitter = require('node:events'); const ignore = require("ignore"); const tar = require("tar"); const fs = require("fs"); +const ID = require("../id"); const childProcess = require('child_process'); const chokidar = require('chokidar'); const inquirer = require("inquirer"); @@ -11,6 +12,8 @@ const { Command } = require("commander"); const { localConfig, globalConfig } = require("../config"); const { paginate } = require('../paginate'); const { functionsListVariables } = require('./functions'); +const { usersGet, usersCreateJWT } = require('./users'); +const { projectsCreateJWT } = require('./projects'); const { questionsRunFunctions } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils'); @@ -31,17 +34,67 @@ const systemTools = { // TODO: Add all runtime needs }; +const JwtManager = { + userJwt: null, + functionJwt: null, + + timerWarn: null, + timerError: null, + + async setup(userId = null) { + if(this.timerWarn) { + clearTimeout(this.timerWarn); + } + + if(this.timerError) { + clearTimeout(this.timerError); + } + + this.timerWarn = setTimeout(() => { + log("Warning: Authorized JWT will expire in 5 minutes. Please stop and re-run the command to refresh tokens for 1 hour."); + }, 1000 * 60 * 55); // 55 mins + + this.timerError = setTimeout(() => { + log("Warning: Authorized JWT just expired. Please stop and re-run the command to obtain new tokens with 1 hour validity."); + log("Some Appwrite API communication is not authorized now.") + }, 1000 * 60 * 60); // 60 mins + + if(userId) { + await usersGet({ + userId, + parseOutput: false + }); + const userResponse = await usersCreateJWT({ + userId, + duration: 60*60, + parseOutput: false + }); + this.userJwt = userResponse.jwt; + } + + const functionResponse = await projectsCreateJWT({ + projectId: localConfig.getProject().projectId, + // TODO: There must be better way to get the list + scopes: ["sessions.write","users.read","users.write","teams.read","teams.write","databases.read","databases.write","collections.read","collections.write","attributes.read","attributes.write","indexes.read","indexes.write","documents.read","documents.write","files.read","files.write","buckets.read","buckets.write","functions.read","functions.write","execution.read","execution.write","locale.read","avatars.read","health.read","providers.read","providers.write","messages.read","messages.write","topics.read","topics.write","subscribers.read","subscribers.write","targets.read","targets.write","rules.read","rules.write","migrations.read","migrations.write","vcs.read","vcs.write","assistant.read"], + duration: 60*60, + parseOutput: false + }); + this.functionJwt = functionResponse.jwt; + } +}; + const Queue = { files: [], locked: false, events: new EventEmitter(), + debounce: null, push(file) { if(!this.files.includes(file)) { this.files.push(file); } if(!this.locked) { - this.events.emit('reload', { files: this.files }); + this._trigger(); } }, lock() { @@ -51,13 +104,23 @@ const Queue = { unlock() { this.locked = false; if(this.files.length > 0) { - this.events.emit('reload', { files: this.files }); + this._trigger(); } + }, + _trigger() { + if(this.debounce) { + return; + } + + this.debounce = setTimeout(() => { + this.events.emit('reload', { files: this.files }); + this.debounce = null; + }, 300); } }; async function dockerStop(id) { - delete activeDockerIds[id]; + delete activeDockerIds[id]; const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { stdio: 'pipe', }); @@ -92,7 +155,7 @@ async function dockerBuild(func, variables) { const functionDir = path.join(process.cwd(), func.path); - const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`; + const id = ID.unique(); const params = [ 'run' ]; params.push('--name', id); @@ -102,8 +165,8 @@ async function dockerBuild(func, variables) { params.push('-e', 'OPEN_RUNTIMES_SECRET='); params.push('-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`); - for(const v of variables) { - params.push('-e', `${v.key}=${v.value}`); + for(const k of Object.keys(variables)) { + params.push('-e', `${k}=${variables[k]}`); } params.push(imageName, 'sh', '-c', `helpers/build.sh "${func.commands}"`); @@ -167,7 +230,7 @@ async function dockerStart(func, variables, port) { const functionDir = path.join(process.cwd(), func.path); - const id = `${new Date().getTime().toString(16)}${Math.round(Math.random() * 1000000000).toString(16)}`; + const id = ID.unique(); const params = [ 'run' ]; params.push('--rm'); @@ -178,8 +241,8 @@ async function dockerStart(func, variables, port) { params.push('-e', 'OPEN_RUNTIMES_ENV=development'); params.push('-e', 'OPEN_RUNTIMES_SECRET='); - for(const v of variables) { - params.push('-e', `${v.key}=${v.value}`); + for(const k of Object.keys(variables)) { + params.push('-e', `${k}=${variables[k]}`); } params.push('-v', `${functionDir}/.appwrite/logs.txt:/mnt/logs/dev_logs.log:rw`); @@ -215,7 +278,7 @@ async function dockerCleanup() { } } -const runFunction = async ({ port, engine, functionId, noVariables, noReload } = {}) => { +const runFunction = async ({ port, engine, functionId, noVariables, noReload, userId } = {}) => { // Selection if(!functionId) { const answers = await inquirer.prompt(questionsRunFunctions[0]); @@ -265,14 +328,10 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } = } if(engine === 'docker') { - log('💡 Hint: Using system is faster, but using Docker simulates the production environment precisely.'); - if(!systemHasCommand('docker')) { return error("Please install Docker first: https://docs.docker.com/engine/install/"); } } else if(engine === 'system') { - log('💡 Hint: Docker simulates the production environment precisely, but using system is faster'); - for(const command of tool.commands) { if(!systemHasCommand(command.command)) { return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`); @@ -317,25 +376,45 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } = fs.writeFileSync(errorsPath, ''); } - let variables = []; + const variables = {}; if(!noVariables) { if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set."); } else { - const { variables: remoteVariables } = await paginate(functionsListVariables, { - functionId: func['$id'], - parseOutput: false - }, 100, 'variables'); - - remoteVariables.forEach((v) => { - variables.push({ - key: v.key, - value: v.value + try { + const { variables: remoteVariables } = await paginate(functionsListVariables, { + functionId: func['$id'], + parseOutput: false + }, 100, 'variables'); + + remoteVariables.forEach((v) => { + variables[v.key] = v.value; }); - }); + } catch(err) { + error("Could not fetch remote variables: " + err.message); + error("Function will run locally, but will not have your function's environment variables set."); + } } } + variables['APPWRITE_FUNCTION_API_ENDPOINT'] = globalConfig.getFrom('endpoint'); + variables['APPWRITE_FUNCTION_ID'] = func.$id; + variables['APPWRITE_FUNCTION_NAME'] = func.name; + variables['APPWRITE_FUNCTION_DEPLOYMENT'] = ''; // TODO: Implement when relevant + variables['APPWRITE_FUNCTION_PROJECT_ID'] = localConfig.getProject().projectId; + variables['APPWRITE_FUNCTION_RUNTIME_NAME'] = ''; // TODO: Implement when relevant + variables['APPWRITE_FUNCTION_RUNTIME_VERSION'] = ''; // TODO: Implement when relevant + + await JwtManager.setup(userId); + + const headers = {}; + headers['x-appwrite-key'] = JwtManager.functionJwt ?? ''; + headers['x-appwrite-trigger'] = 'http'; + headers['x-appwrite-event'] = ''; + headers['x-appwrite-user-id'] = userId ?? ''; + headers['x-appwrite-user-jwt'] = JwtManager.userJwt ?? ''; + variables['OPEN_RUNTIMES_HEADERS'] = JSON.stringify(headers); + await dockerPull(func); await dockerBuild(func, variables); await dockerStart(func, variables, port); @@ -348,6 +427,7 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload } = }); if(!noReload) { + // TODO: Stop previous job mid-way if new deployment is ready, I think? chokidar.watch('.', { cwd: path.join(process.cwd(), func.path), ignoreInitial: true, @@ -460,6 +540,7 @@ run .option(`--functionId `, `Function ID`) .option(`--port `, `Local port`) .option(`--engine `, `Local engine, "system" or "docker"`) + .option(`--userId `, `ID of user to impersonate`) .option(`--noVariables`, `Prevent pulling variables from function settings`) .option(`--noReload`, `Prevent live reloading of server when changes are made to function files`) .action(actionRunner(runFunction)); diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index bcf48dccf..b416305eb 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -419,7 +419,7 @@ class Global extends Config { return this.get(Global.PREFERENCE_CURRENT); } - setCurrentLogin(endpoint) { + setCurrentLogin(id) { this.set(Global.PREFERENCE_CURRENT, endpoint); } @@ -516,7 +516,6 @@ class Global extends Config { this.setTo(Global.PREFERENCE_KEY, key); } - hasFrom(key) { const current = this.getCurrentLogin(); diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 7e819145c..6befd56c0 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -820,11 +820,11 @@ const questionsRunFunctions = [ message: "Which engine would you like to use?", choices: [ { - name: "Docker", - value: "docker", + name: "Docker (recommended, simulates production precisely)", + value: "docker", }, { - name: "System", + name: "System (faster and easier to debug)", value: "system", }, ], 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 134/198] 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 135/198] 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 136/198] 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 137/198] 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 138/198] 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 139/198] 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 140/198] 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 141/198] 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 142/198] 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 143/198] 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 144/198] 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 145/198] 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 146/198] 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 147/198] 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 148/198] 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 149/198] 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 150/198] 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 151/198] 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 152/198] 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 153/198] 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 154/198] 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 155/198] 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 156/198] 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 157/198] 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 158/198] 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 159/198] 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 160/198] 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 161/198] 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 162/198] 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)), ]; From c024a8193960ba9f21a0484e828b29dcfc1bc0f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 12 Jun 2024 08:57:53 +0000 Subject: [PATCH 163/198] Finish local development --- templates/cli/lib/commands/run.js.twig | 393 ++++++++++++++----------- templates/cli/lib/questions.js.twig | 24 +- 2 files changed, 227 insertions(+), 190 deletions(-) diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index ccf609327..88539792c 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -21,17 +21,81 @@ const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils'); const activeDockerIds = {}; const openRuntimesVersion = 'v3'; +const runtimeNames = { + 'node': 'Node.js', + 'php': 'PHP', + 'ruby': 'Ruby', + 'python': 'Python', + 'python-ml': 'Python (ML)', + 'deno': 'Deno', + 'dart': 'Dart', + 'dotnet': '.NET', + 'java': 'Java', + 'swift': 'Swift', + 'kotlin': 'Kotlin', + 'bun': 'Bun' +}; const systemTools = { 'node': { isCompiled: false, startCommand: "node src/server.js", - commands: [ - { command: "node", docs: "https://nodejs.org/en/download/package-manager" }, - { command: "npm", docs: "https://nodejs.org/en/download/package-manager" }, - ], dependencyFiles: [ "package.json", "package-lock.json" ] }, - // TODO: Add all runtime needs + 'php': { + isCompiled: false, + startCommand: "php src/server.php", + dependencyFiles: [ "composer.json", "composer.lock" ] + }, + 'ruby': { + isCompiled: false, + startCommand: "bundle exec puma -b tcp://0.0.0.0:3000 -e production", + dependencyFiles: [ "Gemfile", "Gemfile.lock" ] + }, + 'python': { + isCompiled: false, + startCommand: "python3 src/server.py", + dependencyFiles: [ "requirements.txt", "requirements.lock" ] + }, + 'python-ml': { + isCompiled: false, + startCommand: "python3 src/server.py", + dependencyFiles: [ "requirements.txt", "requirements.lock" ] + }, + 'deno': { + isCompiled: false, + startCommand: "deno start", + dependencyFiles: [ ] + }, + 'dart': { + isCompiled: true, + startCommand: "src/function/server", + dependencyFiles: [ ] + }, + 'dotnet': { + isCompiled: true, + startCommand: "dotnet src/function/DotNetRuntime.dll", + dependencyFiles: [ ] + }, + 'java': { + isCompiled: true, + startCommand: "java -jar src/function/java-runtime-1.0.0.jar", + dependencyFiles: [ ] + }, + 'swift': { + isCompiled: true, + startCommand: "src/function/Runtime serve --env production --hostname 0.0.0.0 --port 3000", + dependencyFiles: [ ] + }, + 'kotlin': { + isCompiled: true, + startCommand: "java -jar src/function/kotlin-runtime-1.0.0.jar", + dependencyFiles: [ ] + }, + 'bun': { + isCompiled: false, + startCommand: "bun src/server.ts", + dependencyFiles: [ "package.json", "package-lock.json", "bun.lockb" ] + }, }; const JwtManager = { @@ -74,7 +138,7 @@ const JwtManager = { const functionResponse = await projectsCreateJWT({ projectId: localConfig.getProject().projectId, - // TODO: There must be better way to get the list + // TODO: Once we have endpoint for this, use it scopes: ["sessions.write","users.read","users.write","teams.read","teams.write","databases.read","databases.write","collections.read","collections.write","attributes.read","attributes.write","indexes.read","indexes.write","documents.read","documents.write","files.read","files.write","buckets.read","buckets.write","functions.read","functions.write","execution.read","execution.write","locale.read","avatars.read","health.read","providers.read","providers.write","messages.read","messages.write","topics.read","topics.write","subscribers.read","subscribers.write","targets.read","targets.write","rules.read","rules.write","migrations.read","migrations.write","vcs.read","vcs.write","assistant.read"], duration: 60*60, parseOutput: false @@ -129,10 +193,11 @@ async function dockerStop(id) { } async function dockerPull(func) { - return; // TODO: Remove log('Pulling Docker image of function runtime ...'); - const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); + const runtimeChunks = func.runtime.split("-"); + const runtimeVersion = runtimeChunks.pop(); + const runtimeName = runtimeChunks.join("-"); const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; const pullProcess = childProcess.spawn('docker', ['pull', imageName], { @@ -148,9 +213,11 @@ async function dockerPull(func) { } async function dockerBuild(func, variables) { - log('Building function using Docker engine ...'); + log('Building function using Docker ...'); - const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); + const runtimeChunks = func.runtime.split("-"); + const runtimeVersion = runtimeChunks.pop(); + const runtimeName = runtimeChunks.join("-"); const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; const functionDir = path.join(process.cwd(), func.path); @@ -215,7 +282,7 @@ async function dockerBuild(func, variables) { } async function dockerStart(func, variables, port) { - log('Starting function using Docker engine ...'); + log('Starting function using Docker ...'); log("Permissions, events, CRON and timeouts dont apply when running locally."); @@ -223,7 +290,10 @@ async function dockerStart(func, variables, port) { success(`Visit http://localhost:${port}/ to execute your function.`); - const [ runtimeName, runtimeVersion ] = func.runtime.split('-', 2); + + const runtimeChunks = func.runtime.split("-"); + const runtimeVersion = runtimeChunks.pop(); + const runtimeName = runtimeChunks.join("-"); const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; const tool = systemTools[runtimeName]; @@ -278,7 +348,7 @@ async function dockerCleanup() { } } -const runFunction = async ({ port, engine, functionId, noVariables, noReload, userId } = {}) => { +const runFunction = async ({ port, functionId, noVariables, noReload, userId } = {}) => { // Selection if(!functionId) { const answers = await inquirer.prompt(questionsRunFunctions[0]); @@ -291,7 +361,7 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload, us throw new Error("Function '" + functionId + "' not found.") } - const runtimeName = func.runtime.split('-')[0]; + const runtimeName = func.runtime.split("-").slice(0, -1).join("-"); const tool = systemTools[runtimeName]; // Configuration: Port @@ -318,25 +388,8 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload, us } // Configuration: Engine - if(engine !== "system" && engine !== "docker") { - engine = null; - } - - if(!engine) { - const answers = await inquirer.prompt(questionsRunFunctions[2]); - engine = answers.engine; - } - - if(engine === 'docker') { - if(!systemHasCommand('docker')) { - return error("Please install Docker first: https://docs.docker.com/engine/install/"); - } - } else if(engine === 'system') { - for(const command of tool.commands) { - if(!systemHasCommand(command.command)) { - return error(`Your system is missing command "${command.command}". Please install it first: ${command.docs}`); - } - } + if(!systemHasCommand('docker')) { + return error("Please install Docker first: https://docs.docker.com/engine/install/"); } // Settings @@ -360,167 +413,164 @@ const runFunction = async ({ port, engine, functionId, noVariables, noReload, us process.exit(); }); - if(engine === "docker") { - const logsPath = path.join(process.cwd(), func.path, '.appwrite/logs.txt'); - const errorsPath = path.join(process.cwd(), func.path, '.appwrite/errors.txt'); + const logsPath = path.join(process.cwd(), func.path, '.appwrite/logs.txt'); + const errorsPath = path.join(process.cwd(), func.path, '.appwrite/errors.txt'); - if(!fs.existsSync(path.dirname(logsPath))) { - fs.mkdirSync(path.dirname(logsPath), { recursive: true }); - } + if(!fs.existsSync(path.dirname(logsPath))) { + fs.mkdirSync(path.dirname(logsPath), { recursive: true }); + } - if (!fs.existsSync(logsPath)) { - fs.writeFileSync(logsPath, ''); - } + if (!fs.existsSync(logsPath)) { + fs.writeFileSync(logsPath, ''); + } - if (!fs.existsSync(errorsPath)) { - fs.writeFileSync(errorsPath, ''); - } + if (!fs.existsSync(errorsPath)) { + fs.writeFileSync(errorsPath, ''); + } - const variables = {}; - if(!noVariables) { - if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { - error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set."); - } else { - try { - const { variables: remoteVariables } = await paginate(functionsListVariables, { - functionId: func['$id'], - parseOutput: false - }, 100, 'variables'); - - remoteVariables.forEach((v) => { - variables[v.key] = v.value; - }); - } catch(err) { - error("Could not fetch remote variables: " + err.message); - error("Function will run locally, but will not have your function's environment variables set."); - } + const variables = {}; + if(!noVariables) { + if (globalConfig.getEndpoint() === '' || globalConfig.getCookie() === '') { + error("No user is signed in. To sign in, run: appwrite login. Function will run locally, but will not have your function's environment variables set."); + } else { + try { + const { variables: remoteVariables } = await paginate(functionsListVariables, { + functionId: func['$id'], + parseOutput: false + }, 100, 'variables'); + + remoteVariables.forEach((v) => { + variables[v.key] = v.value; + }); + } catch(err) { + error("Could not fetch remote variables: " + err.message); + error("Function will run locally, but will not have your function's environment variables set."); } } + } - variables['APPWRITE_FUNCTION_API_ENDPOINT'] = globalConfig.getFrom('endpoint'); - variables['APPWRITE_FUNCTION_ID'] = func.$id; - variables['APPWRITE_FUNCTION_NAME'] = func.name; - variables['APPWRITE_FUNCTION_DEPLOYMENT'] = ''; // TODO: Implement when relevant - variables['APPWRITE_FUNCTION_PROJECT_ID'] = localConfig.getProject().projectId; - variables['APPWRITE_FUNCTION_RUNTIME_NAME'] = ''; // TODO: Implement when relevant - variables['APPWRITE_FUNCTION_RUNTIME_VERSION'] = ''; // TODO: Implement when relevant - - await JwtManager.setup(userId); - - const headers = {}; - headers['x-appwrite-key'] = JwtManager.functionJwt ?? ''; - headers['x-appwrite-trigger'] = 'http'; - headers['x-appwrite-event'] = ''; - headers['x-appwrite-user-id'] = userId ?? ''; - headers['x-appwrite-user-jwt'] = JwtManager.userJwt ?? ''; - variables['OPEN_RUNTIMES_HEADERS'] = JSON.stringify(headers); - - await dockerPull(func); - await dockerBuild(func, variables); - await dockerStart(func, variables, port); - - new Tail(logsPath).on("line", function(data) { - console.log(data); - }); - new Tail(errorsPath).on("line", function(data) { - console.log(data); + variables['APPWRITE_FUNCTION_API_ENDPOINT'] = globalConfig.getFrom('endpoint'); + variables['APPWRITE_FUNCTION_ID'] = func.$id; + variables['APPWRITE_FUNCTION_NAME'] = func.name; + variables['APPWRITE_FUNCTION_DEPLOYMENT'] = ''; // TODO: Implement when relevant + variables['APPWRITE_FUNCTION_PROJECT_ID'] = localConfig.getProject().projectId; + variables['APPWRITE_FUNCTION_RUNTIME_NAME'] = runtimeNames[runtimeName] ?? ''; + variables['APPWRITE_FUNCTION_RUNTIME_VERSION'] = func.runtime; + + await JwtManager.setup(userId); + + const headers = {}; + headers['x-appwrite-key'] = JwtManager.functionJwt ?? ''; + headers['x-appwrite-trigger'] = 'http'; + headers['x-appwrite-event'] = ''; + headers['x-appwrite-user-id'] = userId ?? ''; + headers['x-appwrite-user-jwt'] = JwtManager.userJwt ?? ''; + variables['OPEN_RUNTIMES_HEADERS'] = JSON.stringify(headers); + + await dockerPull(func); + await dockerBuild(func, variables); + await dockerStart(func, variables, port); + + new Tail(logsPath).on("line", function(data) { + console.log(data); + }); + new Tail(errorsPath).on("line", function(data) { + console.log(data); + }); + + if(!noReload) { + chokidar.watch('.', { + cwd: path.join(process.cwd(), func.path), + ignoreInitial: true, + ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite', '.appwrite/', '.appwrite/*', '.appwrite/**', '.appwrite/*.*', '.appwrite/**/*.*' ] + }).on('all', async (_event, filePath) => { + Queue.push(filePath); }); + } - if(!noReload) { - // TODO: Stop previous job mid-way if new deployment is ready, I think? - chokidar.watch('.', { - cwd: path.join(process.cwd(), func.path), - ignoreInitial: true, - ignored: [ ...(func.ignore ?? []), 'code.tar.gz', '.appwrite', '.appwrite/', '.appwrite/*', '.appwrite/**', '.appwrite/*.*', '.appwrite/**/*.*' ] - }).on('all', async (_event, filePath) => { - Queue.push(filePath); - }); + Queue.events.on('reload', async ({ files }) => { + Queue.lock(); + + log('Live-reloading due to file changes: '); + for(const file of files) { + log(`- ${file}`); } - Queue.events.on('reload', async ({ files }) => { - Queue.lock(); + try { + log('Stopping the function ...'); - log('Live-reloading due to file changes: '); - for(const file of files) { - log(`- ${file}`); + for(const id in activeDockerIds) { + await dockerStop(id); } - try { - log('Stopping the function ...'); - - for(const id in activeDockerIds) { - await dockerStop(id); - } - - const dependencyFile = files.find((filePath) => tool.dependencyFiles.includes(filePath)); - if(tool.isCompiled || dependencyFile) { - log(`Rebuilding the function due to cange in ${dependencyFile} ...`); - await dockerBuild(func, variables); - await dockerStart(func, variables, port); + const dependencyFile = files.find((filePath) => tool.dependencyFiles.includes(filePath)); + if(tool.isCompiled || dependencyFile) { + log(`Rebuilding the function due to cange in ${dependencyFile} ...`); + await dockerBuild(func, variables); + await dockerStart(func, variables, port); + } else { + log('Hot-swapping function files ...'); + + const functionPath = path.join(process.cwd(), func.path); + const hotSwapPath = path.join(functionPath, '.appwrite/hot-swap'); + const buildPath = path.join(functionPath, '.appwrite/build.tar.gz'); + + // Prepare temp folder + if (!fs.existsSync(hotSwapPath)) { + fs.mkdirSync(hotSwapPath, { recursive: true }); } else { - log('Hot-swapping function files ...'); - - const functionPath = path.join(process.cwd(), func.path); - const hotSwapPath = path.join(functionPath, '.appwrite/hot-swap'); - const buildPath = path.join(functionPath, '.appwrite/build.tar.gz'); - - // Prepare temp folder - if (!fs.existsSync(hotSwapPath)) { - fs.mkdirSync(hotSwapPath, { recursive: true }); - } else { - fs.rmSync(hotSwapPath, { recursive: true, force: true }); - fs.mkdirSync(hotSwapPath, { recursive: true }); - } + fs.rmSync(hotSwapPath, { recursive: true, force: true }); + fs.mkdirSync(hotSwapPath, { recursive: true }); + } - await tar - .extract({ - gzip: true, - sync: true, - cwd: hotSwapPath, - file: buildPath - }); - - const ignorer = ignore(); - ignorer.add('.appwrite'); - if (func.ignore) { - ignorer.add(func.ignore); - } + await tar + .extract({ + gzip: true, + sync: true, + cwd: hotSwapPath, + file: buildPath + }); - const filesToCopy = getAllFiles(functionPath).map((file) => path.relative(functionPath, file)).filter((file) => !ignorer.ignores(file)); - for(const f of filesToCopy) { - const filePath = path.join(hotSwapPath, f); - if (fs.existsSync(filePath)) { - fs.rmSync(filePath, { force: true }); - } + const ignorer = ignore(); + ignorer.add('.appwrite'); + if (func.ignore) { + ignorer.add(func.ignore); + } - const fileDir = path.dirname(filePath); - if (!fs.existsSync(fileDir)) { - fs.mkdirSync(fileDir, { recursive: true }); - } + const filesToCopy = getAllFiles(functionPath).map((file) => path.relative(functionPath, file)).filter((file) => !ignorer.ignores(file)); + for(const f of filesToCopy) { + const filePath = path.join(hotSwapPath, f); + if (fs.existsSync(filePath)) { + fs.rmSync(filePath, { force: true }); + } - const sourcePath = path.join(functionPath, f); - fs.copyFileSync(sourcePath, filePath); + const fileDir = path.dirname(filePath); + if (!fs.existsSync(fileDir)) { + fs.mkdirSync(fileDir, { recursive: true }); } - await tar - .create({ - gzip: true, - sync: true, - cwd: hotSwapPath, - file: buildPath - }, ['.']); - - fs.rmSync(hotSwapPath, { recursive: true, force: true }); - - await dockerStart(func, variables, port); + const sourcePath = path.join(functionPath, f); + fs.copyFileSync(sourcePath, filePath); } - } catch(err) { - console.error(err); - } finally { - Queue.unlock(); + + await tar + .create({ + gzip: true, + sync: true, + cwd: hotSwapPath, + file: buildPath + }, ['.']); + + fs.rmSync(hotSwapPath, { recursive: true, force: true }); + + await dockerStart(func, variables, port); } - }); - } + } catch(err) { + console.error(err); + } finally { + Queue.unlock(); + } + }); } const run = new Command("run") @@ -539,7 +589,6 @@ run .description("Run functions in the current directory.") .option(`--functionId `, `Function ID`) .option(`--port `, `Local port`) - .option(`--engine `, `Local engine, "system" or "docker"`) .option(`--userId `, `ID of user to impersonate`) .option(`--noVariables`, `Prevent pulling variables from function settings`) .option(`--noReload`, `Prevent live reloading of server when changes are made to function files`) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 6befd56c0..dcd04e3af 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -16,7 +16,7 @@ const JSONbig = require("json-bigint")({ storeAsString: false }); const whenOverride = (answers) => answers.override === undefined ? true : answers.override; const getIgnores = (runtime) => { - const languge = runtime.split('-')[0]; + const languge = runtime.split("-").slice(0, -1).join("-"); switch (languge) { case 'cpp': @@ -36,6 +36,7 @@ const getIgnores = (runtime) => { case 'php': return ['vendor']; case 'python': + case 'python-ml': return ['__pypackages__']; case 'ruby': return ['vendor']; @@ -49,7 +50,7 @@ const getIgnores = (runtime) => { }; const getEntrypoint = (runtime) => { - const languge = runtime.split('-')[0]; + const languge = runtime.split("-").slice(0, -1).join("-"); switch (languge) { case 'dart': @@ -63,6 +64,7 @@ const getEntrypoint = (runtime) => { case 'php': return 'src/index.php'; case 'python': + case 'python-ml': return 'src/main.py'; case 'ruby': return 'lib/main.rb'; @@ -84,7 +86,7 @@ const getEntrypoint = (runtime) => { }; const getInstallCommand = (runtime) => { - const languge = runtime.split('-')[0]; + const languge = runtime.split("-").slice(0, -1).join("-"); switch (languge) { case 'dart': @@ -98,6 +100,7 @@ const getInstallCommand = (runtime) => { case 'php': return 'composer install'; case 'python': + case 'python-ml': return 'pip install -r requirements.txt'; case 'ruby': return 'bundle install'; @@ -814,21 +817,6 @@ const questionsRunFunctions = [ }); }, }, - { - type: "list", - name: "engine", - message: "Which engine would you like to use?", - choices: [ - { - name: "Docker (recommended, simulates production precisely)", - value: "docker", - }, - { - name: "System (faster and easier to debug)", - value: "system", - }, - ], - }, ]; module.exports = { From 7b0f655cc1abc2a52082962854dee38e4862c02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 14 Jun 2024 14:22:29 +0000 Subject: [PATCH 164/198] PR review changes --- 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 6459f4840..532bfaa95 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -203,7 +203,7 @@ const commandDescriptions = { "databases": `The databases command allows you to create structured collections of documents, query and filter lists of documents.`, "init": `The init command provides a convenient wrapper for creating and initializing project, functions, collections, buckets, teams and messaging in Appwrite.`, "push": `The push command provides a convenient wrapper for pushing your functions, collections, buckets, teams and messaging.`, - "run": `The dev command allows you to run project locally to allow easy development and quick debugging.`, + "run": `The run command allows you to run project locally to allow easy development and quick debugging.`, "functions": `The functions command allows you view, create and manage your Cloud Functions.`, "health": `The health command allows you to both validate and monitor your {{ spec.title|caseUcfirst }} server's health.`, "pull": `The pull command helps you pull your {{ spec.title|caseUcfirst }} project, functions, collections, buckets, teams and messaging`, From 2ac1569ac703882aa33bb7406b2a627c80458fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 14 Jun 2024 14:26:51 +0000 Subject: [PATCH 165/198] PR review changes --- templates/cli/lib/commands/pull.js.twig | 8 ++++---- templates/cli/lib/commands/push.js.twig | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 22bdc3ae9..1bc6214e6 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -264,7 +264,7 @@ pull pull .command("collection") - .command("collections") + .alias("collections") .description("Pulling your {{ spec.title|caseUcfirst }} collections") .option(`-d, --databaseId `, `Database ID`) .option(`-a, --all`, `Flag to pullialize all databases`) @@ -272,19 +272,19 @@ pull pull .command("bucket") - .command("buckets") + .alias("buckets") .description("Pulling your Appwrite buckets") .action(actionRunner(pullBucket)) pull .command("team") - .command("teams") + .alias("teams") .description("Pulling your Appwrite teams") .action(actionRunner(pullTeam)) pull .command("topic") - .command("topics") + .alias("topics") .description("Initialise your Appwrite messaging topics") .action(actionRunner(pullMessagingTopic)) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 4eb801e12..b94cea789 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1101,7 +1101,6 @@ const pushMessagingTopic = async ({ all, yes } = {}) => { } const push = new Command("push") - .alias("deploy") .description(commandDescriptions['push']) .option(`-a, --all`, `Flag to push all resources`) .option(`-y, --yes`, `Flag to confirm all warnings`) From 90e4b9d332dfba51cfda01a5a41600252f1925d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 14 Jun 2024 14:29:55 +0000 Subject: [PATCH 166/198] Remove leftovers --- templates/cli/lib/commands/push.js.twig | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 416d54227..294c2b5b4 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1335,8 +1335,6 @@ const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => const push = new Command("push") .alias('deploy') .description(commandDescriptions['push']) - .option(`-a, --all`, `Flag to push all resources`) - .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushResources)); push @@ -1344,8 +1342,6 @@ push .alias("functions") .description("Push functions in the current directory.") .option(`-f, --functionId `, `Function ID`) - .option(`-a, --all`, `Flag to push all functions`) - .option(`-y, --yes`, `Flag to confirm all warnings`) .option(`-A, --async`, `Don't wait for functions deployments status`) .action(actionRunner(pushFunction)); @@ -1353,32 +1349,24 @@ push .command("collection") .alias("collections") .description("Push collections in the current project.") - .option(`-a, --all`, `Flag to push all collections`) - .option(`-a, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushCollection)); push .command("bucket") .alias("buckets") .description("Push buckets in the current project.") - .option(`-a, --all`, `Flag to push all buckets`) - .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushBucket)); push .command("team") .alias("teams") .description("Push teams in the current project.") - .option(`-a, --all`, `Flag to push all teams`) - .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushTeam)); push .command("topic") .alias("topics") .description("Push messaging topics in the current project.") - .option(`-a, --all`, `Flag to deploy all topics`) - .option(`-y, --yes`, `Flag to confirm all warnings`) .action(actionRunner(pushMessagingTopic)); module.exports = { From c1e497d7c2190493beb90d7c65d89a0d89e1129e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 14 Jun 2024 14:34:56 +0000 Subject: [PATCH 167/198] Fix after merge --- templates/cli/lib/commands/push.js.twig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 294c2b5b4..a2672743f 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -1337,6 +1337,16 @@ const push = new Command("push") .description(commandDescriptions['push']) .action(actionRunner(pushResources)); +push + .command("all") + .description("Push all resource.") + .action(actionRunner(pushResources)); + +push + .command("project") + .description("Push project name, services and auth settings") + .action(actionRunner(pushProject)); + push .command("function") .alias("functions") From fcde6cba9bc7ff4c00a859ad0729b97428bca786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 17 Jun 2024 14:41:06 +0200 Subject: [PATCH 168/198] PR review changes --- templates/cli/lib/commands/run.js.twig | 342 +-------------------- templates/cli/lib/emulation/docker.js.twig | 170 ++++++++++ templates/cli/lib/emulation/utils.js.twig | 164 ++++++++++ templates/cli/lib/questions.js.twig | 8 +- 4 files changed, 346 insertions(+), 338 deletions(-) create mode 100644 templates/cli/lib/emulation/docker.js.twig create mode 100644 templates/cli/lib/emulation/utils.js.twig diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index 88539792c..1a100c8ed 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -17,336 +17,8 @@ const { projectsCreateJWT } = require('./projects'); const { questionsRunFunctions } = require("../questions"); const { actionRunner, success, log, error, commandDescriptions, drawTable } = require("../parser"); const { systemHasCommand, isPortTaken, getAllFiles } = require('../utils'); - -const activeDockerIds = {}; - -const openRuntimesVersion = 'v3'; -const runtimeNames = { - 'node': 'Node.js', - 'php': 'PHP', - 'ruby': 'Ruby', - 'python': 'Python', - 'python-ml': 'Python (ML)', - 'deno': 'Deno', - 'dart': 'Dart', - 'dotnet': '.NET', - 'java': 'Java', - 'swift': 'Swift', - 'kotlin': 'Kotlin', - 'bun': 'Bun' -}; -const systemTools = { - 'node': { - isCompiled: false, - startCommand: "node src/server.js", - dependencyFiles: [ "package.json", "package-lock.json" ] - }, - 'php': { - isCompiled: false, - startCommand: "php src/server.php", - dependencyFiles: [ "composer.json", "composer.lock" ] - }, - 'ruby': { - isCompiled: false, - startCommand: "bundle exec puma -b tcp://0.0.0.0:3000 -e production", - dependencyFiles: [ "Gemfile", "Gemfile.lock" ] - }, - 'python': { - isCompiled: false, - startCommand: "python3 src/server.py", - dependencyFiles: [ "requirements.txt", "requirements.lock" ] - }, - 'python-ml': { - isCompiled: false, - startCommand: "python3 src/server.py", - dependencyFiles: [ "requirements.txt", "requirements.lock" ] - }, - 'deno': { - isCompiled: false, - startCommand: "deno start", - dependencyFiles: [ ] - }, - 'dart': { - isCompiled: true, - startCommand: "src/function/server", - dependencyFiles: [ ] - }, - 'dotnet': { - isCompiled: true, - startCommand: "dotnet src/function/DotNetRuntime.dll", - dependencyFiles: [ ] - }, - 'java': { - isCompiled: true, - startCommand: "java -jar src/function/java-runtime-1.0.0.jar", - dependencyFiles: [ ] - }, - 'swift': { - isCompiled: true, - startCommand: "src/function/Runtime serve --env production --hostname 0.0.0.0 --port 3000", - dependencyFiles: [ ] - }, - 'kotlin': { - isCompiled: true, - startCommand: "java -jar src/function/kotlin-runtime-1.0.0.jar", - dependencyFiles: [ ] - }, - 'bun': { - isCompiled: false, - startCommand: "bun src/server.ts", - dependencyFiles: [ "package.json", "package-lock.json", "bun.lockb" ] - }, -}; - -const JwtManager = { - userJwt: null, - functionJwt: null, - - timerWarn: null, - timerError: null, - - async setup(userId = null) { - if(this.timerWarn) { - clearTimeout(this.timerWarn); - } - - if(this.timerError) { - clearTimeout(this.timerError); - } - - this.timerWarn = setTimeout(() => { - log("Warning: Authorized JWT will expire in 5 minutes. Please stop and re-run the command to refresh tokens for 1 hour."); - }, 1000 * 60 * 55); // 55 mins - - this.timerError = setTimeout(() => { - log("Warning: Authorized JWT just expired. Please stop and re-run the command to obtain new tokens with 1 hour validity."); - log("Some Appwrite API communication is not authorized now.") - }, 1000 * 60 * 60); // 60 mins - - if(userId) { - await usersGet({ - userId, - parseOutput: false - }); - const userResponse = await usersCreateJWT({ - userId, - duration: 60*60, - parseOutput: false - }); - this.userJwt = userResponse.jwt; - } - - const functionResponse = await projectsCreateJWT({ - projectId: localConfig.getProject().projectId, - // TODO: Once we have endpoint for this, use it - scopes: ["sessions.write","users.read","users.write","teams.read","teams.write","databases.read","databases.write","collections.read","collections.write","attributes.read","attributes.write","indexes.read","indexes.write","documents.read","documents.write","files.read","files.write","buckets.read","buckets.write","functions.read","functions.write","execution.read","execution.write","locale.read","avatars.read","health.read","providers.read","providers.write","messages.read","messages.write","topics.read","topics.write","subscribers.read","subscribers.write","targets.read","targets.write","rules.read","rules.write","migrations.read","migrations.write","vcs.read","vcs.write","assistant.read"], - duration: 60*60, - parseOutput: false - }); - this.functionJwt = functionResponse.jwt; - } -}; - -const Queue = { - files: [], - locked: false, - events: new EventEmitter(), - debounce: null, - push(file) { - if(!this.files.includes(file)) { - this.files.push(file); - } - - if(!this.locked) { - this._trigger(); - } - }, - lock() { - this.files = []; - this.locked = true; - }, - unlock() { - this.locked = false; - if(this.files.length > 0) { - this._trigger(); - } - }, - _trigger() { - if(this.debounce) { - return; - } - - this.debounce = setTimeout(() => { - this.events.emit('reload', { files: this.files }); - this.debounce = null; - }, 300); - } -}; - -async function dockerStop(id) { - delete activeDockerIds[id]; - const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { - stdio: 'pipe', - }); - - await new Promise((res) => { stopProcess.on('close', res) }); -} - -async function dockerPull(func) { - log('Pulling Docker image of function runtime ...'); - - const runtimeChunks = func.runtime.split("-"); - const runtimeVersion = runtimeChunks.pop(); - const runtimeName = runtimeChunks.join("-"); - const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; - - const pullProcess = childProcess.spawn('docker', ['pull', imageName], { - stdio: 'pipe', - pwd: path.join(process.cwd(), func.path) - }); - - pullProcess.stderr.on('data', (data) => { - process.stderr.write(`\n${data}$ `); - }); - - await new Promise((res) => { pullProcess.on('close', res) }); -} - -async function dockerBuild(func, variables) { - log('Building function using Docker ...'); - - const runtimeChunks = func.runtime.split("-"); - const runtimeVersion = runtimeChunks.pop(); - const runtimeName = runtimeChunks.join("-"); - const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; - - const functionDir = path.join(process.cwd(), func.path); - - const id = ID.unique(); - - const params = [ 'run' ]; - params.push('--name', id); - params.push('-v', `${functionDir}/:/mnt/code:rw`); - params.push('-e', 'APPWRITE_ENV=development'); - params.push('-e', 'OPEN_RUNTIMES_ENV=development'); - params.push('-e', 'OPEN_RUNTIMES_SECRET='); - params.push('-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`); - - for(const k of Object.keys(variables)) { - params.push('-e', `${k}=${variables[k]}`); - } - - params.push(imageName, 'sh', '-c', `helpers/build.sh "${func.commands}"`); - - const buildProcess = childProcess.spawn('docker', params, { - stdio: 'pipe', - pwd: functionDir - }); - - buildProcess.stdout.on('data', (data) => { - process.stdout.write(`\n${data}`); - }); - - buildProcess.stderr.on('data', (data) => { - process.stderr.write(`\n${data}`); - }); - - await new Promise((res) => { buildProcess.on('close', res) }); - - const copyPath = path.join(process.cwd(), func.path, '.appwrite', 'build.tar.gz'); - const copyDir = path.dirname(copyPath); - if (!fs.existsSync(copyDir)) { - fs.mkdirSync(copyDir, { recursive: true }); - } - - const copyProcess = childProcess.spawn('docker', ['cp', `${id}:/mnt/code/code.tar.gz`, copyPath], { - stdio: 'pipe', - pwd: functionDir - }); - - await new Promise((res) => { copyProcess.on('close', res) }); - - const cleanupProcess = childProcess.spawn('docker', ['rm', '--force', id], { - stdio: 'pipe', - pwd: functionDir - }); - - await new Promise((res) => { cleanupProcess.on('close', res) }); - - delete activeDockerIds[id]; - - const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); - if (fs.existsSync(tempPath)) { - fs.rmSync(tempPath, { force: true }); - } -} - -async function dockerStart(func, variables, port) { - log('Starting function using Docker ...'); - - log("Permissions, events, CRON and timeouts dont apply when running locally."); - - log('💡 Hint: Function automatically restarts when you edit your code.'); - - success(`Visit http://localhost:${port}/ to execute your function.`); - - - const runtimeChunks = func.runtime.split("-"); - const runtimeVersion = runtimeChunks.pop(); - const runtimeName = runtimeChunks.join("-"); - const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; - - const tool = systemTools[runtimeName]; - - const functionDir = path.join(process.cwd(), func.path); - - const id = ID.unique(); - - const params = [ 'run' ]; - params.push('--rm'); - params.push('-d'); - params.push('--name', id); - params.push('-p', `${port}:3000`); - params.push('-e', 'APPWRITE_ENV=development'); - params.push('-e', 'OPEN_RUNTIMES_ENV=development'); - params.push('-e', 'OPEN_RUNTIMES_SECRET='); - - for(const k of Object.keys(variables)) { - params.push('-e', `${k}=${variables[k]}`); - } - - params.push('-v', `${functionDir}/.appwrite/logs.txt:/mnt/logs/dev_logs.log:rw`); - params.push('-v', `${functionDir}/.appwrite/errors.txt:/mnt/logs/dev_errors.log:rw`); - params.push('-v', `${functionDir}/.appwrite/build.tar.gz:/mnt/code/code.tar.gz:ro`); - params.push(imageName, 'sh', '-c', `helpers/start.sh "${tool.startCommand}"`); - - childProcess.spawn('docker', params, { - stdio: 'pipe', - pwd: functionDir - }); - - activeDockerIds[id] = true; -} - -async function dockerCleanup() { - const ids = Object.keys(activeDockerIds); - for await (const id of ids) { - await dockerStop(id); - } - - const functions = localConfig.getFunctions(); - for(const func of functions) { - const appwritePath = path.join(process.cwd(), func.path, '.appwrite'); - if (fs.existsSync(appwritePath)) { - fs.rmSync(appwritePath, { recursive: true, force: true }); - } - - const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); - if (fs.existsSync(tempPath)) { - fs.rmSync(tempPath, { force: true }); - } - } -} +const { openRuntimesVersion, runtimeNames, systemTools, JwtManager, Queue } = require('../emulation/utils'); +const { dockerStop, dockerCleanup, dockerStart, dockerBuild, dockerPull, dockerStopActive } = require('../emulation/docker'); const runFunction = async ({ port, functionId, noVariables, noReload, userId } = {}) => { // Selection @@ -377,7 +49,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = const taken = await isPortTaken(port); if(taken) { - log(`Port ${port} is already used.`); + log(`Port ${port} is already in use by another process.`); port = null; } } @@ -389,7 +61,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = // Configuration: Engine if(!systemHasCommand('docker')) { - return error("Please install Docker first: https://docs.docker.com/engine/install/"); + return error("Docker Engine is required for local development. Please install Docker using: https://docs.docker.com/engine/install/"); } // Settings @@ -402,7 +74,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = log("Local function configuration:"); drawTable([settings]); - log('If you wish to change local settings, update appwrite.json file and rerun the command. To deploy the function, run: appwrite push function'); + log('If you wish to change your local settings, update the appwrite.json file and rerun the `appwrite run` command.'); await dockerCleanup(); @@ -499,9 +171,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = try { log('Stopping the function ...'); - for(const id in activeDockerIds) { - await dockerStop(id); - } + await dockerStopActive(); const dependencyFile = files.find((filePath) => tool.dependencyFiles.includes(filePath)); if(tool.isCompiled || dependencyFile) { diff --git a/templates/cli/lib/emulation/docker.js.twig b/templates/cli/lib/emulation/docker.js.twig new file mode 100644 index 000000000..fa0b942c5 --- /dev/null +++ b/templates/cli/lib/emulation/docker.js.twig @@ -0,0 +1,170 @@ +const activeDockerIds = {}; + +export async function dockerStop(id) { + delete activeDockerIds[id]; + const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { + stdio: 'pipe', + }); + + await new Promise((res) => { stopProcess.on('close', res) }); +} + +export async function dockerPull(func) { + log('Pulling Docker image of function runtime ...'); + + const runtimeChunks = func.runtime.split("-"); + const runtimeVersion = runtimeChunks.pop(); + const runtimeName = runtimeChunks.join("-"); + const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; + + const pullProcess = childProcess.spawn('docker', ['pull', imageName], { + stdio: 'pipe', + pwd: path.join(process.cwd(), func.path) + }); + + pullProcess.stderr.on('data', (data) => { + process.stderr.write(`\n${data}$ `); + }); + + await new Promise((res) => { pullProcess.on('close', res) }); +} + +export async function dockerBuild(func, variables) { + log('Building function using Docker ...'); + + const runtimeChunks = func.runtime.split("-"); + const runtimeVersion = runtimeChunks.pop(); + const runtimeName = runtimeChunks.join("-"); + const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; + + const functionDir = path.join(process.cwd(), func.path); + + const id = ID.unique(); + + const params = [ 'run' ]; + params.push('--name', id); + params.push('-v', `${functionDir}/:/mnt/code:rw`); + params.push('-e', 'APPWRITE_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_SECRET='); + params.push('-e', `OPEN_RUNTIMES_ENTRYPOINT=${func.entrypoint}`); + + for(const k of Object.keys(variables)) { + params.push('-e', `${k}=${variables[k]}`); + } + + params.push(imageName, 'sh', '-c', `helpers/build.sh "${func.commands}"`); + + const buildProcess = childProcess.spawn('docker', params, { + stdio: 'pipe', + pwd: functionDir + }); + + buildProcess.stdout.on('data', (data) => { + process.stdout.write(`\n${data}`); + }); + + buildProcess.stderr.on('data', (data) => { + process.stderr.write(`\n${data}`); + }); + + await new Promise((res) => { buildProcess.on('close', res) }); + + const copyPath = path.join(process.cwd(), func.path, '.appwrite', 'build.tar.gz'); + const copyDir = path.dirname(copyPath); + if (!fs.existsSync(copyDir)) { + fs.mkdirSync(copyDir, { recursive: true }); + } + + const copyProcess = childProcess.spawn('docker', ['cp', `${id}:/mnt/code/code.tar.gz`, copyPath], { + stdio: 'pipe', + pwd: functionDir + }); + + await new Promise((res) => { copyProcess.on('close', res) }); + + const cleanupProcess = childProcess.spawn('docker', ['rm', '--force', id], { + stdio: 'pipe', + pwd: functionDir + }); + + await new Promise((res) => { cleanupProcess.on('close', res) }); + + delete activeDockerIds[id]; + + const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); + if (fs.existsSync(tempPath)) { + fs.rmSync(tempPath, { force: true }); + } +} + +export async function dockerStart(func, variables, port) { + log('Starting function using Docker ...'); + + log("Permissions, events, CRON and timeouts dont apply when running locally."); + + log('💡 Hint: Function automatically restarts when you edit your code.'); + + success(`Visit http://localhost:${port}/ to execute your function.`); + + + const runtimeChunks = func.runtime.split("-"); + const runtimeVersion = runtimeChunks.pop(); + const runtimeName = runtimeChunks.join("-"); + const imageName = `openruntimes/${runtimeName}:${openRuntimesVersion}-${runtimeVersion}`; + + const tool = systemTools[runtimeName]; + + const functionDir = path.join(process.cwd(), func.path); + + const id = ID.unique(); + + const params = [ 'run' ]; + params.push('--rm'); + params.push('-d'); + params.push('--name', id); + params.push('-p', `${port}:3000`); + params.push('-e', 'APPWRITE_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_ENV=development'); + params.push('-e', 'OPEN_RUNTIMES_SECRET='); + + for(const k of Object.keys(variables)) { + params.push('-e', `${k}=${variables[k]}`); + } + + params.push('-v', `${functionDir}/.appwrite/logs.txt:/mnt/logs/dev_logs.log:rw`); + params.push('-v', `${functionDir}/.appwrite/errors.txt:/mnt/logs/dev_errors.log:rw`); + params.push('-v', `${functionDir}/.appwrite/build.tar.gz:/mnt/code/code.tar.gz:ro`); + params.push(imageName, 'sh', '-c', `helpers/start.sh "${tool.startCommand}"`); + + childProcess.spawn('docker', params, { + stdio: 'pipe', + pwd: functionDir + }); + + activeDockerIds[id] = true; +} + +export async function dockerCleanup() { + await dockerStop(); + + const functions = localConfig.getFunctions(); + for(const func of functions) { + const appwritePath = path.join(process.cwd(), func.path, '.appwrite'); + if (fs.existsSync(appwritePath)) { + fs.rmSync(appwritePath, { recursive: true, force: true }); + } + + const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); + if (fs.existsSync(tempPath)) { + fs.rmSync(tempPath, { force: true }); + } + } +} + +export async function dockerStopActive() { + const ids = Object.keys(activeDockerIds); + for await (const id of ids) { + await dockerStop(id); + } +} \ No newline at end of file diff --git a/templates/cli/lib/emulation/utils.js.twig b/templates/cli/lib/emulation/utils.js.twig new file mode 100644 index 000000000..a7011f3eb --- /dev/null +++ b/templates/cli/lib/emulation/utils.js.twig @@ -0,0 +1,164 @@ +export const openRuntimesVersion = 'v3'; + +export const runtimeNames = { + 'node': 'Node.js', + 'php': 'PHP', + 'ruby': 'Ruby', + 'python': 'Python', + 'python-ml': 'Python (ML)', + 'deno': 'Deno', + 'dart': 'Dart', + 'dotnet': '.NET', + 'java': 'Java', + 'swift': 'Swift', + 'kotlin': 'Kotlin', + 'bun': 'Bun' +}; + +export const systemTools = { + 'node': { + isCompiled: false, + startCommand: "node src/server.js", + dependencyFiles: [ "package.json", "package-lock.json" ] + }, + 'php': { + isCompiled: false, + startCommand: "php src/server.php", + dependencyFiles: [ "composer.json", "composer.lock" ] + }, + 'ruby': { + isCompiled: false, + startCommand: "bundle exec puma -b tcp://0.0.0.0:3000 -e production", + dependencyFiles: [ "Gemfile", "Gemfile.lock" ] + }, + 'python': { + isCompiled: false, + startCommand: "python3 src/server.py", + dependencyFiles: [ "requirements.txt", "requirements.lock" ] + }, + 'python-ml': { + isCompiled: false, + startCommand: "python3 src/server.py", + dependencyFiles: [ "requirements.txt", "requirements.lock" ] + }, + 'deno': { + isCompiled: false, + startCommand: "deno start", + dependencyFiles: [ ] + }, + 'dart': { + isCompiled: true, + startCommand: "src/function/server", + dependencyFiles: [ ] + }, + 'dotnet': { + isCompiled: true, + startCommand: "dotnet src/function/DotNetRuntime.dll", + dependencyFiles: [ ] + }, + 'java': { + isCompiled: true, + startCommand: "java -jar src/function/java-runtime-1.0.0.jar", + dependencyFiles: [ ] + }, + 'swift': { + isCompiled: true, + startCommand: "src/function/Runtime serve --env production --hostname 0.0.0.0 --port 3000", + dependencyFiles: [ ] + }, + 'kotlin': { + isCompiled: true, + startCommand: "java -jar src/function/kotlin-runtime-1.0.0.jar", + dependencyFiles: [ ] + }, + 'bun': { + isCompiled: false, + startCommand: "bun src/server.ts", + dependencyFiles: [ "package.json", "package-lock.json", "bun.lockb" ] + }, +}; + +export const JwtManager = { + userJwt: null, + functionJwt: null, + + timerWarn: null, + timerError: null, + + async setup(userId = null) { + if(this.timerWarn) { + clearTimeout(this.timerWarn); + } + + if(this.timerError) { + clearTimeout(this.timerError); + } + + this.timerWarn = setTimeout(() => { + log("Warning: Authorized JWT will expire in 5 minutes. Please stop and re-run the command to refresh tokens for 1 hour."); + }, 1000 * 60 * 55); // 55 mins + + this.timerError = setTimeout(() => { + log("Warning: Authorized JWT just expired. Please stop and re-run the command to obtain new tokens with 1 hour validity."); + log("Some Appwrite API communication is not authorized now.") + }, 1000 * 60 * 60); // 60 mins + + if(userId) { + await usersGet({ + userId, + parseOutput: false + }); + const userResponse = await usersCreateJWT({ + userId, + duration: 60*60, + parseOutput: false + }); + this.userJwt = userResponse.jwt; + } + + const functionResponse = await projectsCreateJWT({ + projectId: localConfig.getProject().projectId, + // TODO: Once we have endpoint for this, use it + scopes: ["sessions.write","users.read","users.write","teams.read","teams.write","databases.read","databases.write","collections.read","collections.write","attributes.read","attributes.write","indexes.read","indexes.write","documents.read","documents.write","files.read","files.write","buckets.read","buckets.write","functions.read","functions.write","execution.read","execution.write","locale.read","avatars.read","health.read","providers.read","providers.write","messages.read","messages.write","topics.read","topics.write","subscribers.read","subscribers.write","targets.read","targets.write","rules.read","rules.write","migrations.read","migrations.write","vcs.read","vcs.write","assistant.read"], + duration: 60*60, + parseOutput: false + }); + this.functionJwt = functionResponse.jwt; + } +}; + +export const Queue = { + files: [], + locked: false, + events: new EventEmitter(), + debounce: null, + push(file) { + if(!this.files.includes(file)) { + this.files.push(file); + } + + if(!this.locked) { + this._trigger(); + } + }, + lock() { + this.files = []; + this.locked = true; + }, + unlock() { + this.locked = false; + if(this.files.length > 0) { + this._trigger(); + } + }, + _trigger() { + if(this.debounce) { + return; + } + + this.debounce = setTimeout(() => { + this.events.emit('reload', { files: this.files }); + this.debounce = null; + }, 300); + } +}; \ No newline at end of file diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 83b104011..c0843fcd7 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -814,15 +814,19 @@ const questionsRunFunctions = [ (async () => { if (typeof value !== 'number' && isNaN(+value)) { - throw Error(`You need to provide a number.`); + throw Error(`Port needs to be a valid integer between 1024 and 65536.`); } value = +value; + if(value < 1024 || value > 65536) { + throw Error(`Port needs to be a valid integer between 1024 and 65536.`); + } + const taken = await isPortTaken(value); if(taken) { - throw Error(`Port ${value} is taken. Pick another one.`); + throw Error(`Port ${value} is in use by another process. Pick a different one.`); } })().then(() => { done(null, true); From 797104fd36cbf72df97c9431515bd963a6dda4d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 17 Jun 2024 14:47:35 +0200 Subject: [PATCH 169/198] More PR review changes --- templates/cli/lib/commands/run.js.twig | 22 ++++++++++--- templates/cli/lib/questions.js.twig | 45 +------------------------- 2 files changed, 19 insertions(+), 48 deletions(-) diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index 1a100c8ed..cf0981e66 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -49,14 +49,28 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = const taken = await isPortTaken(port); if(taken) { - log(`Port ${port} is already in use by another process.`); - port = null; + error(`Port ${port} is already in use by another process.`); + return; } } if(!port) { - const answers = await inquirer.prompt(questionsRunFunctions[1]); - port = answers.port; + let portFound = fale; + port = 3000; + while(port < 3100) { + const taken = await isPortTaken(port); + if(!taken) { + portFound = true; + break; + } + + port++; + } + + if(!portFound) { + error('Could not auto-suggest an available port. Please configure port with `appwrite run --port YOUR_PORT` command.'); + return; + } } // Configuration: Engine diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index c0843fcd7..17417a3b6 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -791,50 +791,7 @@ const questionsRunFunctions = [ }) return choices; } - }, - { - type: "input", - name: "port", - message: 'Which port would you like function to listen on?', - default: async () => { - let port = 3000; - while(port < 3100) { - const taken = await isPortTaken(port); - if(!taken) { - return port; - } - - port++; - } - - return 3000; - }, - validate: function(value) { - const done = this.async(); - - (async () => { - if (typeof value !== 'number' && isNaN(+value)) { - throw Error(`Port needs to be a valid integer between 1024 and 65536.`); - } - - value = +value; - - if(value < 1024 || value > 65536) { - throw Error(`Port needs to be a valid integer between 1024 and 65536.`); - } - - const taken = await isPortTaken(value); - - if(taken) { - throw Error(`Port ${value} is in use by another process. Pick a different one.`); - } - })().then(() => { - done(null, true); - }).catch((err) => { - done(err.message); - }); - }, - }, + } ]; module.exports = { From cebd23e6a5493b2d475a58d7f2c6029ea7374ca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 17 Jun 2024 14:59:56 +0200 Subject: [PATCH 170/198] PR review changes --- src/SDK/Language/CLI.php | 10 ++++++++++ templates/cli/lib/commands/run.js.twig | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 432732d87..bfab5f4f6 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -250,6 +250,16 @@ public function getFiles(): array 'destination' => 'lib/commands/run.js', 'template' => 'cli/lib/commands/run.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/emulation/docker.js', + 'template' => 'cli/lib/emulation/docker.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/emulation/utils.js', + 'template' => 'cli/lib/emulation/utils.js.twig', + ], [ 'scope' => 'service', 'destination' => '/lib/commands/{{service.name | caseDash}}.js', diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index cf0981e66..593bd5a84 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -68,7 +68,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = } if(!portFound) { - error('Could not auto-suggest an available port. Please configure port with `appwrite run --port YOUR_PORT` command.'); + error('Could not find an available port. Please select a port with `appwrite run --port YOUR_PORT` command.'); return; } } From d3fcdf80b5ec052fe3e011e88e4efa00335e4ae5 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:39:41 -0400 Subject: [PATCH 171/198] fix: typo --- templates/cli/lib/commands/pull.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index c4f4fefe3..b4d4c4954 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -202,7 +202,7 @@ pull .command("function") .alias("functions") .description("Pulling your {{ spec.title|caseUcfirst }} cloud function") - .action(actionRunner(pullFunction)) + .action(actionRunner(pullFunctions)) pull .command("collection") From fe3bcaa20ca3ae1ea19b1e394954f454bbd470ce Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:39:51 -0400 Subject: [PATCH 172/198] fix: missing import --- templates/cli/index.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index bc99de8b8..39882577f 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 } = 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 { run } = require("./lib/commands/run"); From 6711fbd98396b5923f99d282a425ad6af962dbd9 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Tue, 18 Jun 2024 15:40:06 -0400 Subject: [PATCH 173/198] feat: expanding functions update paramateters --- templates/cli/lib/commands/push.js.twig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index a2672743f..dc299ec16 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -765,6 +765,11 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero logging: func.logging, entrypoint: func.entrypoint, commands: func.commands, + providerRepositoryId: func.providerRepositoryId ?? "", + installationId: func.installationId ?? '', + providerBranch: func.providerBranch ?? '', + providerRootDirectory: func.providerRootDirectory ?? '', + providerSilentMode: func.providerSilentMode ?? false, vars: JSON.stringify(response.vars), parseOutput: false }); From 88b97a9a1fa463cca964b1da320daabb8717a38c Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 19 Jun 2024 08:41:22 -0400 Subject: [PATCH 174/198] chore: Module to commonjs --- templates/cli/lib/emulation/docker.js.twig | 35 ++++++++++++++-------- templates/cli/lib/emulation/utils.js.twig | 24 ++++++++++----- 2 files changed, 39 insertions(+), 20 deletions(-) diff --git a/templates/cli/lib/emulation/docker.js.twig b/templates/cli/lib/emulation/docker.js.twig index fa0b942c5..08544ad97 100644 --- a/templates/cli/lib/emulation/docker.js.twig +++ b/templates/cli/lib/emulation/docker.js.twig @@ -1,6 +1,6 @@ const activeDockerIds = {}; -export async function dockerStop(id) { +async function dockerStop(id) { delete activeDockerIds[id]; const stopProcess = childProcess.spawn('docker', ['rm', '--force', id], { stdio: 'pipe', @@ -9,7 +9,7 @@ export async function dockerStop(id) { await new Promise((res) => { stopProcess.on('close', res) }); } -export async function dockerPull(func) { +async function dockerPull(func) { log('Pulling Docker image of function runtime ...'); const runtimeChunks = func.runtime.split("-"); @@ -29,7 +29,7 @@ export async function dockerPull(func) { await new Promise((res) => { pullProcess.on('close', res) }); } -export async function dockerBuild(func, variables) { +async function dockerBuild(func, variables) { log('Building function using Docker ...'); const runtimeChunks = func.runtime.split("-"); @@ -63,7 +63,7 @@ export async function dockerBuild(func, variables) { buildProcess.stdout.on('data', (data) => { process.stdout.write(`\n${data}`); }); - + buildProcess.stderr.on('data', (data) => { process.stderr.write(`\n${data}`); }); @@ -95,10 +95,10 @@ export async function dockerBuild(func, variables) { const tempPath = path.join(process.cwd(), func.path, 'code.tar.gz'); if (fs.existsSync(tempPath)) { fs.rmSync(tempPath, { force: true }); - } + } } -export async function dockerStart(func, variables, port) { +async function dockerStart(func, variables, port) { log('Starting function using Docker ...'); log("Permissions, events, CRON and timeouts dont apply when running locally."); @@ -106,8 +106,8 @@ export async function dockerStart(func, variables, port) { log('💡 Hint: Function automatically restarts when you edit your code.'); success(`Visit http://localhost:${port}/ to execute your function.`); - - + + const runtimeChunks = func.runtime.split("-"); const runtimeVersion = runtimeChunks.pop(); const runtimeName = runtimeChunks.join("-"); @@ -136,7 +136,7 @@ export async function dockerStart(func, variables, port) { params.push('-v', `${functionDir}/.appwrite/errors.txt:/mnt/logs/dev_errors.log:rw`); params.push('-v', `${functionDir}/.appwrite/build.tar.gz:/mnt/code/code.tar.gz:ro`); params.push(imageName, 'sh', '-c', `helpers/start.sh "${tool.startCommand}"`); - + childProcess.spawn('docker', params, { stdio: 'pipe', pwd: functionDir @@ -145,7 +145,7 @@ export async function dockerStart(func, variables, port) { activeDockerIds[id] = true; } -export async function dockerCleanup() { +async function dockerCleanup() { await dockerStop(); const functions = localConfig.getFunctions(); @@ -162,9 +162,18 @@ export async function dockerCleanup() { } } -export async function dockerStopActive() { +async function dockerStopActive() { const ids = Object.keys(activeDockerIds); for await (const id of ids) { await dockerStop(id); - } -} \ No newline at end of file + } +} + +module.exports = { + dockerStop, + dockerPull, + dockerBuild, + dockerStart, + dockerCleanup, + dockerStopActive, +} diff --git a/templates/cli/lib/emulation/utils.js.twig b/templates/cli/lib/emulation/utils.js.twig index a7011f3eb..eefa096b6 100644 --- a/templates/cli/lib/emulation/utils.js.twig +++ b/templates/cli/lib/emulation/utils.js.twig @@ -1,6 +1,8 @@ -export const openRuntimesVersion = 'v3'; +const EventEmitter = require('node:events'); -export const runtimeNames = { +const openRuntimesVersion = 'v3'; + +const runtimeNames = { 'node': 'Node.js', 'php': 'PHP', 'ruby': 'Ruby', @@ -15,7 +17,7 @@ export const runtimeNames = { 'bun': 'Bun' }; -export const systemTools = { +const systemTools = { 'node': { isCompiled: false, startCommand: "node src/server.js", @@ -78,7 +80,7 @@ export const systemTools = { }, }; -export const JwtManager = { +const JwtManager = { userJwt: null, functionJwt: null, @@ -115,7 +117,7 @@ export const JwtManager = { }); this.userJwt = userResponse.jwt; } - + const functionResponse = await projectsCreateJWT({ projectId: localConfig.getProject().projectId, // TODO: Once we have endpoint for this, use it @@ -127,7 +129,7 @@ export const JwtManager = { } }; -export const Queue = { +const Queue = { files: [], locked: false, events: new EventEmitter(), @@ -161,4 +163,12 @@ export const Queue = { this.debounce = null; }, 300); } -}; \ No newline at end of file +}; + +module.exports = { + openRuntimesVersion, + runtimeNames, + systemTools, + JwtManager, + Queue +} From d32af691cd0ba4821b5578dae5127baf31e63ec5 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:04:20 -0400 Subject: [PATCH 175/198] feat: Adding register command --- templates/cli/index.js.twig | 3 ++- templates/cli/lib/commands/generic.js.twig | 7 +++++++ templates/cli/lib/parser.js.twig | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 39882577f..596d1b864 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, migrate, register } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); const { pull } = require("./lib/commands/pull"); const { run } = require("./lib/commands/run"); @@ -62,6 +62,7 @@ program .showSuggestionAfterError() {% if sdk.test != "true" %} .addCommand(whoami) + .addCommand(register) .addCommand(login) .addCommand(init) .addCommand(pull) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 1a258d664..b2634a2b3 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -128,6 +128,12 @@ const whoami = new Command("whoami") drawTable(data) })); +const register = new Command("register") + .description(commandDescriptions['register']) + .action(actionRunner(async () => { + log('Access https://cloud.appwrite.io/register to register an account') + })); + const login = new Command("login") .description(commandDescriptions['login']) .option(`--email [email]`, `User email`) @@ -299,6 +305,7 @@ module.exports = { {% if sdk.test != "true" %} loginCommand, whoami, + register, login, logout, {% endif %} diff --git a/templates/cli/lib/parser.js.twig b/templates/cli/lib/parser.js.twig index 44451e814..94292d82a 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -222,6 +222,7 @@ const commandDescriptions = { "login": `The login command allows you to authenticate and manage a user account.`, "logout": `The logout command allows you to logout of your {{ spec.title|caseUcfirst }} account.`, "whoami": `The whoami command gives information about the currently logged in user.`, + "register": `Prints link to register an {{ spec.title|caseUcfirst }} account..`, "console" : `The console command allows gives you access to the APIs used by the Appwrite console.`, "assistant": `The assistant command allows you to interact with the Appwrite Assistant AI`, "messaging": `The messaging command allows you to send messages.`, From 438cfb09a597af23b9ffdb945a081637e39e5bdb Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:06:56 -0400 Subject: [PATCH 176/198] refactor: Local development error string. --- 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 17417a3b6..a39a0e170 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -781,7 +781,7 @@ const questionsRunFunctions = [ choices: () => { let functions = localConfig.getFunctions(); if (functions.length === 0) { - throw new Error("No functions found in the current directory."); + throw new Error("No functions found in the current directory, use 'appwrite init function' to create one"); } let choices = functions.map((func, idx) => { return { From 3b9d6adca3b805f0f88a8ac5a742bd39c28f8597 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:10:55 -0400 Subject: [PATCH 177/198] refactor: tip for invalid logins --- templates/cli/lib/commands/generic.js.twig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index b2634a2b3..4e5dc9678 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -81,6 +81,9 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { } else { globalConfig.removeSession(id); globalConfig.setCurrentSession(oldCurrent); + if(endpoint !== DEFAULT_ENDPOINT && error.response === 'user_invalid_credentials'){ + log('remember to use --endpoint for self-hosted instances') + } throw error; } } From e8248bcd458c1aecec5c8922084fcdc4ae5cabee Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:15:47 -0400 Subject: [PATCH 178/198] refactor: tip for no organizations --- 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 a39a0e170..1c060f87d 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -163,7 +163,7 @@ const questionsInitProject = [ }) if (choices.length == 0) { - throw new Error("No organizations found. Please create a new organization.") + throw new Error(`No organizations found. Please create a new organization in this url ${globalConfig.getEndpoint().replace('/v1', '/console/onboarding')}`) } return choices; From fbd10ef6945e713762cc54c605e4477c8d47d17f Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:23:26 -0400 Subject: [PATCH 179/198] fix: Show Organizations and Projects instead of an object --- templates/cli/lib/commands/init.js.twig | 8 ++++---- templates/cli/lib/questions.js.twig | 10 ++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 9b8db081c..a9bd8bc40 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -53,16 +53,16 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { answers.project = {}; answers.organization = {}; - answers.organization.id = organizationId ?? (await inquirer.prompt(questionsInitProject[2])).organization; + answers.organization = 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; + answers.project = projectId ?? (await inquirer.prompt(questionsInitProject[4])).id; try { await projectsGet({ projectId, parseOutput: false }); } catch (e) { if (e.code === 404) { answers.start = 'new'; - answers.id = answers.project.id; + answers.id = answers.project; answers.project = answers.project.name; } else { throw e; @@ -74,7 +74,7 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { response = await projectsCreate({ projectId: answers.id, name: answers.project, - teamId: answers.organization.id, + teamId: answers.organization, parseOutput: false }) diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 1c060f87d..fd96c916d 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -155,10 +155,7 @@ const questionsInitProject = [ let choices = teams.map((team, idx) => { return { name: `${team.name} (${team['$id']})`, - value: { - name: team.name, - id: team['$id'] - } + value: team['$id'] } }) @@ -199,10 +196,7 @@ const questionsInitProject = [ let choices = projects.map((project) => { return { name: `${project.name} (${project['$id']})`, - value: { - name: project.name, - id: project['$id'] - } + value: project['$id'] } }) From 725fd50bd2497e5629a49a4c7f2c5934f9fdd865 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:28:32 -0400 Subject: [PATCH 180/198] fix: Add function success creation instructions --- templates/cli/lib/commands/init.js.twig | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index a9bd8bc40..2c818a094 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -273,6 +273,7 @@ const initFunction = async () => { localConfig.addFunction(data); success(); + log(`* use 'appwrite dev function' for local development\n${' '.repeat(7)}* use 'appwrite push functions' to push the function to your {{ spec.title|caseUcfirst }} instance`) } const init = new Command("init") From 237e38bc41d053a5b14c92079553633159070ead Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:33:31 -0400 Subject: [PATCH 181/198] fix: some appwrite dev function fixes --- templates/cli/lib/commands/run.js.twig | 8 ++++---- templates/cli/lib/emulation/docker.js.twig | 5 +++++ templates/cli/lib/emulation/utils.js.twig | 1 + 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index 593bd5a84..e5574396d 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -55,7 +55,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = } if(!port) { - let portFound = fale; + let portFound = false; port = 3000; while(port < 3100) { const taken = await isPortTaken(port); @@ -194,7 +194,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = await dockerStart(func, variables, port); } else { log('Hot-swapping function files ...'); - + const functionPath = path.join(process.cwd(), func.path); const hotSwapPath = path.join(functionPath, '.appwrite/hot-swap'); const buildPath = path.join(functionPath, '.appwrite/build.tar.gz'); @@ -226,7 +226,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = const filePath = path.join(hotSwapPath, f); if (fs.existsSync(filePath)) { fs.rmSync(filePath, { force: true }); - } + } const fileDir = path.dirname(filePath); if (!fs.existsSync(fileDir)) { @@ -244,7 +244,7 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = cwd: hotSwapPath, file: buildPath }, ['.']); - + fs.rmSync(hotSwapPath, { recursive: true, force: true }); await dockerStart(func, variables, port); diff --git a/templates/cli/lib/emulation/docker.js.twig b/templates/cli/lib/emulation/docker.js.twig index 08544ad97..dce468a10 100644 --- a/templates/cli/lib/emulation/docker.js.twig +++ b/templates/cli/lib/emulation/docker.js.twig @@ -1,3 +1,8 @@ +const childProcess = require('child_process'); +const { localConfig } = require("../config"); +const path = require('path'); +const fs = require('fs'); + const activeDockerIds = {}; async function dockerStop(id) { diff --git a/templates/cli/lib/emulation/utils.js.twig b/templates/cli/lib/emulation/utils.js.twig index eefa096b6..8e22eb703 100644 --- a/templates/cli/lib/emulation/utils.js.twig +++ b/templates/cli/lib/emulation/utils.js.twig @@ -1,4 +1,5 @@ const EventEmitter = require('node:events'); +const { projectsCreateJWT } = require('../commands/projects'); const openRuntimesVersion = 'v3'; From 7fe7fad1e350562d339d0b31fbd96c0b4f44c109 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:42:16 -0400 Subject: [PATCH 182/198] fix: project init --- templates/cli/lib/commands/init.js.twig | 2 +- templates/cli/lib/config.js.twig | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index 2c818a094..a79f171bc 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -80,7 +80,7 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { localConfig.setProject(response['$id']); } else { - localConfig.setProject(answers.project.id); + localConfig.setProject(answers.project); } success(); diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index c454a2558..93a0ff591 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -333,13 +333,16 @@ class Local extends Config { }; } - setProject(projectId, projectName = '', projectSettings = {}) { + setProject(projectId, projectName = '', projectSettings = undefined) { this.set("projectId", projectId); if (projectName !== '') { this.set("projectName", projectName); } + if(projectSettings === undefined){ + return; + } const settings = { services: { From fa697208f15de5fa6c9e459865a91409083a5d93 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 09:47:17 -0400 Subject: [PATCH 183/198] fix: updating pulling scheme for 1.6.x branch --- example.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example.php b/example.php index bcfdc2c5f..a9e158e73 100644 --- a/example.php +++ b/example.php @@ -42,7 +42,7 @@ function getSSLPage($url) { $platform = 'console'; // $platform = 'server'; - $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/1.5.x/app/config/specs/swagger2-latest-{$platform}.json"); + $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/1.6.x/app/config/specs/swagger2-latest-{$platform}.json"); if(empty($spec)) { throw new Exception('Failed to fetch spec from Appwrite server'); From 1bd130ad570a0444de6848fdb90eb9d39a18226c Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:12:30 -0400 Subject: [PATCH 184/198] fix: undefined old session --- templates/cli/lib/config.js.twig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 93a0ff591..d02194aaa 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -417,8 +417,10 @@ class Global extends Config { return this.get(Global.PREFERENCE_CURRENT); } - setCurrentSession(endpoint) { - this.set(Global.PREFERENCE_CURRENT, endpoint); + setCurrentSession(session) { + if(session !== undefined) { + this.set(Global.PREFERENCE_CURRENT, session); + } } getSessionIds() { From 6e0412cd5181484d4900f0924ca8e45bcea681b0 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Fri, 21 Jun 2024 10:21:28 -0400 Subject: [PATCH 185/198] fix: local dev --- templates/cli/lib/emulation/docker.js.twig | 3 +++ templates/cli/lib/emulation/utils.js.twig | 2 ++ 2 files changed, 5 insertions(+) diff --git a/templates/cli/lib/emulation/docker.js.twig b/templates/cli/lib/emulation/docker.js.twig index dce468a10..fe5033955 100644 --- a/templates/cli/lib/emulation/docker.js.twig +++ b/templates/cli/lib/emulation/docker.js.twig @@ -2,6 +2,9 @@ const childProcess = require('child_process'); const { localConfig } = require("../config"); const path = require('path'); const fs = require('fs'); +const { log,success } = require("../parser"); +const { openRuntimesVersion, systemTools } = require("./utils"); +const ID = require("../id"); const activeDockerIds = {}; diff --git a/templates/cli/lib/emulation/utils.js.twig b/templates/cli/lib/emulation/utils.js.twig index 8e22eb703..d36d5cda0 100644 --- a/templates/cli/lib/emulation/utils.js.twig +++ b/templates/cli/lib/emulation/utils.js.twig @@ -1,5 +1,7 @@ const EventEmitter = require('node:events'); const { projectsCreateJWT } = require('../commands/projects'); +const { localConfig } = require("../config"); + const openRuntimesVersion = 'v3'; From c9d4570cceb398de9e2898a436a025f54a8247b1 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:24:10 +0400 Subject: [PATCH 186/198] Update templates/cli/lib/commands/generic.js.twig --- 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 4e5dc9678..491deb635 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -82,7 +82,7 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { globalConfig.removeSession(id); globalConfig.setCurrentSession(oldCurrent); if(endpoint !== DEFAULT_ENDPOINT && error.response === 'user_invalid_credentials'){ - log('remember to use --endpoint for self-hosted instances') + log('Use the --endpoint option for self-hosted instances') } throw error; } From 0d04b38c6bbc1462220b7f6d85693f51aa1d2697 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:24:16 +0400 Subject: [PATCH 187/198] Update templates/cli/lib/commands/generic.js.twig --- 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 491deb635..6d237e3a7 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 register = new Command("register") .description(commandDescriptions['register']) .action(actionRunner(async () => { - log('Access https://cloud.appwrite.io/register to register an account') + log('Visit https://cloud.appwrite.io/register to create an account') })); const login = new Command("login") From c09ea496dd2690af5cfacb361a7d7cff55223f98 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:24:24 +0400 Subject: [PATCH 188/198] 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 a79f171bc..f0293da5e 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -273,7 +273,7 @@ const initFunction = async () => { localConfig.addFunction(data); success(); - log(`* use 'appwrite dev function' for local development\n${' '.repeat(7)}* use 'appwrite push functions' to push the function to your {{ spec.title|caseUcfirst }} instance`) + log(`* Use 'appwrite dev function' for local development\n${' '.repeat(7)}* Use 'appwrite push functions' to push the function to your {{ spec.title|caseUcfirst }} instance`) } const init = new Command("init") From bc6f25e8efeffb0facc4c511bc773c041082f985 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:24:32 +0400 Subject: [PATCH 189/198] 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 fd96c916d..4d5df9bf5 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -775,7 +775,7 @@ const questionsRunFunctions = [ choices: () => { let functions = localConfig.getFunctions(); if (functions.length === 0) { - throw new Error("No functions found in the current directory, use 'appwrite init function' to create one"); + throw new Error("No functions found in the current directory. Use 'appwrite init function' to create one"); } let choices = functions.map((func, idx) => { return { From 80509c5998a636ce89db0f023b9d4712afe83977 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:24:37 +0400 Subject: [PATCH 190/198] 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 4d5df9bf5..e0afdffbc 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -160,7 +160,7 @@ const questionsInitProject = [ }) if (choices.length == 0) { - throw new Error(`No organizations found. Please create a new organization in this url ${globalConfig.getEndpoint().replace('/v1', '/console/onboarding')}`) + throw new Error(`No organizations found. Please create a new organization at ${globalConfig.getEndpoint().replace('/v1', '/console/onboarding')}`) } return choices; From eec04ca39368ee90a8485969ab6936a6eae0c33f Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:24:50 +0400 Subject: [PATCH 191/198] Update templates/cli/lib/config.js.twig --- templates/cli/lib/config.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index d02194aaa..8e263ba77 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -340,7 +340,7 @@ class Local extends Config { this.set("projectName", projectName); } - if(projectSettings === undefined){ + if (projectSettings === undefined) { return; } From b07adcb3e8ecfd227266e3c192777a0397c5cf31 Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:24:58 +0400 Subject: [PATCH 192/198] Update templates/cli/lib/config.js.twig --- templates/cli/lib/config.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 8e263ba77..f4f152a15 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -418,7 +418,7 @@ class Global extends Config { } setCurrentSession(session) { - if(session !== undefined) { + if (session !== undefined) { this.set(Global.PREFERENCE_CURRENT, session); } } From 8405ebb29fed39fca8f0a49301e0ebcd2ccb39dd Mon Sep 17 00:00:00 2001 From: Christy Jacob Date: Tue, 25 Jun 2024 13:29:03 +0400 Subject: [PATCH 193/198] Update templates/cli/lib/parser.js.twig --- 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 94292d82a..12358131e 100644 --- a/templates/cli/lib/parser.js.twig +++ b/templates/cli/lib/parser.js.twig @@ -222,7 +222,7 @@ const commandDescriptions = { "login": `The login command allows you to authenticate and manage a user account.`, "logout": `The logout command allows you to logout of your {{ spec.title|caseUcfirst }} account.`, "whoami": `The whoami command gives information about the currently logged in user.`, - "register": `Prints link to register an {{ spec.title|caseUcfirst }} account..`, + "register": `Outputs the link to create an {{ spec.title|caseUcfirst }} account..`, "console" : `The console command allows gives you access to the APIs used by the Appwrite console.`, "assistant": `The assistant command allows you to interact with the Appwrite Assistant AI`, "messaging": `The messaging command allows you to send messages.`, From c772d4f4e53818dbfbd14e1c14874af246cb8509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 26 Jun 2024 13:39:05 +0000 Subject: [PATCH 194/198] Changes after manual QA --- composer.lock | 292 +++++++++++++-------- templates/cli/install.ps1.twig | 2 +- templates/cli/install.sh.twig | 2 +- templates/cli/lib/commands/generic.js.twig | 1 + templates/cli/lib/commands/init.js.twig | 83 ++++-- templates/cli/lib/commands/pull.js.twig | 18 +- templates/cli/lib/commands/push.js.twig | 49 ++-- templates/cli/lib/commands/run.js.twig | 5 +- templates/cli/lib/questions.js.twig | 93 ++++--- templates/cli/lib/utils.js.twig | 2 +- 10 files changed, 334 insertions(+), 213 deletions(-) diff --git a/composer.lock b/composer.lock index ad6ab16c8..954fffab7 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "matthiasmullie/minify", - "version": "1.3.71", + "version": "1.3.73", "source": { "type": "git", "url": "https://github.com/matthiasmullie/minify.git", - "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1" + "reference": "cb7a9297b4ab070909cefade30ee95054d4ae87a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", - "reference": "ae42a47d7fecc1fbb7277b2f2d84c37a33edc3b1", + "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/cb7a9297b4ab070909cefade30ee95054d4ae87a", + "reference": "cb7a9297b4ab070909cefade30ee95054d4ae87a", "shasum": "" }, "require": { @@ -67,7 +67,7 @@ ], "support": { "issues": "https://github.com/matthiasmullie/minify/issues", - "source": "https://github.com/matthiasmullie/minify/tree/1.3.71" + "source": "https://github.com/matthiasmullie/minify/tree/1.3.73" }, "funding": [ { @@ -75,7 +75,7 @@ "type": "github" } ], - "time": "2023-04-25T20:33:03+00:00" + "time": "2024-03-15T10:27:10+00:00" }, { "name": "matthiasmullie/path-converter", @@ -132,16 +132,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + "reference": "0424dff1c58f028c451efff2045f5d92410bd540" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", - "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540", + "reference": "0424dff1c58f028c451efff2045f5d92410bd540", "shasum": "" }, "require": { @@ -191,7 +191,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0" }, "funding": [ { @@ -207,20 +207,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", - "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c", + "reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c", "shasum": "" }, "require": { @@ -271,7 +271,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0" }, "funding": [ { @@ -287,20 +287,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-06-19T12:30:46+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", - "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433", + "reference": "77fa7995ac1b21ab60769b7323d600a991a90433", "shasum": "" }, "require": { @@ -351,7 +351,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0" }, "funding": [ { @@ -367,7 +367,7 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "twig/twig", @@ -445,16 +445,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.4.3", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "64fcfd0e28a6b8078a19dbf9127be2ee645b92ec" + "reference": "d4de825332842a7dee1ff350f0fd6caafa930d79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/64fcfd0e28a6b8078a19dbf9127be2ee645b92ec", - "reference": "64fcfd0e28a6b8078a19dbf9127be2ee645b92ec", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/d4de825332842a7dee1ff350f0fd6caafa930d79", + "reference": "d4de825332842a7dee1ff350f0fd6caafa930d79", "shasum": "" }, "require": { @@ -463,26 +463,26 @@ "ext-reflection": "*", "ext-simplexml": "*", "fidry/cpu-core-counter": "^1.1.0", - "jean85/pretty-package-versions": "^2.0.5", + "jean85/pretty-package-versions": "^2.0.6", "php": "~8.2.0 || ~8.3.0", - "phpunit/php-code-coverage": "^10.1.11 || ^11.0.0", + "phpunit/php-code-coverage": "^10.1.14 || ^11.0.3", "phpunit/php-file-iterator": "^4.1.0 || ^5.0.0", "phpunit/php-timer": "^6.0.0 || ^7.0.0", - "phpunit/phpunit": "^10.5.9 || ^11.0.3", - "sebastian/environment": "^6.0.1 || ^7.0.0", - "symfony/console": "^6.4.3 || ^7.0.3", - "symfony/process": "^6.4.3 || ^7.0.3" + "phpunit/phpunit": "^10.5.20 || ^11.1.3", + "sebastian/environment": "^6.1.0 || ^7.1.0", + "symfony/console": "^6.4.7 || ^7.1.0", + "symfony/process": "^6.4.7 || ^7.1.0" }, "require-dev": { "doctrine/coding-standard": "^12.0.0", "ext-pcov": "*", "ext-posix": "*", - "phpstan/phpstan": "^1.10.58", - "phpstan/phpstan-deprecation-rules": "^1.1.4", - "phpstan/phpstan-phpunit": "^1.3.15", - "phpstan/phpstan-strict-rules": "^1.5.2", - "squizlabs/php_codesniffer": "^3.9.0", - "symfony/filesystem": "^6.4.3 || ^7.0.3" + "phpstan/phpstan": "^1.11.2", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "squizlabs/php_codesniffer": "^3.10.1", + "symfony/filesystem": "^6.4.3 || ^7.1.0" }, "bin": [ "bin/paratest", @@ -523,7 +523,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.4.3" + "source": "https://github.com/paratestphp/paratest/tree/v7.4.5" }, "funding": [ { @@ -535,7 +535,7 @@ "type": "paypal" } ], - "time": "2024-02-20T07:24:02+00:00" + "time": "2024-05-31T13:59:20+00:00" }, { "name": "fidry/cpu-core-counter", @@ -659,16 +659,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.11.1", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", - "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", "shasum": "" }, "require": { @@ -676,11 +676,12 @@ }, "conflict": { "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" + "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", @@ -706,7 +707,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" }, "funding": [ { @@ -714,7 +715,7 @@ "type": "tidelift" } ], - "time": "2023-03-08T13:26:56+00:00" + "time": "2024-06-12T14:39:25+00:00" }, { "name": "nikic/php-parser", @@ -894,16 +895,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.13", + "version": "10.1.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "d51c3aec14896d5e80b354fad58e998d1980f8f8" + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d51c3aec14896d5e80b354fad58e998d1980f8f8", - "reference": "d51c3aec14896d5e80b354fad58e998d1980f8f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", "shasum": "" }, "require": { @@ -960,7 +961,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.13" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" }, "funding": [ { @@ -968,7 +969,7 @@ "type": "github" } ], - "time": "2024-03-09T16:54:15+00:00" + "time": "2024-03-12T15:33:41+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1215,16 +1216,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.12", + "version": "10.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "41a9886b85ac7bf3929853baf96b95361cd69d2b" + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/41a9886b85ac7bf3929853baf96b95361cd69d2b", - "reference": "41a9886b85ac7bf3929853baf96b95361cd69d2b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f124e3e3e561006047b532fd0431bf5bb6b9015", + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015", "shasum": "" }, "require": { @@ -1296,7 +1297,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.24" }, "funding": [ { @@ -1312,7 +1313,7 @@ "type": "tidelift" } ], - "time": "2024-03-09T12:04:07+00:00" + "time": "2024-06-20T13:09:54+00:00" }, { "name": "psr/container", @@ -1739,16 +1740,16 @@ }, { "name": "sebastian/environment", - "version": "6.0.1", + "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", - "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "" }, "require": { @@ -1763,7 +1764,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -1791,7 +1792,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" }, "funding": [ { @@ -1799,7 +1800,7 @@ "type": "github" } ], - "time": "2023-04-11T05:39:26+00:00" + "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", @@ -2285,16 +2286,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.9.0", + "version": "3.9.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", - "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/aac1f6f347a5c5ac6bc98ad395007df00990f480", + "reference": "aac1f6f347a5c5ac6bc98ad395007df00990f480", "shasum": "" }, "require": { @@ -2361,20 +2362,20 @@ "type": "open_collective" } ], - "time": "2024-02-16T15:06:51+00:00" + "time": "2024-04-23T20:25:34+00:00" }, { "name": "symfony/console", - "version": "v7.0.4", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f" + "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6b099f3306f7c9c2d2786ed736d0026b2903205f", - "reference": "6b099f3306f7c9c2d2786ed736d0026b2903205f", + "url": "https://api.github.com/repos/symfony/console/zipball/9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", + "reference": "9b008f2d7b21c74ef4d0c3de6077a642bc55ece3", "shasum": "" }, "require": { @@ -2438,7 +2439,74 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.0.4" + "source": "https://github.com/symfony/console/tree/v7.1.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:57:53+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" }, "funding": [ { @@ -2454,20 +2522,20 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:20+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", - "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/64647a7c30b2283f5d49b874d84a18fc22054b7a", + "reference": "64647a7c30b2283f5d49b874d84a18fc22054b7a", "shasum": "" }, "require": { @@ -2516,7 +2584,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.30.0" }, "funding": [ { @@ -2532,20 +2600,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.29.0", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", - "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/a95281b0be0d9ab48050ebd988b967875cdb9fdb", + "reference": "a95281b0be0d9ab48050ebd988b967875cdb9fdb", "shasum": "" }, "require": { @@ -2597,7 +2665,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.30.0" }, "funding": [ { @@ -2613,20 +2681,20 @@ "type": "tidelift" } ], - "time": "2024-01-29T20:11:03+00:00" + "time": "2024-05-31T15:07:36+00:00" }, { "name": "symfony/process", - "version": "v7.0.4", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9" + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9", - "reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9", + "url": "https://api.github.com/repos/symfony/process/zipball/febf90124323a093c7ee06fdb30e765ca3c20028", + "reference": "febf90124323a093c7ee06fdb30e765ca3c20028", "shasum": "" }, "require": { @@ -2658,7 +2726,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.0.4" + "source": "https://github.com/symfony/process/tree/v7.1.1" }, "funding": [ { @@ -2674,25 +2742,26 @@ "type": "tidelift" } ], - "time": "2024-02-22T20:27:20+00:00" + "time": "2024-05-31T14:57:53+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.4.1", + "version": "v3.5.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", - "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -2700,7 +2769,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "3.4-dev" + "dev-main": "3.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -2740,7 +2809,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" }, "funding": [ { @@ -2756,20 +2825,20 @@ "type": "tidelift" } ], - "time": "2023-12-26T14:02:43+00:00" + "time": "2024-04-18T09:32:20+00:00" }, { "name": "symfony/string", - "version": "v7.0.4", + "version": "v7.1.1", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b" + "reference": "60bc311c74e0af215101235aa6f471bcbc032df2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b", - "reference": "f5832521b998b0bec40bee688ad5de98d4cf111b", + "url": "https://api.github.com/repos/symfony/string/zipball/60bc311c74e0af215101235aa6f471bcbc032df2", + "reference": "60bc311c74e0af215101235aa6f471bcbc032df2", "shasum": "" }, "require": { @@ -2783,6 +2852,7 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { + "symfony/emoji": "^7.1", "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", @@ -2826,7 +2896,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.0.4" + "source": "https://github.com/symfony/string/tree/v7.1.1" }, "funding": [ { @@ -2842,7 +2912,7 @@ "type": "tidelift" } ], - "time": "2024-02-01T13:17:36+00:00" + "time": "2024-06-04T06:40:14+00:00" }, { "name": "theseer/tokenizer", diff --git a/templates/cli/install.ps1.twig b/templates/cli/install.ps1.twig index 7491a4f16..b3dffba93 100644 --- a/templates/cli/install.ps1.twig +++ b/templates/cli/install.ps1.twig @@ -79,8 +79,8 @@ function CleanUp { function InstallCompleted { Write-Host "[4/4] Finishing Installation ... " cleanup - Write-Host "🤘 May the force be with you." Write-Host "To get started with {{ spec.title | caseUcfirst }} CLI, please visit {{ sdk.url }}/docs/command-line" + Write-Host "As first step, you can login to your {{ spec.title | caseUcfirst }} account using 'appwrite login'" } diff --git a/templates/cli/install.sh.twig b/templates/cli/install.sh.twig index af0f26330..7faa92a6a 100644 --- a/templates/cli/install.sh.twig +++ b/templates/cli/install.sh.twig @@ -139,8 +139,8 @@ cleanup() { installCompleted() { echo "[4/4] Wrapping up installation ... " cleanup - printf "🤘 May the force be with you. \n" echo "🚀 To get started with {{ spec.title | caseUcfirst }} CLI, please visit {{ sdk.url }}/docs/command-line" + echo "As first step, you can login to your {{ spec.title | caseUcfirst }} account using 'appwrite login'" } # Installation Starts here diff --git a/templates/cli/lib/commands/generic.js.twig b/templates/cli/lib/commands/generic.js.twig index 6d237e3a7..865ff24c8 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -89,6 +89,7 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { } success("Signed in as user with ID: " + account.$id); + info("Next you can create or link to your project using 'appwrite init project'"); }; const whoami = new Command("whoami") diff --git a/templates/cli/lib/commands/init.js.twig b/templates/cli/lib/commands/init.js.twig index f0293da5e..c3f24cfb8 100644 --- a/templates/cli/lib/commands/init.js.twig +++ b/templates/cli/lib/commands/init.js.twig @@ -17,12 +17,31 @@ const { questionsCreateBucket, questionsCreateMessagingTopic, questionsCreateCollection, - questionsInitProject + questionsInitProject, + questionsInitResources, + questionsCreateTeam } = require("../questions"); -const { success, log, error, actionRunner } = require("../parser"); +const { success, log, error, actionRunner, commandDescriptions } = require("../parser"); const { accountGet } = require("./account"); const { sdkForConsole } = require("../sdks"); +const initResources = async () => { + const actions = { + function: initFunction, + collection: initCollection, + bucket: initBucket, + team: initTeam, + message: initTopic + } + + const answers = await inquirer.prompt(questionsInitResources[0]); + + const action = actions[answers.resource]; + if (action !== undefined) { + await action({ returnOnZero: true }); + } +}; + const initProject = async ({ organizationId, projectId, projectName } = {}) => { let response = {}; @@ -83,7 +102,13 @@ const initProject = async ({ organizationId, projectId, projectName } = {}) => { localConfig.setProject(answers.project); } - success(); + success(`Project successfully ${answers.start === 'existing' ? 'linked' : 'created'}. Details are now stored in appwrite.json file.`); + + log("Next you can use 'appwrite init' to create resources in your project, or use 'appwrite pull' and 'appwite push' to synchronize your project.") + + if(answers.start === 'existing') { + log("Since you connected to an existing project, we highly recommend to run 'appwrite pull all' to synchronize all of your existing resources."); + } } const initBucket = async () => { @@ -96,6 +121,19 @@ const initBucket = async () => { enabled: true, }); success(); + log("Next you can use 'appwrite push bucket' to deploy the changes."); +}; + +const initTeam = async () => { + const answers = await inquirer.prompt(questionsCreateTeam) + + localConfig.addTeam({ + $id: answers.id === 'unique()' ? ID.unique() : answers.id, + name: answers.bucket, + }); + + success(); + log("Next you can use 'appwrite push team' to deploy the changes."); }; const initCollection = async () => { @@ -103,16 +141,16 @@ const initCollection = async () => { const newDatabase = (answers.method ?? '').toLowerCase() !== 'existing'; if (!newDatabase) { - answers.databaseId = answers.database.$id; - answers.databaseName = answers.database.name; + answers.databaseId = answers.database; + answers.databaseName = localConfig.getDatabase(answers.database).name; } - const databaseId = answers.database_id === 'unique()' ? ID.unique() : answers.database_id; + const databaseId = answers.databaseId === 'unique()' ? ID.unique() : answers.databaseId; - if (newDatabase || !localConfig.getDatabase(answers.database_id).$id) { + if (newDatabase || !localConfig.getDatabase(answers.databaseId)) { localConfig.addDatabase({ $id: databaseId, - name: answers.database_name, + name: answers.databaseName, enabled: true }); } @@ -128,6 +166,7 @@ const initCollection = async () => { }); success(); + log("Next you can use 'appwrite push collection' to deploy the changes."); }; const initTopic = async () => { @@ -140,6 +179,7 @@ const initTopic = async () => { }); success(); + log("Next you can use 'appwrite push topic' to deploy the changes."); }; const initFunction = async () => { @@ -175,7 +215,7 @@ const initFunction = async () => { fs.mkdirSync(templatesDir, "777"); const repo = "https://github.com/{{ sdk.gitUserName }}/templates"; const api = `https://api.github.com/repos/{{ sdk.gitUserName }}/templates/contents/${answers.runtime.name}` - const templates = ['Starter']; + const templates = ['starter']; let selected = undefined; try { @@ -188,13 +228,12 @@ const initFunction = async () => { log('Loading templates...'); } - const sparse = selected ? `${answers.runtime.name}/${selected.template}` : answers.runtime.name; + const sparse = (selected ? `${answers.runtime.name}/${selected.template}` : answers.runtime.name).toLowerCase(); 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 ${sparse}`; - /* Force use CMD as powershell does not support && */ if (process.platform === 'win32') { gitInitCommands = 'cmd /c "' + gitInitCommands + '"'; @@ -273,14 +312,16 @@ const initFunction = async () => { localConfig.addFunction(data); success(); - log(`* Use 'appwrite dev function' for local development\n${' '.repeat(7)}* Use 'appwrite push functions' to push the function to your {{ spec.title|caseUcfirst }} instance`) + log("Next you can use 'appwrite run function' to develop a function locally. To deploy the function, use 'appwrite push function'"); } const init = new Command("init") - .description('Init an {{ spec.title|caseUcfirst }} project') - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }) + .description(commandDescriptions['init']) + .action(actionRunner(initResources)); + +init + .command("project") + .description("Init a new {{ spec.title|caseUcfirst }} project") .option("--organizationId ", "{{ spec.title|caseUcfirst }} organization ID") .option("--projectId ", "{{ spec.title|caseUcfirst }} project ID") .option("--projectName ", "{{ spec.title|caseUcfirst }} project ID") @@ -288,21 +329,31 @@ const init = new Command("init") init .command("function") + .alias("functions") .description("Init a new {{ spec.title|caseUcfirst }} function") .action(actionRunner(initFunction)); init .command("bucket") + .alias("buckets") .description("Init a new {{ spec.title|caseUcfirst }} bucket") .action(actionRunner(initBucket)); +init + .command("team") + .alias("teams") + .description("Init a new {{ spec.title|caseUcfirst }} team") + .action(actionRunner(initTeam)); + init .command("collection") + .alias("collections") .description("Init a new {{ spec.title|caseUcfirst }} collection") .action(actionRunner(initCollection)); init .command("topic") + .alias("topics") .description("Init a new {{ spec.title|caseUcfirst }} topic") .action(actionRunner(initTopic)); diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index b4d4c4954..0b56d0d45 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -30,11 +30,9 @@ const pullResources = async () => { } else { const answers = await inquirer.prompt(questionsPullResources[0]); - for (let resource of answers.resources) { - const action = actions[resource]; - if (action !== undefined) { - await action(); - } + const action = actions[answers.resource]; + if (action !== undefined) { + await action({ returnOnZero: true }); } } }; @@ -183,15 +181,15 @@ const pullMessagingTopic = async () => { const pull = new Command("pull") .description(commandDescriptions['pull']) - .configureHelp({ - helpWidth: process.stdout.columns || 80 - }); + .action(actionRunner(pullResources)); pull .command("all") .description("Pull all resource.") - .action(actionRunner(pullResources)); - + .action(actionRunner(() => { + cliConfig.all = true; + return pullResources(); + })); pull .command("project") diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index dc299ec16..98c74f5e5 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -580,11 +580,9 @@ const pushResources = async () => { } else { const answers = await inquirer.prompt(questionsPushResources[0]); - for (let resource of answers.resources) { - const action = actions[resource]; - if (action !== undefined) { - await action({ returnOnZero: true }); - } + const action = actions[answers.resource]; + if (action !== undefined) { + await action({ returnOnZero: true }); } } }; @@ -593,9 +591,9 @@ const pushProject = async () => { try { const projectId = localConfig.getProject().projectId; const projectName = localConfig.getProject().projectName; - const settings = localConfig.getProject().projectSettings; + const settings = localConfig.getProject().projectSettings ?? {}; - log(`Updating project ${projectName} ( ${projectId} )`); + log(`Updating project ${projectId}`); if (projectName) { await projectsUpdate({ @@ -605,7 +603,6 @@ const pushProject = async () => { }); } - if (settings.services) { log('Updating service statuses'); for (let [service, status] of Object.entries(settings.services)) { @@ -643,7 +640,7 @@ const pushProject = async () => { } } - success(); + success("Project configuration updated."); } catch (e) { throw e; } @@ -664,7 +661,7 @@ const pushFunction = async ({ functionId, async, returnOnZero } = { returnOnZero log('No functions found, skipping'); return; } - throw new Error("No functions found in the current directory."); + throw new Error("No functions found in the current directory. Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); } functionIds.push(...functions.map((func) => { return func.$id; @@ -970,7 +967,7 @@ const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { return; } - throw new Error("No collections found in the current directory. Run `{{ language.params.executableName }} pull collection` to fetch all your collections."); + throw new Error("No collections found in the current directory. Use 'appwrite pull collections' to synchronize existing one, or use 'appwrite init collection' to create a new one."); } collections.push(...localConfig.getCollections()); } else { @@ -1102,7 +1099,7 @@ const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { 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."); + throw new Error("No buckets found in the current directory. Use 'appwrite pull buckets' to synchronize existing one, or use 'appwrite init bucket' to create a new one."); } bucketIds.push(...configBuckets.map((b) => b.$id)); } @@ -1127,15 +1124,6 @@ const pushBucket = async ({ returnOnZero } = { returnOnZero: false }) => { bucketId: bucket['$id'], parseOutput: false, }) - log(`Bucket ${bucket.name} ( ${bucket['$id']} ) already exists.`); - - if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushBuckets[1]) - if (answers.override.toLowerCase() !== "yes") { - log(`Received "${answers.override}". Skipping ${bucket.name} ( ${bucket['$id']} )`); - continue; - } - } log(`Updating bucket ...`) @@ -1193,7 +1181,7 @@ const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { 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."); + throw new Error("No teams found in the current directory. Use 'appwrite pull teams' to synchronize existing one, or use 'appwrite init team' to create a new one."); } teamIds.push(...configTeams.map((t) => t.$id)); } @@ -1218,15 +1206,6 @@ const pushTeam = async ({ returnOnZero } = { returnOnZero: false }) => { teamId: team['$id'], parseOutput: false, }) - log(`Team ${team.name} ( ${team['$id']} ) already exists.`); - - if (!cliConfig.force) { - const answers = await inquirer.prompt(questionsPushTeams[1]) - if (answers.override.toLowerCase() !== "yes") { - log(`Received "${answers.override}". Skipping ${team.name} ( ${team['$id']} )`); - continue; - } - } log(`Updating team ...`) @@ -1269,7 +1248,7 @@ const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => 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."); + throw new Error("No topics found in the current directory. Use 'appwrite pull topics' to synchronize existing one, or use 'appwrite init topic' to create a new one."); } topicsIds.push(...configTopics.map((b) => b.$id)); } @@ -1338,14 +1317,16 @@ const pushMessagingTopic = async ({ returnOnZero } = { returnOnZero: false }) => } const push = new Command("push") - .alias('deploy') .description(commandDescriptions['push']) .action(actionRunner(pushResources)); push .command("all") .description("Push all resource.") - .action(actionRunner(pushResources)); + .action(actionRunner(() => { + cliConfig.all = true; + return pushResources(); + })); push .command("project") diff --git a/templates/cli/lib/commands/run.js.twig b/templates/cli/lib/commands/run.js.twig index e5574396d..5d05e21cc 100644 --- a/templates/cli/lib/commands/run.js.twig +++ b/templates/cli/lib/commands/run.js.twig @@ -129,8 +129,8 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = variables[v.key] = v.value; }); } catch(err) { - error("Could not fetch remote variables: " + err.message); - error("Function will run locally, but will not have your function's environment variables set."); + log("Could not fetch remote variables: " + err.message); + log("Function will run locally, but will not have your function's environment variables set."); } } } @@ -258,7 +258,6 @@ const runFunction = async ({ port, functionId, noVariables, noReload, userId } = } const run = new Command("run") - .alias("dev") .description(commandDescriptions['run']) .configureHelp({ helpWidth: process.stdout.columns || 80 diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index e0afdffbc..639e8509d 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -123,7 +123,7 @@ const questionsInitProject = [ type: "confirm", name: "override", message: - `An {{ spec.title|caseUcfirst }} project ( ${localConfig.getProject()['projectName']} ) is already associated with the current directory. Would you like to override`, + `An {{ spec.title|caseUcfirst }} project ( ${localConfig.getProject()['projectId']} ) is already associated with the current directory. Would you like to override`, when() { return Object.keys(localConfig.getProject()).length !== 0; } @@ -135,11 +135,11 @@ const questionsInitProject = [ message: "How would you like to start?", choices: [ { - name: "Create a new {{ spec.title|caseUcfirst }} project", + name: "Create new project", value: "new" }, { - name: "Link this directory to an existing {{ spec.title|caseUcfirst }} project", + name: "Link directory to an existing project", value: "existing" } ] @@ -147,7 +147,7 @@ const questionsInitProject = [ { type: "search-list", name: "organization", - message: "Choose the project organization", + message: "Choose your organization", choices: async () => { let client = await sdkForConsole(true); const { teams } = await paginate(teamsList, { parseOutput: false, sdk: client }, 100, 'teams'); @@ -211,8 +211,8 @@ const questionsInitProject = [ ]; const questionsPullResources = [ { - type: "checkbox", - name: "resources", + type: "list", + name: "resource", message: "Which resources would you like to pull?", choices: [ { name: 'Project', value: 'project' }, @@ -325,6 +325,21 @@ const questionsCreateBucket = [ } ]; +const questionsCreateTeam = [ + { + type: "input", + name: "bucket", + message: "What would you like to name your team?", + default: "My Awesome Team" + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your team?", + default: "unique()" + } +]; + const questionsCreateCollection = [ { type: "list", @@ -332,8 +347,7 @@ const questionsCreateCollection = [ 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; + return localConfig.getDatabases().length !== 0; } }, { @@ -341,12 +355,12 @@ const questionsCreateCollection = [ name: "database", message: "Choose the collection database", choices: async () => { - const { databases } = await paginate(databasesList, { parseOutput: false }, 100, 'databases'); + const databases = localConfig.getDatabases(); let choices = databases.map((database, idx) => { return { name: `${database.name} (${database.$id})`, - value: { $id: database.$id, name: database.name } + value: database.$id } }) @@ -552,8 +566,8 @@ const questionsLogout = [ const questionsPushResources = [ { - type: "checkbox", - name: "resources", + type: "list", + name: "resource", message: "Which resources would you like to push?", choices: [ { name: 'Project', value: 'project' }, @@ -564,7 +578,22 @@ const questionsPushResources = [ { name: 'Topics', value: 'messages' } ] } -] +]; + +const questionsInitResources = [ + { + type: "list", + name: "resource", + message: "Which resource would you create?", + choices: [ + { name: 'Function', value: 'function' }, + { name: 'Collection', value: 'collection' }, + { name: 'Bucket', value: 'bucket' }, + { name: 'Team', value: 'team' }, + { name: 'Topic', value: 'message' } + ] + } +]; const questionsPushFunctions = [ { @@ -576,7 +605,7 @@ const questionsPushFunctions = [ let functions = localConfig.getFunctions(); checkDeployConditions(localConfig) if (functions.length === 0) { - throw new Error("No functions found in the current directory."); + throw new Error("No functions found in the current directory Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); } let choices = functions.map((func, idx) => { return { @@ -605,7 +634,7 @@ const questionsPushCollections = [ checkDeployConditions(localConfig) 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."); + throw new Error("No collections found in the current directory. Use 'appwrite pull collections' to synchronize existing one, or use 'appwrite init collection' to create a new one."); } return collections.map(collection => { return { @@ -632,7 +661,7 @@ const questionsPushBuckets = [ let buckets = localConfig.getBuckets(); checkDeployConditions(localConfig) if (buckets.length === 0) { - throw new Error("No buckets found in the current directory. Run `appwrite pull bucket` to fetch all your buckets."); + throw new Error("No buckets found in the current directory. Use 'appwrite pull buckets' to synchronize existing one, or use 'appwrite init bucket' to create a new one."); } return buckets.map(bucket => { return { @@ -641,12 +670,7 @@ const questionsPushBuckets = [ } }); } - }, - { - type: "input", - name: "override", - message: 'Are you sure you want to override this bucket? This can lead to loss of data! Type "YES" to confirm.' - }, + } ] const questionsPushMessagingTopics = [ @@ -657,7 +681,7 @@ const questionsPushMessagingTopics = [ choices: () => { 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."); + throw new Error("No topics found in the current directory. Use 'appwrite pull topics' to synchronize existing one, or use 'appwrite init topic' to create a new one."); } return topics.map(topic => { return { @@ -699,7 +723,7 @@ const questionsPushTeams = [ let teams = localConfig.getTeams(); checkDeployConditions(localConfig); if (teams.length === 0) { - throw new Error("No teams found in the current directory. Run `appwrite pull team` to fetch all your teams."); + throw new Error("No teams found in the current directory. Use 'appwrite pull teams' to synchronize existing one, or use 'appwrite init team' to create a new one."); } return teams.map(team => { return { @@ -709,18 +733,13 @@ const questionsPushTeams = [ }); } }, - { - type: "input", - name: "override", - message: 'Are you sure you want to override this team? This can lead to loss of data! Type "YES" to confirm.' - }, ]; const questionsListFactors = [ { type: "list", name: "factor", - message: "Your account is protected by multiple factors. Which factor would you like to use to authenticate?", + message: "Your account is protected by multi-factor authentication. Please choose one for verification.", choices: async () => { let client = await sdkForConsole(false); const factors = await accountListMfaFactors({ @@ -730,19 +749,19 @@ const questionsListFactors = [ const choices = [ { - name: `TOTP (Time-based One-time Password)`, + name: `Authenticator app (Get a code from a third-party authenticator app)`, value: 'totp' }, { - name: `E-mail`, + name: `Email (Get a security code at your Appwrite email address)`, value: 'email' }, { - name: `Phone (SMS)`, + name: `SMS (Get a security code on your Appwrite phone number)`, value: 'phone' }, { - name: `Recovery code`, + name: `Recovery code (Use one of your recovery codes for verification)`, value: 'recoveryCode' } ].filter((ch) => factors[ch.value] === true); @@ -775,7 +794,7 @@ const questionsRunFunctions = [ choices: () => { let functions = localConfig.getFunctions(); if (functions.length === 0) { - throw new Error("No functions found in the current directory. Use 'appwrite init function' to create one"); + throw new Error("No functions found in the current directory. Use 'appwrite pull functions' to synchronize existing one, or use 'appwrite init function' to create a new one."); } let choices = functions.map((func, idx) => { return { @@ -810,5 +829,7 @@ module.exports = { questionsListFactors, questionsMfaChallenge, questionsRunFunctions, - questionGetEndpoint + questionGetEndpoint, + questionsInitResources, + questionsCreateTeam }; diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index c0dde328c..a2cf3e3c1 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -54,7 +54,7 @@ function systemHasCommand(command) { const checkDeployConditions = (localConfig) => { if (Object.keys(localConfig.data).length === 0) { - throw new Error("No appwrite.json file found in the current directory. This command must be run in the folder holding your appwrite.json file. Please run this command again in the folder containing your appwrite.json file, or run appwrite init project."); + throw new Error("No appwrite.json file found in the current directory. Please run this command again in the folder containing your appwrite.json file, or run 'appwrite init project' to link current directory to an Appwrite project."); } } From d17c8055851380b2f788d3da11e538fa843a8395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 26 Jun 2024 15:07:31 +0000 Subject: [PATCH 195/198] Fix bug --- 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 865ff24c8..f4445ea64 100644 --- a/templates/cli/lib/commands/generic.js.twig +++ b/templates/cli/lib/commands/generic.js.twig @@ -89,7 +89,7 @@ const loginCommand = async ({ email, password, endpoint, mfa, code }) => { } success("Signed in as user with ID: " + account.$id); - info("Next you can create or link to your project using 'appwrite init project'"); + log("Next you can create or link to your project using 'appwrite init project'"); }; const whoami = new Command("whoami") From a0e0a4fd1aa21dd3232b012fe3d97612d34fc896 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:16:18 -0400 Subject: [PATCH 196/198] fix(cli): Apply non destructive changes --- templates/cli/lib/commands/push.js.twig | 159 +++++++++++++++++++++++- 1 file changed, 154 insertions(+), 5 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 98c74f5e5..80add3886 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -25,6 +25,16 @@ const { databasesCreateUrlAttribute, databasesCreateIpAttribute, databasesCreateEnumAttribute, + databasesUpdateBooleanAttribute, + databasesUpdateStringAttribute, + databasesUpdateIntegerAttribute, + databasesUpdateFloatAttribute, + databasesUpdateEmailAttribute, + databasesUpdateDatetimeAttribute, + databasesUpdateUrlAttribute, + databasesUpdateIpAttribute, + databasesUpdateEnumAttribute, + databasesUpdateRelationshipAttribute, databasesCreateRelationshipAttribute, databasesDeleteAttribute, databasesListAttributes, @@ -396,6 +406,123 @@ const createAttribute = async (databaseId, collectionId, attribute) => { }) } } + +const updateAttribute = async (databaseId, collectionId, attribute) => { + switch (attribute.type) { + case 'string': + switch (attribute.format) { + case 'email': + return await databasesUpdateEmailAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'url': + return await databasesUpdateUrlAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'ip': + return await databasesUpdateIpAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'enum': + return await databasesUpdateEnumAttribute({ + databaseId, + collectionId, + key: attribute.key, + elements: attribute.elements, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + default: + return await databasesUpdateStringAttribute({ + databaseId, + collectionId, + key: attribute.key, + size: attribute.size, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + + } + case 'integer': + return await databasesUpdateIntegerAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + min: parseInt(attribute.min.toString()), + max: parseInt(attribute.max.toString()), + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'double': + return databasesUpdateFloatAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + min: parseFloat(attribute.min.toString()), + max: parseFloat(attribute.max.toString()), + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'boolean': + return databasesUpdateBooleanAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'datetime': + return databasesUpdateDatetimeAttribute({ + databaseId, + collectionId, + key: attribute.key, + required: attribute.required, + xdefault: attribute.default, + array: attribute.array, + parseOutput: false + }) + case 'relationship': + return databasesUpdateRelationshipAttribute({ + 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']} )`); @@ -407,25 +534,39 @@ const deleteAttribute = async (collection, attribute) => { }); } + /** * Check if attribute non-changeable fields has been changed * If so return the differences as an object. * @param remote * @param local * @param collection + * @param recraeting when true will check only non-changeable keys * @returns {undefined|{reason: string, action: *, attribute, key: string}} */ -const checkAttributeChanges = (remote, local, collection) => { +const checkAttributeChanges = (remote, local, collection, recraeting = true) => { if (local === undefined) { return undefined; } const keyName = `${chalk.yellow(local.key)} in ${collection.name} (${collection['$id']})`; - const action = chalk.cyan('recreating'); + const action = chalk.cyan(recraeting ? 'recreating' : 'changing'); let reason = ''; + let attribute = remote; for (let key of Object.keys(remote)) { if (changeableKeys.includes(key)) { + if (!recraeting) { + if (remote[key] !== local[key]) { + const bol = reason === '' ? '' : '\n'; + reason += `${bol}${key} changed from ${chalk.red(remote[key])} to ${chalk.green(local[key])}`; + attribute = local; + } + } + continue; + } + + if (!recraeting) { continue; } @@ -435,7 +576,7 @@ const checkAttributeChanges = (remote, local, collection) => { } } - return reason === '' ? undefined : { key: keyName, attribute: remote, reason, action }; + return reason === '' ? undefined : { key: keyName, attribute, reason, action }; } /** @@ -468,9 +609,12 @@ 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); + const changes = remoteAttributes.map((attribute) => checkAttributeChanges(attribute, attributesContains(attribute, localAttributes), collection, false)) + .filter(attribute => attribute !== undefined) + .filter(attribute => conflicts.filter(attr => attribute.key === attr.key).length !== 1); let changedAttributes = []; - const changing = [...deleting, ...adding, ...conflicts] + const changing = [...deleting, ...adding, ...conflicts, ...changes] if (changing.length === 0) { return changedAttributes; } @@ -502,6 +646,11 @@ const attributesToCreate = async (remoteAttributes, localAttributes, collection) remoteAttributes = remoteAttributes.filter((attribute) => !attributesContains(attribute, changedAttributes)) } + if (changes.length > 0) { + changedAttributes = changes.map((change) => change.attribute); + await Promise.all(changedAttributes.map((changed) => updateAttribute(collection['databaseId'],collection['$id'], changed))); + } + 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)] @@ -1063,7 +1212,7 @@ const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { 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']} )`); + log(`No destructive changes has been detected. Skipping ${collection.name} ( ${collection['$id']} )`); continue; } } From 2a6b24fb82dd834cf714cba6a9ee99ce86db1477 Mon Sep 17 00:00:00 2001 From: Binyamin Yawitz <316103+byawitz@users.noreply.github.com> Date: Wed, 26 Jun 2024 13:55:38 -0400 Subject: [PATCH 197/198] fix(cli): Ask for all changes --- templates/cli/lib/commands/push.js.twig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index 80add3886..1349b73e1 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -625,7 +625,7 @@ const attributesToCreate = async (remoteAttributes, localAttributes, collection) return { Key: change.key, Action: change.action, Reason: change.reason, }; })); - if (!cliConfig.force && (deleting.length > 0 || conflicts.length > 0)) { + if (!cliConfig.force) { if (deleting.length > 0) { log(`Attribute deletion will cause ${chalk.red('loss of data')}`); } @@ -1212,7 +1212,6 @@ const pushCollection = async ({ returnOnZero } = { returnOnZero: false }) => { attributes = await attributesToCreate(collection.remoteVersion.attributes, collection.attributes, collection); if (Array.isArray(attributes) && attributes.length <= 0) { - log(`No destructive changes has been detected. Skipping ${collection.name} ( ${collection['$id']} )`); continue; } } From 2a1050195ba044c5c9b7f2a8f368c015cb97c509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 3 Jul 2024 14:27:42 +0000 Subject: [PATCH 198/198] Remove temporary CI/CD --- .github/workflows/cli-beta-publish.yaml | 47 ------------------------- 1 file changed, 47 deletions(-) delete mode 100644 .github/workflows/cli-beta-publish.yaml diff --git a/.github/workflows/cli-beta-publish.yaml b/.github/workflows/cli-beta-publish.yaml deleted file mode 100644 index 13373df64..000000000 --- a/.github/workflows/cli-beta-publish.yaml +++ /dev/null @@ -1,47 +0,0 @@ -name: CLI beta Publish -on: - pull_request: - paths: - - 'templates/cli/**.twig' - - 'src/SDK/Language/CLI.php' - -env: - PACKAGE_NAME: "${{ vars.PACKAGE_NAME }}@0.16.0${{ github.event.pull_request.head.sha }}" - -jobs: - publish: - environment: cli-testing - permissions: - contents: write - runs-on: ubuntu-latest - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - name: Setup Composer dependencies - run: docker run --rm --volume "$(pwd)":/app composer install --ignore-platform-reqs - - name: Generate SDKS - run: docker run --rm -v "$(pwd)":/app -w /app php:8.1-cli php example.php - - name: Fix permission - run: sudo chown -R 1001:1001 examples - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 'latest' - registry-url: 'https://registry.npmjs.org' - - name: Setup - working-directory: ./examples/cli/ - run: npm install - - name: Set version - working-directory: ./examples/cli/ - run: | - sed -i "s#appwrite-cli#${{ vars.PACKAGE_NAME }}#g" package.json - sed -i "s#0.16.0#0.16.0${{ github.event.pull_request.head.sha }}#g" package.json - - name: Publish - working-directory: examples/cli/ - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - name: Instruction - run: | - echo "Install it by running npm install ${{ env.PACKAGE_NAME }}" - echo "Run it using npx ${{ env.PACKAGE_NAME }}"