diff --git a/example.php b/example.php index 9eafe7b9a..8d5b54b1b 100644 --- a/example.php +++ b/example.php @@ -42,7 +42,7 @@ function getSSLPage($url) { $platform = 'console'; // $platform = 'server'; - $version = '1.7.x'; + $version = '1.8.x'; $spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/{$version}/app/config/specs/swagger2-{$version}-{$platform}.json"); if(empty($spec)) { diff --git a/templates/cli/lib/client.js.twig b/templates/cli/lib/client.js.twig index 110673e73..44c06630b 100644 --- a/templates/cli/lib/client.js.twig +++ b/templates/cli/lib/client.js.twig @@ -136,11 +136,6 @@ class Client { }, }), }); - - const warnings = response.headers.get('x-{{ spec.title | lower }}-warning'); - if (warnings) { - warnings.split(';').forEach((warning) => console.log(`${chalk.yellow.bold("ℹ Warning:")} ${chalk.yellow(warning)}`)); - } } catch (error) { throw new {{spec.title | caseUcfirst}}Exception(error.message); } diff --git a/templates/cli/lib/commands/pull.js.twig b/templates/cli/lib/commands/pull.js.twig index 89d0beadc..612a566ea 100644 --- a/templates/cli/lib/commands/pull.js.twig +++ b/templates/cli/lib/commands/pull.js.twig @@ -9,6 +9,7 @@ const { projectsGet } = require("./projects"); const { functionsList, functionsGetDeploymentDownload, functionsListDeployments } = require("./functions"); const { sitesList, sitesGetDeploymentDownload, sitesListDeployments } = require("./sites"); const { databasesGet, databasesListCollections, databasesList } = require("./databases"); +const { gridsListDatabases, gridsGetDatabase, gridsListTables } = require("./grids"); const { storageListBuckets } = require("./storage"); const { localConfig } = require("../config"); const { paginate } = require("../paginate"); @@ -21,6 +22,7 @@ const pullResources = async () => { functions: pullFunctions, sites: pullSites, collections: pullCollection, + tables: pullTable, buckets: pullBucket, teams: pullTeam, messages: pullMessagingTopic @@ -285,6 +287,7 @@ const pullSites = async ({ code, withVariables }) => { } const pullCollection = async () => { + warn("appwrite pull collection has been deprecated. Please consider using 'appwrite pull tables' instead"); log("Fetching collections ..."); let total = 0; @@ -336,6 +339,58 @@ const pullCollection = async () => { success(`Successfully pulled ${chalk.bold(total)} collections.`); } +const pullTable = async () => { + log("Fetching tables ..."); + let total = 0; + + const fetchResponse = await gridsListDatabases({ + queries: [JSON.stringify({ method: 'limit', values: [1] })], + parseOutput: false + }); + if (fetchResponse["databases"].length <= 0) { + log("No tables found."); + success(`Successfully pulled ${chalk.bold(total)} tables.`); + return; + } + + let databases = cliConfig.ids; + + if (databases.length === 0) { + if (cliConfig.all) { + databases = (await paginate(gridsListDatabases, { parseOutput: false }, 100, 'databases')).databases.map(database => database.$id); + } else { + databases = (await inquirer.prompt(questionsPullCollection)).databases; + } + } + + for (const databaseId of databases) { + const database = await gridsGetDatabase({ + databaseId, + parseOutput: false + }); + + total++; + log(`Pulling all tables from ${chalk.bold(database['name'])} database ...`); + + localConfig.addDatabase(database); + + const { tables } = await paginate(gridsListTables, { + databaseId, + parseOutput: false + }, 100, 'tables'); + + for (const table of tables) { + localConfig.addTable({ + ...table, + '$createdAt': undefined, + '$updatedAt': undefined + }); + } + } + + success(`Successfully pulled ${chalk.bold(total)} tables.`); +} + const pullBucket = async () => { log("Fetching buckets ..."); let total = 0; @@ -447,9 +502,15 @@ pull pull .command("collection") .alias("collections") - .description("Pull your {{ spec.title|caseUcfirst }} collections") + .description("Pull your {{ spec.title|caseUcfirst }} collections (deprecated, please use 'pull tables' instead)") .action(actionRunner(pullCollection)) +pull + .command("table") + .alias("tables") + .description("Pull your {{ spec.title|caseUcfirst }} tables") + .action(actionRunner(pullTable)) + pull .command("bucket") .alias("buckets") diff --git a/templates/cli/lib/commands/push.js.twig b/templates/cli/lib/commands/push.js.twig index d52a65fca..87a7b5f5e 100644 --- a/templates/cli/lib/commands/push.js.twig +++ b/templates/cli/lib/commands/push.js.twig @@ -6,10 +6,10 @@ const inquirer = require("inquirer"); const JSONbig = require("json-bigint")({ storeAsString: false }); const { Command } = require("commander"); const ID = require("../id"); -const { localConfig, globalConfig, KeysAttributes, KeysFunction, KeysSite, whitelistKeys, KeysTopics, KeysStorage, KeysTeams, KeysCollection } = require("../config"); +const { localConfig, globalConfig, KeysAttributes, KeysFunction, KeysSite, whitelistKeys, KeysTopics, KeysStorage, KeysTeams, KeysCollection, KeysTable } = require("../config"); const { Spinner, SPINNER_ARC, SPINNER_DOTS } = require('../spinner'); const { paginate } = require('../paginate'); -const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsPushSites, questionsGetEntrypoint, questionsPushCollections, questionPushChanges, questionPushChangesConfirmation, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); +const { questionsPushBuckets, questionsPushTeams, questionsPushFunctions, questionsPushSites, questionsGetEntrypoint, questionsPushCollections, questionsPushTables, questionPushChanges, questionPushChangesConfirmation, questionsPushMessagingTopics, questionsPushResources } = require("../questions"); const { cliConfig, actionRunner, success, warn, log, hint, error, commandDescriptions, drawTable } = require("../parser"); const { proxyCreateFunctionRule, proxyCreateSiteRule, proxyListRules } = require('./proxy'); const { consoleVariables } = require('./console'); @@ -49,6 +49,10 @@ const { databasesListIndexes, databasesUpdateCollection } = require("./databases"); +const { + gridsGetDatabase, + gridsGetTable +} = require("./grids"); const { storageGetBucket, storageUpdateBucket, storageCreateBucket } = require("./storage"); @@ -919,6 +923,7 @@ const pushResources = async () => { functions: pushFunction, sites: pushSite, collections: pushCollection, + tables: pushTable, buckets: pushBucket, teams: pushTeam, messages: pushMessagingTopic @@ -1672,7 +1677,150 @@ const pushFunction = async ({ functionId, async, code, withVariables } = { retur } } +const pushTable = async ({ returnOnZero, attempts } = { returnOnZero: false }) => { + const tables = []; + + if (attempts) { + pollMaxDebounces = attempts; + } + + if (cliConfig.all) { + checkDeployConditions(localConfig); + tables.push(...localConfig.getTables()); + } else { + const answers = await inquirer.prompt(questionsPushTables) + if (answers.tables) { + const configTables = new Map(); + localConfig.getTables().forEach((c) => { + configTables.set(`${c['databaseId']}|${c['$id']}`, c); + }); + answers.tables.forEach((a) => { + const table = configTables.get(a); + tables.push(table); + }) + } + } + + if (tables.length === 0) { + log("No tables found."); + hint("Use 'appwrite pull tables' to synchronize existing one, or use 'appwrite init table' to create a new one."); + return; + } + + const databases = Array.from(new Set(tables.map(table => table['databaseId']))); + + // Parallel db actions + await Promise.all(databases.map(async (databaseId) => { + const localDatabase = localConfig.getDatabase(databaseId); + + try { + const database = await gridsGetDatabase({ + databaseId: databaseId, + parseOutput: false, + }); + + if (database.name !== (localDatabase.name ?? databaseId)) { + await databasesUpdate({ + databaseId: databaseId, + name: localDatabase.name ?? databaseId, + parseOutput: false + }) + + success(`Updated ${localDatabase.name} ( ${databaseId} ) name`); + } + } catch (err) { + log(`Database ${databaseId} not found. Creating it now ...`); + + await databasesCreate({ + databaseId: databaseId, + name: localDatabase.name ?? databaseId, + parseOutput: false, + }); + } + })); + + + if (!(await approveChanges(tables, gridsGetTable, KeysTable, 'tableId', 'tables', ['columns', 'indexes'], 'databaseId', 'databaseId',))) { + return; + } + // Parallel collection actions + await Promise.all(tables.map(async (table) => { + try { + const remoteTable = await gridsGetTable({ + databaseId: table['databaseId'], + tableId: table['$id'], + parseOutput: false, + }); + + if (remoteTable.name !== table.name) { + await databasesUpdateTable({ + databaseId: table['databaseId'], + tableId: table['$id'], + name: table.name, + name: table.name, + parseOutput: false + }) + + success(`Updated ${table.name} ( ${table['$id']} ) name`); + } + table.remoteVersion = remoteTable; + + table.isExisted = true; + } catch + (e) { + if (Number(e.code) === 404) { + log(`Table ${table.name} does not exist in the project. Creating ... `); + await databasesCreateTable({ + databaseId: table['databaseId'], + tableId: table['$id'], + name: table.name, + documentSecurity: table.documentSecurity, + permissions: table['$permissions'], + parseOutput: false + }) + } else { + throw e; + } + } + })) + let numberOfTables = 0; + // Serialize attribute actions + for (let table of tables) { + let columns = table.columns; + let indexes = table.indexes; + + if (table.isExisted) { + columns = await attributesToCreate(table.remoteVersion.columns, table.columns, table); + indexes = await attributesToCreate(table.remoteVersion.indexes, table.indexes, table, true); + + if ((Array.isArray(columns) && columns.length <= 0) && (Array.isArray(indexes) && indexes.length <= 0)) { + continue; + } + + } + + log(`Pushing table ${table.name} ( ${table['databaseId']} - ${table['$id']} ) attributes`) + + try { + await createAttributes(columns, table) + } catch (e) { + throw e; + } + + try { + await createIndexes(indexes, table); + } catch (e) { + throw e; + } + numberOfTables++; + success(`Successfully pushed ${table.name} ( ${table['$id']} )`); + } + + success(`Successfully pushed ${numberOfTables} tables`); +} + const pushCollection = async ({ returnOnZero, attempts } = { returnOnZero: false }) => { + warn("appwrite push collection has been deprecated. Please consider using 'appwrite push tables' instead"); const collections = []; if (attempts) { @@ -2083,10 +2231,17 @@ push push .command("collection") .alias("collections") - .description("Push collections in the current project.") + .description("Push collections in the current project. (deprecated, please use 'push tables' instead)") .option(`-a, --attempts `, `Max number of attempts before timing out. default: 30.`) .action(actionRunner(pushCollection)); +push + .command("table") + .alias("tables") + .description("Push tables in the current project.") + .option(`-a, --attempts `, `Max number of attempts before timing out. default: 30.`) + .action(actionRunner(pushTable)); + push .command("bucket") .alias("buckets") diff --git a/templates/cli/lib/config.js.twig b/templates/cli/lib/config.js.twig index 548274183..326a217ae 100644 --- a/templates/cli/lib/config.js.twig +++ b/templates/cli/lib/config.js.twig @@ -9,6 +9,7 @@ const KeysSite = new Set(["path", "$id", "name", "enabled", "logging", "timeout" const KeysFunction = new Set(["path", "$id", "execute", "name", "enabled", "logging", "runtime", "specification", "scopes", "events", "schedule", "timeout", "entrypoint", "commands", "vars"]); const KeysDatabase = new Set(["$id", "name", "enabled"]); const KeysCollection = new Set(["$id", "$permissions", "databaseId", "name", "enabled", "documentSecurity", "attributes", "indexes"]); +const KeysTable = new Set(["$id", "$permissions", "databaseId", "name", "enabled", "documentSecurity", "columns", "indexes"]); const KeysStorage = new Set(["$id", "$permissions", "fileSecurity", "name", "enabled", "maximumFileSize", "allowedFileExtensions", "compression", "encryption", "antivirus"]); const KeysTopics = new Set(["$id", "name", "subscribe"]); const KeysTeams = new Set(["$id", "name"]); @@ -39,6 +40,33 @@ const KeysAttributes = new Set([ // Strings "encrypt", ]); +const KeysColumns = new Set([ + "key", + "type", + "required", + "array", + "size", + "default", + // integer and float + "min", + "max", + // email, enum, URL, IP, and datetime + "format", + // enum + "elements", + // relationship + "relatedCollection", + "relationType", + "twoWay", + "twoWayKey", + "onDelete", + "side", + // Indexes + "attributes", + "orders", + // Strings + "encrypt", +]); const KeyIndexes = new Set(["key", "type", "status", "attributes", "orders"]); function whitelistKeys(value, keys, nestedKeys = {}) { @@ -310,6 +338,50 @@ class Local extends Config { this.set("collections", collections); } + getTables() { + if (!this.has("tables")) { + return []; + } + return this.get("tables"); + } + + getTable($id) { + if (!this.has("tables")) { + return {}; + } + + let tables = this.get("tables"); + for (let i = 0; i < tables.length; i++) { + if (tables[i]['$id'] == $id) { + return tables[i]; + } + } + + return {}; + } + + addTable(props) { + props = whitelistKeys(props, KeysTable, { + columns: KeysColumns, + indexes: KeyIndexes + }); + + if (!this.has("tables")) { + this.set("tables", []); + } + + let tables = this.get("tables"); + for (let i = 0; i < tables.length; i++) { + if (tables[i]['$id'] == props['$id'] && tables[i]['databaseId'] == props['databaseId']) { + tables[i] = props; + this.set("tables", tables); + return; + } + } + tables.push(props); + this.set("tables", tables); + } + getBuckets() { if (!this.has("buckets")) { return []; @@ -713,5 +785,6 @@ module.exports = { KeysStorage, KeysTeams, KeysCollection, + KeysTable, whitelistKeys }; diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index a9ce99f6e..6b9df0e80 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -255,10 +255,11 @@ const questionsPullResources = [ choices: [ { name: `Settings ${chalk.blackBright(`(Project)`)}`, value: 'settings' }, { name: `Functions ${chalk.blackBright(`(Deployment)`)}`, value: 'functions' }, - { name: `Collections ${chalk.blackBright(`(Databases)`)}`, value: 'collections' }, + { name: `Tables ${chalk.blackBright(`(Grids)`)}`, value: 'tables' }, { name: `Buckets ${chalk.blackBright(`(Storage)`)}`, value: 'buckets' }, { name: `Teams ${chalk.blackBright(`(Auth)`)}`, value: 'teams' }, - { name: `Topics ${chalk.blackBright(`(Messaging)`)}`, value: 'messages' } + { name: `Topics ${chalk.blackBright(`(Messaging)`)}`, value: 'messages' }, + { name: `Collections ${chalk.blackBright(`(Database)`)}`, value: 'collections' } ] } ] @@ -666,10 +667,11 @@ const questionsPushResources = [ choices: [ { name: `Settings ${chalk.blackBright(`(Project)`)}`, value: 'settings' }, { name: `Functions ${chalk.blackBright(`(Deployment)`)}`, value: 'functions' }, - { name: `Collections ${chalk.blackBright(`(Databases)`)}`, value: 'collections' }, + { name: `Tables ${chalk.blackBright(`(Grids)`)}`, value: 'tables' }, { name: `Buckets ${chalk.blackBright(`(Storage)`)}`, value: 'buckets' }, { name: `Teams ${chalk.blackBright(`(Auth)`)}`, value: 'teams' }, - { name: `Topics ${chalk.blackBright(`(Messaging)`)}`, value: 'messages' } + { name: `Topics ${chalk.blackBright(`(Messaging)`)}`, value: 'messages' }, + { name: `Collections ${chalk.blackBright(`(Database)`)}`, value: 'collections' } ] } ]; @@ -682,10 +684,11 @@ const questionsInitResources = [ choices: [ { name: 'Function', value: 'function' }, { name: 'Site', value: 'site' }, - { name: 'Collection', value: 'collection' }, + { name: 'Table', value: 'table' }, { name: 'Bucket', value: 'bucket' }, { name: 'Team', value: 'team' }, - { name: 'Topic', value: 'message' } + { name: 'Topic', value: 'message' }, + { name: 'Collection', value: 'collection' } ] } ]; @@ -753,6 +756,27 @@ const questionsPushCollections = [ } ]; +const questionsPushTables = [ + { + type: "checkbox", + name: "tables", + message: "Which tables would you like to push?", + validate: (value) => validateRequired('table', value), + when: () => localConfig.getTables().length > 0, + choices: () => { + let tables = localConfig.getTables(); + checkDeployConditions(localConfig) + + return tables.map(table => { + return { + name: `${table.name} (${table['databaseId']} - ${table['$id']})`, + value: `${table['databaseId']}|${table['$id']}` + } + }); + } + } +]; + const questionPushChanges = [ { type: "input", @@ -1002,5 +1026,6 @@ module.exports = { questionsCreateTeam, questionPushChanges, questionPushChangesConfirmation, - questionsCreateSite + questionsCreateSite, + questionsPushTables };