diff --git a/README.md b/README.md index bfb12f1..de2a704 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Appwrite Command Line SDK ![License](https://img.shields.io/github/license/appwrite/sdk-for-cli.svg?style=flat-square) -![Version](https://img.shields.io/badge/api%20version-0.15.0-blue.svg?style=flat-square) +![Version](https://img.shields.io/badge/api%20version-0.15.1-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) [![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite) [![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord) @@ -94,12 +94,6 @@ You can also fetch all the collections in your current project using appwrite init collection ``` -The CLI also comes with a convenient `--all` flag to perform both these steps at once. - -```sh -appwrite init --all -``` - * ### Creating and deploying cloud functions The CLI makes it extremely easy to create and deploy Appwrite's cloud functions. Initialise your new function using @@ -145,12 +139,6 @@ Similarly, you can deploy all your collections to your Appwrite server using appwrite deploy collections ``` -The `deploy` command also comes with a convenient `--all` flag to deploy all your functions and collections at once. - -```sh -appwrite deploy --all -``` - > ### Note > By default, requests to domains with self signed SSL certificates (or no certificates) are disabled. If you trust the domain, you can bypass the certificate validation using ```sh diff --git a/lib/commands/account.js b/lib/commands/account.js index 474a2f2..c117d5a 100644 --- a/lib/commands/account.js +++ b/lib/commands/account.js @@ -784,7 +784,7 @@ account account .command(`updatePhone`) - .description(`Update currently logged in user account phone number. After changing phone number, the user confirmation status will get reset. A new confirmation SMS is not sent automatically however you can use the phone confirmation endpoint again to send the confirmation SMS.`) + .description(`Update the currently logged in user's phone number. After updating the phone number, the phone verification status will be reset. A confirmation SMS is not sent automatically, however you can use the [POST /account/verification/phone](/docs/client/account#accountCreatePhoneVerification) endpoint to send a confirmation SMS.`) .requiredOption(`--number `, `Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.`) .requiredOption(`--password `, `User password. Must be at least 8 chars.`) .action(actionRunner(accountUpdatePhone)) @@ -856,7 +856,7 @@ account account .command(`createOAuth2Session`) .description(`Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. If there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user.. `) - .requiredOption(`--provider `, `OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, bitbucket, bitly, box, dailymotion, discord, dropbox, facebook, github, gitlab, google, linkedin, microsoft, notion, okta, paypal, paypalSandbox, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoom.`) + .requiredOption(`--provider `, `OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, autodesk, bitbucket, bitly, box, dailymotion, discord, dropbox, facebook, github, gitlab, google, linkedin, microsoft, notion, okta, paypal, paypalSandbox, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, yahoo, yammer, yandex, zoom.`) .option(`--success `, `URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.`) .option(`--failure `, `URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.`) .option(`--scopes `, `A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long.`) @@ -864,14 +864,14 @@ account account .command(`createPhoneSession`) - .description(`Sends the user a SMS with a secret key for creating a session. Use the returned user ID and the secret to submit a request to the [PUT /account/sessions/phone](/docs/client/account#accountUpdatePhoneSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.`) + .description(`Sends the user an SMS with a secret key for creating a session. Use the returned user ID and secret and submit a request to the [PUT /account/sessions/phone](/docs/client/account#accountUpdatePhoneSession) endpoint to complete the login process. The secret sent to the user's phone is valid for 15 minutes.`) .requiredOption(`--userId `, `Unique Id. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.`) .requiredOption(`--number `, `Phone number. Format this number with a leading '+' and a country code, e.g., +16175551212.`) .action(actionRunner(accountCreatePhoneSession)) account .command(`updatePhoneSession`) - .description(`Use this endpoint to complete creating the session with the Magic URL. Both the **userId** and **secret** arguments will be passed as query parameters to the redirect URL you have provided when sending your request to the [POST /account/sessions/magic-url](/docs/client/account#accountCreateMagicURLSession) endpoint. Please note that in order to avoid a [Redirect Attack](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.md) the only valid redirect URLs are the ones from domains you have set when adding your platforms in the console interface.`) + .description(`Use this endpoint to complete creating a session with SMS. Use the **userId** from the [createPhoneSession](/docs/client/account#accountCreatePhoneSession) endpoint and the **secret** received via SMS to successfully update and confirm the phone session.`) .requiredOption(`--userId `, `User ID.`) .requiredOption(`--secret `, `Valid verification token.`) .action(actionRunner(accountUpdatePhoneSession)) @@ -914,7 +914,7 @@ account account .command(`createPhoneVerification`) - .description(`Use this endpoint to send a verification message to your user's phone number to confirm they are the valid owners of that address. The provided secret should allow you to complete the verification process by verifying both the **userId** and **secret** parameters. Learn more about how to [complete the verification process](/docs/client/account#accountUpdatePhoneVerification). The verification link sent to the user's phone number is valid for 15 minutes.`) + .description(`Use this endpoint to send a verification SMS to the currently logged in user. This endpoint is meant for use after updating a user's phone number using the [accountUpdatePhone](/docs/client/account#accountUpdatePhone) endpoint. Learn more about how to [complete the verification process](/docs/client/account#accountUpdatePhoneVerification). The verification code sent to the user's phone number is valid for 15 minutes.`) .action(actionRunner(accountCreatePhoneVerification)) account diff --git a/lib/commands/deploy.js b/lib/commands/deploy.js index a0d8051..7175a36 100644 --- a/lib/commands/deploy.js +++ b/lib/commands/deploy.js @@ -6,34 +6,37 @@ const { questionsDeployFunctions, questionsGetEntrypoint, questionsDeployCollect const { actionRunner, success, log, error, commandDescriptions } = require("../parser"); const { functionsGet, functionsCreate, functionsUpdate, functionsCreateDeployment, functionsUpdateDeployment } = require('./functions'); const { - databaseCreateBooleanAttribute, - databaseGetCollection, - databaseCreateCollection, - databaseCreateStringAttribute, - databaseCreateIntegerAttribute, - databaseCreateFloatAttribute, - databaseCreateEmailAttribute, - databaseCreateIndex, - databaseCreateUrlAttribute, - databaseCreateIpAttribute, - databaseCreateEnumAttribute, - databaseDeleteAttribute, - databaseListAttributes, - databaseListIndexes, - databaseDeleteIndex -} = require("./database"); + databasesGet, + databasesCreate, + databasesCreateBooleanAttribute, + databasesGetCollection, + databasesCreateCollection, + databasesCreateStringAttribute, + databasesCreateIntegerAttribute, + databasesCreateFloatAttribute, + databasesCreateEmailAttribute, + databasesCreateIndex, + databasesCreateUrlAttribute, + databasesCreateIpAttribute, + databasesCreateEnumAttribute, + databasesDeleteAttribute, + databasesListAttributes, + databasesListIndexes, + databasesDeleteIndex +} = require("./databases"); const POOL_DEBOUNCE = 2000; // in milliseconds const POOL_MAX_DEBOUNCES = 30; const awaitPools = { - wipeAttributes: async (collectionId, iteration = 1) => { + wipeAttributes: async (databaseId, collectionId, iteration = 1) => { if (iteration > POOL_MAX_DEBOUNCES) { return false; } // TODO: Pagination? - const { attributes: remoteAttributes } = await databaseListAttributes({ + const { attributes: remoteAttributes } = await databasesListAttributes({ + databaseId, collectionId, limit: 100, parseOutput: false @@ -44,15 +47,16 @@ const awaitPools = { } await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.wipeAttributes(collectionId, iteration + 1); + return await awaitPools.wipeAttributes(databaseId, collectionId, iteration + 1); }, - wipeIndexes: async (collectionId, iteration = 1) => { + wipeIndexes: async (databaseId, collectionId, iteration = 1) => { if (iteration > POOL_MAX_DEBOUNCES) { return false; } // TODO: Pagination? - const { indexes: remoteIndexes } = await databaseListIndexes({ + const { indexes: remoteIndexes } = await databasesListIndexes({ + databaseId, collectionId, limit: 100, parseOutput: false @@ -63,15 +67,16 @@ const awaitPools = { } await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.wipeIndexes(collectionId, iteration + 1); + return await awaitPools.wipeIndexes(databaseId, collectionId, iteration + 1); }, - expectAttributes: async (collectionId, attributeKeys, iteration = 1) => { + expectAttributes: async (databaseId, collectionId, attributeKeys, iteration = 1) => { if (iteration > POOL_MAX_DEBOUNCES) { return false; } // TODO: Pagination? - const { attributes: remoteAttributes } = await databaseListAttributes({ + const { attributes: remoteAttributes } = await databasesListAttributes({ + databaseId, collectionId, limit: 100, parseOutput: false @@ -94,15 +99,16 @@ const awaitPools = { } await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.expectAttributes(collectionId, attributeKeys, iteration + 1); + return await awaitPools.expectAttributes(databaseId, collectionId, attributeKeys, iteration + 1); }, - expectIndexes: async (collectionId, indexKeys, iteration = 1) => { + expectIndexes: async (databaseId, collectionId, indexKeys, iteration = 1) => { if (iteration > POOL_MAX_DEBOUNCES) { return false; } // TODO: Pagination? - const { indexes: remoteIndexes } = await databaseListIndexes({ + const { indexes: remoteIndexes } = await databasesListIndexes({ + databaseId, collectionId, limit: 100, parseOutput: false @@ -125,32 +131,49 @@ const awaitPools = { } await new Promise(resolve => setTimeout(resolve, POOL_DEBOUNCE)); - return await awaitPools.expectIndexes(collectionId, indexKeys, iteration + 1); + return await awaitPools.expectIndexes(databaseId, collectionId, indexKeys, iteration + 1); }, } const deploy = new Command("deploy") .description(commandDescriptions['deploy']) - .option("--all", "Flag to deploy collections and functions") - .action(actionRunner(async ({ all }, command) => { - if (all == undefined) { - command.help() - } - - try { - await deployFunction(); - } catch (e) { - error(e.message); - } - await deployCollection() + .action(actionRunner(async (_options, command) => { + command.help() })); -const deployFunction = async () => { +const deployFunction = async ({ functionId, all } = {}) => { let response = {}; - let answers = await inquirer.prompt(questionsDeployFunctions) - let functions = answers.functions.map((func) => JSONbig.parse(func)) + const functionIds = []; + + if(functionId) { + functionIds.push(functionId); + } else if(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; + })); + } + if(functionIds.length <= 0) { + const answers = await inquirer.prompt(questionsDeployFunctions); + functionIds.push(...answers.functions); + } + + let functions = functionIds.map((id) => { + const functions = localConfig.getFunctions(); + const func = functions.find((f) => f.$id === id); + + if(!func) { + throw new Error("Function '" + id + "' not found.") + } + + return func; + }); + for (let func of functions) { log(`Deploying function ${func.name} ( ${func['$id']} )`) @@ -230,12 +253,13 @@ const deployFunction = async () => { } } -const createAttribute = async (collectionId, attribute) => { +const createAttribute = async (databaseId, collectionId, attribute) => { switch (attribute.type) { case 'string': switch (attribute.format) { case 'email': - return await databaseCreateEmailAttribute({ + return await databasesCreateEmailAttribute({ + databaseId, collectionId, key: attribute.key, required: attribute.required, @@ -244,7 +268,8 @@ const createAttribute = async (collectionId, attribute) => { parseOutput: false }) case 'url': - return await databaseCreateUrlAttribute({ + return await databasesCreateUrlAttribute({ + databaseId, collectionId, key: attribute.key, required: attribute.required, @@ -253,7 +278,8 @@ const createAttribute = async (collectionId, attribute) => { parseOutput: false }) case 'ip': - return await databaseCreateIpAttribute({ + return await databasesCreateIpAttribute({ + databaseId, collectionId, key: attribute.key, required: attribute.required, @@ -262,7 +288,8 @@ const createAttribute = async (collectionId, attribute) => { parseOutput: false }) case 'enum': - return await databaseCreateEnumAttribute({ + return await databasesCreateEnumAttribute({ + databaseId, collectionId, key: attribute.key, elements: attribute.elements, @@ -272,7 +299,8 @@ const createAttribute = async (collectionId, attribute) => { parseOutput: false }) default: - return await databaseCreateStringAttribute({ + return await databasesCreateStringAttribute({ + databaseId, collectionId, key: attribute.key, size: attribute.size, @@ -284,7 +312,8 @@ const createAttribute = async (collectionId, attribute) => { } case 'integer': - return await databaseCreateIntegerAttribute({ + return await databasesCreateIntegerAttribute({ + databaseId, collectionId, key: attribute.key, required: attribute.required, @@ -295,7 +324,8 @@ const createAttribute = async (collectionId, attribute) => { parseOutput: false }) case 'double': - return databaseCreateFloatAttribute({ + return databasesCreateFloatAttribute({ + databaseId, collectionId, key: attribute.key, required: attribute.required, @@ -306,7 +336,9 @@ const createAttribute = async (collectionId, attribute) => { parseOutput: false }) case 'boolean': - return databaseCreateBooleanAttribute({ + return databasesCreateBooleanAttribute({ + databaseId, + databaseId, collectionId, key: attribute.key, required: attribute.required, @@ -317,15 +349,55 @@ const createAttribute = async (collectionId, attribute) => { } } -const deployCollection = async () => { +const deployCollection = async ({ all } = {}) => { let response = {}; - let answers = await inquirer.prompt(questionsDeployCollections[0]) - let collections = answers.collections.map((collection) => JSONbig.parse(collection)); + + let collectionIds = []; + const configCollections = localConfig.getCollections(); + + if(all) { + if (configCollections.length === 0) { + throw new Error("No collections found in the current directory. Run `appwrite init collection` to fetch all your collections."); + } + collectionIds.push(...configCollections.map((c) => c.$id)); + } + + if(collectionIds.length <= 0) { + let answers = await inquirer.prompt(questionsDeployCollections[0]) + collectionIds.push(...answers.collections); + } + + let collections = []; + + for(const collectionId of collectionIds) { + const idCollections = configCollections.filter((c) => c.$id === collectionId); + collections.push(...idCollections); + } for (let collection of collections) { log(`Deploying collection ${collection.name} ( ${collection['$id']} )`) + + let databaseId; + + try { + const database = await databasesGet({ + databaseId: collection.databaseId, + parseOutput: false, + }); + databaseId = database.$id; + } catch(err) { + log(`Database ${collection.databaseId} not found. Creating it now...`); + const database = await databasesCreate({ + databaseId: collection.databaseId, + name: collection.databaseId, + parseOutput: false, + }); + databaseId = database.$id; + } + try { - response = await databaseGetCollection({ + response = await databasesGetCollection({ + databaseId, collectionId: collection['$id'], parseOutput: false, }) @@ -340,51 +412,55 @@ const deployCollection = async () => { log(`Updating attributes ... `); // TODO: Pagination? - const { indexes: remoteIndexes } = await databaseListIndexes({ + const { indexes: remoteIndexes } = await databasesListIndexes({ + databaseId, collectionId: collection['$id'], limit: 100, parseOutput: false }); await Promise.all(remoteIndexes.map(async index => { - await databaseDeleteIndex({ + await databasesDeleteIndex({ + databaseId, collectionId: collection['$id'], key: index.key, parseOutput: false }); })); - const deleteIndexesPoolStatus = await awaitPools.wipeIndexes(collection['$id']); + const deleteIndexesPoolStatus = await awaitPools.wipeIndexes(databaseId, collection['$id']); if (!deleteIndexesPoolStatus) { throw new Error("Index deletion did not finish for too long."); } // TODO: Pagination? - const { attributes: remoteAttributes } = await databaseListAttributes({ + const { attributes: remoteAttributes } = await databasesListAttributes({ + databaseId, collectionId: collection['$id'], limit: 100, parseOutput: false }); await Promise.all(remoteAttributes.map(async attribute => { - await databaseDeleteAttribute({ + await databasesDeleteAttribute({ + databaseId, collectionId: collection['$id'], key: attribute.key, parseOutput: false }); })); - const deleteAttributesPoolStatus = await awaitPools.wipeAttributes(collection['$id']); + const deleteAttributesPoolStatus = await awaitPools.wipeAttributes(databaseId, collection['$id']); if (!deleteAttributesPoolStatus) { throw new Error("Attribute deletion did not finish for too long."); } await Promise.all(collection.attributes.map(async attribute => { - await createAttribute(collection['$id'], attribute); + await createAttribute(databaseId, collection['$id'], attribute); })); const attributeKeys = collection.attributes.map(attribute => attribute.key); - const createPoolStatus = await awaitPools.expectAttributes(collection['$id'], attributeKeys); + const createPoolStatus = await awaitPools.expectAttributes(databaseId, collection['$id'], attributeKeys); if (!createPoolStatus) { throw new Error("Attribute creation did not finish for too long."); } @@ -393,7 +469,8 @@ const deployCollection = async () => { log(`Creating indexes ...`) await Promise.all(collection.indexes.map(async index => { - await databaseCreateIndex({ + await databasesCreateIndex({ + databaseId, collectionId: collection['$id'], key: index.key, type: index.type, @@ -404,7 +481,7 @@ const deployCollection = async () => { })); const indexKeys = collection.indexes.map(attribute => attribute.key); - const indexPoolStatus = await awaitPools.expectIndexes(collection['$id'], indexKeys); + const indexPoolStatus = await awaitPools.expectIndexes(databaseId, collection['$id'], indexKeys); if (!indexPoolStatus) { throw new Error("Index creation did not finish for too long."); } @@ -413,7 +490,8 @@ const deployCollection = async () => { } catch (e) { if (e.code == 404) { log(`Collection ${collection.name} does not exist in the project. Creating ... `); - response = await databaseCreateCollection({ + response = await databasesCreateCollection({ + databaseId, collectionId: collection['$id'], name: collection.name, permission: collection.permission, @@ -424,11 +502,11 @@ const deployCollection = async () => { log(`Creating attributes ... `); await Promise.all(collection.attributes.map(async attribute => { - await createAttribute(collection['$id'], attribute); + await createAttribute(databaseId, collection['$id'], attribute); })); const attributeKeys = collection.attributes.map(attribute => attribute.key); - const attributePoolStatus = await awaitPools.expectAttributes(collection['$id'], attributeKeys); + const attributePoolStatus = await awaitPools.expectAttributes(databaseId, collection['$id'], attributeKeys); if (!attributePoolStatus) { throw new Error("Attribute creation did not finish for too long."); } @@ -437,7 +515,8 @@ const deployCollection = async () => { log(`Creating indexes ...`); await Promise.all(collection.indexes.map(async index => { - await databaseCreateIndex({ + await databasesCreateIndex({ + databaseId, collectionId: collection['$id'], key: index.key, type: index.type, @@ -448,7 +527,7 @@ const deployCollection = async () => { })); const indexKeys = collection.indexes.map(attribute => attribute.key); - const indexPoolStatus = await awaitPools.expectIndexes(collection['$id'], indexKeys); + const indexPoolStatus = await awaitPools.expectIndexes(databaseId, collection['$id'], indexKeys); if (!indexPoolStatus) { throw new Error("Index creation did not finish for too long."); } @@ -466,11 +545,14 @@ const deployCollection = async () => { deploy .command("function") .description("Deploy functions in the current directory.") + .option(`--functionId `, `Function ID`) + .option(`--all`, `Flag to deploy all functions`) .action(actionRunner(deployFunction)); deploy .command("collection") .description("Deploy collections in the current project.") + .option(`--all`, `Flag to deploy all functions`) .action(actionRunner(deployCollection)); module.exports = { diff --git a/lib/commands/generic.js b/lib/commands/generic.js index f70c9bb..5c827c0 100644 --- a/lib/commands/generic.js +++ b/lib/commands/generic.js @@ -5,7 +5,7 @@ const { sdkForConsole } = require("../sdks"); const { globalConfig, localConfig } = require("../config"); const { actionRunner, success, parseBool, commandDescriptions, log, parse } = require("../parser"); const { questionsLogin } = require("../questions"); -const { accountCreateSession, accountDeleteSession } = require("./account"); +const { accountCreateEmailSession, accountDeleteSession } = require("./account"); const login = new Command("login") .description(commandDescriptions['login']) @@ -14,7 +14,7 @@ const login = new Command("login") let client = await sdkForConsole(false); - await accountCreateSession({ + await accountCreateEmailSession({ email: answers.email, password: answers.password, parseOutput: false, diff --git a/lib/commands/init.js b/lib/commands/init.js index 002606d..fd75c80 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -6,22 +6,16 @@ const inquirer = require("inquirer"); const { teamsCreate } = require("./teams"); const { projectsCreate } = require("./projects"); const { functionsCreate } = require("./functions"); -const { databaseListCollections } = require("./database"); +const { databasesListCollections, databasesList } = require("./databases"); const { sdkForConsole } = require("../sdks"); const { localConfig } = require("../config"); -const { questionsInitProject, questionsInitFunction } = require("../questions"); +const { questionsInitProject, questionsInitFunction, questionsInitCollection } = require("../questions"); const { success, log, actionRunner, commandDescriptions } = require("../parser"); const init = new Command("init") .description(commandDescriptions['init']) - .option("--all", "Flag to initialize projects and collection") - .action(actionRunner(async ({ all }, command) => { - if (all == undefined) { - command.help() - } - - await initProject(); - await initCollection() + .action(actionRunner(async (_options, command) => { + command.help(); })); const initProject = async () => { @@ -40,7 +34,7 @@ const initProject = async () => { let teamId = response['$id']; response = await projectsCreate({ - projectId: 'unique()', + projectId: answers.id, name: answers.project, teamId, parseOutput: false @@ -54,6 +48,7 @@ const initProject = async () => { } const initFunction = async () => { + // TODO: Add CI/CD support (ID, name, runtime) let answers = await inquirer.prompt(questionsInitFunction) if (fs.existsSync(path.join(process.cwd(), 'functions', answers.name))) { @@ -65,7 +60,7 @@ const initFunction = async () => { } let response = await functionsCreate({ - functionId: 'unique()', + functionId: answers.id, name: answers.name, runtime: answers.runtime.id, parseOutput: false @@ -110,20 +105,41 @@ const initFunction = async () => { success(); } -const initCollection = async () => { - // TODO: Pagination? - let response = await databaseListCollections({ - limit: 100, - parseOutput: false - }) +const initCollection = async ({ all, databaseId } = {}) => { + const databaseIds = []; + + if(databaseId) { + databaseIds.push(databaseId); + } else if(all) { + let allDatabases = await databasesList({ + parseOutput: false + }) - let collections = response.collections; - log(`Found ${collections.length} collections`); + databaseIds.push(...allDatabases.databases.map((d) => d.$id)); + } + + if(databaseIds.length <= 0) { + let answers = await inquirer.prompt(questionsInitCollection) + if (!answers.databases) process.exit(1) + databaseIds.push(...answers.databases); + } - collections.forEach(async collection => { - log(`Fetching ${collection.name} ...`); - localConfig.addCollection(collection); - }); + for(const databaseId of databaseIds) { + // TODO: Pagination? + let response = await databasesListCollections({ + databaseId, + limit: 100, + parseOutput: false + }) + + let collections = response.collections; + log(`Found ${collections.length} collections`); + + collections.forEach(async collection => { + log(`Fetching ${collection.name} ...`); + localConfig.addCollection(collection); + }); + } success(); } @@ -141,6 +157,8 @@ init init .command("collection") .description("Initialise your Appwrite collections") + .option(`--databaseId `, `Database ID`) + .option(`--all`, `Flag to initialize all databases`) .action(actionRunner(initCollection)) module.exports = { diff --git a/lib/parser.js b/lib/parser.js index 7783d07..2f906cd 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -150,7 +150,7 @@ const logo = "\n _ _ _ ___ __ _____ const commandDescriptions = { "account": `The account command allows you to authenticate and manage a user account.`, "avatars": `The avatars command aims to help you complete everyday tasks related to your app image, icons, and avatars.`, - "database": `The database command allows you to create structured collections of documents, query and filter lists of documents.`, + "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.`, "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 Appwrite server's health.`, diff --git a/lib/questions.js b/lib/questions.js index 2cdabfa..a74a302 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -1,6 +1,7 @@ const { localConfig } = require('./config'); const { projectsList } = require('./commands/projects'); const { functionsListRuntimes } = require('./commands/functions'); +const { databasesList } = require('./commands/databases'); const JSONbig = require("json-bigint")({ storeAsString: false }); const getIgnores = (runtime) => { @@ -108,6 +109,15 @@ const questionsInitProject = [ return answers.start == "new"; }, }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your project?", + default: "myAwesomeProject", + when(answers) { + return answers.start == "new"; + }, + }, { type: "list", name: "project", @@ -146,6 +156,12 @@ const questionsInitFunction = [ 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: "myAwesomeFunction" + }, { type: "list", name: "runtime", @@ -166,6 +182,31 @@ const questionsInitFunction = [ } ]; +const questionsInitCollection = [ + { + type: "checkbox", + name: "databases", + message: "From which database would you like to init collections?", + choices: async () => { + let response = await databasesList({ + parseOutput: false + }) + let databases = response["databases"] + + if(databases.length <= 0) { + throw new Error("No databases found. Please create one in project console.") + } + let choices = databases.map((database, idx) => { + return { + name: `${database.name} (${database.$id})`, + value: database.$id + } + }) + return choices; + } + } +]; + const questionsLogin = [ { type: "input", @@ -204,8 +245,8 @@ const questionsDeployFunctions = [ } let choices = functions.map((func, idx) => { return { - name: `${func.name} (${func['$id']})`, - value: JSONbig.stringify(func) + name: `${func.name} (${func.$id})`, + value: func.$id } }) return choices; @@ -226,7 +267,7 @@ const questionsDeployCollections = [ let choices = collections.map((collection, idx) => { return { name: `${collection.name} (${collection['$id']})`, - value: JSONbig.stringify(collection) + value: collection.$id } }) return choices; @@ -258,6 +299,7 @@ module.exports = { questionsInitProject, questionsLogin, questionsInitFunction, + questionsInitCollection, questionsDeployFunctions, questionsDeployCollections, questionsGetEntrypoint